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,217 @@
|
|
|
1
|
+
"""Auto-instrumentation for LLM SDK calls.
|
|
2
|
+
|
|
3
|
+
Patches httpx and requests to inject Dispatch trace context headers on
|
|
4
|
+
requests destined for the sidecar proxy. This enables automatic trace
|
|
5
|
+
correlation for any LLM SDK (OpenAI, Anthropic, etc.) without user
|
|
6
|
+
code changes.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
Called automatically by grpc_listener.py before user code imports.
|
|
10
|
+
Not intended to be called directly by user code.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
PROXY_HOST = "" # Set at instrument time from env
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _is_proxy_bound(url: Any) -> bool:
|
|
23
|
+
"""Check if a request URL targets the sidecar proxy."""
|
|
24
|
+
if not PROXY_HOST:
|
|
25
|
+
return False
|
|
26
|
+
return str(url).startswith(PROXY_HOST)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_context_headers() -> dict[str, str]:
|
|
30
|
+
"""Build trace context headers from current execution context.
|
|
31
|
+
|
|
32
|
+
Reads from contextvars set by dispatch_agents.events during handler
|
|
33
|
+
execution, so headers are automatically scoped to the current invocation.
|
|
34
|
+
|
|
35
|
+
Also serializes any extra LLM headers (set via extra_headers() context
|
|
36
|
+
manager) into a single ``X-Dispatch-Extra-Headers`` JSON header so
|
|
37
|
+
they can be forwarded by the sidecar proxy without polluting the
|
|
38
|
+
header namespace.
|
|
39
|
+
"""
|
|
40
|
+
import json
|
|
41
|
+
|
|
42
|
+
from .events import get_current_invocation_id, get_current_trace_id
|
|
43
|
+
from .llm import get_extra_llm_headers
|
|
44
|
+
|
|
45
|
+
headers: dict[str, str] = {}
|
|
46
|
+
|
|
47
|
+
trace_id = get_current_trace_id()
|
|
48
|
+
if trace_id:
|
|
49
|
+
headers["X-Dispatch-Trace-Id"] = trace_id
|
|
50
|
+
|
|
51
|
+
invocation_id = get_current_invocation_id()
|
|
52
|
+
if invocation_id:
|
|
53
|
+
headers["X-Dispatch-Invocation-Id"] = invocation_id
|
|
54
|
+
|
|
55
|
+
agent_name = os.environ.get("DISPATCH_AGENT_NAME", "")
|
|
56
|
+
if agent_name:
|
|
57
|
+
headers["X-Dispatch-Agent-Name"] = agent_name
|
|
58
|
+
|
|
59
|
+
extra = get_extra_llm_headers()
|
|
60
|
+
if extra:
|
|
61
|
+
headers["X-Dispatch-Extra-Headers"] = json.dumps(extra)
|
|
62
|
+
|
|
63
|
+
return headers
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def auto_instrument() -> None:
|
|
67
|
+
"""Patch httpx, requests, and subprocess to inject trace context.
|
|
68
|
+
|
|
69
|
+
- httpx/requests: Injects headers on proxy-bound requests (in-process SDKs)
|
|
70
|
+
- subprocess: Injects ANTHROPIC_CUSTOM_HEADERS env var so child processes
|
|
71
|
+
(e.g. Claude Agent SDK CLI) include trace context in their HTTP requests
|
|
72
|
+
|
|
73
|
+
Safe to call multiple times — patches are idempotent.
|
|
74
|
+
"""
|
|
75
|
+
global PROXY_HOST
|
|
76
|
+
PROXY_HOST = os.environ.get("DISPATCH_LLM_PROXY_URL", "")
|
|
77
|
+
|
|
78
|
+
if not PROXY_HOST:
|
|
79
|
+
logger.debug("DISPATCH_LLM_PROXY_URL not set, skipping instrumentation")
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
_patch_httpx()
|
|
83
|
+
_patch_requests()
|
|
84
|
+
_patch_subprocess()
|
|
85
|
+
|
|
86
|
+
logger.info("Auto-instrumentation enabled for proxy at %s", PROXY_HOST)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _patch_httpx() -> None:
|
|
90
|
+
"""Patch httpx.Client.send and httpx.AsyncClient.send."""
|
|
91
|
+
try:
|
|
92
|
+
import httpx
|
|
93
|
+
except ImportError:
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
# Patch sync client
|
|
97
|
+
if not getattr(httpx.Client.send, "_dispatch_patched", False):
|
|
98
|
+
_original_sync_send = httpx.Client.send
|
|
99
|
+
|
|
100
|
+
def _patched_sync_send(self: Any, request: Any, **kwargs: Any) -> Any:
|
|
101
|
+
if _is_proxy_bound(request.url):
|
|
102
|
+
for key, value in _get_context_headers().items():
|
|
103
|
+
request.headers[key] = value
|
|
104
|
+
return _original_sync_send(self, request, **kwargs)
|
|
105
|
+
|
|
106
|
+
_patched_sync_send._dispatch_patched = True # type: ignore[attr-defined]
|
|
107
|
+
httpx.Client.send = _patched_sync_send # type: ignore[method-assign]
|
|
108
|
+
|
|
109
|
+
# Patch async client
|
|
110
|
+
if not getattr(httpx.AsyncClient.send, "_dispatch_patched", False):
|
|
111
|
+
_original_async_send = httpx.AsyncClient.send
|
|
112
|
+
|
|
113
|
+
async def _patched_async_send(self: Any, request: Any, **kwargs: Any) -> Any:
|
|
114
|
+
if _is_proxy_bound(request.url):
|
|
115
|
+
for key, value in _get_context_headers().items():
|
|
116
|
+
request.headers[key] = value
|
|
117
|
+
return await _original_async_send(self, request, **kwargs)
|
|
118
|
+
|
|
119
|
+
_patched_async_send._dispatch_patched = True # type: ignore[attr-defined]
|
|
120
|
+
httpx.AsyncClient.send = _patched_async_send # type: ignore[method-assign]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _patch_requests() -> None:
|
|
124
|
+
"""Patch requests.Session.send for libraries using requests (e.g. Google SDK)."""
|
|
125
|
+
try:
|
|
126
|
+
import requests
|
|
127
|
+
except ImportError:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
if not getattr(requests.Session.send, "_dispatch_patched", False):
|
|
131
|
+
_original_send = requests.Session.send
|
|
132
|
+
|
|
133
|
+
def _patched_send(self: Any, request: Any, **kwargs: Any) -> Any:
|
|
134
|
+
if _is_proxy_bound(request.url):
|
|
135
|
+
for key, value in _get_context_headers().items():
|
|
136
|
+
request.headers[key] = value
|
|
137
|
+
return _original_send(self, request, **kwargs)
|
|
138
|
+
|
|
139
|
+
_patched_send._dispatch_patched = True # type: ignore[attr-defined]
|
|
140
|
+
requests.Session.send = _patched_send # type: ignore[method-assign]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _build_trace_custom_headers() -> str | None:
|
|
144
|
+
"""Build ANTHROPIC_CUSTOM_HEADERS value from current trace context.
|
|
145
|
+
|
|
146
|
+
Returns a newline-separated header string, or None if no trace context.
|
|
147
|
+
The Claude CLI reads this env var and includes the headers on every
|
|
148
|
+
HTTP request it makes to ANTHROPIC_BASE_URL (our sidecar proxy).
|
|
149
|
+
"""
|
|
150
|
+
from .events import get_current_invocation_id, get_current_trace_id
|
|
151
|
+
|
|
152
|
+
parts: list[str] = []
|
|
153
|
+
trace_id = get_current_trace_id()
|
|
154
|
+
if trace_id:
|
|
155
|
+
parts.append(f"X-Dispatch-Trace-Id: {trace_id}")
|
|
156
|
+
invocation_id = get_current_invocation_id()
|
|
157
|
+
if invocation_id:
|
|
158
|
+
parts.append(f"X-Dispatch-Invocation-Id: {invocation_id}")
|
|
159
|
+
return "\n".join(parts) if parts else None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _inject_trace_env(env: dict[str, str] | None) -> dict[str, str] | None:
|
|
163
|
+
"""Inject trace context headers into a subprocess env dict.
|
|
164
|
+
|
|
165
|
+
Sets provider-specific custom header env vars so CLI tools (Claude CLI,
|
|
166
|
+
Gemini CLI, etc.) include trace context in their HTTP requests.
|
|
167
|
+
Each subprocess gets its own env copy at fork time.
|
|
168
|
+
|
|
169
|
+
If env is None (inherit parent env), creates a copy of os.environ.
|
|
170
|
+
Concurrent-safe: reads from ContextVars which are per-async-task.
|
|
171
|
+
|
|
172
|
+
Provider support:
|
|
173
|
+
- ANTHROPIC_CUSTOM_HEADERS: Claude CLI (newline-separated headers)
|
|
174
|
+
- GEMINI_CLI_CUSTOM_HEADERS: Gemini CLI (same format)
|
|
175
|
+
- OpenAI/Cohere/Mistral: No CLI custom header env var — in-process
|
|
176
|
+
SDKs are covered by httpx/requests patches instead.
|
|
177
|
+
"""
|
|
178
|
+
custom_headers = _build_trace_custom_headers()
|
|
179
|
+
if not custom_headers:
|
|
180
|
+
return env
|
|
181
|
+
|
|
182
|
+
import uuid
|
|
183
|
+
|
|
184
|
+
if env is None:
|
|
185
|
+
env = os.environ.copy()
|
|
186
|
+
else:
|
|
187
|
+
env = dict(env) # Don't mutate the caller's dict
|
|
188
|
+
|
|
189
|
+
# Add a unique subprocess ID so the backend can group LLM calls
|
|
190
|
+
# by subprocess within a trace (e.g. multiple subagents in one invocation)
|
|
191
|
+
subprocess_id = str(uuid.uuid4())
|
|
192
|
+
custom_headers += f"\nX-Dispatch-Subprocess-Id: {subprocess_id}"
|
|
193
|
+
|
|
194
|
+
# Provider CLIs that support custom headers via env var
|
|
195
|
+
env["ANTHROPIC_CUSTOM_HEADERS"] = custom_headers
|
|
196
|
+
env["GEMINI_CLI_CUSTOM_HEADERS"] = custom_headers
|
|
197
|
+
return env
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _patch_subprocess() -> None:
|
|
201
|
+
"""Patch subprocess.Popen to inject trace context into child process env.
|
|
202
|
+
|
|
203
|
+
This ensures subprocesses (e.g. Claude Agent SDK CLI) automatically
|
|
204
|
+
include trace headers in their HTTP requests. ContextVars are read
|
|
205
|
+
at spawn time, so concurrent invocations each get the correct trace_id.
|
|
206
|
+
"""
|
|
207
|
+
import subprocess
|
|
208
|
+
|
|
209
|
+
if not getattr(subprocess.Popen.__init__, "_dispatch_patched", False):
|
|
210
|
+
_original_init = subprocess.Popen.__init__
|
|
211
|
+
|
|
212
|
+
def _patched_init(self: Any, args: Any, **kwargs: Any) -> None:
|
|
213
|
+
kwargs["env"] = _inject_trace_env(kwargs.get("env"))
|
|
214
|
+
return _original_init(self, args, **kwargs)
|
|
215
|
+
|
|
216
|
+
_patched_init._dispatch_patched = True # type: ignore[attr-defined]
|
|
217
|
+
subprocess.Popen.__init__ = _patched_init # type: ignore[assignment,method-assign]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Dispatch Agents integrations with external services."""
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# GitHub Integration
|
|
2
|
+
|
|
3
|
+
The SDK includes typed payloads for GitHub webhook events. See `__init__.py` for available event types.
|
|
4
|
+
|
|
5
|
+
## Schema Compliance Testing
|
|
6
|
+
|
|
7
|
+
The GitHub event types are verified against the official [octokit/webhooks](https://github.com/octokit/webhooks) JSON Schema. The schema is version-controlled at `tests/schemas/octokit-webhooks.json`.
|
|
8
|
+
|
|
9
|
+
To update the schema when GitHub releases new webhook types, see [tests/schemas/README.md](../../../tests/schemas/README.md).
|