dispatch_agents 0.9.0__py3-none-any.whl
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.
- agentservice/__init__.py +0 -0
- agentservice/py.typed +0 -0
- agentservice/v1/__init__.py +0 -0
- agentservice/v1/message_pb2.py +41 -0
- agentservice/v1/message_pb2.pyi +22 -0
- agentservice/v1/message_pb2_grpc.py +4 -0
- agentservice/v1/request_response_pb2.py +46 -0
- agentservice/v1/request_response_pb2.pyi +54 -0
- agentservice/v1/request_response_pb2_grpc.py +4 -0
- agentservice/v1/service_pb2.py +43 -0
- agentservice/v1/service_pb2.pyi +6 -0
- agentservice/v1/service_pb2_grpc.py +129 -0
- dispatch_agents/__init__.py +281 -0
- dispatch_agents/agent_service.py +135 -0
- dispatch_agents/config.py +490 -0
- dispatch_agents/contrib/__init__.py +1 -0
- dispatch_agents/contrib/claude/__init__.py +246 -0
- dispatch_agents/contrib/openai/__init__.py +167 -0
- dispatch_agents/events.py +986 -0
- dispatch_agents/grpc_server.py +565 -0
- dispatch_agents/instrument.py +217 -0
- dispatch_agents/integrations/__init__.py +1 -0
- dispatch_agents/integrations/github/README.md +9 -0
- dispatch_agents/integrations/github/__init__.py +4268 -0
- dispatch_agents/invocation.py +25 -0
- dispatch_agents/llm.py +1017 -0
- dispatch_agents/llm_langchain.py +394 -0
- dispatch_agents/logging_config.py +133 -0
- dispatch_agents/mcp.py +266 -0
- dispatch_agents/memory.py +264 -0
- dispatch_agents/models.py +748 -0
- dispatch_agents/proxy/__init__.py +6 -0
- dispatch_agents/proxy/server.py +1137 -0
- dispatch_agents/proxy/sse_utils.py +76 -0
- dispatch_agents/py.typed +0 -0
- dispatch_agents/resources.py +68 -0
- dispatch_agents/version.py +19 -0
- dispatch_agents-0.9.0.dist-info/METADATA +20 -0
- dispatch_agents-0.9.0.dist-info/RECORD +43 -0
- dispatch_agents-0.9.0.dist-info/WHEEL +4 -0
- dispatch_agents-0.9.0.dist-info/licenses/LICENSE +191 -0
- dispatch_agents-0.9.0.dist-info/licenses/LICENSE-3rdparty.csv +12 -0
- dispatch_agents-0.9.0.dist-info/licenses/NOTICE +5 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Claude Agent SDK integration for Dispatch Agents.
|
|
2
|
+
|
|
3
|
+
This module provides helpers for configuring MCP servers with the Claude Agent SDK.
|
|
4
|
+
|
|
5
|
+
Usage Example::
|
|
6
|
+
|
|
7
|
+
from dispatch_agents.contrib.claude import get_mcp_servers
|
|
8
|
+
|
|
9
|
+
from claude_agent_sdk import ClaudeAgentOptions, ResultMessage, query
|
|
10
|
+
import dispatch_agents
|
|
11
|
+
|
|
12
|
+
my_options: ClaudeAgentOptions # Module-level, initialized by @init
|
|
13
|
+
|
|
14
|
+
@dispatch_agents.init
|
|
15
|
+
async def setup():
|
|
16
|
+
global my_options
|
|
17
|
+
mcp_servers = await get_mcp_servers()
|
|
18
|
+
my_options = ClaudeAgentOptions(
|
|
19
|
+
mcp_servers=mcp_servers,
|
|
20
|
+
allowed_tools=["mcp__datadog__*"],
|
|
21
|
+
permission_mode="bypassPermissions",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
@dispatch_agents.on(topic="query")
|
|
25
|
+
async def handle_query(payload: QueryRequest) -> QueryResponse:
|
|
26
|
+
async for message in query(prompt=payload.prompt, options=my_options):
|
|
27
|
+
if isinstance(message, ResultMessage) and message.subtype == "success":
|
|
28
|
+
return QueryResponse(result=message.result)
|
|
29
|
+
|
|
30
|
+
Type Compatibility:
|
|
31
|
+
The :func:`get_mcp_servers` function returns a ``dict[str, McpSdkServerConfig]``
|
|
32
|
+
which is directly compatible with ``ClaudeAgentOptions.mcp_servers``.
|
|
33
|
+
|
|
34
|
+
Trace Context:
|
|
35
|
+
Trace context (trace_id, parent_id) is automatically injected into each MCP
|
|
36
|
+
tool call for distributed tracing. This enables correlation of tool calls
|
|
37
|
+
with the parent agent invocation in the Dispatch dashboard.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
import logging
|
|
41
|
+
from datetime import timedelta
|
|
42
|
+
from typing import Any
|
|
43
|
+
|
|
44
|
+
from claude_agent_sdk import McpSdkServerConfig, SdkMcpTool, create_sdk_mcp_server
|
|
45
|
+
from mcp import ClientSession
|
|
46
|
+
from mcp.client.streamable_http import streamablehttp_client
|
|
47
|
+
from mcp.types import Tool
|
|
48
|
+
|
|
49
|
+
from dispatch_agents.mcp import _load_mcp_config, get_mcp_client
|
|
50
|
+
|
|
51
|
+
# Default timeout for MCP tool calls (5 minutes)
|
|
52
|
+
DEFAULT_READ_TIMEOUT_SECONDS = 300.0
|
|
53
|
+
|
|
54
|
+
logger = logging.getLogger(__name__)
|
|
55
|
+
|
|
56
|
+
# Singleton storage for MCP server configs
|
|
57
|
+
_mcp_servers: dict[str, McpSdkServerConfig] | None = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def _get_server_info_and_tools(
|
|
61
|
+
server_name: str,
|
|
62
|
+
url: str,
|
|
63
|
+
headers: dict[str, str],
|
|
64
|
+
) -> tuple[str | None, list[Tool]]:
|
|
65
|
+
"""Connect to MCP server and retrieve server info and tools.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
server_name: Name of the server (for logging).
|
|
69
|
+
url: The MCP server endpoint URL.
|
|
70
|
+
headers: HTTP headers for authentication.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Tuple of (server_version, list of tools). Version may be None if not provided.
|
|
74
|
+
"""
|
|
75
|
+
server_version: str | None = None
|
|
76
|
+
tools: list[Tool] = []
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
async with streamablehttp_client(url=url, headers=headers) as (
|
|
80
|
+
read_stream,
|
|
81
|
+
write_stream,
|
|
82
|
+
_,
|
|
83
|
+
):
|
|
84
|
+
async with ClientSession(
|
|
85
|
+
read_stream,
|
|
86
|
+
write_stream,
|
|
87
|
+
read_timeout_seconds=timedelta(seconds=DEFAULT_READ_TIMEOUT_SECONDS),
|
|
88
|
+
) as session:
|
|
89
|
+
# Initialize and get server info
|
|
90
|
+
init_result = await session.initialize()
|
|
91
|
+
if init_result.serverInfo and init_result.serverInfo.version:
|
|
92
|
+
server_version = init_result.serverInfo.version
|
|
93
|
+
|
|
94
|
+
# Fetch tools list
|
|
95
|
+
tools_result = await session.list_tools()
|
|
96
|
+
tools = list(tools_result.tools)
|
|
97
|
+
|
|
98
|
+
except Exception as e:
|
|
99
|
+
logger.warning(
|
|
100
|
+
f"Failed to connect to MCP server '{server_name}' at {url}: {e}. "
|
|
101
|
+
"The server may not be available yet."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
return server_version, tools
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _create_proxy_tool(
|
|
108
|
+
server_name: str,
|
|
109
|
+
tool: Tool,
|
|
110
|
+
) -> SdkMcpTool[Any]:
|
|
111
|
+
"""Create a proxy tool that forwards calls to an upstream MCP server.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
server_name: The MCP server name (as configured in .mcp.json).
|
|
115
|
+
tool: Tool definition from the upstream server.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
An SdkMcpTool that proxies to the upstream server with trace context.
|
|
119
|
+
"""
|
|
120
|
+
tool_name = tool.name
|
|
121
|
+
description = tool.description or ""
|
|
122
|
+
input_schema = tool.inputSchema if tool.inputSchema else {"type": "object"}
|
|
123
|
+
|
|
124
|
+
async def proxy_handler(args: dict[str, Any]) -> dict[str, Any]:
|
|
125
|
+
"""Proxy handler that forwards to upstream with trace context."""
|
|
126
|
+
try:
|
|
127
|
+
async with get_mcp_client(server_name) as client:
|
|
128
|
+
result = await client.call_tool(tool_name, args)
|
|
129
|
+
|
|
130
|
+
# Convert MCP CallToolResult to Claude SDK format
|
|
131
|
+
content = [
|
|
132
|
+
{"type": c.type, "text": getattr(c, "text", str(c))}
|
|
133
|
+
for c in result.content
|
|
134
|
+
]
|
|
135
|
+
return {"content": content, "is_error": result.isError or False}
|
|
136
|
+
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.error(f"Error calling tool '{tool_name}' on '{server_name}': {e}")
|
|
139
|
+
return {
|
|
140
|
+
"content": [{"type": "text", "text": f"Error: {e}"}],
|
|
141
|
+
"is_error": True,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return SdkMcpTool(
|
|
145
|
+
name=tool_name,
|
|
146
|
+
description=description,
|
|
147
|
+
input_schema=input_schema,
|
|
148
|
+
handler=proxy_handler,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
async def _create_proxy_server(
|
|
153
|
+
server_name: str,
|
|
154
|
+
url: str,
|
|
155
|
+
headers: dict[str, str],
|
|
156
|
+
) -> McpSdkServerConfig:
|
|
157
|
+
"""Create an SDK server that proxies to an HTTP MCP server with trace context.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
server_name: Name for the proxy server.
|
|
161
|
+
url: The upstream MCP server URL.
|
|
162
|
+
headers: HTTP headers for the upstream server.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
McpSdkServerConfig for the proxy server.
|
|
166
|
+
"""
|
|
167
|
+
# Get server info and tools using MCP SDK
|
|
168
|
+
server_version, tools = await _get_server_info_and_tools(server_name, url, headers)
|
|
169
|
+
|
|
170
|
+
# Create proxy tools
|
|
171
|
+
proxy_tools = [_create_proxy_tool(server_name, tool) for tool in tools]
|
|
172
|
+
|
|
173
|
+
# Create SDK server with proxy tools
|
|
174
|
+
return create_sdk_mcp_server(
|
|
175
|
+
name=server_name,
|
|
176
|
+
version=server_version,
|
|
177
|
+
tools=proxy_tools,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
async def get_mcp_servers() -> dict[str, McpSdkServerConfig]:
|
|
182
|
+
"""Get MCP servers for Claude Agent SDK.
|
|
183
|
+
|
|
184
|
+
Returns a singleton dict of SDK server configurations. On first call, loads
|
|
185
|
+
configuration from ``.mcp.json`` and creates server configurations.
|
|
186
|
+
Subsequent calls return the cached servers.
|
|
187
|
+
|
|
188
|
+
Trace context (trace_id, parent_id) is automatically injected into each MCP
|
|
189
|
+
tool call for distributed tracing.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
A dict mapping server names to their SDK server configuration.
|
|
193
|
+
This can be passed directly to ``ClaudeAgentOptions(mcp_servers=...)``.
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
FileNotFoundError: If ``.mcp.json`` config file is not found.
|
|
197
|
+
Ensure ``mcp_servers`` is declared in ``.dispatch.yaml``
|
|
198
|
+
and the agent is deployed.
|
|
199
|
+
|
|
200
|
+
Example::
|
|
201
|
+
|
|
202
|
+
from dispatch_agents.contrib.claude import get_mcp_servers
|
|
203
|
+
from claude_agent_sdk import ClaudeAgentOptions, query
|
|
204
|
+
import dispatch_agents
|
|
205
|
+
|
|
206
|
+
my_options: ClaudeAgentOptions # Initialized by @init
|
|
207
|
+
|
|
208
|
+
@dispatch_agents.init
|
|
209
|
+
async def setup():
|
|
210
|
+
global my_options
|
|
211
|
+
mcp_servers = await get_mcp_servers()
|
|
212
|
+
my_options = ClaudeAgentOptions(
|
|
213
|
+
mcp_servers=mcp_servers,
|
|
214
|
+
allowed_tools=["mcp__datadog__*"],
|
|
215
|
+
permission_mode="bypassPermissions",
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
@dispatch_agents.on(topic="query")
|
|
219
|
+
async def handle(payload: QueryRequest) -> QueryResponse:
|
|
220
|
+
async for message in query(prompt=payload.prompt, options=my_options):
|
|
221
|
+
if isinstance(message, ResultMessage) and message.subtype == "success":
|
|
222
|
+
return QueryResponse(result=message.result)
|
|
223
|
+
|
|
224
|
+
See Also:
|
|
225
|
+
- Claude Agent SDK documentation for ``ClaudeAgentOptions``.
|
|
226
|
+
"""
|
|
227
|
+
global _mcp_servers
|
|
228
|
+
|
|
229
|
+
if _mcp_servers is not None:
|
|
230
|
+
return _mcp_servers
|
|
231
|
+
|
|
232
|
+
config = _load_mcp_config()
|
|
233
|
+
servers: dict[str, McpSdkServerConfig] = {}
|
|
234
|
+
|
|
235
|
+
for server_name, server_config in config.get("mcpServers", {}).items():
|
|
236
|
+
url = server_config.get("url", "")
|
|
237
|
+
headers = dict(server_config.get("headers", {}))
|
|
238
|
+
|
|
239
|
+
proxy_server = await _create_proxy_server(server_name, url, headers)
|
|
240
|
+
servers[server_name] = proxy_server
|
|
241
|
+
|
|
242
|
+
_mcp_servers = servers
|
|
243
|
+
return _mcp_servers
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
__all__ = ["McpSdkServerConfig", "get_mcp_servers"]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""OpenAI Agents SDK integration for Dispatch Agents.
|
|
2
|
+
|
|
3
|
+
This module provides helpers for configuring MCP servers with the OpenAI Agents SDK.
|
|
4
|
+
|
|
5
|
+
Usage Example::
|
|
6
|
+
|
|
7
|
+
from agents import Agent, Runner
|
|
8
|
+
from dispatch_agents.contrib.openai import get_mcp_servers
|
|
9
|
+
import dispatch_agents
|
|
10
|
+
|
|
11
|
+
my_agent: Agent # Module-level, initialized by @init
|
|
12
|
+
|
|
13
|
+
@dispatch_agents.init
|
|
14
|
+
async def setup():
|
|
15
|
+
global my_agent
|
|
16
|
+
mcp_servers = await get_mcp_servers()
|
|
17
|
+
my_agent = Agent(
|
|
18
|
+
name="MyAssistant",
|
|
19
|
+
instructions="Use MCP tools to answer questions.",
|
|
20
|
+
mcp_servers=mcp_servers,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
@dispatch_agents.on(topic="query")
|
|
24
|
+
async def handle_query(payload: QueryRequest) -> QueryResponse:
|
|
25
|
+
result = await Runner.run(my_agent, payload.prompt)
|
|
26
|
+
return QueryResponse(result=result.final_output)
|
|
27
|
+
|
|
28
|
+
Type Compatibility:
|
|
29
|
+
The :func:`get_mcp_servers` function returns a ``list[MCPServerStreamableHttp]``
|
|
30
|
+
which is directly compatible with ``Agent.mcp_servers``.
|
|
31
|
+
|
|
32
|
+
Trace Context:
|
|
33
|
+
Trace context (trace_id, parent_id) is automatically injected into each MCP
|
|
34
|
+
tool call via the ``_meta`` field in the MCP protocol. This enables distributed
|
|
35
|
+
tracing across agent invocations.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from typing import Any
|
|
39
|
+
|
|
40
|
+
from agents.mcp import MCPServerStreamableHttp
|
|
41
|
+
from agents.mcp.util import MCPToolMetaContext
|
|
42
|
+
|
|
43
|
+
from dispatch_agents.events import (
|
|
44
|
+
get_current_invocation_id,
|
|
45
|
+
get_current_trace_id,
|
|
46
|
+
get_invocation_id_for_trace,
|
|
47
|
+
)
|
|
48
|
+
from dispatch_agents.mcp import _load_mcp_config
|
|
49
|
+
|
|
50
|
+
# Singleton storage for MCP server connections
|
|
51
|
+
_mcp_servers: list[MCPServerStreamableHttp] | None = None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _trace_meta_resolver(context: MCPToolMetaContext) -> dict[str, Any] | None:
|
|
55
|
+
"""Inject trace context into every MCP tool call via _meta.
|
|
56
|
+
|
|
57
|
+
This resolver is called by the OpenAI Agents SDK before each tool invocation.
|
|
58
|
+
The returned dict is passed to the MCP server in the ``_meta`` field of the
|
|
59
|
+
JSON-RPC request, enabling distributed tracing.
|
|
60
|
+
|
|
61
|
+
Uses a fallback mechanism when Python context variables aren't properly
|
|
62
|
+
propagated (e.g., when the SDK uses task pools created at init time).
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
context: Context information about the tool invocation (unused but required
|
|
66
|
+
by the MCPToolMetaResolver protocol).
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
A dict with trace context fields, or None if no trace context is available.
|
|
70
|
+
"""
|
|
71
|
+
meta: dict[str, Any] = {}
|
|
72
|
+
|
|
73
|
+
trace_id = get_current_trace_id()
|
|
74
|
+
invocation_id = get_current_invocation_id()
|
|
75
|
+
|
|
76
|
+
# If invocation_id isn't available from context variables, try fallback lookup.
|
|
77
|
+
# This handles cases where the SDK doesn't properly propagate Python context
|
|
78
|
+
# (e.g., task pools, worker threads, or contexts created during init).
|
|
79
|
+
if not invocation_id and trace_id:
|
|
80
|
+
invocation_id = get_invocation_id_for_trace(trace_id)
|
|
81
|
+
|
|
82
|
+
if trace_id:
|
|
83
|
+
meta["dispatch_trace_id"] = trace_id
|
|
84
|
+
if invocation_id:
|
|
85
|
+
meta["dispatch_invocation_id"] = invocation_id
|
|
86
|
+
|
|
87
|
+
return meta if meta else None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
async def get_mcp_servers() -> list[MCPServerStreamableHttp]:
|
|
91
|
+
"""Get MCP servers for OpenAI Agents SDK.
|
|
92
|
+
|
|
93
|
+
Returns a singleton list of connected MCP servers. On first call, loads
|
|
94
|
+
configuration from ``.mcp.json``, creates ``MCPServerStreamableHttp``
|
|
95
|
+
instances, and connects to each server. Subsequent calls return the
|
|
96
|
+
same connected servers.
|
|
97
|
+
|
|
98
|
+
Trace context is automatically injected into each MCP tool call via the
|
|
99
|
+
``tool_meta_resolver`` callback, which populates the ``_meta`` field in
|
|
100
|
+
the MCP protocol. This enables distributed tracing without requiring
|
|
101
|
+
per-request connection setup.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
A list of connected ``MCPServerStreamableHttp`` instances ready for use
|
|
105
|
+
with ``Agent(mcp_servers=...)``.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
FileNotFoundError: If ``.mcp.json`` config file is not found.
|
|
109
|
+
Ensure ``mcp_servers`` is declared in ``.dispatch.yaml``
|
|
110
|
+
and the agent is deployed.
|
|
111
|
+
|
|
112
|
+
Example::
|
|
113
|
+
|
|
114
|
+
from agents import Agent, Runner
|
|
115
|
+
from dispatch_agents.contrib.openai import get_mcp_servers
|
|
116
|
+
import dispatch_agents
|
|
117
|
+
|
|
118
|
+
my_agent: Agent # Initialized by @init
|
|
119
|
+
|
|
120
|
+
@dispatch_agents.init
|
|
121
|
+
async def setup():
|
|
122
|
+
global my_agent
|
|
123
|
+
mcp_servers = await get_mcp_servers()
|
|
124
|
+
my_agent = Agent(
|
|
125
|
+
name="MyAgent",
|
|
126
|
+
instructions="Use MCP tools to help the user.",
|
|
127
|
+
mcp_servers=mcp_servers,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
@dispatch_agents.on(topic="query")
|
|
131
|
+
async def handle(payload: QueryRequest) -> QueryResponse:
|
|
132
|
+
result = await Runner.run(my_agent, payload.prompt)
|
|
133
|
+
return QueryResponse(result=result.final_output)
|
|
134
|
+
|
|
135
|
+
See Also:
|
|
136
|
+
- OpenAI Agents SDK documentation for ``Agent`` and ``Runner``.
|
|
137
|
+
"""
|
|
138
|
+
global _mcp_servers
|
|
139
|
+
|
|
140
|
+
if _mcp_servers is not None:
|
|
141
|
+
return _mcp_servers
|
|
142
|
+
|
|
143
|
+
config = _load_mcp_config()
|
|
144
|
+
servers: list[MCPServerStreamableHttp] = []
|
|
145
|
+
|
|
146
|
+
for server_name, server_config in config.get("mcpServers", {}).items():
|
|
147
|
+
headers = dict(server_config.get("headers", {}))
|
|
148
|
+
|
|
149
|
+
server = MCPServerStreamableHttp(
|
|
150
|
+
name=server_name,
|
|
151
|
+
params={
|
|
152
|
+
"url": server_config.get("url", ""),
|
|
153
|
+
"headers": headers,
|
|
154
|
+
},
|
|
155
|
+
cache_tools_list=True,
|
|
156
|
+
tool_meta_resolver=_trace_meta_resolver,
|
|
157
|
+
# Override the default 5-second timeout which is too short for LLM tool calls
|
|
158
|
+
client_session_timeout_seconds=300.0,
|
|
159
|
+
)
|
|
160
|
+
await server.connect()
|
|
161
|
+
servers.append(server)
|
|
162
|
+
|
|
163
|
+
_mcp_servers = servers
|
|
164
|
+
return _mcp_servers
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
__all__ = ["MCPServerStreamableHttp", "get_mcp_servers"]
|