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.
Files changed (94) hide show
  1. proxilion/__init__.py +136 -0
  2. proxilion/audit/__init__.py +133 -0
  3. proxilion/audit/base_exporters.py +527 -0
  4. proxilion/audit/compliance/__init__.py +130 -0
  5. proxilion/audit/compliance/base.py +457 -0
  6. proxilion/audit/compliance/eu_ai_act.py +603 -0
  7. proxilion/audit/compliance/iso27001.py +544 -0
  8. proxilion/audit/compliance/soc2.py +491 -0
  9. proxilion/audit/events.py +493 -0
  10. proxilion/audit/explainability.py +1173 -0
  11. proxilion/audit/exporters/__init__.py +58 -0
  12. proxilion/audit/exporters/aws_s3.py +636 -0
  13. proxilion/audit/exporters/azure_storage.py +608 -0
  14. proxilion/audit/exporters/cloud_base.py +468 -0
  15. proxilion/audit/exporters/gcp_storage.py +570 -0
  16. proxilion/audit/exporters/multi_exporter.py +498 -0
  17. proxilion/audit/hash_chain.py +652 -0
  18. proxilion/audit/logger.py +543 -0
  19. proxilion/caching/__init__.py +49 -0
  20. proxilion/caching/tool_cache.py +633 -0
  21. proxilion/context/__init__.py +73 -0
  22. proxilion/context/context_window.py +556 -0
  23. proxilion/context/message_history.py +505 -0
  24. proxilion/context/session.py +735 -0
  25. proxilion/contrib/__init__.py +51 -0
  26. proxilion/contrib/anthropic.py +609 -0
  27. proxilion/contrib/google.py +1012 -0
  28. proxilion/contrib/langchain.py +641 -0
  29. proxilion/contrib/mcp.py +893 -0
  30. proxilion/contrib/openai.py +646 -0
  31. proxilion/core.py +3058 -0
  32. proxilion/decorators.py +966 -0
  33. proxilion/engines/__init__.py +287 -0
  34. proxilion/engines/base.py +266 -0
  35. proxilion/engines/casbin_engine.py +412 -0
  36. proxilion/engines/opa_engine.py +493 -0
  37. proxilion/engines/simple.py +437 -0
  38. proxilion/exceptions.py +887 -0
  39. proxilion/guards/__init__.py +54 -0
  40. proxilion/guards/input_guard.py +522 -0
  41. proxilion/guards/output_guard.py +634 -0
  42. proxilion/observability/__init__.py +198 -0
  43. proxilion/observability/cost_tracker.py +866 -0
  44. proxilion/observability/hooks.py +683 -0
  45. proxilion/observability/metrics.py +798 -0
  46. proxilion/observability/session_cost_tracker.py +1063 -0
  47. proxilion/policies/__init__.py +67 -0
  48. proxilion/policies/base.py +304 -0
  49. proxilion/policies/builtin.py +486 -0
  50. proxilion/policies/registry.py +376 -0
  51. proxilion/providers/__init__.py +201 -0
  52. proxilion/providers/adapter.py +468 -0
  53. proxilion/providers/anthropic_adapter.py +330 -0
  54. proxilion/providers/gemini_adapter.py +391 -0
  55. proxilion/providers/openai_adapter.py +294 -0
  56. proxilion/py.typed +0 -0
  57. proxilion/resilience/__init__.py +81 -0
  58. proxilion/resilience/degradation.py +615 -0
  59. proxilion/resilience/fallback.py +555 -0
  60. proxilion/resilience/retry.py +554 -0
  61. proxilion/scheduling/__init__.py +57 -0
  62. proxilion/scheduling/priority_queue.py +419 -0
  63. proxilion/scheduling/scheduler.py +459 -0
  64. proxilion/security/__init__.py +244 -0
  65. proxilion/security/agent_trust.py +968 -0
  66. proxilion/security/behavioral_drift.py +794 -0
  67. proxilion/security/cascade_protection.py +869 -0
  68. proxilion/security/circuit_breaker.py +428 -0
  69. proxilion/security/cost_limiter.py +690 -0
  70. proxilion/security/idor_protection.py +460 -0
  71. proxilion/security/intent_capsule.py +849 -0
  72. proxilion/security/intent_validator.py +495 -0
  73. proxilion/security/memory_integrity.py +767 -0
  74. proxilion/security/rate_limiter.py +509 -0
  75. proxilion/security/scope_enforcer.py +680 -0
  76. proxilion/security/sequence_validator.py +636 -0
  77. proxilion/security/trust_boundaries.py +784 -0
  78. proxilion/streaming/__init__.py +70 -0
  79. proxilion/streaming/detector.py +761 -0
  80. proxilion/streaming/transformer.py +674 -0
  81. proxilion/timeouts/__init__.py +55 -0
  82. proxilion/timeouts/decorators.py +477 -0
  83. proxilion/timeouts/manager.py +545 -0
  84. proxilion/tools/__init__.py +69 -0
  85. proxilion/tools/decorators.py +493 -0
  86. proxilion/tools/registry.py +732 -0
  87. proxilion/types.py +339 -0
  88. proxilion/validation/__init__.py +93 -0
  89. proxilion/validation/pydantic_schema.py +351 -0
  90. proxilion/validation/schema.py +651 -0
  91. proxilion-0.0.1.dist-info/METADATA +872 -0
  92. proxilion-0.0.1.dist-info/RECORD +94 -0
  93. proxilion-0.0.1.dist-info/WHEEL +4 -0
  94. 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)