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,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).