carlyemail-toolkit 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,16 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .env
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ .DS_Store
11
+ infra/venv/
12
+ infra/Pulumi.*.yaml.bak
13
+ .pulumi/
14
+ docs-static/
15
+ node_modules/
16
+ package-lock.json
@@ -0,0 +1,98 @@
1
+ Metadata-Version: 2.5
2
+ Name: carlyemail-toolkit
3
+ Version: 0.1.0
4
+ Summary: CarlyEmail's email tools for the OpenAI Agents SDK, LangChain, LiveKit Agents, and any other framework.
5
+ Project-URL: Homepage, https://carlyemail.com
6
+ Project-URL: Documentation, https://docs.carlyemail.com/integrations/toolkit
7
+ Author: SWH Labs LLC
8
+ License-Expression: MIT
9
+ Keywords: agents,carlyemail,email,langchain,livekit,llm,openai-agents,toolkit
10
+ Requires-Python: >=3.10
11
+ Requires-Dist: carlyemail>=0.4.0
12
+ Provides-Extra: dev
13
+ Requires-Dist: langchain-core<2,>=0.3; extra == 'dev'
14
+ Requires-Dist: livekit-agents>=1.0; extra == 'dev'
15
+ Requires-Dist: openai-agents>=0.1; extra == 'dev'
16
+ Requires-Dist: pytest>=8; extra == 'dev'
17
+ Requires-Dist: ruff>=0.7; extra == 'dev'
18
+ Provides-Extra: langchain
19
+ Requires-Dist: langchain-core<2,>=0.3; extra == 'langchain'
20
+ Provides-Extra: livekit
21
+ Requires-Dist: livekit-agents>=1.0; extra == 'livekit'
22
+ Provides-Extra: openai
23
+ Requires-Dist: openai-agents>=0.1; extra == 'openai'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # carlyemail-toolkit
27
+
28
+ CarlyEmail's email tools for the OpenAI Agents SDK, LangChain, LiveKit Agents,
29
+ and any other agent framework. A real inbox your agent can send, receive and
30
+ reply from, without writing the tools yourself.
31
+
32
+ ```bash
33
+ pip install "carlyemail-toolkit[openai]" # or [langchain], [livekit]
34
+ export CARLYEMAIL_API_KEY=ce_us_...
35
+ ```
36
+
37
+ ```python
38
+ from agents import Agent
39
+ from carlyemail_toolkit.openai import CarlyEmailToolkit
40
+
41
+ agent = Agent(
42
+ name="Inbox",
43
+ instructions="Read the thread before replying. Draft when unsure.",
44
+ tools=CarlyEmailToolkit().get_tools(),
45
+ )
46
+ ```
47
+
48
+ ```python
49
+ from langchain.agents import create_agent
50
+ from carlyemail_toolkit.langchain import CarlyEmailToolkit
51
+
52
+ agent = create_agent("openai:gpt-5-mini", CarlyEmailToolkit().get_tools())
53
+ ```
54
+
55
+ ```python
56
+ from livekit.agents import Agent
57
+ from carlyemail_toolkit.livekit import CarlyEmailToolkit
58
+
59
+ agent = Agent(instructions="...", tools=CarlyEmailToolkit().get_tools())
60
+ ```
61
+
62
+ ```python
63
+ from carlyemail_toolkit import CarlyEmailToolkit # no framework
64
+
65
+ for tool in CarlyEmailToolkit().get_tools():
66
+ tool.name, tool.description, tool.input_schema, tool.read_only
67
+ tool(thread_id="...") # runs it
68
+ ```
69
+
70
+ ## What is in it
71
+
72
+ The 25 tools the hosted [MCP server](https://docs.carlyemail.com/mcp) serves —
73
+ inboxes, threads, messages, drafts, attachments, labels, and account
74
+ verification — with the same names, descriptions and parameter schemas, called
75
+ over the REST API. Pass names to take only some:
76
+
77
+ ```python
78
+ tools = CarlyEmailToolkit().get_tools(["list_messages", "get_thread", "reply_to_message"])
79
+ ```
80
+
81
+ A name that is not a tool raises rather than being dropped.
82
+
83
+ ## Which inbox
84
+
85
+ Every tool takes `inbox_id`, and most work without it. A key scoped to one
86
+ inbox means that inbox. An organization with one inbox means that one. With
87
+ several, the tools that read threads and drafts look across all of them, and
88
+ every other tool refuses and names the inboxes so the agent can ask which —
89
+ the same behaviour as the hosted server.
90
+
91
+ ## Errors
92
+
93
+ A failed call raises the way each framework expects — `RuntimeError` (OpenAI
94
+ Agents SDK), `ToolException` (LangChain, which becomes an error message the
95
+ model reads), `ToolError` (LiveKit Agents) — with the API's message and the
96
+ fix it suggests, in one bounded line.
97
+
98
+ See <https://docs.carlyemail.com/integrations/toolkit>.
@@ -0,0 +1,73 @@
1
+ # carlyemail-toolkit
2
+
3
+ CarlyEmail's email tools for the OpenAI Agents SDK, LangChain, LiveKit Agents,
4
+ and any other agent framework. A real inbox your agent can send, receive and
5
+ reply from, without writing the tools yourself.
6
+
7
+ ```bash
8
+ pip install "carlyemail-toolkit[openai]" # or [langchain], [livekit]
9
+ export CARLYEMAIL_API_KEY=ce_us_...
10
+ ```
11
+
12
+ ```python
13
+ from agents import Agent
14
+ from carlyemail_toolkit.openai import CarlyEmailToolkit
15
+
16
+ agent = Agent(
17
+ name="Inbox",
18
+ instructions="Read the thread before replying. Draft when unsure.",
19
+ tools=CarlyEmailToolkit().get_tools(),
20
+ )
21
+ ```
22
+
23
+ ```python
24
+ from langchain.agents import create_agent
25
+ from carlyemail_toolkit.langchain import CarlyEmailToolkit
26
+
27
+ agent = create_agent("openai:gpt-5-mini", CarlyEmailToolkit().get_tools())
28
+ ```
29
+
30
+ ```python
31
+ from livekit.agents import Agent
32
+ from carlyemail_toolkit.livekit import CarlyEmailToolkit
33
+
34
+ agent = Agent(instructions="...", tools=CarlyEmailToolkit().get_tools())
35
+ ```
36
+
37
+ ```python
38
+ from carlyemail_toolkit import CarlyEmailToolkit # no framework
39
+
40
+ for tool in CarlyEmailToolkit().get_tools():
41
+ tool.name, tool.description, tool.input_schema, tool.read_only
42
+ tool(thread_id="...") # runs it
43
+ ```
44
+
45
+ ## What is in it
46
+
47
+ The 25 tools the hosted [MCP server](https://docs.carlyemail.com/mcp) serves —
48
+ inboxes, threads, messages, drafts, attachments, labels, and account
49
+ verification — with the same names, descriptions and parameter schemas, called
50
+ over the REST API. Pass names to take only some:
51
+
52
+ ```python
53
+ tools = CarlyEmailToolkit().get_tools(["list_messages", "get_thread", "reply_to_message"])
54
+ ```
55
+
56
+ A name that is not a tool raises rather than being dropped.
57
+
58
+ ## Which inbox
59
+
60
+ Every tool takes `inbox_id`, and most work without it. A key scoped to one
61
+ inbox means that inbox. An organization with one inbox means that one. With
62
+ several, the tools that read threads and drafts look across all of them, and
63
+ every other tool refuses and names the inboxes so the agent can ask which —
64
+ the same behaviour as the hosted server.
65
+
66
+ ## Errors
67
+
68
+ A failed call raises the way each framework expects — `RuntimeError` (OpenAI
69
+ Agents SDK), `ToolException` (LangChain, which becomes an error message the
70
+ model reads), `ToolError` (LiveKit Agents) — with the API's message and the
71
+ fix it suggests, in one bounded line.
72
+
73
+ See <https://docs.carlyemail.com/integrations/toolkit>.
@@ -0,0 +1,29 @@
1
+ """CarlyEmail tools for agent frameworks.
2
+
3
+ from carlyemail_toolkit.openai import CarlyEmailToolkit # OpenAI Agents SDK
4
+ from carlyemail_toolkit.langchain import CarlyEmailToolkit # LangChain
5
+ from carlyemail_toolkit.livekit import CarlyEmailToolkit # LiveKit Agents
6
+ from carlyemail_toolkit import CarlyEmailToolkit # anything else
7
+
8
+ tools = CarlyEmailToolkit().get_tools()
9
+
10
+ Each reads `CARLYEMAIL_API_KEY`, or takes `api_key=`, or a `CarlyEmail` client.
11
+ The tools are the ones the hosted MCP server serves, called over the REST API.
12
+ """
13
+
14
+ from carlyemail_toolkit.errors import ToolkitError, error_message
15
+ from carlyemail_toolkit.toolkit import BaseToolkit, BoundTool, CarlyEmailToolkit
16
+ from carlyemail_toolkit.tools import FIRST_INBOX, TOOLS, Tool
17
+
18
+ __all__ = [
19
+ "FIRST_INBOX",
20
+ "TOOLS",
21
+ "BaseToolkit",
22
+ "BoundTool",
23
+ "CarlyEmailToolkit",
24
+ "Tool",
25
+ "ToolkitError",
26
+ "error_message",
27
+ ]
28
+
29
+ __version__ = "0.1.0"
@@ -0,0 +1,51 @@
1
+ """What a failed tool call says to the framework that made it.
2
+
3
+ Every adapter raises the exception its framework expects, and every one of them
4
+ builds the message here, so a refused send reads the same whether the agent is
5
+ on LangChain or LiveKit.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from carlyemail import CarlyEmailError
11
+
12
+ #: A validation error body can list every field it disliked. That is useful to
13
+ #: a person and useless to a model, which reads the first sentence and acts.
14
+ MAX_MESSAGE_LENGTH = 500
15
+
16
+
17
+ class ToolkitError(Exception):
18
+ """A refusal made on this side of the wire, before any request went out.
19
+
20
+ Carries `code` and `fix` the way the API's own errors do, so a caller that
21
+ already handles `CarlyEmailError` has nothing new to learn.
22
+ """
23
+
24
+ def __init__(self, message: str, *, code: str, fix: str) -> None:
25
+ super().__init__(message)
26
+ self.code = code
27
+ self.fix = fix
28
+
29
+
30
+ def error_message(error: BaseException) -> str:
31
+ """One bounded sentence or three: what went wrong, what clears it, where to read more.
32
+
33
+ The API answers errors with `message`, `fix` and `docs`, and the SDK keeps
34
+ all three on the exception. The message alone tells the model it failed;
35
+ the fix is what lets it do something about it on the next turn.
36
+ """
37
+ if isinstance(error, ToolkitError):
38
+ text = f"{error} {error.fix}"
39
+ elif isinstance(error, CarlyEmailError):
40
+ parts = [str(error)]
41
+ if error.fix:
42
+ parts.append(str(error.fix))
43
+ if error.docs:
44
+ parts.append(f"See {error.docs}")
45
+ tag = f"{error.code}, HTTP {error.status}" if error.code else f"HTTP {error.status}"
46
+ text = f"{' '.join(parts)} ({tag})"
47
+ else:
48
+ text = f"{type(error).__name__}: {error}"
49
+ if len(text) > MAX_MESSAGE_LENGTH:
50
+ return text[:MAX_MESSAGE_LENGTH] + "…"
51
+ return text
@@ -0,0 +1,42 @@
1
+ """CarlyEmail tools for LangChain.
2
+
3
+ from langchain.agents import create_agent
4
+ from carlyemail_toolkit.langchain import CarlyEmailToolkit
5
+
6
+ agent = create_agent("openai:gpt-5-mini", CarlyEmailToolkit().get_tools())
7
+
8
+ These are the hosted MCP server's tools as native `BaseTool`s, with no MCP
9
+ client in the loop. For a loader, a retriever and a verified inbound router,
10
+ `langchain-carlyemail` is the package.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ from typing import Any
17
+
18
+ from langchain_core.tools import BaseTool, StructuredTool, ToolException
19
+
20
+ from carlyemail_toolkit.errors import error_message
21
+ from carlyemail_toolkit.toolkit import BaseToolkit
22
+ from carlyemail_toolkit.tools import Tool
23
+
24
+
25
+ class CarlyEmailToolkit(BaseToolkit[BaseTool]):
26
+ def _build(self, tool: Tool) -> BaseTool:
27
+ def run(**arguments: Any) -> str:
28
+ try:
29
+ return json.dumps(tool.run(self.client, arguments))
30
+ except Exception as error:
31
+ # With `handle_tool_error` set, a ToolException becomes an
32
+ # error ToolMessage the model can read and act on, instead of
33
+ # ending the run.
34
+ raise ToolException(error_message(error)) from error
35
+
36
+ return StructuredTool.from_function(
37
+ func=run,
38
+ name=tool.name,
39
+ description=tool.description,
40
+ args_schema=tool.input_schema,
41
+ handle_tool_error=True,
42
+ )
@@ -0,0 +1,39 @@
1
+ """CarlyEmail tools for LiveKit Agents.
2
+
3
+ from livekit.agents import Agent
4
+ from carlyemail_toolkit.livekit import CarlyEmailToolkit
5
+
6
+ agent = Agent(instructions="...", tools=CarlyEmailToolkit().get_tools())
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import json
13
+
14
+ from livekit.agents.llm import RawFunctionTool, ToolError, function_tool
15
+
16
+ from carlyemail_toolkit.errors import error_message
17
+ from carlyemail_toolkit.toolkit import BaseToolkit
18
+ from carlyemail_toolkit.tools import Tool
19
+
20
+
21
+ class CarlyEmailToolkit(BaseToolkit[RawFunctionTool]):
22
+ def _build(self, tool: Tool) -> RawFunctionTool:
23
+ async def run(raw_arguments: dict[str, object]) -> str:
24
+ try:
25
+ result = await asyncio.to_thread(tool.run, self.client, raw_arguments)
26
+ except Exception as error:
27
+ # A ToolError is what the agent reads back to the person; any
28
+ # other exception is treated as the tool having broken.
29
+ raise ToolError(error_message(error)) from error
30
+ return json.dumps(result)
31
+
32
+ return function_tool(
33
+ run,
34
+ raw_schema={
35
+ "name": tool.name,
36
+ "description": tool.description,
37
+ "parameters": tool.input_schema,
38
+ },
39
+ )
@@ -0,0 +1,45 @@
1
+ """CarlyEmail tools for the OpenAI Agents SDK.
2
+
3
+ from agents import Agent
4
+ from carlyemail_toolkit.openai import CarlyEmailToolkit
5
+
6
+ agent = Agent(name="Inbox", instructions="...", tools=CarlyEmailToolkit().get_tools())
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import asyncio
12
+ import json
13
+ from typing import Any
14
+
15
+ from agents import FunctionTool
16
+
17
+ from carlyemail_toolkit.errors import error_message
18
+ from carlyemail_toolkit.toolkit import BaseToolkit
19
+ from carlyemail_toolkit.tools import Tool
20
+
21
+
22
+ class CarlyEmailToolkit(BaseToolkit[FunctionTool]):
23
+ def _build(self, tool: Tool) -> FunctionTool:
24
+ async def on_invoke_tool(context: Any, input_str: str) -> str:
25
+ arguments = json.loads(input_str) if input_str else {}
26
+ try:
27
+ result = await asyncio.to_thread(tool.run, self.client, arguments)
28
+ except Exception as error:
29
+ # `failure_error_function` belongs to the decorator, not to a
30
+ # FunctionTool built by hand. Raising is what makes the SDK
31
+ # record a failed call rather than a successful one whose
32
+ # output happens to describe a failure.
33
+ raise RuntimeError(error_message(error)) from error
34
+ return json.dumps(result)
35
+
36
+ return FunctionTool(
37
+ name=tool.name,
38
+ description=tool.description,
39
+ params_json_schema=tool.input_schema,
40
+ on_invoke_tool=on_invoke_tool,
41
+ # Strict mode makes every property required. The server's schemas
42
+ # mark what is actually required, and an optional `cc` the model
43
+ # must fill in on every call is a worse tool, not a safer one.
44
+ strict_json_schema=False,
45
+ )
File without changes
@@ -0,0 +1,111 @@
1
+ """The toolkit shape every adapter shares, and the framework-free one."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from typing import Any, Generic, TypeVar
7
+
8
+ from carlyemail import CarlyEmail
9
+
10
+ from carlyemail_toolkit.tools import TOOLS, Tool
11
+
12
+ T = TypeVar("T")
13
+
14
+
15
+ class BaseToolkit(Generic[T]): # noqa: UP046 — the package runs on 3.10
16
+ """Builds one framework-native tool per entry in `TOOLS`.
17
+
18
+ Subclasses say how in `_build`; everything else — the client, the filter
19
+ by name — is the same on every framework.
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ client: CarlyEmail | None = None,
25
+ *,
26
+ api_key: str | None = None,
27
+ base_url: str | None = None,
28
+ ) -> None:
29
+ self.client = client or CarlyEmail(api_key=api_key, base_url=base_url)
30
+ self._built: dict[str, T] = {tool.name: self._build(tool) for tool in TOOLS}
31
+
32
+ def _build(self, tool: Tool) -> T:
33
+ raise NotImplementedError
34
+
35
+ def get_tools(self, names: Iterable[str] | None = None) -> list[T]:
36
+ """All tools, or the named ones in the order named.
37
+
38
+ A name that is not a tool is an error, not an omission. Silently
39
+ dropping it is how an agent ships with `send_message` misspelled and
40
+ nobody finds out until it cannot send.
41
+ """
42
+ if names is None:
43
+ return list(self._built.values())
44
+ wanted = list(names)
45
+ unknown = [name for name in wanted if name not in self._built]
46
+ if unknown:
47
+ raise ValueError(
48
+ f"Unknown tool(s): {', '.join(unknown)}. " f"Available: {', '.join(self._built)}."
49
+ )
50
+ return [self._built[name] for name in wanted]
51
+
52
+ def names(self) -> list[str]:
53
+ return list(self._built)
54
+
55
+
56
+ class BoundTool:
57
+ """A tool tied to one client. Call it with keyword arguments.
58
+
59
+ For frameworks the toolkit has no adapter for: everything a tool
60
+ definition needs is on it — `name`, `description`, `input_schema`, the
61
+ annotations — and calling it runs the tool.
62
+ """
63
+
64
+ def __init__(self, tool: Tool, client: CarlyEmail) -> None:
65
+ self.tool = tool
66
+ self.client = client
67
+
68
+ @property
69
+ def name(self) -> str:
70
+ return self.tool.name
71
+
72
+ @property
73
+ def title(self) -> str:
74
+ return self.tool.title
75
+
76
+ @property
77
+ def description(self) -> str:
78
+ return self.tool.description
79
+
80
+ @property
81
+ def input_schema(self) -> dict[str, Any]:
82
+ return self.tool.input_schema
83
+
84
+ @property
85
+ def read_only(self) -> bool:
86
+ return self.tool.read_only
87
+
88
+ @property
89
+ def destructive(self) -> bool:
90
+ return self.tool.destructive
91
+
92
+ @property
93
+ def idempotent(self) -> bool:
94
+ return self.tool.idempotent
95
+
96
+ @property
97
+ def open_world(self) -> bool:
98
+ return self.tool.open_world
99
+
100
+ def __call__(self, **arguments: Any) -> Any:
101
+ return self.tool.run(self.client, arguments)
102
+
103
+ def __repr__(self) -> str:
104
+ return f"<BoundTool {self.name}>"
105
+
106
+
107
+ class CarlyEmailToolkit(BaseToolkit[BoundTool]):
108
+ """The tools with no framework around them."""
109
+
110
+ def _build(self, tool: Tool) -> BoundTool:
111
+ return BoundTool(tool, self.client)