mirror of
https://github.com/hwchase17/langchain.git
synced 2025-06-28 17:38:36 +00:00
Office365 Tool (#6306)
#### Background With the development of [structured tools](https://blog.langchain.dev/structured-tools/), the LangChain team expanded the platform's functionality to meet the needs of new applications. The GMail tool, empowered by structured tools, now supports multiple arguments and powerful search capabilities, demonstrating LangChain's ability to interact with dynamic data sources like email servers. #### Challenge The current GMail tool only supports GMail, while users often utilize other email services like Outlook in Office365. Additionally, the proposed calendar tool in PR https://github.com/hwchase17/langchain/pull/652 only works with Google Calendar, not Outlook. #### Changes This PR implements an Office365 integration for LangChain, enabling seamless email and calendar functionality with a single authentication process. #### Future Work With the core Office365 integration complete, future work could include integrating other Office365 tools such as Tasks and Address Book. #### Who can review? @hwchase17 or @vowelparrot can review this PR #### Appendix @janscas, I utilized your [O365](https://github.com/O365/python-o365) library extensively. Given the rising popularity of LangChain and similar AI frameworks, the convergence of libraries like O365 and tools like this one is likely. So, I wanted to keep you updated on our progress. --------- Co-authored-by: Dev 2049 <dev.dev2049@gmail.com>
This commit is contained in:
parent
a15afc102c
commit
d84a3bcf7a
238
docs/extras/modules/agents/toolkits/office365.ipynb
Normal file
238
docs/extras/modules/agents/toolkits/office365.ipynb
Normal file
@ -0,0 +1,238 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Office365 Toolkit\n",
|
||||
"\n",
|
||||
"This notebook walks through connecting LangChain to Office365 email and calendar.\n",
|
||||
"\n",
|
||||
"To use this toolkit, you will need to set up your credentials explained in the [Microsoft Graph authentication and authorization overview](https://learn.microsoft.com/en-us/graph/auth/). Once you've received a CLIENT_ID and CLIENT_SECRET, you can input them as environmental variables below."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!pip install --upgrade O365 > /dev/null\n",
|
||||
"!pip install beautifulsoup4 > /dev/null # This is optional but is useful for parsing HTML messages"
|
||||
]
|
||||
},
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Assign Environmental Variables\n",
|
||||
"\n",
|
||||
"The toolkit will read the CLIENT_ID and CLIENT_SECRET environmental variables to authenticate the user so you need to set them here. You will also need to set your OPENAI_API_KEY to use the agent later."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Set environmental variables here"
|
||||
]
|
||||
},
|
||||
{
|
||||
"attachments": {},
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Create the Toolkit and Get Tools\n",
|
||||
"\n",
|
||||
"To start, you need to create the toolkit, so you can access its tools later."
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 3,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"[O365SearchEvents(name='events_search', description=\" Use this tool to search for the user's calendar events. The input must be the start and end datetimes for the search query. The output is a JSON list of all the events in the user's calendar between the start and end times. You can assume that the user can not schedule any meeting over existing meetings, and that the user is busy during meetings. Any times without events are free for the user. \", args_schema=<class 'langchain.tools.office365.events_search.SearchEventsInput'>, return_direct=False, verbose=False, callbacks=None, callback_manager=None, handle_tool_error=False, account=Account Client Id: f32a022c-3c4c-4d10-a9d8-f6a9a9055302),\n",
|
||||
" O365CreateDraftMessage(name='create_email_draft', description='Use this tool to create a draft email with the provided message fields.', args_schema=<class 'langchain.tools.office365.create_draft_message.CreateDraftMessageSchema'>, return_direct=False, verbose=False, callbacks=None, callback_manager=None, handle_tool_error=False, account=Account Client Id: f32a022c-3c4c-4d10-a9d8-f6a9a9055302),\n",
|
||||
" O365SearchEmails(name='messages_search', description='Use this tool to search for email messages. The input must be a valid Microsoft Graph v1.0 $search query. The output is a JSON list of the requested resource.', args_schema=<class 'langchain.tools.office365.messages_search.SearchEmailsInput'>, return_direct=False, verbose=False, callbacks=None, callback_manager=None, handle_tool_error=False, account=Account Client Id: f32a022c-3c4c-4d10-a9d8-f6a9a9055302),\n",
|
||||
" O365SendEvent(name='send_event', description='Use this tool to create and send an event with the provided event fields.', args_schema=<class 'langchain.tools.office365.send_event.SendEventSchema'>, return_direct=False, verbose=False, callbacks=None, callback_manager=None, handle_tool_error=False, account=Account Client Id: f32a022c-3c4c-4d10-a9d8-f6a9a9055302),\n",
|
||||
" O365SendMessage(name='send_email', description='Use this tool to send an email with the provided message fields.', args_schema=<class 'langchain.tools.office365.send_message.SendMessageSchema'>, return_direct=False, verbose=False, callbacks=None, callback_manager=None, handle_tool_error=False, account=Account Client Id: f32a022c-3c4c-4d10-a9d8-f6a9a9055302)]"
|
||||
]
|
||||
},
|
||||
"execution_count": 3,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"from langchain.agents.agent_toolkits import O365Toolkit\n",
|
||||
"\n",
|
||||
"toolkit = O365Toolkit()\n",
|
||||
"tools = toolkit.get_tools()\n",
|
||||
"tools"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Use within an Agent"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 4,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"from langchain import OpenAI\n",
|
||||
"from langchain.agents import initialize_agent, AgentType"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 5,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"llm = OpenAI(temperature=0)\n",
|
||||
"agent = initialize_agent(\n",
|
||||
" tools=toolkit.get_tools(),\n",
|
||||
" llm=llm,\n",
|
||||
" verbose=False,\n",
|
||||
" agent=AgentType.STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION,\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 6,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"'The draft email was created correctly.'"
|
||||
]
|
||||
},
|
||||
"execution_count": 6,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"agent.run(\"Create an email draft for me to edit of a letter from the perspective of a sentient parrot\"\n",
|
||||
" \" who is looking to collaborate on some research with her\"\n",
|
||||
" \" estranged friend, a cat. Under no circumstances may you send the message, however.\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 12,
|
||||
"metadata": {
|
||||
"tags": []
|
||||
},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"\"I found one draft in your drafts folder about collaboration. It was sent on 2023-06-16T18:22:17+0000 and the subject was 'Collaboration Request'.\""
|
||||
]
|
||||
},
|
||||
"execution_count": 12,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"agent.run(\"Could you search in my drafts folder and let me know if any of them are about collaboration?\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"/home/vscode/langchain-py-env/lib/python3.11/site-packages/O365/utils/windows_tz.py:639: PytzUsageWarning: The zone attribute is specific to pytz's interface; please migrate to a new time zone provider. For more details on how to do so, see https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html\n",
|
||||
" iana_tz.zone if isinstance(iana_tz, tzinfo) else iana_tz)\n",
|
||||
"/home/vscode/langchain-py-env/lib/python3.11/site-packages/O365/utils/utils.py:463: PytzUsageWarning: The zone attribute is specific to pytz's interface; please migrate to a new time zone provider. For more details on how to do so, see https://pytz-deprecation-shim.readthedocs.io/en/latest/migration.html\n",
|
||||
" timezone = date_time.tzinfo.zone if date_time.tzinfo is not None else None\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"'I have scheduled a meeting with a sentient parrot to discuss research collaborations on October 3, 2023 at 2 pm Easter Time. Please let me know if you need to make any changes.'"
|
||||
]
|
||||
},
|
||||
"execution_count": 8,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"agent.run(\"Can you schedule a 30 minute meeting with a sentient parrot to discuss research collaborations on October 3, 2023 at 2 pm Easter Time?\")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"\"Yes, you have an event on October 3, 2023 with a sentient parrot. The event is titled 'Meeting with sentient parrot' and is scheduled from 6:00 PM to 6:30 PM.\""
|
||||
]
|
||||
},
|
||||
"execution_count": 9,
|
||||
"metadata": {},
|
||||
"output_type": "execute_result"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"agent.run(\"Can you tell me if I have any events on October 3, 2023 in Eastern Time, and if so, tell me if any of them are with a sentient parrot?\")"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3 (ipykernel)",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython3",
|
||||
"version": "3.11.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 4
|
||||
}
|
1
langchain/agents/agent_toolkits/office365/__init__.py
Normal file
1
langchain/agents/agent_toolkits/office365/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Gmail toolkit."""
|
38
langchain/agents/agent_toolkits/office365/toolkit.py
Normal file
38
langchain/agents/agent_toolkits/office365/toolkit.py
Normal file
@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from langchain.agents.agent_toolkits.base import BaseToolkit
|
||||
from langchain.tools import BaseTool
|
||||
from langchain.tools.office365.create_draft_message import O365CreateDraftMessage
|
||||
from langchain.tools.office365.events_search import O365SearchEvents
|
||||
from langchain.tools.office365.messages_search import O365SearchEmails
|
||||
from langchain.tools.office365.send_event import O365SendEvent
|
||||
from langchain.tools.office365.send_message import O365SendMessage
|
||||
from langchain.tools.office365.utils import authenticate
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from O365 import Account
|
||||
|
||||
|
||||
class O365Toolkit(BaseToolkit):
|
||||
"""Toolkit for interacting with Office365."""
|
||||
|
||||
account: Account = Field(default_factory=authenticate)
|
||||
|
||||
class Config:
|
||||
"""Pydantic config."""
|
||||
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
def get_tools(self) -> List[BaseTool]:
|
||||
"""Get the tools in the toolkit."""
|
||||
return [
|
||||
O365SearchEvents(account=self.account),
|
||||
O365CreateDraftMessage(account=self.account),
|
||||
O365SearchEmails(account=self.account),
|
||||
O365SendEvent(account=self.account),
|
||||
O365SendMessage(account=self.account),
|
||||
]
|
17
langchain/tools/office365/__init__ .py
Normal file
17
langchain/tools/office365/__init__ .py
Normal file
@ -0,0 +1,17 @@
|
||||
"""O365 tools."""
|
||||
|
||||
from langchain.tools.office365.create_draft_message import O365CreateDraftMessage
|
||||
from langchain.tools.office365.events_search import O365SearchEvents
|
||||
from langchain.tools.office365.messages_search import O365SearchEmails
|
||||
from langchain.tools.office365.send_event import O365SendEvent
|
||||
from langchain.tools.office365.send_message import O365SendMessage
|
||||
from langchain.tools.office365.utils import authenticate
|
||||
|
||||
__all__ = [
|
||||
"O365SearchEmails",
|
||||
"O365SearchEvents",
|
||||
"O365CreateDraftMessage",
|
||||
"O365SendMessage",
|
||||
"O365SendEvent",
|
||||
"authenticate",
|
||||
]
|
16
langchain/tools/office365/base.py
Normal file
16
langchain/tools/office365/base.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""Base class for Gmail tools."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
from langchain.tools.base import BaseTool
|
||||
from langchain.tools.office365.utils import authenticate
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from O365 import Account
|
||||
|
||||
|
||||
class O365BaseTool(BaseTool):
|
||||
account: Account = Field(default_factory=authenticate)
|
78
langchain/tools/office365/create_draft_message.py
Normal file
78
langchain/tools/office365/create_draft_message.py
Normal file
@ -0,0 +1,78 @@
|
||||
from typing import List, Optional, Type
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langchain.callbacks.manager import (
|
||||
AsyncCallbackManagerForToolRun,
|
||||
CallbackManagerForToolRun,
|
||||
)
|
||||
from langchain.tools.office365.base import O365BaseTool
|
||||
|
||||
|
||||
class CreateDraftMessageSchema(BaseModel):
|
||||
body: str = Field(
|
||||
...,
|
||||
description="The message body to include in the draft.",
|
||||
)
|
||||
to: List[str] = Field(
|
||||
...,
|
||||
description="The list of recipients.",
|
||||
)
|
||||
subject: str = Field(
|
||||
...,
|
||||
description="The subject of the message.",
|
||||
)
|
||||
cc: Optional[List[str]] = Field(
|
||||
None,
|
||||
description="The list of CC recipients.",
|
||||
)
|
||||
bcc: Optional[List[str]] = Field(
|
||||
None,
|
||||
description="The list of BCC recipients.",
|
||||
)
|
||||
|
||||
|
||||
class O365CreateDraftMessage(O365BaseTool):
|
||||
name: str = "create_email_draft"
|
||||
description: str = (
|
||||
"Use this tool to create a draft email with the provided message fields."
|
||||
)
|
||||
args_schema: Type[CreateDraftMessageSchema] = CreateDraftMessageSchema
|
||||
|
||||
def _run(
|
||||
self,
|
||||
body: str,
|
||||
to: List[str],
|
||||
subject: str,
|
||||
cc: Optional[List[str]] = None,
|
||||
bcc: Optional[List[str]] = None,
|
||||
run_manager: Optional[CallbackManagerForToolRun] = None,
|
||||
) -> str:
|
||||
# Get mailbox object
|
||||
mailbox = self.account.mailbox()
|
||||
message = mailbox.new_message()
|
||||
|
||||
# Assign message values
|
||||
message.body = body
|
||||
message.subject = subject
|
||||
message.to.add(to)
|
||||
if cc is not None:
|
||||
message.cc.add(cc)
|
||||
if bcc is not None:
|
||||
message.bcc.add(cc)
|
||||
|
||||
message.save_draft()
|
||||
|
||||
output = "Draft created: " + str(message)
|
||||
return output
|
||||
|
||||
async def _arun(
|
||||
self,
|
||||
message: str,
|
||||
to: List[str],
|
||||
subject: str,
|
||||
cc: Optional[List[str]] = None,
|
||||
bcc: Optional[List[str]] = None,
|
||||
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
|
||||
) -> str:
|
||||
raise NotImplementedError(f"The tool {self.name} does not support async yet.")
|
141
langchain/tools/office365/events_search.py
Normal file
141
langchain/tools/office365/events_search.py
Normal file
@ -0,0 +1,141 @@
|
||||
"""Util that Searches calendar events in Office 365.
|
||||
|
||||
Free, but setup is required. See link below.
|
||||
https://learn.microsoft.com/en-us/graph/auth/
|
||||
"""
|
||||
|
||||
from datetime import datetime as dt
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
|
||||
from pydantic import BaseModel, Extra, Field
|
||||
|
||||
from langchain.callbacks.manager import (
|
||||
AsyncCallbackManagerForToolRun,
|
||||
CallbackManagerForToolRun,
|
||||
)
|
||||
from langchain.tools.office365.base import O365BaseTool
|
||||
from langchain.tools.office365.utils import clean_body
|
||||
|
||||
|
||||
class SearchEventsInput(BaseModel):
|
||||
"""Input for SearchEmails Tool."""
|
||||
|
||||
"""From https://learn.microsoft.com/en-us/graph/search-query-parameter"""
|
||||
|
||||
start_datetime: str = Field(
|
||||
description=(
|
||||
" The start datetime for the search query in the following format: "
|
||||
' YYYY-MM-DDTHH:MM:SS±hh:mm, where "T" separates the date and time '
|
||||
" components, and the time zone offset is specified as ±hh:mm. "
|
||||
' For example: "2023-06-09T10:30:00+03:00" represents June 9th, '
|
||||
" 2023, at 10:30 AM in a time zone with a positive offset of 3 "
|
||||
" hours from Coordinated Universal Time (UTC)."
|
||||
)
|
||||
)
|
||||
end_datetime: str = Field(
|
||||
description=(
|
||||
" The end datetime for the search query in the following format: "
|
||||
' YYYY-MM-DDTHH:MM:SS±hh:mm, where "T" separates the date and time '
|
||||
" components, and the time zone offset is specified as ±hh:mm. "
|
||||
' For example: "2023-06-09T10:30:00+03:00" represents June 9th, '
|
||||
" 2023, at 10:30 AM in a time zone with a positive offset of 3 "
|
||||
" hours from Coordinated Universal Time (UTC)."
|
||||
)
|
||||
)
|
||||
max_results: int = Field(
|
||||
default=10,
|
||||
description="The maximum number of results to return.",
|
||||
)
|
||||
truncate: bool = Field(
|
||||
default=True,
|
||||
description=(
|
||||
"Whether the event's body is trucated to meet token number limits. Set to "
|
||||
"False for searches that will retrieve very few results, otherwise, set to "
|
||||
"True."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class O365SearchEvents(O365BaseTool):
|
||||
"""Class for searching calendar events in Office 365
|
||||
|
||||
Free, but setup is required
|
||||
"""
|
||||
|
||||
name: str = "events_search"
|
||||
args_schema: Type[BaseModel] = SearchEventsInput
|
||||
description: str = (
|
||||
" Use this tool to search for the user's calendar events."
|
||||
" The input must be the start and end datetimes for the search query."
|
||||
" The output is a JSON list of all the events in the user's calendar"
|
||||
" between the start and end times. You can assume that the user can "
|
||||
" not schedule any meeting over existing meetings, and that the user "
|
||||
"is busy during meetings. Any times without events are free for the user. "
|
||||
)
|
||||
|
||||
class Config:
|
||||
"""Configuration for this pydantic object."""
|
||||
|
||||
extra = Extra.forbid
|
||||
|
||||
def _run(
|
||||
self,
|
||||
start_datetime: str,
|
||||
end_datetime: str,
|
||||
max_results: int = 10,
|
||||
truncate: bool = True,
|
||||
run_manager: Optional[CallbackManagerForToolRun] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
TRUNCATE_LIMIT = 150
|
||||
|
||||
# Get calendar object
|
||||
schedule = self.account.schedule()
|
||||
calendar = schedule.get_default_calendar()
|
||||
|
||||
# Process the date range parameters
|
||||
start_datetime_query = dt.strptime(start_datetime, "%Y-%m-%dT%H:%M:%S%z")
|
||||
end_datetime_query = dt.strptime(end_datetime, "%Y-%m-%dT%H:%M:%S%z")
|
||||
|
||||
# Run the query
|
||||
q = calendar.new_query("start").greater_equal(start_datetime_query)
|
||||
q.chain("and").on_attribute("end").less_equal(end_datetime_query)
|
||||
events = calendar.get_events(query=q, include_recurring=True, limit=max_results)
|
||||
|
||||
# Generate output dict
|
||||
output_events = []
|
||||
for event in events:
|
||||
output_event = {}
|
||||
output_event["organizer"] = event.organizer
|
||||
|
||||
output_event["subject"] = event.subject
|
||||
|
||||
if truncate:
|
||||
output_event["body"] = clean_body(event.body)[:TRUNCATE_LIMIT]
|
||||
else:
|
||||
output_event["body"] = clean_body(event.body)
|
||||
|
||||
# Get the time zone from the search parameters
|
||||
time_zone = start_datetime_query.tzinfo
|
||||
# Assign the datetimes in the search time zone
|
||||
output_event["start_datetime"] = event.start.astimezone(time_zone).strftime(
|
||||
"%Y-%m-%dT%H:%M:%S%z"
|
||||
)
|
||||
output_event["end_datetime"] = event.end.astimezone(time_zone).strftime(
|
||||
"%Y-%m-%dT%H:%M:%S%z"
|
||||
)
|
||||
output_event["modified_date"] = event.modified.astimezone(
|
||||
time_zone
|
||||
).strftime("%Y-%m-%dT%H:%M:%S%z")
|
||||
|
||||
output_events.append(output_event)
|
||||
|
||||
return output_events
|
||||
|
||||
async def _arun(
|
||||
self,
|
||||
query: str,
|
||||
max_results: int = 10,
|
||||
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Run the tool."""
|
||||
raise NotImplementedError
|
134
langchain/tools/office365/messages_search.py
Normal file
134
langchain/tools/office365/messages_search.py
Normal file
@ -0,0 +1,134 @@
|
||||
"""Util that Searches email messages in Office 365.
|
||||
|
||||
Free, but setup is required. See link below.
|
||||
https://learn.microsoft.com/en-us/graph/auth/
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List, Optional, Type
|
||||
|
||||
from pydantic import BaseModel, Extra, Field
|
||||
|
||||
from langchain.callbacks.manager import (
|
||||
AsyncCallbackManagerForToolRun,
|
||||
CallbackManagerForToolRun,
|
||||
)
|
||||
from langchain.tools.office365.base import O365BaseTool
|
||||
from langchain.tools.office365.utils import clean_body
|
||||
|
||||
|
||||
class SearchEmailsInput(BaseModel):
|
||||
"""Input for SearchEmails Tool."""
|
||||
|
||||
"""From https://learn.microsoft.com/en-us/graph/search-query-parameter"""
|
||||
|
||||
folder: str = Field(
|
||||
default=None,
|
||||
description=(
|
||||
" If the user wants to search in only one folder, the name of the folder. "
|
||||
'Default folders are "inbox", "drafts", "sent items", "deleted ttems", but '
|
||||
"users can search custom folders as well."
|
||||
),
|
||||
)
|
||||
query: str = Field(
|
||||
description=(
|
||||
"The Microsoift Graph v1.0 $search query. Example filters include "
|
||||
"from:sender, from:sender, to:recipient, subject:subject, "
|
||||
"recipients:list_of_recipients, body:excitement, importance:high, "
|
||||
"received>2022-12-01, received<2021-12-01, sent>2022-12-01, "
|
||||
"sent<2021-12-01, hasAttachments:true attachment:api-catalog.md, "
|
||||
"cc:samanthab@contoso.com, bcc:samanthab@contoso.com, body:excitement date "
|
||||
"range example: received:2023-06-08..2023-06-09 matching example: "
|
||||
"from:amy OR from:david."
|
||||
)
|
||||
)
|
||||
max_results: int = Field(
|
||||
default=10,
|
||||
description="The maximum number of results to return.",
|
||||
)
|
||||
truncate: bool = Field(
|
||||
default=True,
|
||||
description=(
|
||||
"Whether the email body is trucated to meet token number limits. Set to "
|
||||
"False for searches that will retrieve very few results, otherwise, set to "
|
||||
"True"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class O365SearchEmails(O365BaseTool):
|
||||
"""Class for searching email messages in Office 365
|
||||
|
||||
Free, but setup is required
|
||||
"""
|
||||
|
||||
name: str = "messages_search"
|
||||
args_schema: Type[BaseModel] = SearchEmailsInput
|
||||
description: str = (
|
||||
"Use this tool to search for email messages."
|
||||
" The input must be a valid Microsoft Graph v1.0 $search query."
|
||||
" The output is a JSON list of the requested resource."
|
||||
)
|
||||
|
||||
class Config:
|
||||
"""Configuration for this pydantic object."""
|
||||
|
||||
extra = Extra.forbid
|
||||
|
||||
def _run(
|
||||
self,
|
||||
query: str,
|
||||
folder: str = "",
|
||||
max_results: int = 10,
|
||||
truncate: bool = True,
|
||||
run_manager: Optional[CallbackManagerForToolRun] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
# Get mailbox object
|
||||
mailbox = self.account.mailbox()
|
||||
|
||||
# Pull the folder if the user wants to search in a folder
|
||||
if folder != "":
|
||||
mailbox = mailbox.get_folder(folder_name=folder)
|
||||
|
||||
# Retrieve messages based on query
|
||||
query = mailbox.q().search(query)
|
||||
messages = mailbox.get_messages(limit=max_results, query=query)
|
||||
|
||||
# Generate output dict
|
||||
output_messages = []
|
||||
for message in messages:
|
||||
output_message = {}
|
||||
output_message["from"] = message.sender
|
||||
|
||||
if truncate:
|
||||
output_message["body"] = message.body_preview
|
||||
else:
|
||||
output_message["body"] = clean_body(message.body)
|
||||
|
||||
output_message["subject"] = message.subject
|
||||
|
||||
output_message["date"] = message.modified.strftime("%Y-%m-%dT%H:%M:%S%z")
|
||||
|
||||
output_message["to"] = []
|
||||
for recipient in message.to._recipients:
|
||||
output_message["to"].append(str(recipient))
|
||||
|
||||
output_message["cc"] = []
|
||||
for recipient in message.cc._recipients:
|
||||
output_message["cc"].append(str(recipient))
|
||||
|
||||
output_message["bcc"] = []
|
||||
for recipient in message.bcc._recipients:
|
||||
output_message["bcc"].append(str(recipient))
|
||||
|
||||
output_messages.append(output_message)
|
||||
|
||||
return output_messages
|
||||
|
||||
async def _arun(
|
||||
self,
|
||||
query: str,
|
||||
max_results: int = 10,
|
||||
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Run the tool."""
|
||||
raise NotImplementedError
|
96
langchain/tools/office365/send_event.py
Normal file
96
langchain/tools/office365/send_event.py
Normal file
@ -0,0 +1,96 @@
|
||||
"""Util that sends calendar events in Office 365.
|
||||
|
||||
Free, but setup is required. See link below.
|
||||
https://learn.microsoft.com/en-us/graph/auth/
|
||||
"""
|
||||
|
||||
from datetime import datetime as dt
|
||||
from typing import List, Optional, Type
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langchain.callbacks.manager import (
|
||||
AsyncCallbackManagerForToolRun,
|
||||
CallbackManagerForToolRun,
|
||||
)
|
||||
from langchain.tools.office365.base import O365BaseTool
|
||||
|
||||
|
||||
class SendEventSchema(BaseModel):
|
||||
"""Input for CreateEvent Tool."""
|
||||
|
||||
body: str = Field(
|
||||
...,
|
||||
description="The message body to include in the event.",
|
||||
)
|
||||
attendees: List[str] = Field(
|
||||
...,
|
||||
description="The list of attendees for the event.",
|
||||
)
|
||||
subject: str = Field(
|
||||
...,
|
||||
description="The subject of the event.",
|
||||
)
|
||||
start_datetime: str = Field(
|
||||
description=" The start datetime for the event in the following format: "
|
||||
' YYYY-MM-DDTHH:MM:SS±hh:mm, where "T" separates the date and time '
|
||||
" components, and the time zone offset is specified as ±hh:mm. "
|
||||
' For example: "2023-06-09T10:30:00+03:00" represents June 9th, '
|
||||
" 2023, at 10:30 AM in a time zone with a positive offset of 3 "
|
||||
" hours from Coordinated Universal Time (UTC).",
|
||||
)
|
||||
end_datetime: str = Field(
|
||||
description=" The end datetime for the event in the following format: "
|
||||
' YYYY-MM-DDTHH:MM:SS±hh:mm, where "T" separates the date and time '
|
||||
" components, and the time zone offset is specified as ±hh:mm. "
|
||||
' For example: "2023-06-09T10:30:00+03:00" represents June 9th, '
|
||||
" 2023, at 10:30 AM in a time zone with a positive offset of 3 "
|
||||
" hours from Coordinated Universal Time (UTC).",
|
||||
)
|
||||
|
||||
|
||||
class O365SendEvent(O365BaseTool):
|
||||
name: str = "send_event"
|
||||
description: str = (
|
||||
"Use this tool to create and send an event with the provided event fields."
|
||||
)
|
||||
args_schema: Type[SendEventSchema] = SendEventSchema
|
||||
|
||||
def _run(
|
||||
self,
|
||||
body: str,
|
||||
attendees: List[str],
|
||||
subject: str,
|
||||
start_datetime: str,
|
||||
end_datetime: str,
|
||||
run_manager: Optional[CallbackManagerForToolRun] = None,
|
||||
) -> str:
|
||||
# Get calendar object
|
||||
schedule = self.account.schedule()
|
||||
calendar = schedule.get_default_calendar()
|
||||
|
||||
event = calendar.new_event()
|
||||
|
||||
event.body = body
|
||||
event.subject = subject
|
||||
event.start = dt.strptime(start_datetime, "%Y-%m-%dT%H:%M:%S%z")
|
||||
event.end = dt.strptime(end_datetime, "%Y-%m-%dT%H:%M:%S%z")
|
||||
for attendee in attendees:
|
||||
event.attendees.add(attendee)
|
||||
|
||||
# TO-DO: Look into PytzUsageWarning
|
||||
event.save()
|
||||
|
||||
output = "Event sent: " + str(event)
|
||||
return output
|
||||
|
||||
async def _arun(
|
||||
self,
|
||||
message: str,
|
||||
to: List[str],
|
||||
subject: str,
|
||||
cc: Optional[List[str]] = None,
|
||||
bcc: Optional[List[str]] = None,
|
||||
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
|
||||
) -> str:
|
||||
raise NotImplementedError(f"The tool {self.name} does not support async yet.")
|
78
langchain/tools/office365/send_message.py
Normal file
78
langchain/tools/office365/send_message.py
Normal file
@ -0,0 +1,78 @@
|
||||
from typing import List, Optional, Type
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langchain.callbacks.manager import (
|
||||
AsyncCallbackManagerForToolRun,
|
||||
CallbackManagerForToolRun,
|
||||
)
|
||||
from langchain.tools.office365.base import O365BaseTool
|
||||
|
||||
|
||||
class SendMessageSchema(BaseModel):
|
||||
body: str = Field(
|
||||
...,
|
||||
description="The message body to be sent.",
|
||||
)
|
||||
to: List[str] = Field(
|
||||
...,
|
||||
description="The list of recipients.",
|
||||
)
|
||||
subject: str = Field(
|
||||
...,
|
||||
description="The subject of the message.",
|
||||
)
|
||||
cc: Optional[List[str]] = Field(
|
||||
None,
|
||||
description="The list of CC recipients.",
|
||||
)
|
||||
bcc: Optional[List[str]] = Field(
|
||||
None,
|
||||
description="The list of BCC recipients.",
|
||||
)
|
||||
|
||||
|
||||
class O365SendMessage(O365BaseTool):
|
||||
name: str = "send_email"
|
||||
description: str = (
|
||||
"Use this tool to send an email with the provided message fields."
|
||||
)
|
||||
args_schema: Type[SendMessageSchema] = SendMessageSchema
|
||||
|
||||
def _run(
|
||||
self,
|
||||
body: str,
|
||||
to: List[str],
|
||||
subject: str,
|
||||
cc: Optional[List[str]] = None,
|
||||
bcc: Optional[List[str]] = None,
|
||||
run_manager: Optional[CallbackManagerForToolRun] = None,
|
||||
) -> str:
|
||||
# Get mailbox object
|
||||
mailbox = self.account.mailbox()
|
||||
message = mailbox.new_message()
|
||||
|
||||
# Assign message values
|
||||
message.body = body
|
||||
message.subject = subject
|
||||
message.to.add(to)
|
||||
if cc is not None:
|
||||
message.cc.add(cc)
|
||||
if bcc is not None:
|
||||
message.bcc.add(cc)
|
||||
|
||||
message.send()
|
||||
|
||||
output = "Message sent: " + str(message)
|
||||
return output
|
||||
|
||||
async def _arun(
|
||||
self,
|
||||
message: str,
|
||||
to: List[str],
|
||||
subject: str,
|
||||
cc: Optional[List[str]] = None,
|
||||
bcc: Optional[List[str]] = None,
|
||||
run_manager: Optional[AsyncCallbackManagerForToolRun] = None,
|
||||
) -> str:
|
||||
raise NotImplementedError(f"The tool {self.name} does not support async yet.")
|
74
langchain/tools/office365/utils.py
Normal file
74
langchain/tools/office365/utils.py
Normal file
@ -0,0 +1,74 @@
|
||||
"""O365 tool utils."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from O365 import Account
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def clean_body(body: str) -> str:
|
||||
"""Clean body of a message or event."""
|
||||
try:
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
try:
|
||||
# Remove HTML
|
||||
soup = BeautifulSoup(str(body), "html.parser")
|
||||
body = soup.get_text()
|
||||
|
||||
# Remove return characters
|
||||
body = "".join(body.splitlines())
|
||||
|
||||
# Remove extra spaces
|
||||
body = " ".join(body.split())
|
||||
|
||||
return str(body)
|
||||
except Exception:
|
||||
return str(body)
|
||||
except ImportError:
|
||||
return str(body)
|
||||
|
||||
|
||||
def authenticate() -> Account:
|
||||
"""Authenticate using the Microsoft Grah API"""
|
||||
try:
|
||||
from O365 import Account
|
||||
except ImportError as e:
|
||||
raise ImportError(
|
||||
"Cannot import 0365. Please install the package with `pip install O365`."
|
||||
) from e
|
||||
|
||||
if "CLIENT_ID" in os.environ and "CLIENT_SECRET" in os.environ:
|
||||
client_id = os.environ["CLIENT_ID"]
|
||||
client_secret = os.environ["CLIENT_SECRET"]
|
||||
credentials = (client_id, client_secret)
|
||||
else:
|
||||
logger.error(
|
||||
"Error: The CLIENT_ID and CLIENT_SECRET environmental variables have not "
|
||||
"been set. Visit the following link on how to acquire these authorization "
|
||||
"tokens: https://learn.microsoft.com/en-us/graph/auth/"
|
||||
)
|
||||
return None
|
||||
|
||||
account = Account(credentials)
|
||||
|
||||
if account.is_authenticated is False:
|
||||
if not account.authenticate(
|
||||
scopes=[
|
||||
"https://graph.microsoft.com/Mail.ReadWrite",
|
||||
"https://graph.microsoft.com/Mail.Send",
|
||||
"https://graph.microsoft.com/Calendars.ReadWrite",
|
||||
"https://graph.microsoft.com/MailboxSettings.ReadWrite",
|
||||
]
|
||||
):
|
||||
print("Error: Could not authenticate")
|
||||
return None
|
||||
else:
|
||||
return account
|
||||
else:
|
||||
return account
|
Loading…
Reference in New Issue
Block a user