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,555 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Fallback chains for AI operations.
|
|
3
|
+
|
|
4
|
+
Provides ordered fallback mechanisms for models and tools,
|
|
5
|
+
allowing graceful handling of failures.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import inspect
|
|
12
|
+
import logging
|
|
13
|
+
import threading
|
|
14
|
+
from collections.abc import Awaitable, Callable
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from typing import Any, Generic, TypeVar
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
T = TypeVar("T")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FallbackCondition(Enum):
|
|
26
|
+
"""Conditions that trigger fallback."""
|
|
27
|
+
|
|
28
|
+
ALWAYS = "always" # Always try this fallback if previous failed
|
|
29
|
+
ON_TIMEOUT = "on_timeout" # Only on timeout errors
|
|
30
|
+
ON_RATE_LIMIT = "on_rate_limit" # Only on rate limit errors
|
|
31
|
+
ON_UNAVAILABLE = "on_unavailable" # Only when service unavailable
|
|
32
|
+
ON_ERROR = "on_error" # Only on general errors
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class FallbackOption:
|
|
37
|
+
"""
|
|
38
|
+
A single fallback option in a chain.
|
|
39
|
+
|
|
40
|
+
Attributes:
|
|
41
|
+
name: Identifier for this fallback option.
|
|
42
|
+
handler: Callable that handles the request.
|
|
43
|
+
priority: Priority order (lower = higher priority).
|
|
44
|
+
conditions: Conditions under which this fallback applies.
|
|
45
|
+
enabled: Whether this fallback is currently enabled.
|
|
46
|
+
metadata: Additional metadata about this option.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
>>> option = FallbackOption(
|
|
50
|
+
... name="gpt-4o",
|
|
51
|
+
... handler=call_gpt4,
|
|
52
|
+
... priority=1,
|
|
53
|
+
... )
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
name: str
|
|
57
|
+
handler: Callable[..., Any]
|
|
58
|
+
priority: int = 0
|
|
59
|
+
conditions: set[FallbackCondition] = field(
|
|
60
|
+
default_factory=lambda: {FallbackCondition.ALWAYS}
|
|
61
|
+
)
|
|
62
|
+
enabled: bool = True
|
|
63
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
64
|
+
|
|
65
|
+
def matches_condition(self, exception: Exception) -> bool:
|
|
66
|
+
"""
|
|
67
|
+
Check if exception matches any of the fallback conditions.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
exception: The exception that occurred.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
True if this fallback should be attempted.
|
|
74
|
+
"""
|
|
75
|
+
if not self.enabled:
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
if FallbackCondition.ALWAYS in self.conditions:
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
# Check specific conditions based on exception type
|
|
82
|
+
exception_type = type(exception).__name__.lower()
|
|
83
|
+
exception_msg = str(exception).lower()
|
|
84
|
+
|
|
85
|
+
if FallbackCondition.ON_TIMEOUT in self.conditions:
|
|
86
|
+
if "timeout" in exception_type or "timeout" in exception_msg:
|
|
87
|
+
return True
|
|
88
|
+
|
|
89
|
+
if FallbackCondition.ON_RATE_LIMIT in self.conditions:
|
|
90
|
+
if "ratelimit" in exception_type or "rate limit" in exception_msg:
|
|
91
|
+
return True
|
|
92
|
+
if "429" in exception_msg:
|
|
93
|
+
return True
|
|
94
|
+
|
|
95
|
+
if FallbackCondition.ON_UNAVAILABLE in self.conditions:
|
|
96
|
+
if "unavailable" in exception_msg or "503" in exception_msg:
|
|
97
|
+
return True
|
|
98
|
+
|
|
99
|
+
return FallbackCondition.ON_ERROR in self.conditions
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass
|
|
103
|
+
class FallbackResult(Generic[T]):
|
|
104
|
+
"""
|
|
105
|
+
Result of a fallback chain execution.
|
|
106
|
+
|
|
107
|
+
Attributes:
|
|
108
|
+
success: Whether any option succeeded.
|
|
109
|
+
result: The result value if successful.
|
|
110
|
+
used_fallback: Whether a fallback was used (not the primary).
|
|
111
|
+
fallback_name: Name of the fallback that succeeded.
|
|
112
|
+
attempts: Number of attempts made.
|
|
113
|
+
exceptions: List of exceptions encountered.
|
|
114
|
+
execution_time: Total execution time in seconds.
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
>>> result = await chain.execute_async(prompt="Hello")
|
|
118
|
+
>>> if result.success:
|
|
119
|
+
... print(f"Got result from {result.fallback_name}")
|
|
120
|
+
... print(result.result)
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
success: bool
|
|
124
|
+
result: T | None = None
|
|
125
|
+
used_fallback: bool = False
|
|
126
|
+
fallback_name: str | None = None
|
|
127
|
+
attempts: int = 0
|
|
128
|
+
exceptions: list[tuple[str, Exception]] = field(default_factory=list)
|
|
129
|
+
execution_time: float = 0.0
|
|
130
|
+
|
|
131
|
+
def to_dict(self) -> dict[str, Any]:
|
|
132
|
+
"""Convert to dictionary."""
|
|
133
|
+
return {
|
|
134
|
+
"success": self.success,
|
|
135
|
+
"used_fallback": self.used_fallback,
|
|
136
|
+
"fallback_name": self.fallback_name,
|
|
137
|
+
"attempts": self.attempts,
|
|
138
|
+
"execution_time": self.execution_time,
|
|
139
|
+
"exception_types": [
|
|
140
|
+
(name, type(e).__name__) for name, e in self.exceptions
|
|
141
|
+
],
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class FallbackChain(Generic[T]):
|
|
146
|
+
"""
|
|
147
|
+
Ordered chain of fallback options.
|
|
148
|
+
|
|
149
|
+
Executes options in priority order until one succeeds.
|
|
150
|
+
Supports both synchronous and asynchronous handlers.
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
>>> chain = FallbackChain([
|
|
154
|
+
... FallbackOption("claude-opus", call_claude, priority=1),
|
|
155
|
+
... FallbackOption("gpt-4o", call_gpt4, priority=2),
|
|
156
|
+
... FallbackOption("local-llama", call_local, priority=3),
|
|
157
|
+
... ])
|
|
158
|
+
>>>
|
|
159
|
+
>>> # Execute with fallback
|
|
160
|
+
>>> result = await chain.execute_async(prompt="Hello")
|
|
161
|
+
>>> if result.success:
|
|
162
|
+
... print(result.result)
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def __init__(
|
|
166
|
+
self,
|
|
167
|
+
options: list[FallbackOption] | None = None,
|
|
168
|
+
stop_on_success: bool = True,
|
|
169
|
+
) -> None:
|
|
170
|
+
"""
|
|
171
|
+
Initialize the fallback chain.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
options: List of fallback options.
|
|
175
|
+
stop_on_success: Whether to stop after first success.
|
|
176
|
+
"""
|
|
177
|
+
self._options: list[FallbackOption] = []
|
|
178
|
+
self._lock = threading.RLock()
|
|
179
|
+
self.stop_on_success = stop_on_success
|
|
180
|
+
|
|
181
|
+
if options:
|
|
182
|
+
for option in options:
|
|
183
|
+
self.add_option(option)
|
|
184
|
+
|
|
185
|
+
def add_option(self, option: FallbackOption) -> FallbackChain[T]:
|
|
186
|
+
"""
|
|
187
|
+
Add a fallback option.
|
|
188
|
+
|
|
189
|
+
Options are kept sorted by priority.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
option: The option to add.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Self for chaining.
|
|
196
|
+
"""
|
|
197
|
+
with self._lock:
|
|
198
|
+
self._options.append(option)
|
|
199
|
+
self._options.sort(key=lambda o: o.priority)
|
|
200
|
+
return self
|
|
201
|
+
|
|
202
|
+
def remove_option(self, name: str) -> bool:
|
|
203
|
+
"""
|
|
204
|
+
Remove a fallback option by name.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
name: Name of the option to remove.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
True if option was found and removed.
|
|
211
|
+
"""
|
|
212
|
+
with self._lock:
|
|
213
|
+
for i, option in enumerate(self._options):
|
|
214
|
+
if option.name == name:
|
|
215
|
+
self._options.pop(i)
|
|
216
|
+
return True
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
def enable_option(self, name: str) -> bool:
|
|
220
|
+
"""Enable a fallback option."""
|
|
221
|
+
with self._lock:
|
|
222
|
+
for option in self._options:
|
|
223
|
+
if option.name == name:
|
|
224
|
+
option.enabled = True
|
|
225
|
+
return True
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
def disable_option(self, name: str) -> bool:
|
|
229
|
+
"""Disable a fallback option."""
|
|
230
|
+
with self._lock:
|
|
231
|
+
for option in self._options:
|
|
232
|
+
if option.name == name:
|
|
233
|
+
option.enabled = False
|
|
234
|
+
return True
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
def get_options(self) -> list[FallbackOption]:
|
|
238
|
+
"""Get all options (copy)."""
|
|
239
|
+
with self._lock:
|
|
240
|
+
return list(self._options)
|
|
241
|
+
|
|
242
|
+
def execute(
|
|
243
|
+
self,
|
|
244
|
+
*args: Any,
|
|
245
|
+
primary: Callable[..., T] | None = None,
|
|
246
|
+
**kwargs: Any,
|
|
247
|
+
) -> FallbackResult[T]:
|
|
248
|
+
"""
|
|
249
|
+
Execute the fallback chain synchronously.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
*args: Arguments to pass to handlers.
|
|
253
|
+
primary: Optional primary handler to try first.
|
|
254
|
+
**kwargs: Keyword arguments to pass to handlers.
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
FallbackResult with execution details.
|
|
258
|
+
"""
|
|
259
|
+
start_time = datetime.now(timezone.utc)
|
|
260
|
+
result = FallbackResult[T](success=False)
|
|
261
|
+
|
|
262
|
+
# Build execution order
|
|
263
|
+
handlers: list[tuple[str, Callable[..., Any], bool]] = []
|
|
264
|
+
if primary:
|
|
265
|
+
handlers.append(("primary", primary, False))
|
|
266
|
+
|
|
267
|
+
with self._lock:
|
|
268
|
+
for option in self._options:
|
|
269
|
+
if option.enabled:
|
|
270
|
+
handlers.append((option.name, option.handler, True))
|
|
271
|
+
|
|
272
|
+
for name, handler, is_fallback in handlers:
|
|
273
|
+
result.attempts += 1
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
# Check if handler is async
|
|
277
|
+
if inspect.iscoroutinefunction(handler):
|
|
278
|
+
# Run async handler in event loop
|
|
279
|
+
loop = asyncio.new_event_loop()
|
|
280
|
+
try:
|
|
281
|
+
value = loop.run_until_complete(handler(*args, **kwargs))
|
|
282
|
+
finally:
|
|
283
|
+
loop.close()
|
|
284
|
+
else:
|
|
285
|
+
value = handler(*args, **kwargs)
|
|
286
|
+
|
|
287
|
+
result.success = True
|
|
288
|
+
result.result = value
|
|
289
|
+
result.used_fallback = is_fallback
|
|
290
|
+
result.fallback_name = name
|
|
291
|
+
|
|
292
|
+
logger.debug(f"Fallback chain: '{name}' succeeded")
|
|
293
|
+
break
|
|
294
|
+
|
|
295
|
+
except Exception as e:
|
|
296
|
+
result.exceptions.append((name, e))
|
|
297
|
+
logger.warning(f"Fallback chain: '{name}' failed: {e}")
|
|
298
|
+
|
|
299
|
+
# Check if next option matches condition
|
|
300
|
+
# (simplified - in full implementation would check conditions)
|
|
301
|
+
|
|
302
|
+
result.execution_time = (
|
|
303
|
+
datetime.now(timezone.utc) - start_time
|
|
304
|
+
).total_seconds()
|
|
305
|
+
return result
|
|
306
|
+
|
|
307
|
+
async def execute_async(
|
|
308
|
+
self,
|
|
309
|
+
*args: Any,
|
|
310
|
+
primary: Callable[..., Awaitable[T]] | None = None,
|
|
311
|
+
**kwargs: Any,
|
|
312
|
+
) -> FallbackResult[T]:
|
|
313
|
+
"""
|
|
314
|
+
Execute the fallback chain asynchronously.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
*args: Arguments to pass to handlers.
|
|
318
|
+
primary: Optional primary async handler to try first.
|
|
319
|
+
**kwargs: Keyword arguments to pass to handlers.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
FallbackResult with execution details.
|
|
323
|
+
"""
|
|
324
|
+
start_time = datetime.now(timezone.utc)
|
|
325
|
+
result = FallbackResult[T](success=False)
|
|
326
|
+
|
|
327
|
+
# Build execution order
|
|
328
|
+
handlers: list[tuple[str, Callable[..., Any], bool]] = []
|
|
329
|
+
if primary:
|
|
330
|
+
handlers.append(("primary", primary, False))
|
|
331
|
+
|
|
332
|
+
with self._lock:
|
|
333
|
+
for option in self._options:
|
|
334
|
+
if option.enabled:
|
|
335
|
+
handlers.append((option.name, option.handler, True))
|
|
336
|
+
|
|
337
|
+
for name, handler, is_fallback in handlers:
|
|
338
|
+
result.attempts += 1
|
|
339
|
+
|
|
340
|
+
try:
|
|
341
|
+
# Check if handler is async
|
|
342
|
+
if inspect.iscoroutinefunction(handler):
|
|
343
|
+
value = await handler(*args, **kwargs)
|
|
344
|
+
else:
|
|
345
|
+
# Run sync handler in thread pool
|
|
346
|
+
loop = asyncio.get_event_loop()
|
|
347
|
+
value = await loop.run_in_executor(
|
|
348
|
+
None, lambda h=handler: h(*args, **kwargs)
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
result.success = True
|
|
352
|
+
result.result = value
|
|
353
|
+
result.used_fallback = is_fallback
|
|
354
|
+
result.fallback_name = name
|
|
355
|
+
|
|
356
|
+
logger.debug(f"Fallback chain: '{name}' succeeded")
|
|
357
|
+
break
|
|
358
|
+
|
|
359
|
+
except Exception as e:
|
|
360
|
+
result.exceptions.append((name, e))
|
|
361
|
+
logger.warning(f"Fallback chain: '{name}' failed: {e}")
|
|
362
|
+
|
|
363
|
+
result.execution_time = (
|
|
364
|
+
datetime.now(timezone.utc) - start_time
|
|
365
|
+
).total_seconds()
|
|
366
|
+
return result
|
|
367
|
+
|
|
368
|
+
def __len__(self) -> int:
|
|
369
|
+
"""Get number of options."""
|
|
370
|
+
return len(self._options)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
class ModelFallback(FallbackChain[str]):
|
|
374
|
+
"""
|
|
375
|
+
Specialized fallback chain for LLM models.
|
|
376
|
+
|
|
377
|
+
Provides convenience methods for common model fallback patterns.
|
|
378
|
+
|
|
379
|
+
Example:
|
|
380
|
+
>>> fallback = ModelFallback()
|
|
381
|
+
>>> fallback.add_model("claude-opus-4-5", call_claude_opus)
|
|
382
|
+
>>> fallback.add_model("gpt-4o", call_gpt4o)
|
|
383
|
+
>>> result = await fallback.complete(prompt="Hello")
|
|
384
|
+
"""
|
|
385
|
+
|
|
386
|
+
def __init__(self) -> None:
|
|
387
|
+
"""Initialize model fallback chain."""
|
|
388
|
+
super().__init__()
|
|
389
|
+
self._model_stats: dict[str, dict[str, int]] = {}
|
|
390
|
+
|
|
391
|
+
def add_model(
|
|
392
|
+
self,
|
|
393
|
+
model_name: str,
|
|
394
|
+
handler: Callable[..., str | Awaitable[str]],
|
|
395
|
+
priority: int | None = None,
|
|
396
|
+
**metadata: Any,
|
|
397
|
+
) -> ModelFallback:
|
|
398
|
+
"""
|
|
399
|
+
Add a model to the fallback chain.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
model_name: Name of the model.
|
|
403
|
+
handler: Function to call the model.
|
|
404
|
+
priority: Priority (auto-assigned if None).
|
|
405
|
+
**metadata: Additional model metadata.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
Self for chaining.
|
|
409
|
+
"""
|
|
410
|
+
if priority is None:
|
|
411
|
+
priority = len(self._options)
|
|
412
|
+
|
|
413
|
+
option = FallbackOption(
|
|
414
|
+
name=model_name,
|
|
415
|
+
handler=handler,
|
|
416
|
+
priority=priority,
|
|
417
|
+
metadata={"model_name": model_name, **metadata},
|
|
418
|
+
)
|
|
419
|
+
self.add_option(option)
|
|
420
|
+
self._model_stats[model_name] = {"calls": 0, "successes": 0, "failures": 0}
|
|
421
|
+
return self
|
|
422
|
+
|
|
423
|
+
async def complete(
|
|
424
|
+
self,
|
|
425
|
+
prompt: str,
|
|
426
|
+
**kwargs: Any,
|
|
427
|
+
) -> FallbackResult[str]:
|
|
428
|
+
"""
|
|
429
|
+
Complete a prompt using the model fallback chain.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
prompt: The prompt to complete.
|
|
433
|
+
**kwargs: Additional arguments for the model.
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
FallbackResult with the completion.
|
|
437
|
+
"""
|
|
438
|
+
result = await self.execute_async(prompt=prompt, **kwargs)
|
|
439
|
+
|
|
440
|
+
# Update stats
|
|
441
|
+
if result.fallback_name and result.fallback_name in self._model_stats:
|
|
442
|
+
self._model_stats[result.fallback_name]["calls"] += 1
|
|
443
|
+
if result.success:
|
|
444
|
+
self._model_stats[result.fallback_name]["successes"] += 1
|
|
445
|
+
else:
|
|
446
|
+
self._model_stats[result.fallback_name]["failures"] += 1
|
|
447
|
+
|
|
448
|
+
return result
|
|
449
|
+
|
|
450
|
+
def get_model_stats(self) -> dict[str, dict[str, int]]:
|
|
451
|
+
"""Get statistics for each model."""
|
|
452
|
+
return dict(self._model_stats)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
class ToolFallback(FallbackChain[Any]):
|
|
456
|
+
"""
|
|
457
|
+
Specialized fallback chain for tools.
|
|
458
|
+
|
|
459
|
+
Provides convenience methods for tool fallback patterns.
|
|
460
|
+
|
|
461
|
+
Example:
|
|
462
|
+
>>> fallback = ToolFallback()
|
|
463
|
+
>>> fallback.add_tool("google_search", google_search)
|
|
464
|
+
>>> fallback.add_tool("bing_search", bing_search)
|
|
465
|
+
>>> fallback.add_tool("cached", get_cached)
|
|
466
|
+
>>> result = await fallback.invoke(query="test")
|
|
467
|
+
"""
|
|
468
|
+
|
|
469
|
+
def __init__(self) -> None:
|
|
470
|
+
"""Initialize tool fallback chain."""
|
|
471
|
+
super().__init__()
|
|
472
|
+
self._tool_stats: dict[str, dict[str, int]] = {}
|
|
473
|
+
|
|
474
|
+
def add_tool(
|
|
475
|
+
self,
|
|
476
|
+
tool_name: str,
|
|
477
|
+
handler: Callable[..., Any | Awaitable[Any]],
|
|
478
|
+
priority: int | None = None,
|
|
479
|
+
conditions: set[FallbackCondition] | None = None,
|
|
480
|
+
**metadata: Any,
|
|
481
|
+
) -> ToolFallback:
|
|
482
|
+
"""
|
|
483
|
+
Add a tool to the fallback chain.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
tool_name: Name of the tool.
|
|
487
|
+
handler: Function to call the tool.
|
|
488
|
+
priority: Priority (auto-assigned if None).
|
|
489
|
+
conditions: Fallback conditions.
|
|
490
|
+
**metadata: Additional tool metadata.
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
Self for chaining.
|
|
494
|
+
"""
|
|
495
|
+
if priority is None:
|
|
496
|
+
priority = len(self._options)
|
|
497
|
+
|
|
498
|
+
option = FallbackOption(
|
|
499
|
+
name=tool_name,
|
|
500
|
+
handler=handler,
|
|
501
|
+
priority=priority,
|
|
502
|
+
conditions=conditions or {FallbackCondition.ALWAYS},
|
|
503
|
+
metadata={"tool_name": tool_name, **metadata},
|
|
504
|
+
)
|
|
505
|
+
self.add_option(option)
|
|
506
|
+
self._tool_stats[tool_name] = {"calls": 0, "successes": 0, "failures": 0}
|
|
507
|
+
return self
|
|
508
|
+
|
|
509
|
+
async def invoke(self, **kwargs: Any) -> FallbackResult[Any]:
|
|
510
|
+
"""
|
|
511
|
+
Invoke a tool using the fallback chain.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
**kwargs: Arguments for the tool.
|
|
515
|
+
|
|
516
|
+
Returns:
|
|
517
|
+
FallbackResult with the tool output.
|
|
518
|
+
"""
|
|
519
|
+
result = await self.execute_async(**kwargs)
|
|
520
|
+
|
|
521
|
+
# Update stats
|
|
522
|
+
if result.fallback_name and result.fallback_name in self._tool_stats:
|
|
523
|
+
self._tool_stats[result.fallback_name]["calls"] += 1
|
|
524
|
+
if result.success:
|
|
525
|
+
self._tool_stats[result.fallback_name]["successes"] += 1
|
|
526
|
+
else:
|
|
527
|
+
self._tool_stats[result.fallback_name]["failures"] += 1
|
|
528
|
+
|
|
529
|
+
return result
|
|
530
|
+
|
|
531
|
+
def invoke_sync(self, **kwargs: Any) -> FallbackResult[Any]:
|
|
532
|
+
"""
|
|
533
|
+
Invoke a tool synchronously.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
**kwargs: Arguments for the tool.
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
FallbackResult with the tool output.
|
|
540
|
+
"""
|
|
541
|
+
result = self.execute(**kwargs)
|
|
542
|
+
|
|
543
|
+
# Update stats
|
|
544
|
+
if result.fallback_name and result.fallback_name in self._tool_stats:
|
|
545
|
+
self._tool_stats[result.fallback_name]["calls"] += 1
|
|
546
|
+
if result.success:
|
|
547
|
+
self._tool_stats[result.fallback_name]["successes"] += 1
|
|
548
|
+
else:
|
|
549
|
+
self._tool_stats[result.fallback_name]["failures"] += 1
|
|
550
|
+
|
|
551
|
+
return result
|
|
552
|
+
|
|
553
|
+
def get_tool_stats(self) -> dict[str, dict[str, int]]:
|
|
554
|
+
"""Get statistics for each tool."""
|
|
555
|
+
return dict(self._tool_stats)
|