chuk-tool-processor 0.6.12__py3-none-any.whl → 0.6.14__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.
Potentially problematic release.
This version of chuk-tool-processor might be problematic. Click here for more details.
- chuk_tool_processor/core/__init__.py +1 -1
- chuk_tool_processor/core/exceptions.py +10 -4
- chuk_tool_processor/core/processor.py +97 -97
- chuk_tool_processor/execution/strategies/inprocess_strategy.py +142 -150
- chuk_tool_processor/execution/strategies/subprocess_strategy.py +200 -205
- chuk_tool_processor/execution/tool_executor.py +82 -84
- chuk_tool_processor/execution/wrappers/caching.py +102 -103
- chuk_tool_processor/execution/wrappers/rate_limiting.py +45 -42
- chuk_tool_processor/execution/wrappers/retry.py +23 -25
- chuk_tool_processor/logging/__init__.py +23 -17
- chuk_tool_processor/logging/context.py +40 -45
- chuk_tool_processor/logging/formatter.py +22 -21
- chuk_tool_processor/logging/helpers.py +24 -38
- chuk_tool_processor/logging/metrics.py +11 -13
- chuk_tool_processor/mcp/__init__.py +8 -12
- chuk_tool_processor/mcp/mcp_tool.py +124 -112
- chuk_tool_processor/mcp/register_mcp_tools.py +17 -17
- chuk_tool_processor/mcp/setup_mcp_http_streamable.py +11 -13
- chuk_tool_processor/mcp/setup_mcp_sse.py +11 -13
- chuk_tool_processor/mcp/setup_mcp_stdio.py +7 -9
- chuk_tool_processor/mcp/stream_manager.py +168 -204
- chuk_tool_processor/mcp/transport/__init__.py +4 -4
- chuk_tool_processor/mcp/transport/base_transport.py +43 -58
- chuk_tool_processor/mcp/transport/http_streamable_transport.py +145 -163
- chuk_tool_processor/mcp/transport/sse_transport.py +217 -255
- chuk_tool_processor/mcp/transport/stdio_transport.py +188 -190
- chuk_tool_processor/models/__init__.py +1 -1
- chuk_tool_processor/models/execution_strategy.py +16 -21
- chuk_tool_processor/models/streaming_tool.py +28 -25
- chuk_tool_processor/models/tool_call.py +19 -34
- chuk_tool_processor/models/tool_export_mixin.py +22 -8
- chuk_tool_processor/models/tool_result.py +40 -77
- chuk_tool_processor/models/validated_tool.py +14 -16
- chuk_tool_processor/plugins/__init__.py +1 -1
- chuk_tool_processor/plugins/discovery.py +10 -10
- chuk_tool_processor/plugins/parsers/__init__.py +1 -1
- chuk_tool_processor/plugins/parsers/base.py +1 -2
- chuk_tool_processor/plugins/parsers/function_call_tool.py +13 -8
- chuk_tool_processor/plugins/parsers/json_tool.py +4 -3
- chuk_tool_processor/plugins/parsers/openai_tool.py +12 -7
- chuk_tool_processor/plugins/parsers/xml_tool.py +4 -4
- chuk_tool_processor/registry/__init__.py +12 -12
- chuk_tool_processor/registry/auto_register.py +22 -30
- chuk_tool_processor/registry/decorators.py +127 -129
- chuk_tool_processor/registry/interface.py +26 -23
- chuk_tool_processor/registry/metadata.py +27 -22
- chuk_tool_processor/registry/provider.py +17 -18
- chuk_tool_processor/registry/providers/__init__.py +16 -19
- chuk_tool_processor/registry/providers/memory.py +18 -25
- chuk_tool_processor/registry/tool_export.py +42 -51
- chuk_tool_processor/utils/validation.py +15 -16
- {chuk_tool_processor-0.6.12.dist-info → chuk_tool_processor-0.6.14.dist-info}/METADATA +1 -1
- chuk_tool_processor-0.6.14.dist-info/RECORD +60 -0
- chuk_tool_processor-0.6.12.dist-info/RECORD +0 -60
- {chuk_tool_processor-0.6.12.dist-info → chuk_tool_processor-0.6.14.dist-info}/WHEEL +0 -0
- {chuk_tool_processor-0.6.12.dist-info → chuk_tool_processor-0.6.14.dist-info}/top_level.txt +0 -0
|
@@ -7,44 +7,46 @@ Two layers of limits are enforced:
|
|
|
7
7
|
* **Global** - ``<N requests> / <period>`` over *all* tools.
|
|
8
8
|
* **Per-tool** - independent ``<N requests> / <period>`` windows.
|
|
9
9
|
|
|
10
|
-
A simple sliding-window algorithm with timestamp queues is used.
|
|
10
|
+
A simple sliding-window algorithm with timestamp queues is used.
|
|
11
11
|
`asyncio.Lock` guards shared state so the wrapper can be used safely from
|
|
12
12
|
multiple coroutines.
|
|
13
13
|
"""
|
|
14
|
+
|
|
14
15
|
from __future__ import annotations
|
|
15
16
|
|
|
16
17
|
import asyncio
|
|
17
18
|
import inspect
|
|
18
19
|
import time
|
|
19
|
-
from typing import Any
|
|
20
|
+
from typing import Any
|
|
20
21
|
|
|
22
|
+
from chuk_tool_processor.logging import get_logger
|
|
21
23
|
from chuk_tool_processor.models.tool_call import ToolCall
|
|
22
24
|
from chuk_tool_processor.models.tool_result import ToolResult
|
|
23
|
-
from chuk_tool_processor.logging import get_logger
|
|
24
25
|
|
|
25
26
|
logger = get_logger("chuk_tool_processor.execution.wrappers.rate_limiting")
|
|
26
27
|
|
|
28
|
+
|
|
27
29
|
# --------------------------------------------------------------------------- #
|
|
28
30
|
# Core limiter
|
|
29
31
|
# --------------------------------------------------------------------------- #
|
|
30
32
|
class RateLimiter:
|
|
31
33
|
"""
|
|
32
34
|
Async-native rate limiter for controlling execution frequency.
|
|
33
|
-
|
|
35
|
+
|
|
34
36
|
Implements a sliding window algorithm to enforce rate limits both globally
|
|
35
37
|
and per-tool. All operations are thread-safe using asyncio locks.
|
|
36
38
|
"""
|
|
37
|
-
|
|
39
|
+
|
|
38
40
|
def __init__(
|
|
39
41
|
self,
|
|
40
42
|
*,
|
|
41
|
-
global_limit:
|
|
43
|
+
global_limit: int | None = None,
|
|
42
44
|
global_period: float = 60.0,
|
|
43
|
-
tool_limits:
|
|
45
|
+
tool_limits: dict[str, tuple[int, float]] | None = None,
|
|
44
46
|
) -> None:
|
|
45
47
|
"""
|
|
46
48
|
Initialize the rate limiter.
|
|
47
|
-
|
|
49
|
+
|
|
48
50
|
Args:
|
|
49
51
|
global_limit: Maximum global requests per period (None = no limit)
|
|
50
52
|
global_period: Time period in seconds for the global limit
|
|
@@ -55,13 +57,13 @@ class RateLimiter:
|
|
|
55
57
|
self.tool_limits = tool_limits or {}
|
|
56
58
|
|
|
57
59
|
# Timestamp queues
|
|
58
|
-
self._global_ts:
|
|
59
|
-
self._tool_ts:
|
|
60
|
+
self._global_ts: list[float] = []
|
|
61
|
+
self._tool_ts: dict[str, list[float]] = {}
|
|
60
62
|
|
|
61
63
|
# Locks for thread safety
|
|
62
64
|
self._global_lock = asyncio.Lock()
|
|
63
|
-
self._tool_locks:
|
|
64
|
-
|
|
65
|
+
self._tool_locks: dict[str, asyncio.Lock] = {}
|
|
66
|
+
|
|
65
67
|
logger.debug(
|
|
66
68
|
f"Initialized rate limiter: global={global_limit}/{global_period}s, "
|
|
67
69
|
f"tool-specific={len(self.tool_limits)} tools"
|
|
@@ -77,7 +79,7 @@ class RateLimiter:
|
|
|
77
79
|
async with self._global_lock:
|
|
78
80
|
now = time.monotonic()
|
|
79
81
|
cutoff = now - self.global_period
|
|
80
|
-
|
|
82
|
+
|
|
81
83
|
# Prune expired timestamps
|
|
82
84
|
self._global_ts = [t for t in self._global_ts if t > cutoff]
|
|
83
85
|
|
|
@@ -88,7 +90,7 @@ class RateLimiter:
|
|
|
88
90
|
|
|
89
91
|
# Calculate wait time until a slot becomes available
|
|
90
92
|
wait = (self._global_ts[0] + self.global_period) - now
|
|
91
|
-
|
|
93
|
+
|
|
92
94
|
logger.debug(f"Global rate limit reached, waiting {wait:.2f}s")
|
|
93
95
|
await asyncio.sleep(wait)
|
|
94
96
|
|
|
@@ -105,7 +107,7 @@ class RateLimiter:
|
|
|
105
107
|
async with lock:
|
|
106
108
|
now = time.monotonic()
|
|
107
109
|
cutoff = now - period
|
|
108
|
-
|
|
110
|
+
|
|
109
111
|
# Prune expired timestamps in-place
|
|
110
112
|
buf[:] = [t for t in buf if t > cutoff]
|
|
111
113
|
|
|
@@ -116,7 +118,7 @@ class RateLimiter:
|
|
|
116
118
|
|
|
117
119
|
# Calculate wait time until a slot becomes available
|
|
118
120
|
wait = (buf[0] + period) - now
|
|
119
|
-
|
|
121
|
+
|
|
120
122
|
logger.debug(f"Tool '{tool}' rate limit reached, waiting {wait:.2f}s")
|
|
121
123
|
await asyncio.sleep(wait)
|
|
122
124
|
|
|
@@ -124,32 +126,32 @@ class RateLimiter:
|
|
|
124
126
|
async def wait(self, tool: str) -> None:
|
|
125
127
|
"""
|
|
126
128
|
Block until rate limits allow execution.
|
|
127
|
-
|
|
129
|
+
|
|
128
130
|
This method blocks until both global and tool-specific rate limits
|
|
129
131
|
allow one more execution of the specified tool.
|
|
130
|
-
|
|
132
|
+
|
|
131
133
|
Args:
|
|
132
134
|
tool: Name of the tool being executed
|
|
133
135
|
"""
|
|
134
136
|
await self._acquire_global()
|
|
135
137
|
await self._acquire_tool(tool)
|
|
136
|
-
|
|
137
|
-
async def check_limits(self, tool: str) ->
|
|
138
|
+
|
|
139
|
+
async def check_limits(self, tool: str) -> tuple[bool, bool]:
|
|
138
140
|
"""
|
|
139
141
|
Check if the tool would be rate limited without consuming a slot.
|
|
140
|
-
|
|
142
|
+
|
|
141
143
|
This is a non-blocking method useful for checking limits without
|
|
142
144
|
affecting the rate limiting state.
|
|
143
|
-
|
|
145
|
+
|
|
144
146
|
Args:
|
|
145
147
|
tool: Name of the tool to check
|
|
146
|
-
|
|
148
|
+
|
|
147
149
|
Returns:
|
|
148
150
|
Tuple of (global_limit_reached, tool_limit_reached)
|
|
149
151
|
"""
|
|
150
152
|
global_limited = False
|
|
151
153
|
tool_limited = False
|
|
152
|
-
|
|
154
|
+
|
|
153
155
|
# Check global limit
|
|
154
156
|
if self.global_limit is not None:
|
|
155
157
|
async with self._global_lock:
|
|
@@ -157,7 +159,7 @@ class RateLimiter:
|
|
|
157
159
|
cutoff = now - self.global_period
|
|
158
160
|
active_ts = [t for t in self._global_ts if t > cutoff]
|
|
159
161
|
global_limited = len(active_ts) >= self.global_limit
|
|
160
|
-
|
|
162
|
+
|
|
161
163
|
# Check tool limit
|
|
162
164
|
if tool in self.tool_limits:
|
|
163
165
|
limit, period = self.tool_limits[tool]
|
|
@@ -167,7 +169,7 @@ class RateLimiter:
|
|
|
167
169
|
buf = self._tool_ts.get(tool, [])
|
|
168
170
|
active_ts = [t for t in buf if t > cutoff]
|
|
169
171
|
tool_limited = len(active_ts) >= limit
|
|
170
|
-
|
|
172
|
+
|
|
171
173
|
return global_limited, tool_limited
|
|
172
174
|
|
|
173
175
|
|
|
@@ -177,7 +179,7 @@ class RateLimiter:
|
|
|
177
179
|
class RateLimitedToolExecutor:
|
|
178
180
|
"""
|
|
179
181
|
Executor wrapper that applies rate limiting to tool executions.
|
|
180
|
-
|
|
182
|
+
|
|
181
183
|
This wrapper delegates to another executor but ensures that all
|
|
182
184
|
tool calls respect the configured rate limits.
|
|
183
185
|
"""
|
|
@@ -185,48 +187,48 @@ class RateLimitedToolExecutor:
|
|
|
185
187
|
def __init__(self, executor: Any, limiter: RateLimiter) -> None:
|
|
186
188
|
"""
|
|
187
189
|
Initialize the rate-limited executor.
|
|
188
|
-
|
|
190
|
+
|
|
189
191
|
Args:
|
|
190
192
|
executor: The underlying executor to wrap
|
|
191
193
|
limiter: The RateLimiter that controls execution frequency
|
|
192
194
|
"""
|
|
193
195
|
self.executor = executor
|
|
194
196
|
self.limiter = limiter
|
|
195
|
-
logger.debug(
|
|
197
|
+
logger.debug("Initialized rate-limited executor")
|
|
196
198
|
|
|
197
199
|
async def execute(
|
|
198
200
|
self,
|
|
199
|
-
calls:
|
|
200
|
-
timeout:
|
|
201
|
+
calls: list[ToolCall],
|
|
202
|
+
timeout: float | None = None,
|
|
201
203
|
use_cache: bool = True,
|
|
202
|
-
) ->
|
|
204
|
+
) -> list[ToolResult]:
|
|
203
205
|
"""
|
|
204
206
|
Execute tool calls while respecting rate limits.
|
|
205
|
-
|
|
207
|
+
|
|
206
208
|
This method blocks until rate limits allow execution, then delegates
|
|
207
209
|
to the underlying executor.
|
|
208
|
-
|
|
210
|
+
|
|
209
211
|
Args:
|
|
210
212
|
calls: List of tool calls to execute
|
|
211
213
|
timeout: Optional timeout for execution
|
|
212
214
|
use_cache: Whether to use cached results (forwarded to underlying executor)
|
|
213
|
-
|
|
215
|
+
|
|
214
216
|
Returns:
|
|
215
217
|
List of tool results
|
|
216
218
|
"""
|
|
217
219
|
if not calls:
|
|
218
220
|
return []
|
|
219
|
-
|
|
221
|
+
|
|
220
222
|
# Block for each call *before* dispatching to the wrapped executor
|
|
221
223
|
for c in calls:
|
|
222
224
|
await self.limiter.wait(c.tool)
|
|
223
|
-
|
|
225
|
+
|
|
224
226
|
# Check if the executor has a use_cache parameter
|
|
225
227
|
if hasattr(self.executor, "execute"):
|
|
226
228
|
sig = inspect.signature(self.executor.execute)
|
|
227
229
|
if "use_cache" in sig.parameters:
|
|
228
230
|
return await self.executor.execute(calls, timeout=timeout, use_cache=use_cache)
|
|
229
|
-
|
|
231
|
+
|
|
230
232
|
# Fall back to standard execute method
|
|
231
233
|
return await self.executor.execute(calls, timeout=timeout)
|
|
232
234
|
|
|
@@ -237,7 +239,7 @@ class RateLimitedToolExecutor:
|
|
|
237
239
|
def rate_limited(limit: int, period: float = 60.0):
|
|
238
240
|
"""
|
|
239
241
|
Class decorator that marks a Tool with default rate-limit metadata.
|
|
240
|
-
|
|
242
|
+
|
|
241
243
|
This allows higher-level code to detect and configure rate limiting
|
|
242
244
|
for the tool class.
|
|
243
245
|
|
|
@@ -246,17 +248,18 @@ def rate_limited(limit: int, period: float = 60.0):
|
|
|
246
248
|
class WeatherTool:
|
|
247
249
|
async def execute(self, location: str) -> Dict[str, Any]:
|
|
248
250
|
# Implementation
|
|
249
|
-
|
|
251
|
+
|
|
250
252
|
Args:
|
|
251
253
|
limit: Maximum number of calls allowed in the period
|
|
252
254
|
period: Time period in seconds
|
|
253
|
-
|
|
255
|
+
|
|
254
256
|
Returns:
|
|
255
257
|
Decorated class with rate limit metadata
|
|
256
258
|
"""
|
|
259
|
+
|
|
257
260
|
def decorator(cls):
|
|
258
261
|
cls._rate_limit = limit
|
|
259
262
|
cls._rate_period = period
|
|
260
263
|
return cls
|
|
261
264
|
|
|
262
|
-
return decorator
|
|
265
|
+
return decorator
|
|
@@ -6,13 +6,14 @@ Adds exponential-back-off retry logic and *deadline-aware* timeout handling so a
|
|
|
6
6
|
`timeout=` passed by callers is treated as the **total wall-clock budget** for
|
|
7
7
|
all attempts of a single tool call.
|
|
8
8
|
"""
|
|
9
|
+
|
|
9
10
|
from __future__ import annotations
|
|
10
11
|
|
|
11
12
|
import asyncio
|
|
12
13
|
import random
|
|
13
14
|
import time
|
|
14
|
-
from datetime import
|
|
15
|
-
from typing import Any
|
|
15
|
+
from datetime import UTC, datetime
|
|
16
|
+
from typing import Any
|
|
16
17
|
|
|
17
18
|
from chuk_tool_processor.logging import get_logger
|
|
18
19
|
from chuk_tool_processor.models.tool_call import ToolCall
|
|
@@ -33,8 +34,8 @@ class RetryConfig:
|
|
|
33
34
|
base_delay: float = 1.0,
|
|
34
35
|
max_delay: float = 60.0,
|
|
35
36
|
jitter: bool = True,
|
|
36
|
-
retry_on_exceptions:
|
|
37
|
-
retry_on_error_substrings:
|
|
37
|
+
retry_on_exceptions: list[type[Exception]] | None = None,
|
|
38
|
+
retry_on_error_substrings: list[str] | None = None,
|
|
38
39
|
):
|
|
39
40
|
if max_retries < 0:
|
|
40
41
|
raise ValueError("max_retries cannot be negative")
|
|
@@ -52,8 +53,8 @@ class RetryConfig:
|
|
|
52
53
|
self,
|
|
53
54
|
attempt: int,
|
|
54
55
|
*,
|
|
55
|
-
error:
|
|
56
|
-
error_str:
|
|
56
|
+
error: Exception | None = None,
|
|
57
|
+
error_str: str | None = None,
|
|
57
58
|
) -> bool:
|
|
58
59
|
"""Return *True* iff another retry is allowed for this attempt."""
|
|
59
60
|
if attempt >= self.max_retries:
|
|
@@ -66,17 +67,14 @@ class RetryConfig:
|
|
|
66
67
|
if error is not None and any(isinstance(error, exc) for exc in self.retry_on_exceptions):
|
|
67
68
|
return True
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
return True
|
|
71
|
-
|
|
72
|
-
return False
|
|
70
|
+
return bool(error_str and any(substr in error_str for substr in self.retry_on_error_substrings))
|
|
73
71
|
|
|
74
72
|
# --------------------------------------------------------------------- #
|
|
75
73
|
# Back-off
|
|
76
74
|
# --------------------------------------------------------------------- #
|
|
77
75
|
def get_delay(self, attempt: int) -> float:
|
|
78
76
|
"""Exponential back-off delay for *attempt* (0-based)."""
|
|
79
|
-
delay = min(self.base_delay * (2
|
|
77
|
+
delay = min(self.base_delay * (2**attempt), self.max_delay)
|
|
80
78
|
if self.jitter:
|
|
81
79
|
delay *= 0.5 + random.random() # jitter in [0.5, 1.5)
|
|
82
80
|
return delay
|
|
@@ -94,8 +92,8 @@ class RetryableToolExecutor:
|
|
|
94
92
|
self,
|
|
95
93
|
executor: Any,
|
|
96
94
|
*,
|
|
97
|
-
default_config:
|
|
98
|
-
tool_configs:
|
|
95
|
+
default_config: RetryConfig | None = None,
|
|
96
|
+
tool_configs: dict[str, RetryConfig] | None = None,
|
|
99
97
|
):
|
|
100
98
|
self.executor = executor
|
|
101
99
|
self.default_config = default_config or RetryConfig()
|
|
@@ -109,15 +107,15 @@ class RetryableToolExecutor:
|
|
|
109
107
|
|
|
110
108
|
async def execute(
|
|
111
109
|
self,
|
|
112
|
-
calls:
|
|
110
|
+
calls: list[ToolCall],
|
|
113
111
|
*,
|
|
114
|
-
timeout:
|
|
112
|
+
timeout: float | None = None,
|
|
115
113
|
use_cache: bool = True,
|
|
116
|
-
) ->
|
|
114
|
+
) -> list[ToolResult]:
|
|
117
115
|
if not calls:
|
|
118
116
|
return []
|
|
119
117
|
|
|
120
|
-
out:
|
|
118
|
+
out: list[ToolResult] = []
|
|
121
119
|
for call in calls:
|
|
122
120
|
cfg = self._config_for(call.tool)
|
|
123
121
|
out.append(await self._execute_single(call, cfg, timeout, use_cache))
|
|
@@ -130,11 +128,11 @@ class RetryableToolExecutor:
|
|
|
130
128
|
self,
|
|
131
129
|
call: ToolCall,
|
|
132
130
|
cfg: RetryConfig,
|
|
133
|
-
timeout:
|
|
131
|
+
timeout: float | None,
|
|
134
132
|
use_cache: bool,
|
|
135
133
|
) -> ToolResult:
|
|
136
134
|
attempt = 0
|
|
137
|
-
last_error:
|
|
135
|
+
last_error: str | None = None
|
|
138
136
|
pid = 0
|
|
139
137
|
machine = "unknown"
|
|
140
138
|
|
|
@@ -156,8 +154,8 @@ class RetryableToolExecutor:
|
|
|
156
154
|
tool=call.tool,
|
|
157
155
|
result=None,
|
|
158
156
|
error=f"Timeout after {timeout}s",
|
|
159
|
-
start_time=datetime.now(
|
|
160
|
-
end_time=datetime.now(
|
|
157
|
+
start_time=datetime.now(UTC),
|
|
158
|
+
end_time=datetime.now(UTC),
|
|
161
159
|
machine=machine,
|
|
162
160
|
pid=pid,
|
|
163
161
|
attempts=attempt,
|
|
@@ -168,7 +166,7 @@ class RetryableToolExecutor:
|
|
|
168
166
|
# ---------------------------------------------------------------- #
|
|
169
167
|
# Execute one attempt
|
|
170
168
|
# ---------------------------------------------------------------- #
|
|
171
|
-
start_time = datetime.now(
|
|
169
|
+
start_time = datetime.now(UTC)
|
|
172
170
|
try:
|
|
173
171
|
kwargs = {"timeout": remaining} if remaining is not None else {}
|
|
174
172
|
if hasattr(self.executor, "use_cache"):
|
|
@@ -215,7 +213,7 @@ class RetryableToolExecutor:
|
|
|
215
213
|
attempt += 1
|
|
216
214
|
continue
|
|
217
215
|
|
|
218
|
-
end_time = datetime.now(
|
|
216
|
+
end_time = datetime.now(UTC)
|
|
219
217
|
return ToolResult(
|
|
220
218
|
tool=call.tool,
|
|
221
219
|
result=None,
|
|
@@ -246,8 +244,8 @@ def retryable(
|
|
|
246
244
|
base_delay: float = 1.0,
|
|
247
245
|
max_delay: float = 60.0,
|
|
248
246
|
jitter: bool = True,
|
|
249
|
-
retry_on_exceptions:
|
|
250
|
-
retry_on_error_substrings:
|
|
247
|
+
retry_on_exceptions: list[type[Exception]] | None = None,
|
|
248
|
+
retry_on_error_substrings: list[str] | None = None,
|
|
251
249
|
):
|
|
252
250
|
"""
|
|
253
251
|
Class decorator that attaches a :class:`RetryConfig` to a *tool* class.
|
|
@@ -11,40 +11,44 @@ Key components:
|
|
|
11
11
|
- Metrics collection for tools and parsers
|
|
12
12
|
- Async-friendly context managers for spans and requests
|
|
13
13
|
"""
|
|
14
|
+
|
|
14
15
|
from __future__ import annotations
|
|
15
16
|
|
|
16
17
|
import logging
|
|
17
18
|
import sys
|
|
18
19
|
|
|
20
|
+
|
|
19
21
|
# Auto-initialize shutdown error suppression when logging package is imported
|
|
20
22
|
def _initialize_shutdown_fixes():
|
|
21
23
|
"""Initialize shutdown error suppression when the package is imported."""
|
|
22
24
|
try:
|
|
23
25
|
from .context import _setup_shutdown_error_suppression
|
|
26
|
+
|
|
24
27
|
_setup_shutdown_error_suppression()
|
|
25
28
|
except ImportError:
|
|
26
29
|
pass
|
|
27
30
|
|
|
31
|
+
|
|
28
32
|
# Initialize when package is imported
|
|
29
33
|
_initialize_shutdown_fixes()
|
|
30
34
|
|
|
31
35
|
# Import internal modules in correct order to avoid circular imports
|
|
32
36
|
# First, formatter has no internal dependencies
|
|
33
|
-
from .formatter import StructuredFormatter
|
|
34
|
-
|
|
35
37
|
# Second, context only depends on formatter
|
|
36
|
-
from .context import LogContext,
|
|
38
|
+
from .context import LogContext, StructuredAdapter, get_logger, log_context # noqa: E402
|
|
39
|
+
from .formatter import StructuredFormatter # noqa: E402
|
|
37
40
|
|
|
38
41
|
# Third, helpers depend on context
|
|
39
|
-
from .helpers import log_context_span,
|
|
42
|
+
from .helpers import log_context_span, log_tool_call, request_logging # noqa: E402
|
|
40
43
|
|
|
41
44
|
# Fourth, metrics depend on helpers and context
|
|
42
|
-
from .metrics import
|
|
45
|
+
from .metrics import MetricsLogger, metrics # noqa: E402
|
|
43
46
|
|
|
44
47
|
__all__ = [
|
|
45
48
|
"get_logger",
|
|
46
49
|
"log_context",
|
|
47
50
|
"LogContext",
|
|
51
|
+
"StructuredAdapter",
|
|
48
52
|
"log_context_span",
|
|
49
53
|
"request_logging",
|
|
50
54
|
"log_tool_call",
|
|
@@ -53,6 +57,7 @@ __all__ = [
|
|
|
53
57
|
"setup_logging",
|
|
54
58
|
]
|
|
55
59
|
|
|
60
|
+
|
|
56
61
|
# --------------------------------------------------------------------------- #
|
|
57
62
|
# Setup function for configuring logging
|
|
58
63
|
# --------------------------------------------------------------------------- #
|
|
@@ -63,7 +68,7 @@ async def setup_logging(
|
|
|
63
68
|
) -> None:
|
|
64
69
|
"""
|
|
65
70
|
Set up the logging system.
|
|
66
|
-
|
|
71
|
+
|
|
67
72
|
Args:
|
|
68
73
|
level: Logging level (default: INFO)
|
|
69
74
|
structured: Whether to use structured JSON logging
|
|
@@ -72,39 +77,40 @@ async def setup_logging(
|
|
|
72
77
|
# Get the root logger
|
|
73
78
|
root_logger = logging.getLogger("chuk_tool_processor")
|
|
74
79
|
root_logger.setLevel(level)
|
|
75
|
-
|
|
80
|
+
|
|
76
81
|
# Create formatter
|
|
77
|
-
formatter =
|
|
78
|
-
|
|
82
|
+
formatter = (
|
|
83
|
+
StructuredFormatter()
|
|
84
|
+
if structured
|
|
85
|
+
else logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
|
79
86
|
)
|
|
80
|
-
|
|
87
|
+
|
|
81
88
|
# Always add a dummy handler and remove it to satisfy test expectations
|
|
82
89
|
dummy_handler = logging.StreamHandler()
|
|
83
90
|
root_logger.addHandler(dummy_handler)
|
|
84
91
|
root_logger.removeHandler(dummy_handler)
|
|
85
|
-
|
|
92
|
+
|
|
86
93
|
# Now clear any remaining handlers
|
|
87
94
|
for handler in list(root_logger.handlers):
|
|
88
95
|
root_logger.removeHandler(handler)
|
|
89
|
-
|
|
96
|
+
|
|
90
97
|
# Add console handler
|
|
91
98
|
console_handler = logging.StreamHandler(sys.stderr)
|
|
92
99
|
console_handler.setLevel(level)
|
|
93
100
|
console_handler.setFormatter(formatter)
|
|
94
101
|
root_logger.addHandler(console_handler)
|
|
95
|
-
|
|
102
|
+
|
|
96
103
|
# Add file handler if specified
|
|
97
104
|
if log_file:
|
|
98
105
|
file_handler = logging.FileHandler(log_file)
|
|
99
106
|
file_handler.setLevel(level)
|
|
100
107
|
file_handler.setFormatter(formatter)
|
|
101
108
|
root_logger.addHandler(file_handler)
|
|
102
|
-
|
|
109
|
+
|
|
103
110
|
# Log startup with internal logger
|
|
104
111
|
internal_logger = logging.getLogger("chuk_tool_processor.logging")
|
|
105
112
|
internal_logger.info(
|
|
106
|
-
"Logging initialized",
|
|
107
|
-
extra={"context": {"level": logging.getLevelName(level), "structured": structured}}
|
|
113
|
+
"Logging initialized", extra={"context": {"level": logging.getLevelName(level), "structured": structured}}
|
|
108
114
|
)
|
|
109
115
|
|
|
110
116
|
|
|
@@ -115,4 +121,4 @@ root_logger.setLevel(logging.INFO)
|
|
|
115
121
|
_handler = logging.StreamHandler(sys.stderr)
|
|
116
122
|
_handler.setLevel(logging.INFO)
|
|
117
123
|
_handler.setFormatter(StructuredFormatter())
|
|
118
|
-
root_logger.addHandler(_handler)
|
|
124
|
+
root_logger.addHandler(_handler)
|