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,394 @@
|
|
|
1
|
+
"""LangChain adapter for Dispatch LLM Gateway.
|
|
2
|
+
|
|
3
|
+
Provides a LangChain-compatible ChatModel that uses the Dispatch LLM proxy
|
|
4
|
+
for automatic trace correlation and cost tracking.
|
|
5
|
+
|
|
6
|
+
Example:
|
|
7
|
+
from dispatch_agents.llm_langchain import ChatDispatch
|
|
8
|
+
|
|
9
|
+
# Use as a drop-in replacement for ChatOpenAI
|
|
10
|
+
llm = ChatDispatch(model="gpt-4o")
|
|
11
|
+
|
|
12
|
+
# Works with LangGraph
|
|
13
|
+
agent = create_react_agent(llm, tools=tools)
|
|
14
|
+
|
|
15
|
+
# Works with structured output
|
|
16
|
+
structured_llm = llm.with_structured_output(MySchema)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from langchain_core.callbacks import (
|
|
23
|
+
AsyncCallbackManagerForLLMRun,
|
|
24
|
+
CallbackManagerForLLMRun,
|
|
25
|
+
)
|
|
26
|
+
from langchain_core.language_models.chat_models import BaseChatModel
|
|
27
|
+
from langchain_core.messages import (
|
|
28
|
+
AIMessage,
|
|
29
|
+
BaseMessage,
|
|
30
|
+
HumanMessage,
|
|
31
|
+
SystemMessage,
|
|
32
|
+
ToolMessage,
|
|
33
|
+
)
|
|
34
|
+
from langchain_core.outputs import ChatGeneration, ChatResult
|
|
35
|
+
from pydantic import Field
|
|
36
|
+
|
|
37
|
+
from .llm import LLMClient, LLMResponse
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _get_tool_schema(tool: Any) -> dict[str, Any]:
|
|
41
|
+
"""Extract JSON schema from a tool's args_schema, handling both Pydantic models and dicts."""
|
|
42
|
+
if not hasattr(tool, "args_schema") or not tool.args_schema:
|
|
43
|
+
return {}
|
|
44
|
+
|
|
45
|
+
args_schema = tool.args_schema
|
|
46
|
+
|
|
47
|
+
# If it's already a dict, return it directly
|
|
48
|
+
if isinstance(args_schema, dict):
|
|
49
|
+
return args_schema
|
|
50
|
+
|
|
51
|
+
# If it's a Pydantic model class, call model_json_schema()
|
|
52
|
+
if hasattr(args_schema, "model_json_schema"):
|
|
53
|
+
return args_schema.model_json_schema()
|
|
54
|
+
|
|
55
|
+
# Fallback: try to convert to dict
|
|
56
|
+
return {}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _convert_tools_to_openai_format(tools: Sequence[Any]) -> list[dict[str, Any]]:
|
|
60
|
+
"""Convert various tool formats to OpenAI function calling format.
|
|
61
|
+
|
|
62
|
+
Handles:
|
|
63
|
+
- LangChain BaseTool objects
|
|
64
|
+
- MCP tools (with args_schema as dict)
|
|
65
|
+
- Raw dicts already in OpenAI format
|
|
66
|
+
- Callable functions with docstrings
|
|
67
|
+
"""
|
|
68
|
+
formatted_tools = []
|
|
69
|
+
for tool in tools:
|
|
70
|
+
if hasattr(tool, "name") and hasattr(tool, "description"):
|
|
71
|
+
# LangChain BaseTool or MCP tool
|
|
72
|
+
tool_schema: dict[str, Any] = {
|
|
73
|
+
"type": "function",
|
|
74
|
+
"function": {
|
|
75
|
+
"name": tool.name,
|
|
76
|
+
"description": tool.description or "",
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
# Get parameters schema, handling both Pydantic models and dicts
|
|
80
|
+
params = _get_tool_schema(tool)
|
|
81
|
+
if params:
|
|
82
|
+
tool_schema["function"]["parameters"] = params
|
|
83
|
+
formatted_tools.append(tool_schema)
|
|
84
|
+
elif isinstance(tool, dict):
|
|
85
|
+
# Already in OpenAI format
|
|
86
|
+
formatted_tools.append(tool)
|
|
87
|
+
elif callable(tool):
|
|
88
|
+
# Function with docstring
|
|
89
|
+
import inspect
|
|
90
|
+
|
|
91
|
+
sig = inspect.signature(tool)
|
|
92
|
+
formatted_tools.append(
|
|
93
|
+
{
|
|
94
|
+
"type": "function",
|
|
95
|
+
"function": {
|
|
96
|
+
"name": tool.__name__,
|
|
97
|
+
"description": tool.__doc__ or "",
|
|
98
|
+
"parameters": {
|
|
99
|
+
"type": "object",
|
|
100
|
+
"properties": {
|
|
101
|
+
name: {"type": "string"} for name in sig.parameters
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
return formatted_tools
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _serialize_tool_arguments(args: Any) -> str:
|
|
111
|
+
"""Serialize tool call arguments to a JSON string.
|
|
112
|
+
|
|
113
|
+
OpenAI-compatible APIs expect tool call arguments as a JSON string,
|
|
114
|
+
not a Python dict string representation.
|
|
115
|
+
"""
|
|
116
|
+
import json
|
|
117
|
+
|
|
118
|
+
if isinstance(args, str):
|
|
119
|
+
return args
|
|
120
|
+
if isinstance(args, dict):
|
|
121
|
+
return json.dumps(args)
|
|
122
|
+
# Fallback: try to convert to dict then serialize
|
|
123
|
+
try:
|
|
124
|
+
return json.dumps(dict(args))
|
|
125
|
+
except (TypeError, ValueError):
|
|
126
|
+
return json.dumps({})
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _convert_message_to_dict(message: BaseMessage) -> dict[str, Any]:
|
|
130
|
+
"""Convert a LangChain message to a dict for the LLM API."""
|
|
131
|
+
if isinstance(message, SystemMessage):
|
|
132
|
+
return {"role": "system", "content": message.content}
|
|
133
|
+
elif isinstance(message, HumanMessage):
|
|
134
|
+
return {"role": "user", "content": message.content}
|
|
135
|
+
elif isinstance(message, AIMessage):
|
|
136
|
+
msg_dict: dict[str, Any] = {"role": "assistant", "content": message.content}
|
|
137
|
+
# Include tool calls if present
|
|
138
|
+
if message.tool_calls:
|
|
139
|
+
msg_dict["tool_calls"] = [
|
|
140
|
+
{
|
|
141
|
+
"id": tc["id"],
|
|
142
|
+
"type": "function",
|
|
143
|
+
"function": {
|
|
144
|
+
"name": tc["name"],
|
|
145
|
+
"arguments": _serialize_tool_arguments(tc["args"]),
|
|
146
|
+
},
|
|
147
|
+
}
|
|
148
|
+
for tc in message.tool_calls
|
|
149
|
+
]
|
|
150
|
+
return msg_dict
|
|
151
|
+
elif isinstance(message, ToolMessage):
|
|
152
|
+
# tool_call_id is required by OpenAI-compatible APIs - use message id as fallback
|
|
153
|
+
tool_call_id = message.tool_call_id
|
|
154
|
+
if not tool_call_id:
|
|
155
|
+
# Fallback: use message id or generate one
|
|
156
|
+
tool_call_id = getattr(message, "id", None) or f"call_{id(message)}"
|
|
157
|
+
return {
|
|
158
|
+
"role": "tool",
|
|
159
|
+
"content": str(message.content) if message.content else "",
|
|
160
|
+
"tool_call_id": tool_call_id,
|
|
161
|
+
}
|
|
162
|
+
else:
|
|
163
|
+
# Fallback for other message types
|
|
164
|
+
return {"role": "user", "content": str(message.content)}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _parse_tool_arguments(arguments: Any) -> dict[str, Any]:
|
|
168
|
+
"""Parse tool call arguments to a dict.
|
|
169
|
+
|
|
170
|
+
OpenAI returns arguments as a JSON string, but LangChain expects a dict.
|
|
171
|
+
Handles various input formats gracefully.
|
|
172
|
+
"""
|
|
173
|
+
import json
|
|
174
|
+
|
|
175
|
+
if isinstance(arguments, dict):
|
|
176
|
+
return arguments
|
|
177
|
+
if isinstance(arguments, str):
|
|
178
|
+
if not arguments or arguments.strip() == "":
|
|
179
|
+
return {}
|
|
180
|
+
try:
|
|
181
|
+
parsed = json.loads(arguments)
|
|
182
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
183
|
+
except json.JSONDecodeError:
|
|
184
|
+
return {}
|
|
185
|
+
return {}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _convert_response_to_message(response: LLMResponse) -> AIMessage:
|
|
189
|
+
"""Convert an LLM response to a LangChain AIMessage."""
|
|
190
|
+
# Build tool_calls list if present
|
|
191
|
+
tool_calls = []
|
|
192
|
+
if response.tool_calls:
|
|
193
|
+
for tc in response.tool_calls:
|
|
194
|
+
tool_calls.append(
|
|
195
|
+
{
|
|
196
|
+
"id": tc.id,
|
|
197
|
+
"name": tc.function.name,
|
|
198
|
+
"args": _parse_tool_arguments(tc.function.arguments),
|
|
199
|
+
}
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Build response metadata
|
|
203
|
+
response_metadata = {
|
|
204
|
+
"llm_call_id": response.llm_call_id,
|
|
205
|
+
"model": response.model,
|
|
206
|
+
"provider": response.provider,
|
|
207
|
+
"finish_reason": response.finish_reason,
|
|
208
|
+
"input_tokens": response.input_tokens,
|
|
209
|
+
"output_tokens": response.output_tokens,
|
|
210
|
+
"cost_usd": response.cost_usd,
|
|
211
|
+
"latency_ms": response.latency_ms,
|
|
212
|
+
}
|
|
213
|
+
if response.variant_name:
|
|
214
|
+
response_metadata["variant_name"] = response.variant_name
|
|
215
|
+
|
|
216
|
+
# Build usage metadata
|
|
217
|
+
usage_metadata = {
|
|
218
|
+
"input_tokens": response.input_tokens,
|
|
219
|
+
"output_tokens": response.output_tokens,
|
|
220
|
+
"total_tokens": response.input_tokens + response.output_tokens,
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return AIMessage(
|
|
224
|
+
content=response.content or "",
|
|
225
|
+
tool_calls=tool_calls if tool_calls else [],
|
|
226
|
+
response_metadata=response_metadata,
|
|
227
|
+
usage_metadata=usage_metadata,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class ChatDispatch(BaseChatModel):
|
|
232
|
+
"""LangChain ChatModel that uses Dispatch LLM Gateway.
|
|
233
|
+
|
|
234
|
+
Provides automatic trace correlation and cost tracking for all LLM calls
|
|
235
|
+
when used within a Dispatch agent handler.
|
|
236
|
+
|
|
237
|
+
Example:
|
|
238
|
+
from dispatch_agents.llm_langchain import ChatDispatch
|
|
239
|
+
|
|
240
|
+
# Basic usage
|
|
241
|
+
llm = ChatDispatch(model="gpt-4o")
|
|
242
|
+
response = await llm.ainvoke([HumanMessage(content="Hello!")])
|
|
243
|
+
|
|
244
|
+
# With LangGraph ReAct agent
|
|
245
|
+
agent = create_react_agent(llm, tools=my_tools)
|
|
246
|
+
|
|
247
|
+
# With structured output
|
|
248
|
+
structured_llm = llm.with_structured_output(MyPydanticModel)
|
|
249
|
+
result = await structured_llm.ainvoke([...])
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
model: Model name (e.g., "gpt-4o", "claude-3-5-sonnet").
|
|
253
|
+
Uses org default if not specified.
|
|
254
|
+
provider: Provider name (e.g., "openai", "anthropic").
|
|
255
|
+
Uses org default if not specified.
|
|
256
|
+
temperature: Sampling temperature (0-2), default 1.0
|
|
257
|
+
max_tokens: Maximum tokens in response (optional)
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
model: str | None = Field(default=None, description="Model name to use")
|
|
261
|
+
provider: str | None = Field(default=None, description="Provider name")
|
|
262
|
+
temperature: float = Field(default=1.0, description="Sampling temperature")
|
|
263
|
+
max_tokens: int | None = Field(default=None, description="Max tokens in response")
|
|
264
|
+
|
|
265
|
+
_client: LLMClient | None = None
|
|
266
|
+
|
|
267
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
268
|
+
super().__init__(**kwargs)
|
|
269
|
+
self._client = LLMClient()
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def _llm_type(self) -> str:
|
|
273
|
+
"""Return the type of LLM."""
|
|
274
|
+
return "dispatch"
|
|
275
|
+
|
|
276
|
+
@property
|
|
277
|
+
def _identifying_params(self) -> dict[str, Any]:
|
|
278
|
+
"""Return identifying parameters for caching."""
|
|
279
|
+
return {
|
|
280
|
+
"model": self.model,
|
|
281
|
+
"provider": self.provider,
|
|
282
|
+
"temperature": self.temperature,
|
|
283
|
+
"max_tokens": self.max_tokens,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
def _generate(
|
|
287
|
+
self,
|
|
288
|
+
messages: list[BaseMessage],
|
|
289
|
+
stop: list[str] | None = None,
|
|
290
|
+
run_manager: CallbackManagerForLLMRun | None = None,
|
|
291
|
+
**kwargs: Any,
|
|
292
|
+
) -> ChatResult:
|
|
293
|
+
"""Synchronous generation - wraps async implementation."""
|
|
294
|
+
import asyncio
|
|
295
|
+
|
|
296
|
+
# Get or create event loop
|
|
297
|
+
try:
|
|
298
|
+
loop = asyncio.get_running_loop()
|
|
299
|
+
except RuntimeError:
|
|
300
|
+
loop = None
|
|
301
|
+
|
|
302
|
+
if loop and loop.is_running():
|
|
303
|
+
# We're in an async context, need to run in a new thread
|
|
304
|
+
import concurrent.futures
|
|
305
|
+
|
|
306
|
+
with concurrent.futures.ThreadPoolExecutor() as executor:
|
|
307
|
+
future = executor.submit(
|
|
308
|
+
asyncio.run,
|
|
309
|
+
self._agenerate(messages, stop, run_manager, **kwargs), # type: ignore[arg-type]
|
|
310
|
+
)
|
|
311
|
+
return future.result()
|
|
312
|
+
else:
|
|
313
|
+
# No running loop, we can use asyncio.run
|
|
314
|
+
return asyncio.run(
|
|
315
|
+
self._agenerate(messages, stop, run_manager, **kwargs) # type: ignore[arg-type]
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
async def _agenerate(
|
|
319
|
+
self,
|
|
320
|
+
messages: list[BaseMessage],
|
|
321
|
+
stop: list[str] | None = None,
|
|
322
|
+
run_manager: AsyncCallbackManagerForLLMRun | None = None,
|
|
323
|
+
**kwargs: Any,
|
|
324
|
+
) -> ChatResult:
|
|
325
|
+
"""Async generation using Dispatch LLM Gateway."""
|
|
326
|
+
if self._client is None:
|
|
327
|
+
self._client = LLMClient()
|
|
328
|
+
|
|
329
|
+
# Convert messages to API format
|
|
330
|
+
message_dicts = [_convert_message_to_dict(m) for m in messages]
|
|
331
|
+
|
|
332
|
+
# Extract tools from kwargs if provided (for function calling)
|
|
333
|
+
tools = kwargs.get("tools")
|
|
334
|
+
if tools:
|
|
335
|
+
# Convert tools to OpenAI format, handling various tool types
|
|
336
|
+
formatted_tools = _convert_tools_to_openai_format(tools)
|
|
337
|
+
tools = formatted_tools if formatted_tools else None
|
|
338
|
+
|
|
339
|
+
# Call the LLM Gateway
|
|
340
|
+
response = await self._client.inference(
|
|
341
|
+
messages=message_dicts,
|
|
342
|
+
model=self.model,
|
|
343
|
+
provider=self.provider,
|
|
344
|
+
tools=tools,
|
|
345
|
+
temperature=self.temperature,
|
|
346
|
+
max_tokens=self.max_tokens,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
# Convert response to LangChain format
|
|
350
|
+
ai_message = _convert_response_to_message(response)
|
|
351
|
+
generation = ChatGeneration(message=ai_message)
|
|
352
|
+
|
|
353
|
+
return ChatResult(
|
|
354
|
+
generations=[generation],
|
|
355
|
+
llm_output={
|
|
356
|
+
"model": response.model,
|
|
357
|
+
"provider": response.provider,
|
|
358
|
+
"llm_call_id": response.llm_call_id,
|
|
359
|
+
"cost_usd": response.cost_usd,
|
|
360
|
+
"latency_ms": response.latency_ms,
|
|
361
|
+
},
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
def bind_tools(
|
|
365
|
+
self,
|
|
366
|
+
tools: Sequence[Any],
|
|
367
|
+
**kwargs: Any,
|
|
368
|
+
) -> "ChatDispatch":
|
|
369
|
+
"""Bind tools to this model for function calling.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
tools: List of tools (LangChain tools, MCP tools, functions, or tool dicts)
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
A new ChatDispatch instance with tools bound
|
|
376
|
+
"""
|
|
377
|
+
# Convert tools to OpenAI format, handling various tool types
|
|
378
|
+
formatted_tools = _convert_tools_to_openai_format(tools)
|
|
379
|
+
|
|
380
|
+
# Return a new instance that will pass tools in kwargs.
|
|
381
|
+
# .bind() returns Runnable, not ChatDispatch, but callers expect ChatDispatch.
|
|
382
|
+
bound = (
|
|
383
|
+
self.__class__(
|
|
384
|
+
model=self.model,
|
|
385
|
+
provider=self.provider,
|
|
386
|
+
temperature=self.temperature,
|
|
387
|
+
max_tokens=self.max_tokens,
|
|
388
|
+
)
|
|
389
|
+
.configurable_fields(
|
|
390
|
+
# Store tools in config to be passed through
|
|
391
|
+
)
|
|
392
|
+
.bind(tools=formatted_tools, **kwargs)
|
|
393
|
+
)
|
|
394
|
+
return bound # type: ignore[return-value]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Logging configuration for the Dispatch Agents SDK.
|
|
2
|
+
|
|
3
|
+
Controls the verbosity of SDK logging output via environment variables:
|
|
4
|
+
- DISPATCH_VERBOSE=1 or DISPATCH_VERBOSE=true: Enable verbose/debug logging
|
|
5
|
+
- DISPATCH_LOG_LEVEL=DEBUG/INFO/WARNING/ERROR: Set specific log level
|
|
6
|
+
|
|
7
|
+
By default, the SDK logs at INFO level, hiding debug messages like
|
|
8
|
+
re-subscription events. Enable verbose mode for debugging.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
# SDK logger namespace
|
|
16
|
+
SDK_LOGGER_NAME = "dispatch_agents"
|
|
17
|
+
|
|
18
|
+
# Check if we've already configured logging
|
|
19
|
+
_logging_configured = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _parse_bool_env(value: str | None) -> bool:
|
|
23
|
+
"""Parse boolean from environment variable value."""
|
|
24
|
+
if value is None:
|
|
25
|
+
return False
|
|
26
|
+
return value.lower() in ("1", "true", "yes", "on")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _get_log_level() -> int:
|
|
30
|
+
"""Determine the log level from environment variables.
|
|
31
|
+
|
|
32
|
+
Priority:
|
|
33
|
+
1. DISPATCH_LOG_LEVEL (explicit level)
|
|
34
|
+
2. DISPATCH_VERBOSE (boolean toggle for DEBUG)
|
|
35
|
+
3. Default: WARNING (minimal output)
|
|
36
|
+
"""
|
|
37
|
+
# Check explicit log level first
|
|
38
|
+
log_level_str = os.environ.get("DISPATCH_LOG_LEVEL", "").upper()
|
|
39
|
+
if log_level_str:
|
|
40
|
+
level_map = {
|
|
41
|
+
"DEBUG": logging.DEBUG,
|
|
42
|
+
"INFO": logging.INFO,
|
|
43
|
+
"WARNING": logging.WARNING,
|
|
44
|
+
"WARN": logging.WARNING,
|
|
45
|
+
"ERROR": logging.ERROR,
|
|
46
|
+
"CRITICAL": logging.CRITICAL,
|
|
47
|
+
}
|
|
48
|
+
if log_level_str in level_map:
|
|
49
|
+
return level_map[log_level_str]
|
|
50
|
+
|
|
51
|
+
# Check verbose flag
|
|
52
|
+
if _parse_bool_env(os.environ.get("DISPATCH_VERBOSE")):
|
|
53
|
+
return logging.DEBUG
|
|
54
|
+
|
|
55
|
+
# Default: WARNING to minimize noise
|
|
56
|
+
# Users see errors and warnings, but not routine info/debug
|
|
57
|
+
return logging.WARNING
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def configure_logging(force: bool = False) -> None:
|
|
61
|
+
"""Configure logging for the Dispatch Agents SDK.
|
|
62
|
+
|
|
63
|
+
This sets up the SDK logger with appropriate level and format.
|
|
64
|
+
Called automatically when the SDK is imported, but can be called
|
|
65
|
+
again with force=True to reconfigure.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
force: If True, reconfigure even if already configured
|
|
69
|
+
"""
|
|
70
|
+
global _logging_configured
|
|
71
|
+
|
|
72
|
+
if _logging_configured and not force:
|
|
73
|
+
return
|
|
74
|
+
|
|
75
|
+
# Get the SDK root logger
|
|
76
|
+
sdk_logger = logging.getLogger(SDK_LOGGER_NAME)
|
|
77
|
+
|
|
78
|
+
# Set the log level
|
|
79
|
+
level = _get_log_level()
|
|
80
|
+
sdk_logger.setLevel(level)
|
|
81
|
+
|
|
82
|
+
# Only add handler if none exist (avoid duplicate handlers on force)
|
|
83
|
+
if not sdk_logger.handlers:
|
|
84
|
+
# Create a handler that writes to stderr
|
|
85
|
+
handler = logging.StreamHandler(sys.stderr)
|
|
86
|
+
handler.setLevel(level)
|
|
87
|
+
|
|
88
|
+
# Format: simple for normal use, more detail for debug
|
|
89
|
+
if level == logging.DEBUG:
|
|
90
|
+
formatter = logging.Formatter(
|
|
91
|
+
"%(asctime)s [%(name)s] %(levelname)s: %(message)s",
|
|
92
|
+
datefmt="%H:%M:%S",
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
formatter = logging.Formatter("%(levelname)s: %(message)s")
|
|
96
|
+
|
|
97
|
+
handler.setFormatter(formatter)
|
|
98
|
+
sdk_logger.addHandler(handler)
|
|
99
|
+
|
|
100
|
+
# Prevent propagation to root logger (avoid duplicate messages)
|
|
101
|
+
sdk_logger.propagate = False
|
|
102
|
+
|
|
103
|
+
_logging_configured = True
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def get_logger(name: str | None = None) -> logging.Logger:
|
|
107
|
+
"""Get a logger for SDK modules.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
name: Optional module name (e.g., __name__). If provided,
|
|
111
|
+
creates a child logger under SDK_LOGGER_NAME.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Configured logger instance
|
|
115
|
+
"""
|
|
116
|
+
# Ensure logging is configured
|
|
117
|
+
configure_logging()
|
|
118
|
+
|
|
119
|
+
if name:
|
|
120
|
+
# Create child logger: dispatch_agents.grpc_server, etc.
|
|
121
|
+
if name.startswith(SDK_LOGGER_NAME):
|
|
122
|
+
return logging.getLogger(name)
|
|
123
|
+
return logging.getLogger(f"{SDK_LOGGER_NAME}.{name}")
|
|
124
|
+
return logging.getLogger(SDK_LOGGER_NAME)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def is_verbose() -> bool:
|
|
128
|
+
"""Check if verbose logging is enabled.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
True if DISPATCH_VERBOSE is set or log level is DEBUG
|
|
132
|
+
"""
|
|
133
|
+
return _get_log_level() == logging.DEBUG
|