### Overview Major restructuring from monolithic `arcade-ai` package to modular library architecture with standardized uv-based dependency management.  ### New Package Structure - **`arcade-tdk`** - Lightweight toolkit development kit (core decorators, auth) - **`arcade-core`** - Core execution engine and catalog functionality - **`arcade-serve`** - FastAPI/MCP server components - **`arcade-ai`** - Meta package that includes CLI functionality. Optionally include evals via the `evals` extra. Optionally include all packages via the `all` extra. ### Key Benefits - **Lighter Dependencies**: Toolkits now depend only on `arcade-tdk` (~2 deps) vs full `arcade-ai` (~30+ deps) - **Faster Builds**: uv provides 10-100x faster dependency resolution and installation - **Better Modularity**: Clear separation of concerns, consumers import only what they need - **Standard Tooling**: Eliminates custom poetry scripts, uses standard Python packaging ### Migration Impact - All 20 toolkits converted from poetry → uv with `arcade-tdk` dependencies plus `arcade-ai[evals]` and `arcade-serve` dev dependencies. When developing locally, devs should install toolkits via `make install-local`. - Modern Python 3.10+ type hints throughout - Standardized build system with hatchling backend - Enhanced Makefile with robust toolkit management commands - Removed `arcade dev` CLI command - Reduce the number of files created by `arcade new` and add an option to not generate a tests and evals folder. This foundation enables faster development cycles and cleaner dependency chains for the growing toolkit ecosystem. ### Todo After this PR is merged - [ ] Post-merge workflow(s) (release & publish containers, etc) - [ ] Release order plan. @EricGustin suggests releasing in the following order: 1. `arcade-core` version 0.1.0 2. `arcade-serve` version 0.1.0 and `arcade-tdk` version 0.1.0 3. `arcade-ai` version 2.0.0 4. Patch release for all toolkits (all changes in toolkits are internal refactors) - [ ] [Update docs](https://github.com/ArcadeAI/docs/pull/318) --------- Co-authored-by: Eric Gustin <eric@arcade.dev> Co-authored-by: Eric Gustin <34000337+EricGustin@users.noreply.github.com>
122 lines
4.8 KiB
Python
122 lines
4.8 KiB
Python
from typing import Annotated
|
|
|
|
from arcade_tdk import ToolContext, tool
|
|
from arcade_tdk.auth import Microsoft
|
|
from arcade_tdk.errors import ToolExecutionError
|
|
|
|
from arcade_microsoft.client import get_client
|
|
from arcade_microsoft.outlook_mail._utils import (
|
|
fetch_emails,
|
|
prepare_list_emails_request_config,
|
|
remove_none_values,
|
|
)
|
|
from arcade_microsoft.outlook_mail.enums import (
|
|
EmailFilterProperty,
|
|
FilterOperator,
|
|
WellKnownFolderNames,
|
|
)
|
|
from arcade_microsoft.outlook_mail.message import Message
|
|
|
|
|
|
@tool(requires_auth=Microsoft(scopes=["Mail.Read"]))
|
|
async def list_emails(
|
|
context: ToolContext,
|
|
limit: Annotated[int, "The number of messages to return. Max is 100. Defaults to 5."] = 5,
|
|
pagination_token: Annotated[
|
|
str | None, "The pagination token to continue a previous request"
|
|
] = None,
|
|
) -> Annotated[dict, "A dictionary containing a list of emails"]:
|
|
"""List emails in the user's mailbox across all folders.
|
|
|
|
Since this tool lists email across all folders, it may return sent items, drafts,
|
|
and other items that are not in the inbox.
|
|
"""
|
|
client = get_client(context.get_auth_token_or_empty())
|
|
request_config = prepare_list_emails_request_config(limit)
|
|
message_builder = client.me.messages
|
|
|
|
response = await fetch_emails(message_builder, pagination_token, request_config)
|
|
messages = [Message.from_sdk(msg).to_dict() for msg in response.value or []]
|
|
pagination_token = response.odata_next_link
|
|
|
|
result = {
|
|
"messages": messages,
|
|
"num_messages": len(messages),
|
|
"pagination_token": pagination_token,
|
|
}
|
|
result = remove_none_values(result)
|
|
return result
|
|
|
|
|
|
@tool(requires_auth=Microsoft(scopes=["Mail.Read"]))
|
|
async def list_emails_in_folder(
|
|
context: ToolContext,
|
|
well_known_folder_name: Annotated[
|
|
WellKnownFolderNames | None,
|
|
"The name of the folder to list emails from. Defaults to None.",
|
|
] = None,
|
|
folder_id: Annotated[
|
|
str | None,
|
|
"The ID of the folder to list emails from if the folder is not a well-known folder. "
|
|
"Defaults to None.",
|
|
] = None,
|
|
limit: Annotated[int, "The number of messages to return. Max is 100. Defaults to 5."] = 5,
|
|
pagination_token: Annotated[
|
|
str | None, "The pagination token to continue a previous request"
|
|
] = None,
|
|
) -> Annotated[
|
|
dict, "A dictionary containing a list of emails and a pagination token, if applicable"
|
|
]:
|
|
"""List the user's emails in the specified folder.
|
|
|
|
Exactly one of `well_known_folder_name` or `folder_id` MUST be provided.
|
|
"""
|
|
if not (bool(well_known_folder_name) ^ bool(folder_id)):
|
|
raise ToolExecutionError(
|
|
message="Exactly one of `well_known_folder_name` or `folder_id` must be provided."
|
|
)
|
|
folder_name = well_known_folder_name.value if well_known_folder_name else folder_id
|
|
client = get_client(context.get_auth_token_or_empty())
|
|
request_config = prepare_list_emails_request_config(limit)
|
|
message_builder = client.me.mail_folders.by_mail_folder_id(folder_name).messages # type: ignore[arg-type]
|
|
|
|
response = await fetch_emails(message_builder, pagination_token, request_config)
|
|
messages = [Message.from_sdk(msg).to_dict() for msg in response.value or []]
|
|
pagination_token = response.odata_next_link
|
|
|
|
result = {
|
|
"messages": messages,
|
|
"num_messages": len(messages),
|
|
"pagination_token": pagination_token,
|
|
}
|
|
result = remove_none_values(result)
|
|
return result
|
|
|
|
|
|
@tool(requires_auth=Microsoft(scopes=["Mail.Read"]))
|
|
async def list_emails_by_property(
|
|
context: ToolContext,
|
|
property: Annotated[EmailFilterProperty, "The property to filter the emails by."], # noqa: A002
|
|
operator: Annotated[FilterOperator, "The operator to use for the filter."],
|
|
value: Annotated[str, "The value to filter the emails by"],
|
|
limit: Annotated[int, "The number of messages to return. Max is 100. Defaults to 5."] = 5,
|
|
pagination_token: Annotated[
|
|
str | None, "The pagination token to continue a previous request"
|
|
] = None,
|
|
) -> Annotated[dict, "A dictionary containing a list of emails"]:
|
|
"""List emails in the user's mailbox across all folders filtering by a property."""
|
|
client = get_client(context.get_auth_token_or_empty())
|
|
request_config = prepare_list_emails_request_config(limit, property, operator, value)
|
|
message_builder = client.me.messages
|
|
|
|
response = await fetch_emails(message_builder, pagination_token, request_config)
|
|
messages = [Message.from_sdk(msg).to_dict() for msg in response.value or []]
|
|
pagination_token = response.odata_next_link
|
|
|
|
result = {
|
|
"messages": messages,
|
|
"num_messages": len(messages),
|
|
"pagination_token": pagination_token,
|
|
}
|
|
result = remove_none_values(result)
|
|
return result
|