initial commit
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
"""O365 tools."""
|
||||
|
||||
from langchain_community.tools.office365.create_draft_message import (
|
||||
O365CreateDraftMessage,
|
||||
)
|
||||
from langchain_community.tools.office365.events_search import O365SearchEvents
|
||||
from langchain_community.tools.office365.messages_search import O365SearchEmails
|
||||
from langchain_community.tools.office365.send_event import O365SendEvent
|
||||
from langchain_community.tools.office365.send_message import O365SendMessage
|
||||
from langchain_community.tools.office365.utils import authenticate
|
||||
|
||||
__all__ = [
|
||||
"O365SearchEmails",
|
||||
"O365SearchEvents",
|
||||
"O365CreateDraftMessage",
|
||||
"O365SendMessage",
|
||||
"O365SendEvent",
|
||||
"authenticate",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,20 @@
|
||||
"""Base class for Office 365 tools."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from langchain_core.tools import BaseTool
|
||||
from pydantic import Field
|
||||
|
||||
from langchain_community.tools.office365.utils import authenticate
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from O365 import Account
|
||||
|
||||
|
||||
class O365BaseTool(BaseTool):
|
||||
"""Base class for the Office 365 tools."""
|
||||
|
||||
account: Account = Field(default_factory=authenticate)
|
||||
"""The account object for the Office 365 account."""
|
||||
@@ -0,0 +1,68 @@
|
||||
from typing import List, Optional, Type
|
||||
|
||||
from langchain_core.callbacks import CallbackManagerForToolRun
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langchain_community.tools.office365.base import O365BaseTool
|
||||
|
||||
|
||||
class CreateDraftMessageSchema(BaseModel):
|
||||
"""Input for SendMessageTool."""
|
||||
|
||||
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):
|
||||
"""Tool for creating a draft email in Office 365."""
|
||||
|
||||
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(bcc)
|
||||
|
||||
message.save_draft()
|
||||
|
||||
output = "Draft created: " + str(message)
|
||||
return output
|
||||
@@ -0,0 +1,127 @@
|
||||
"""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 langchain_core.callbacks import CallbackManagerForToolRun
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from langchain_community.tools.office365.base import O365BaseTool
|
||||
from langchain_community.tools.office365.utils import UTC_FORMAT, 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 truncated to meet token number limits. Set to "
|
||||
"False for searches that will retrieve small events, otherwise, set to "
|
||||
"True."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class O365SearchEvents(O365BaseTool):
|
||||
"""Search 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. "
|
||||
)
|
||||
|
||||
model_config = ConfigDict(
|
||||
extra="forbid",
|
||||
)
|
||||
|
||||
def _run(
|
||||
self,
|
||||
start_datetime: str,
|
||||
end_datetime: str,
|
||||
max_results: int = 10,
|
||||
truncate: bool = True,
|
||||
run_manager: Optional[CallbackManagerForToolRun] = None,
|
||||
truncate_limit: int = 150,
|
||||
) -> List[Dict[str, Any]]:
|
||||
# Get calendar object
|
||||
schedule = self.account.schedule()
|
||||
calendar = schedule.get_default_calendar()
|
||||
|
||||
# Process the date range parameters
|
||||
start_datetime_query = dt.strptime(start_datetime, UTC_FORMAT)
|
||||
end_datetime_query = dt.strptime(end_datetime, UTC_FORMAT)
|
||||
|
||||
# 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(
|
||||
UTC_FORMAT
|
||||
)
|
||||
output_event["end_datetime"] = event.end.astimezone(time_zone).strftime(
|
||||
UTC_FORMAT
|
||||
)
|
||||
output_event["modified_date"] = event.modified.astimezone(
|
||||
time_zone
|
||||
).strftime(UTC_FORMAT)
|
||||
|
||||
output_events.append(output_event)
|
||||
|
||||
return output_events
|
||||
@@ -0,0 +1,122 @@
|
||||
"""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 langchain_core.callbacks import CallbackManagerForToolRun
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from langchain_community.tools.office365.base import O365BaseTool
|
||||
from langchain_community.tools.office365.utils import UTC_FORMAT, clean_body
|
||||
|
||||
|
||||
class SearchEmailsInput(BaseModel):
|
||||
"""Input for SearchEmails Tool."""
|
||||
|
||||
"""From https://learn.microsoft.com/en-us/graph/search-query-parameter"""
|
||||
|
||||
folder: str = Field(
|
||||
default="",
|
||||
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 truncated to meet token number limits. Set to "
|
||||
"False for searches that will retrieve small messages, otherwise, set to "
|
||||
"True"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class O365SearchEmails(O365BaseTool):
|
||||
"""Search 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."
|
||||
)
|
||||
|
||||
model_config = ConfigDict(
|
||||
extra="forbid",
|
||||
)
|
||||
|
||||
def _run(
|
||||
self,
|
||||
query: str,
|
||||
folder: str = "",
|
||||
max_results: int = 10,
|
||||
truncate: bool = True,
|
||||
run_manager: Optional[CallbackManagerForToolRun] = None,
|
||||
truncate_limit: int = 150,
|
||||
) -> 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[:truncate_limit]
|
||||
else:
|
||||
output_message["body"] = clean_body(message.body)
|
||||
|
||||
output_message["subject"] = message.subject
|
||||
|
||||
output_message["date"] = message.modified.strftime(UTC_FORMAT)
|
||||
|
||||
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
|
||||
@@ -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 zoneinfo import ZoneInfo
|
||||
|
||||
from langchain_core.callbacks import CallbackManagerForToolRun
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langchain_community.tools.office365.base import O365BaseTool
|
||||
from langchain_community.tools.office365.utils import UTC_FORMAT
|
||||
|
||||
|
||||
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):
|
||||
"""Tool for sending calendar events in Office 365."""
|
||||
|
||||
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
|
||||
try:
|
||||
event.start = dt.fromisoformat(start_datetime).replace(
|
||||
tzinfo=ZoneInfo("UTC")
|
||||
)
|
||||
except ValueError:
|
||||
# fallback for backwards compatibility
|
||||
event.start = dt.strptime(start_datetime, UTC_FORMAT)
|
||||
try:
|
||||
event.end = dt.fromisoformat(end_datetime).replace(tzinfo=ZoneInfo("UTC"))
|
||||
except ValueError:
|
||||
# fallback for backwards compatibility
|
||||
event.end = dt.strptime(end_datetime, UTC_FORMAT)
|
||||
|
||||
for attendee in attendees:
|
||||
event.attendees.add(attendee)
|
||||
|
||||
event.save()
|
||||
|
||||
output = "Event sent: " + str(event)
|
||||
return output
|
||||
@@ -0,0 +1,68 @@
|
||||
from typing import List, Optional, Type
|
||||
|
||||
from langchain_core.callbacks import CallbackManagerForToolRun
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from langchain_community.tools.office365.base import O365BaseTool
|
||||
|
||||
|
||||
class SendMessageSchema(BaseModel):
|
||||
"""Input for SendMessageTool."""
|
||||
|
||||
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):
|
||||
"""Send an email in Office 365."""
|
||||
|
||||
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(bcc)
|
||||
|
||||
message.send()
|
||||
|
||||
output = "Message sent: " + str(message)
|
||||
return output
|
||||
@@ -0,0 +1,79 @@
|
||||
"""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 Graph 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") # noqa: T201
|
||||
return None
|
||||
else:
|
||||
return account
|
||||
else:
|
||||
return account
|
||||
|
||||
|
||||
UTC_FORMAT = "%Y-%m-%dT%H:%M:%S%z"
|
||||
"""UTC format for datetime objects."""
|
||||
Reference in New Issue
Block a user