arcade-mcp/toolkits/microsoft/arcade_microsoft/outlook_mail/tools/read.py
Sam Partee b6b4cd0a4c
🏗️ Restructure: Multi-Package Architecture + uv Migration (#412)
### Overview
Major restructuring from monolithic `arcade-ai` package to modular
library architecture with standardized uv-based dependency management.

![arcade-ai Monorepo
(2)](https://github.com/user-attachments/assets/25f102b0-bb87-4a04-9701-d227d05664b1)

### 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>
2025-06-11 16:48:17 -07:00

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