proxilion 0.0.1__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.
- proxilion/__init__.py +136 -0
- proxilion/audit/__init__.py +133 -0
- proxilion/audit/base_exporters.py +527 -0
- proxilion/audit/compliance/__init__.py +130 -0
- proxilion/audit/compliance/base.py +457 -0
- proxilion/audit/compliance/eu_ai_act.py +603 -0
- proxilion/audit/compliance/iso27001.py +544 -0
- proxilion/audit/compliance/soc2.py +491 -0
- proxilion/audit/events.py +493 -0
- proxilion/audit/explainability.py +1173 -0
- proxilion/audit/exporters/__init__.py +58 -0
- proxilion/audit/exporters/aws_s3.py +636 -0
- proxilion/audit/exporters/azure_storage.py +608 -0
- proxilion/audit/exporters/cloud_base.py +468 -0
- proxilion/audit/exporters/gcp_storage.py +570 -0
- proxilion/audit/exporters/multi_exporter.py +498 -0
- proxilion/audit/hash_chain.py +652 -0
- proxilion/audit/logger.py +543 -0
- proxilion/caching/__init__.py +49 -0
- proxilion/caching/tool_cache.py +633 -0
- proxilion/context/__init__.py +73 -0
- proxilion/context/context_window.py +556 -0
- proxilion/context/message_history.py +505 -0
- proxilion/context/session.py +735 -0
- proxilion/contrib/__init__.py +51 -0
- proxilion/contrib/anthropic.py +609 -0
- proxilion/contrib/google.py +1012 -0
- proxilion/contrib/langchain.py +641 -0
- proxilion/contrib/mcp.py +893 -0
- proxilion/contrib/openai.py +646 -0
- proxilion/core.py +3058 -0
- proxilion/decorators.py +966 -0
- proxilion/engines/__init__.py +287 -0
- proxilion/engines/base.py +266 -0
- proxilion/engines/casbin_engine.py +412 -0
- proxilion/engines/opa_engine.py +493 -0
- proxilion/engines/simple.py +437 -0
- proxilion/exceptions.py +887 -0
- proxilion/guards/__init__.py +54 -0
- proxilion/guards/input_guard.py +522 -0
- proxilion/guards/output_guard.py +634 -0
- proxilion/observability/__init__.py +198 -0
- proxilion/observability/cost_tracker.py +866 -0
- proxilion/observability/hooks.py +683 -0
- proxilion/observability/metrics.py +798 -0
- proxilion/observability/session_cost_tracker.py +1063 -0
- proxilion/policies/__init__.py +67 -0
- proxilion/policies/base.py +304 -0
- proxilion/policies/builtin.py +486 -0
- proxilion/policies/registry.py +376 -0
- proxilion/providers/__init__.py +201 -0
- proxilion/providers/adapter.py +468 -0
- proxilion/providers/anthropic_adapter.py +330 -0
- proxilion/providers/gemini_adapter.py +391 -0
- proxilion/providers/openai_adapter.py +294 -0
- proxilion/py.typed +0 -0
- proxilion/resilience/__init__.py +81 -0
- proxilion/resilience/degradation.py +615 -0
- proxilion/resilience/fallback.py +555 -0
- proxilion/resilience/retry.py +554 -0
- proxilion/scheduling/__init__.py +57 -0
- proxilion/scheduling/priority_queue.py +419 -0
- proxilion/scheduling/scheduler.py +459 -0
- proxilion/security/__init__.py +244 -0
- proxilion/security/agent_trust.py +968 -0
- proxilion/security/behavioral_drift.py +794 -0
- proxilion/security/cascade_protection.py +869 -0
- proxilion/security/circuit_breaker.py +428 -0
- proxilion/security/cost_limiter.py +690 -0
- proxilion/security/idor_protection.py +460 -0
- proxilion/security/intent_capsule.py +849 -0
- proxilion/security/intent_validator.py +495 -0
- proxilion/security/memory_integrity.py +767 -0
- proxilion/security/rate_limiter.py +509 -0
- proxilion/security/scope_enforcer.py +680 -0
- proxilion/security/sequence_validator.py +636 -0
- proxilion/security/trust_boundaries.py +784 -0
- proxilion/streaming/__init__.py +70 -0
- proxilion/streaming/detector.py +761 -0
- proxilion/streaming/transformer.py +674 -0
- proxilion/timeouts/__init__.py +55 -0
- proxilion/timeouts/decorators.py +477 -0
- proxilion/timeouts/manager.py +545 -0
- proxilion/tools/__init__.py +69 -0
- proxilion/tools/decorators.py +493 -0
- proxilion/tools/registry.py +732 -0
- proxilion/types.py +339 -0
- proxilion/validation/__init__.py +93 -0
- proxilion/validation/pydantic_schema.py +351 -0
- proxilion/validation/schema.py +651 -0
- proxilion-0.0.1.dist-info/METADATA +872 -0
- proxilion-0.0.1.dist-info/RECORD +94 -0
- proxilion-0.0.1.dist-info/WHEEL +4 -0
- proxilion-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Timeout management for AI agent operations.
|
|
3
|
+
|
|
4
|
+
Provides configurable timeout handling with deadline tracking
|
|
5
|
+
for managing time budgets across multiple operations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import contextvars
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from contextlib import contextmanager
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TimeoutError(Exception):
|
|
19
|
+
"""
|
|
20
|
+
Raised when an operation exceeds its timeout.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
operation: Name of the operation that timed out.
|
|
24
|
+
timeout: The timeout value that was exceeded.
|
|
25
|
+
elapsed: Actual time elapsed before timeout.
|
|
26
|
+
message: Human-readable error message.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
message: str = "Operation timed out",
|
|
32
|
+
operation: str | None = None,
|
|
33
|
+
timeout: float | None = None,
|
|
34
|
+
elapsed: float | None = None,
|
|
35
|
+
) -> None:
|
|
36
|
+
self.operation = operation
|
|
37
|
+
self.timeout = timeout
|
|
38
|
+
self.elapsed = elapsed
|
|
39
|
+
self.message = message
|
|
40
|
+
super().__init__(self._format_message())
|
|
41
|
+
|
|
42
|
+
def _format_message(self) -> str:
|
|
43
|
+
"""Format the error message with details."""
|
|
44
|
+
parts = [self.message]
|
|
45
|
+
if self.operation:
|
|
46
|
+
parts.append(f"operation={self.operation}")
|
|
47
|
+
if self.timeout is not None:
|
|
48
|
+
parts.append(f"timeout={self.timeout:.2f}s")
|
|
49
|
+
if self.elapsed is not None:
|
|
50
|
+
parts.append(f"elapsed={self.elapsed:.2f}s")
|
|
51
|
+
return " ".join(parts)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class TimeoutConfig:
|
|
56
|
+
"""
|
|
57
|
+
Configuration for timeout management.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
default_timeout: Default timeout for operations in seconds.
|
|
61
|
+
tool_timeouts: Per-tool timeout overrides.
|
|
62
|
+
llm_timeout: Timeout for LLM API calls.
|
|
63
|
+
total_request_timeout: Total request budget timeout.
|
|
64
|
+
warn_at_percent: Percentage of timeout to trigger warning (0-100).
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
>>> config = TimeoutConfig(
|
|
68
|
+
... default_timeout=30.0,
|
|
69
|
+
... tool_timeouts={
|
|
70
|
+
... "web_search": 60.0,
|
|
71
|
+
... "database_query": 10.0,
|
|
72
|
+
... "file_read": 5.0,
|
|
73
|
+
... },
|
|
74
|
+
... llm_timeout=120.0,
|
|
75
|
+
... total_request_timeout=300.0,
|
|
76
|
+
... )
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
default_timeout: float = 30.0
|
|
80
|
+
tool_timeouts: dict[str, float] = field(default_factory=dict)
|
|
81
|
+
llm_timeout: float = 120.0
|
|
82
|
+
total_request_timeout: float = 300.0
|
|
83
|
+
warn_at_percent: float = 80.0
|
|
84
|
+
|
|
85
|
+
def get_timeout(self, operation: str) -> float:
|
|
86
|
+
"""
|
|
87
|
+
Get timeout for a specific operation.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
operation: Name of the operation or tool.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
Timeout value in seconds.
|
|
94
|
+
"""
|
|
95
|
+
return self.tool_timeouts.get(operation, self.default_timeout)
|
|
96
|
+
|
|
97
|
+
def set_tool_timeout(self, tool_name: str, timeout: float) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Set timeout for a specific tool.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
tool_name: Name of the tool.
|
|
103
|
+
timeout: Timeout value in seconds.
|
|
104
|
+
"""
|
|
105
|
+
self.tool_timeouts[tool_name] = timeout
|
|
106
|
+
|
|
107
|
+
def to_dict(self) -> dict[str, Any]:
|
|
108
|
+
"""Convert config to dictionary."""
|
|
109
|
+
return {
|
|
110
|
+
"default_timeout": self.default_timeout,
|
|
111
|
+
"tool_timeouts": dict(self.tool_timeouts),
|
|
112
|
+
"llm_timeout": self.llm_timeout,
|
|
113
|
+
"total_request_timeout": self.total_request_timeout,
|
|
114
|
+
"warn_at_percent": self.warn_at_percent,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def from_dict(cls, data: dict[str, Any]) -> TimeoutConfig:
|
|
119
|
+
"""Create config from dictionary."""
|
|
120
|
+
return cls(
|
|
121
|
+
default_timeout=data.get("default_timeout", 30.0),
|
|
122
|
+
tool_timeouts=data.get("tool_timeouts", {}),
|
|
123
|
+
llm_timeout=data.get("llm_timeout", 120.0),
|
|
124
|
+
total_request_timeout=data.get("total_request_timeout", 300.0),
|
|
125
|
+
warn_at_percent=data.get("warn_at_percent", 80.0),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Context variable for current deadline
|
|
130
|
+
_current_deadline: contextvars.ContextVar[DeadlineContext | None] = contextvars.ContextVar(
|
|
131
|
+
"proxilion_deadline", default=None
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def get_current_deadline() -> DeadlineContext | None:
|
|
136
|
+
"""Get the current deadline context if any."""
|
|
137
|
+
return _current_deadline.get()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class DeadlineContext:
|
|
141
|
+
"""
|
|
142
|
+
Context manager for tracking remaining time with a hard deadline.
|
|
143
|
+
|
|
144
|
+
Tracks the time budget for a request and provides methods to
|
|
145
|
+
check remaining time and whether the deadline has expired.
|
|
146
|
+
Supports nested deadlines (inner deadline cannot exceed outer).
|
|
147
|
+
|
|
148
|
+
Attributes:
|
|
149
|
+
timeout: Total timeout for this deadline context.
|
|
150
|
+
deadline: Monotonic timestamp when deadline expires.
|
|
151
|
+
started_at: Monotonic timestamp when context started.
|
|
152
|
+
operation: Optional name of the operation.
|
|
153
|
+
|
|
154
|
+
Example:
|
|
155
|
+
>>> async with DeadlineContext(timeout=30.0) as deadline:
|
|
156
|
+
... result1 = await call_tool1(timeout=deadline.remaining())
|
|
157
|
+
... result2 = await call_tool2(timeout=deadline.remaining())
|
|
158
|
+
... # Automatically raises TimeoutError if deadline exceeded
|
|
159
|
+
|
|
160
|
+
>>> # With synchronous context manager
|
|
161
|
+
>>> with DeadlineContext(timeout=10.0) as deadline:
|
|
162
|
+
... if deadline.remaining() > 5:
|
|
163
|
+
... do_slow_operation()
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
timeout: float,
|
|
169
|
+
operation: str | None = None,
|
|
170
|
+
raise_on_expire: bool = True,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""
|
|
173
|
+
Initialize deadline context.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
timeout: Timeout in seconds.
|
|
177
|
+
operation: Optional operation name for error messages.
|
|
178
|
+
raise_on_expire: Whether to raise TimeoutError when expired.
|
|
179
|
+
"""
|
|
180
|
+
self.timeout = timeout
|
|
181
|
+
self.operation = operation
|
|
182
|
+
self.raise_on_expire = raise_on_expire
|
|
183
|
+
self._started_at: float | None = None
|
|
184
|
+
self._deadline: float | None = None
|
|
185
|
+
self._parent: DeadlineContext | None = None
|
|
186
|
+
self._token: contextvars.Token | None = None
|
|
187
|
+
self._lock = threading.Lock()
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def started_at(self) -> float:
|
|
191
|
+
"""Get the start time (monotonic)."""
|
|
192
|
+
if self._started_at is None:
|
|
193
|
+
raise RuntimeError("DeadlineContext not started")
|
|
194
|
+
return self._started_at
|
|
195
|
+
|
|
196
|
+
@property
|
|
197
|
+
def deadline(self) -> float:
|
|
198
|
+
"""Get the deadline (monotonic timestamp)."""
|
|
199
|
+
if self._deadline is None:
|
|
200
|
+
raise RuntimeError("DeadlineContext not started")
|
|
201
|
+
return self._deadline
|
|
202
|
+
|
|
203
|
+
def _start(self) -> None:
|
|
204
|
+
"""Start the deadline tracking."""
|
|
205
|
+
now = time.monotonic()
|
|
206
|
+
self._started_at = now
|
|
207
|
+
self._deadline = now + self.timeout
|
|
208
|
+
|
|
209
|
+
# Check for parent deadline
|
|
210
|
+
self._parent = get_current_deadline()
|
|
211
|
+
if self._parent is not None:
|
|
212
|
+
# Inner deadline cannot exceed outer deadline
|
|
213
|
+
parent_deadline = self._parent.deadline
|
|
214
|
+
if self._deadline > parent_deadline:
|
|
215
|
+
self._deadline = parent_deadline
|
|
216
|
+
|
|
217
|
+
# Set as current deadline
|
|
218
|
+
self._token = _current_deadline.set(self)
|
|
219
|
+
|
|
220
|
+
def _stop(self) -> None:
|
|
221
|
+
"""Stop the deadline tracking."""
|
|
222
|
+
if self._token is not None:
|
|
223
|
+
_current_deadline.reset(self._token)
|
|
224
|
+
self._token = None
|
|
225
|
+
|
|
226
|
+
def remaining(self) -> float:
|
|
227
|
+
"""
|
|
228
|
+
Get remaining time until deadline.
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Remaining time in seconds.
|
|
232
|
+
|
|
233
|
+
Raises:
|
|
234
|
+
TimeoutError: If deadline has expired and raise_on_expire is True.
|
|
235
|
+
"""
|
|
236
|
+
with self._lock:
|
|
237
|
+
if self._deadline is None:
|
|
238
|
+
raise RuntimeError("DeadlineContext not started")
|
|
239
|
+
|
|
240
|
+
remaining = self._deadline - time.monotonic()
|
|
241
|
+
if remaining <= 0:
|
|
242
|
+
if self.raise_on_expire:
|
|
243
|
+
raise TimeoutError(
|
|
244
|
+
message="Deadline exceeded",
|
|
245
|
+
operation=self.operation,
|
|
246
|
+
timeout=self.timeout,
|
|
247
|
+
elapsed=self.elapsed(),
|
|
248
|
+
)
|
|
249
|
+
return 0.0
|
|
250
|
+
return remaining
|
|
251
|
+
|
|
252
|
+
def remaining_or_default(self, default: float) -> float:
|
|
253
|
+
"""
|
|
254
|
+
Get remaining time or default if expired.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
default: Default value to return if deadline expired.
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Remaining time or default.
|
|
261
|
+
"""
|
|
262
|
+
try:
|
|
263
|
+
return self.remaining()
|
|
264
|
+
except TimeoutError:
|
|
265
|
+
return default
|
|
266
|
+
|
|
267
|
+
def elapsed(self) -> float:
|
|
268
|
+
"""
|
|
269
|
+
Get elapsed time since start.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Elapsed time in seconds.
|
|
273
|
+
"""
|
|
274
|
+
if self._started_at is None:
|
|
275
|
+
return 0.0
|
|
276
|
+
return time.monotonic() - self._started_at
|
|
277
|
+
|
|
278
|
+
def is_expired(self) -> bool:
|
|
279
|
+
"""
|
|
280
|
+
Check if deadline has passed.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
True if deadline has expired.
|
|
284
|
+
"""
|
|
285
|
+
if self._deadline is None:
|
|
286
|
+
return False
|
|
287
|
+
return time.monotonic() >= self._deadline
|
|
288
|
+
|
|
289
|
+
def check(self) -> None:
|
|
290
|
+
"""
|
|
291
|
+
Check if deadline has expired and raise if so.
|
|
292
|
+
|
|
293
|
+
Raises:
|
|
294
|
+
TimeoutError: If deadline has expired.
|
|
295
|
+
"""
|
|
296
|
+
if self.is_expired():
|
|
297
|
+
raise TimeoutError(
|
|
298
|
+
message="Deadline exceeded",
|
|
299
|
+
operation=self.operation,
|
|
300
|
+
timeout=self.timeout,
|
|
301
|
+
elapsed=self.elapsed(),
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def get_timeout_for_operation(self, operation_timeout: float) -> float:
|
|
305
|
+
"""
|
|
306
|
+
Get effective timeout for a sub-operation.
|
|
307
|
+
|
|
308
|
+
Returns the minimum of the operation's timeout and
|
|
309
|
+
the remaining deadline time.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
operation_timeout: The desired timeout for the operation.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Effective timeout (minimum of operation_timeout and remaining).
|
|
316
|
+
"""
|
|
317
|
+
remaining = self.remaining()
|
|
318
|
+
return min(operation_timeout, remaining)
|
|
319
|
+
|
|
320
|
+
def __enter__(self) -> DeadlineContext:
|
|
321
|
+
"""Enter context (synchronous)."""
|
|
322
|
+
self._start()
|
|
323
|
+
return self
|
|
324
|
+
|
|
325
|
+
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
326
|
+
"""Exit context (synchronous)."""
|
|
327
|
+
self._stop()
|
|
328
|
+
|
|
329
|
+
async def __aenter__(self) -> DeadlineContext:
|
|
330
|
+
"""Enter context (asynchronous)."""
|
|
331
|
+
self._start()
|
|
332
|
+
return self
|
|
333
|
+
|
|
334
|
+
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
335
|
+
"""Exit context (asynchronous)."""
|
|
336
|
+
self._stop()
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class TimeoutManager:
|
|
340
|
+
"""
|
|
341
|
+
Central timeout configuration and management.
|
|
342
|
+
|
|
343
|
+
Manages timeout settings for different operations and provides
|
|
344
|
+
methods to create deadline contexts with appropriate timeouts.
|
|
345
|
+
|
|
346
|
+
Attributes:
|
|
347
|
+
config: The timeout configuration.
|
|
348
|
+
|
|
349
|
+
Example:
|
|
350
|
+
>>> manager = TimeoutManager(TimeoutConfig(
|
|
351
|
+
... default_timeout=30.0,
|
|
352
|
+
... tool_timeouts={"search": 60.0},
|
|
353
|
+
... ))
|
|
354
|
+
>>> timeout = manager.get_timeout("search") # 60.0
|
|
355
|
+
>>> timeout = manager.get_timeout("unknown") # 30.0 (default)
|
|
356
|
+
"""
|
|
357
|
+
|
|
358
|
+
def __init__(self, config: TimeoutConfig | None = None) -> None:
|
|
359
|
+
"""
|
|
360
|
+
Initialize timeout manager.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
config: Timeout configuration. Uses defaults if None.
|
|
364
|
+
"""
|
|
365
|
+
self.config = config or TimeoutConfig()
|
|
366
|
+
self._lock = threading.RLock()
|
|
367
|
+
|
|
368
|
+
def get_timeout(self, operation: str) -> float:
|
|
369
|
+
"""
|
|
370
|
+
Get timeout for a specific operation.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
operation: Name of the operation or tool.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
Timeout value in seconds.
|
|
377
|
+
"""
|
|
378
|
+
with self._lock:
|
|
379
|
+
return self.config.get_timeout(operation)
|
|
380
|
+
|
|
381
|
+
def get_llm_timeout(self) -> float:
|
|
382
|
+
"""
|
|
383
|
+
Get timeout for LLM operations.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
LLM timeout in seconds.
|
|
387
|
+
"""
|
|
388
|
+
return self.config.llm_timeout
|
|
389
|
+
|
|
390
|
+
def get_total_request_timeout(self) -> float:
|
|
391
|
+
"""
|
|
392
|
+
Get total request budget timeout.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
Total request timeout in seconds.
|
|
396
|
+
"""
|
|
397
|
+
return self.config.total_request_timeout
|
|
398
|
+
|
|
399
|
+
def set_tool_timeout(self, tool_name: str, timeout: float) -> None:
|
|
400
|
+
"""
|
|
401
|
+
Set timeout for a specific tool.
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
tool_name: Name of the tool.
|
|
405
|
+
timeout: Timeout value in seconds.
|
|
406
|
+
"""
|
|
407
|
+
with self._lock:
|
|
408
|
+
self.config.set_tool_timeout(tool_name, timeout)
|
|
409
|
+
|
|
410
|
+
def create_deadline(
|
|
411
|
+
self,
|
|
412
|
+
timeout: float | None = None,
|
|
413
|
+
operation: str | None = None,
|
|
414
|
+
) -> DeadlineContext:
|
|
415
|
+
"""
|
|
416
|
+
Create a deadline context with appropriate timeout.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
timeout: Explicit timeout (uses total_request_timeout if None).
|
|
420
|
+
operation: Optional operation name.
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
DeadlineContext for tracking the deadline.
|
|
424
|
+
"""
|
|
425
|
+
effective_timeout = timeout or self.config.total_request_timeout
|
|
426
|
+
return DeadlineContext(timeout=effective_timeout, operation=operation)
|
|
427
|
+
|
|
428
|
+
def create_tool_deadline(self, tool_name: str) -> DeadlineContext:
|
|
429
|
+
"""
|
|
430
|
+
Create a deadline context for a specific tool.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
tool_name: Name of the tool.
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
DeadlineContext with tool-specific timeout.
|
|
437
|
+
"""
|
|
438
|
+
timeout = self.get_timeout(tool_name)
|
|
439
|
+
return DeadlineContext(timeout=timeout, operation=tool_name)
|
|
440
|
+
|
|
441
|
+
def create_llm_deadline(self) -> DeadlineContext:
|
|
442
|
+
"""
|
|
443
|
+
Create a deadline context for LLM operations.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
DeadlineContext with LLM timeout.
|
|
447
|
+
"""
|
|
448
|
+
return DeadlineContext(timeout=self.config.llm_timeout, operation="llm_call")
|
|
449
|
+
|
|
450
|
+
@contextmanager
|
|
451
|
+
def deadline_context(
|
|
452
|
+
self,
|
|
453
|
+
timeout: float | None = None,
|
|
454
|
+
operation: str | None = None,
|
|
455
|
+
):
|
|
456
|
+
"""
|
|
457
|
+
Context manager for deadline tracking.
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
timeout: Explicit timeout.
|
|
461
|
+
operation: Optional operation name.
|
|
462
|
+
|
|
463
|
+
Yields:
|
|
464
|
+
DeadlineContext instance.
|
|
465
|
+
"""
|
|
466
|
+
ctx = self.create_deadline(timeout, operation)
|
|
467
|
+
with ctx:
|
|
468
|
+
yield ctx
|
|
469
|
+
|
|
470
|
+
def get_effective_timeout(
|
|
471
|
+
self,
|
|
472
|
+
operation: str,
|
|
473
|
+
requested_timeout: float | None = None,
|
|
474
|
+
) -> float:
|
|
475
|
+
"""
|
|
476
|
+
Get effective timeout considering current deadline.
|
|
477
|
+
|
|
478
|
+
If there's an active deadline context, returns the minimum
|
|
479
|
+
of the requested timeout and remaining deadline time.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
operation: Name of the operation.
|
|
483
|
+
requested_timeout: Requested timeout (uses config if None).
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Effective timeout in seconds.
|
|
487
|
+
"""
|
|
488
|
+
# Get configured timeout
|
|
489
|
+
config_timeout = self.get_timeout(operation)
|
|
490
|
+
timeout = requested_timeout if requested_timeout is not None else config_timeout
|
|
491
|
+
|
|
492
|
+
# Check for active deadline
|
|
493
|
+
current_deadline = get_current_deadline()
|
|
494
|
+
if current_deadline is not None:
|
|
495
|
+
try:
|
|
496
|
+
remaining = current_deadline.remaining()
|
|
497
|
+
return min(timeout, remaining)
|
|
498
|
+
except TimeoutError:
|
|
499
|
+
# Deadline already expired
|
|
500
|
+
return 0.0
|
|
501
|
+
|
|
502
|
+
return timeout
|
|
503
|
+
|
|
504
|
+
def to_dict(self) -> dict[str, Any]:
|
|
505
|
+
"""Serialize manager configuration to dictionary."""
|
|
506
|
+
return self.config.to_dict()
|
|
507
|
+
|
|
508
|
+
@classmethod
|
|
509
|
+
def from_dict(cls, data: dict[str, Any]) -> TimeoutManager:
|
|
510
|
+
"""Create manager from dictionary."""
|
|
511
|
+
config = TimeoutConfig.from_dict(data)
|
|
512
|
+
return cls(config)
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
# Default timeout manager instance
|
|
516
|
+
_default_manager: TimeoutManager | None = None
|
|
517
|
+
_manager_lock = threading.Lock()
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def get_default_manager() -> TimeoutManager:
|
|
521
|
+
"""
|
|
522
|
+
Get the default timeout manager.
|
|
523
|
+
|
|
524
|
+
Creates one with default settings if none exists.
|
|
525
|
+
|
|
526
|
+
Returns:
|
|
527
|
+
Default TimeoutManager instance.
|
|
528
|
+
"""
|
|
529
|
+
global _default_manager
|
|
530
|
+
with _manager_lock:
|
|
531
|
+
if _default_manager is None:
|
|
532
|
+
_default_manager = TimeoutManager()
|
|
533
|
+
return _default_manager
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def set_default_manager(manager: TimeoutManager) -> None:
|
|
537
|
+
"""
|
|
538
|
+
Set the default timeout manager.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
manager: TimeoutManager to use as default.
|
|
542
|
+
"""
|
|
543
|
+
global _default_manager
|
|
544
|
+
with _manager_lock:
|
|
545
|
+
_default_manager = manager
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool registry and management for AI agents.
|
|
3
|
+
|
|
4
|
+
Provides centralized management of tools with metadata, schemas,
|
|
5
|
+
and discovery capabilities. Makes it easy to manage tools across
|
|
6
|
+
an application and export them to different LLM provider formats.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Centralized tool registration
|
|
10
|
+
- Schema inference from type hints
|
|
11
|
+
- Export to OpenAI, Anthropic, and Gemini formats
|
|
12
|
+
- Category and risk-level filtering
|
|
13
|
+
- Tool execution with authorization
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
>>> from proxilion.tools import (
|
|
17
|
+
... ToolRegistry, ToolDefinition, ToolCategory, RiskLevel,
|
|
18
|
+
... tool, get_global_registry,
|
|
19
|
+
... )
|
|
20
|
+
>>>
|
|
21
|
+
>>> # Create a registry
|
|
22
|
+
>>> registry = ToolRegistry()
|
|
23
|
+
>>>
|
|
24
|
+
>>> # Register tools with decorator
|
|
25
|
+
>>> @tool(
|
|
26
|
+
... name="search_web",
|
|
27
|
+
... description="Search the web",
|
|
28
|
+
... category=ToolCategory.SEARCH,
|
|
29
|
+
... registry=registry,
|
|
30
|
+
... )
|
|
31
|
+
... def search_web(query: str, max_results: int = 10) -> list[dict]:
|
|
32
|
+
... return perform_search(query, max_results)
|
|
33
|
+
>>>
|
|
34
|
+
>>> # Export to OpenAI format
|
|
35
|
+
>>> tools = registry.export_all(format="openai")
|
|
36
|
+
>>>
|
|
37
|
+
>>> # Execute a tool
|
|
38
|
+
>>> result = registry.execute("search_web", query="python async")
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from proxilion.tools.decorators import (
|
|
42
|
+
infer_schema_from_function,
|
|
43
|
+
register_tool,
|
|
44
|
+
tool,
|
|
45
|
+
)
|
|
46
|
+
from proxilion.tools.registry import (
|
|
47
|
+
RiskLevel,
|
|
48
|
+
ToolCategory,
|
|
49
|
+
ToolDefinition,
|
|
50
|
+
ToolExecutionResult,
|
|
51
|
+
ToolRegistry,
|
|
52
|
+
get_global_registry,
|
|
53
|
+
set_global_registry,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
# Registry classes
|
|
58
|
+
"ToolCategory",
|
|
59
|
+
"RiskLevel",
|
|
60
|
+
"ToolDefinition",
|
|
61
|
+
"ToolRegistry",
|
|
62
|
+
"ToolExecutionResult",
|
|
63
|
+
"get_global_registry",
|
|
64
|
+
"set_global_registry",
|
|
65
|
+
# Decorators
|
|
66
|
+
"tool",
|
|
67
|
+
"register_tool",
|
|
68
|
+
"infer_schema_from_function",
|
|
69
|
+
]
|