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.
- carlyemail_toolkit-0.1.0/.gitignore +16 -0
- carlyemail_toolkit-0.1.0/PKG-INFO +98 -0
- carlyemail_toolkit-0.1.0/README.md +73 -0
- carlyemail_toolkit-0.1.0/carlyemail_toolkit/__init__.py +29 -0
- carlyemail_toolkit-0.1.0/carlyemail_toolkit/errors.py +51 -0
- carlyemail_toolkit-0.1.0/carlyemail_toolkit/langchain.py +42 -0
- carlyemail_toolkit-0.1.0/carlyemail_toolkit/livekit.py +39 -0
- carlyemail_toolkit-0.1.0/carlyemail_toolkit/openai.py +45 -0
- carlyemail_toolkit-0.1.0/carlyemail_toolkit/py.typed +0 -0
- carlyemail_toolkit-0.1.0/carlyemail_toolkit/toolkit.py +111 -0
- carlyemail_toolkit-0.1.0/carlyemail_toolkit/tools.json +1258 -0
- carlyemail_toolkit-0.1.0/carlyemail_toolkit/tools.py +427 -0
- carlyemail_toolkit-0.1.0/pyproject.toml +29 -0
|
@@ -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)
|