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.
Files changed (43) hide show
  1. agentservice/__init__.py +0 -0
  2. agentservice/py.typed +0 -0
  3. agentservice/v1/__init__.py +0 -0
  4. agentservice/v1/message_pb2.py +41 -0
  5. agentservice/v1/message_pb2.pyi +22 -0
  6. agentservice/v1/message_pb2_grpc.py +4 -0
  7. agentservice/v1/request_response_pb2.py +46 -0
  8. agentservice/v1/request_response_pb2.pyi +54 -0
  9. agentservice/v1/request_response_pb2_grpc.py +4 -0
  10. agentservice/v1/service_pb2.py +43 -0
  11. agentservice/v1/service_pb2.pyi +6 -0
  12. agentservice/v1/service_pb2_grpc.py +129 -0
  13. dispatch_agents/__init__.py +281 -0
  14. dispatch_agents/agent_service.py +135 -0
  15. dispatch_agents/config.py +490 -0
  16. dispatch_agents/contrib/__init__.py +1 -0
  17. dispatch_agents/contrib/claude/__init__.py +246 -0
  18. dispatch_agents/contrib/openai/__init__.py +167 -0
  19. dispatch_agents/events.py +986 -0
  20. dispatch_agents/grpc_server.py +565 -0
  21. dispatch_agents/instrument.py +217 -0
  22. dispatch_agents/integrations/__init__.py +1 -0
  23. dispatch_agents/integrations/github/README.md +9 -0
  24. dispatch_agents/integrations/github/__init__.py +4268 -0
  25. dispatch_agents/invocation.py +25 -0
  26. dispatch_agents/llm.py +1017 -0
  27. dispatch_agents/llm_langchain.py +394 -0
  28. dispatch_agents/logging_config.py +133 -0
  29. dispatch_agents/mcp.py +266 -0
  30. dispatch_agents/memory.py +264 -0
  31. dispatch_agents/models.py +748 -0
  32. dispatch_agents/proxy/__init__.py +6 -0
  33. dispatch_agents/proxy/server.py +1137 -0
  34. dispatch_agents/proxy/sse_utils.py +76 -0
  35. dispatch_agents/py.typed +0 -0
  36. dispatch_agents/resources.py +68 -0
  37. dispatch_agents/version.py +19 -0
  38. dispatch_agents-0.9.0.dist-info/METADATA +20 -0
  39. dispatch_agents-0.9.0.dist-info/RECORD +43 -0
  40. dispatch_agents-0.9.0.dist-info/WHEEL +4 -0
  41. dispatch_agents-0.9.0.dist-info/licenses/LICENSE +191 -0
  42. dispatch_agents-0.9.0.dist-info/licenses/LICENSE-3rdparty.csv +12 -0
  43. 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"]