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,468 @@
1
+ """
2
+ Provider-agnostic tool call interface.
3
+
4
+ Provides a unified representation of tool calls and responses
5
+ across different LLM providers (OpenAI, Anthropic, Google Gemini).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ import uuid
13
+ from abc import ABC, abstractmethod
14
+ from dataclasses import dataclass, field
15
+ from datetime import datetime, timezone
16
+ from enum import Enum
17
+ from typing import Any, Protocol, runtime_checkable
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class Provider(Enum):
23
+ """Supported LLM providers."""
24
+
25
+ OPENAI = "openai"
26
+ ANTHROPIC = "anthropic"
27
+ GEMINI = "gemini"
28
+ BEDROCK = "bedrock"
29
+ OLLAMA = "ollama"
30
+ UNKNOWN = "unknown"
31
+
32
+
33
+ @dataclass
34
+ class UnifiedToolCall:
35
+ """
36
+ Provider-agnostic tool call representation.
37
+
38
+ Normalizes tool calls from different providers into a common format
39
+ for authorization and execution.
40
+
41
+ Attributes:
42
+ id: Unique identifier for the tool call.
43
+ name: Name of the tool being called.
44
+ arguments: Dictionary of arguments passed to the tool.
45
+ provider: The provider that generated this call.
46
+ raw: Original provider-specific object (for debugging).
47
+ timestamp: When the tool call was extracted.
48
+
49
+ Example:
50
+ >>> # From OpenAI response
51
+ >>> call = UnifiedToolCall.from_openai(response.choices[0].message.tool_calls[0])
52
+ >>> print(f"Tool: {call.name}, Args: {call.arguments}")
53
+ >>>
54
+ >>> # From Anthropic response
55
+ >>> call = UnifiedToolCall.from_anthropic(tool_use_block)
56
+ """
57
+
58
+ id: str
59
+ name: str
60
+ arguments: dict[str, Any]
61
+ provider: Provider = Provider.UNKNOWN
62
+ raw: Any = None
63
+ timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
64
+
65
+ @classmethod
66
+ def from_openai(cls, tool_call: Any) -> UnifiedToolCall:
67
+ """
68
+ Create from OpenAI tool call.
69
+
70
+ Args:
71
+ tool_call: OpenAI ChatCompletionMessageToolCall object.
72
+
73
+ Returns:
74
+ UnifiedToolCall instance.
75
+ """
76
+ # Handle both dict and object forms
77
+ if isinstance(tool_call, dict):
78
+ call_id = tool_call.get("id", str(uuid.uuid4()))
79
+ function = tool_call.get("function", {})
80
+ name = function.get("name", "unknown")
81
+ arguments_str = function.get("arguments", "{}")
82
+ else:
83
+ call_id = getattr(tool_call, "id", str(uuid.uuid4()))
84
+ function = getattr(tool_call, "function", None)
85
+ if function:
86
+ name = getattr(function, "name", "unknown")
87
+ arguments_str = getattr(function, "arguments", "{}")
88
+ else:
89
+ name = "unknown"
90
+ arguments_str = "{}"
91
+
92
+ # Parse arguments JSON
93
+ try:
94
+ if isinstance(arguments_str, str):
95
+ arguments = json.loads(arguments_str)
96
+ else:
97
+ arguments = arguments_str or {}
98
+ except json.JSONDecodeError:
99
+ logger.warning(f"Failed to parse tool call arguments: {arguments_str}")
100
+ arguments = {}
101
+
102
+ return cls(
103
+ id=call_id,
104
+ name=name,
105
+ arguments=arguments,
106
+ provider=Provider.OPENAI,
107
+ raw=tool_call,
108
+ )
109
+
110
+ @classmethod
111
+ def from_anthropic(cls, tool_use: Any) -> UnifiedToolCall:
112
+ """
113
+ Create from Anthropic tool use block.
114
+
115
+ Args:
116
+ tool_use: Anthropic ToolUseBlock object.
117
+
118
+ Returns:
119
+ UnifiedToolCall instance.
120
+ """
121
+ # Handle both dict and object forms
122
+ if isinstance(tool_use, dict):
123
+ call_id = tool_use.get("id", str(uuid.uuid4()))
124
+ name = tool_use.get("name", "unknown")
125
+ arguments = tool_use.get("input", {})
126
+ else:
127
+ call_id = getattr(tool_use, "id", str(uuid.uuid4()))
128
+ name = getattr(tool_use, "name", "unknown")
129
+ arguments = getattr(tool_use, "input", {})
130
+
131
+ return cls(
132
+ id=call_id,
133
+ name=name,
134
+ arguments=arguments if isinstance(arguments, dict) else {},
135
+ provider=Provider.ANTHROPIC,
136
+ raw=tool_use,
137
+ )
138
+
139
+ @classmethod
140
+ def from_gemini(cls, function_call: Any) -> UnifiedToolCall:
141
+ """
142
+ Create from Gemini function call.
143
+
144
+ Args:
145
+ function_call: Gemini FunctionCall object.
146
+
147
+ Returns:
148
+ UnifiedToolCall instance.
149
+ """
150
+ # Handle both dict and object forms
151
+ if isinstance(function_call, dict):
152
+ name = function_call.get("name", "unknown")
153
+ args = function_call.get("args", {})
154
+ else:
155
+ name = getattr(function_call, "name", "unknown")
156
+ # Gemini args can be a Struct protobuf object
157
+ args_raw = getattr(function_call, "args", {})
158
+ if hasattr(args_raw, "items"):
159
+ args = dict(args_raw.items())
160
+ elif isinstance(args_raw, dict):
161
+ args = args_raw
162
+ else:
163
+ # Try to convert Struct to dict
164
+ try:
165
+ args = dict(args_raw)
166
+ except (TypeError, ValueError):
167
+ args = {}
168
+
169
+ # Gemini doesn't provide call IDs, generate one
170
+ return cls(
171
+ id=str(uuid.uuid4()),
172
+ name=name,
173
+ arguments=args,
174
+ provider=Provider.GEMINI,
175
+ raw=function_call,
176
+ )
177
+
178
+ @classmethod
179
+ def from_dict(cls, data: dict[str, Any]) -> UnifiedToolCall:
180
+ """
181
+ Create from dictionary.
182
+
183
+ Args:
184
+ data: Dictionary with tool call data.
185
+
186
+ Returns:
187
+ UnifiedToolCall instance.
188
+ """
189
+ return cls(
190
+ id=data.get("id", str(uuid.uuid4())),
191
+ name=data.get("name", "unknown"),
192
+ arguments=data.get("arguments", {}),
193
+ provider=Provider(data.get("provider", "unknown")),
194
+ )
195
+
196
+ def to_dict(self) -> dict[str, Any]:
197
+ """Convert to dictionary representation."""
198
+ return {
199
+ "id": self.id,
200
+ "name": self.name,
201
+ "arguments": self.arguments,
202
+ "provider": self.provider.value,
203
+ "timestamp": self.timestamp.isoformat(),
204
+ }
205
+
206
+
207
+ @dataclass
208
+ class UnifiedToolResult:
209
+ """
210
+ Provider-agnostic tool result representation.
211
+
212
+ Attributes:
213
+ tool_call_id: ID of the corresponding tool call.
214
+ result: The result value.
215
+ is_error: Whether the result is an error.
216
+ error_message: Error message if is_error is True.
217
+ """
218
+
219
+ tool_call_id: str
220
+ result: Any
221
+ is_error: bool = False
222
+ error_message: str | None = None
223
+
224
+ def to_dict(self) -> dict[str, Any]:
225
+ """Convert to dictionary."""
226
+ return {
227
+ "tool_call_id": self.tool_call_id,
228
+ "result": self.result,
229
+ "is_error": self.is_error,
230
+ "error_message": self.error_message,
231
+ }
232
+
233
+
234
+ @dataclass
235
+ class UnifiedResponse:
236
+ """
237
+ Provider-agnostic response representation.
238
+
239
+ Attributes:
240
+ content: Text content of the response.
241
+ tool_calls: List of tool calls in the response.
242
+ finish_reason: Why the response ended.
243
+ provider: The provider that generated this response.
244
+ usage: Token usage information.
245
+ raw: Original provider-specific response.
246
+ """
247
+
248
+ content: str | None = None
249
+ tool_calls: list[UnifiedToolCall] = field(default_factory=list)
250
+ finish_reason: str | None = None
251
+ provider: Provider = Provider.UNKNOWN
252
+ usage: dict[str, int] = field(default_factory=dict)
253
+ raw: Any = None
254
+
255
+ def has_tool_calls(self) -> bool:
256
+ """Check if response contains tool calls."""
257
+ return len(self.tool_calls) > 0
258
+
259
+ def to_dict(self) -> dict[str, Any]:
260
+ """Convert to dictionary."""
261
+ return {
262
+ "content": self.content,
263
+ "tool_calls": [tc.to_dict() for tc in self.tool_calls],
264
+ "finish_reason": self.finish_reason,
265
+ "provider": self.provider.value,
266
+ "usage": self.usage,
267
+ }
268
+
269
+
270
+ @runtime_checkable
271
+ class ProviderAdapter(Protocol):
272
+ """
273
+ Protocol for provider adapters.
274
+
275
+ Each adapter translates between provider-specific formats
276
+ and the unified format used by Proxilion.
277
+ """
278
+
279
+ @property
280
+ def provider(self) -> Provider:
281
+ """Get the provider type."""
282
+ ...
283
+
284
+ def extract_tool_calls(self, response: Any) -> list[UnifiedToolCall]:
285
+ """
286
+ Extract tool calls from a provider response.
287
+
288
+ Args:
289
+ response: Provider-specific response object.
290
+
291
+ Returns:
292
+ List of unified tool calls.
293
+ """
294
+ ...
295
+
296
+ def extract_response(self, response: Any) -> UnifiedResponse:
297
+ """
298
+ Extract full response including content and tool calls.
299
+
300
+ Args:
301
+ response: Provider-specific response object.
302
+
303
+ Returns:
304
+ UnifiedResponse instance.
305
+ """
306
+ ...
307
+
308
+ def format_tool_result(
309
+ self,
310
+ tool_call: UnifiedToolCall,
311
+ result: Any,
312
+ is_error: bool = False,
313
+ ) -> Any:
314
+ """
315
+ Format tool result for the provider.
316
+
317
+ Args:
318
+ tool_call: The original tool call.
319
+ result: The result to format.
320
+ is_error: Whether the result is an error.
321
+
322
+ Returns:
323
+ Provider-specific message format.
324
+ """
325
+ ...
326
+
327
+ def format_tools(
328
+ self,
329
+ tools: list[Any], # list[ToolDefinition]
330
+ ) -> list[dict[str, Any]]:
331
+ """
332
+ Format tool definitions for the provider.
333
+
334
+ Args:
335
+ tools: List of ToolDefinition objects.
336
+
337
+ Returns:
338
+ List of provider-specific tool definitions.
339
+ """
340
+ ...
341
+
342
+
343
+ class BaseAdapter(ABC):
344
+ """
345
+ Base class for provider adapters.
346
+
347
+ Provides common functionality and enforces the adapter interface.
348
+ """
349
+
350
+ @property
351
+ @abstractmethod
352
+ def provider(self) -> Provider:
353
+ """Get the provider type."""
354
+ ...
355
+
356
+ @abstractmethod
357
+ def extract_tool_calls(self, response: Any) -> list[UnifiedToolCall]:
358
+ """Extract tool calls from response."""
359
+ ...
360
+
361
+ @abstractmethod
362
+ def extract_response(self, response: Any) -> UnifiedResponse:
363
+ """Extract full response."""
364
+ ...
365
+
366
+ @abstractmethod
367
+ def format_tool_result(
368
+ self,
369
+ tool_call: UnifiedToolCall,
370
+ result: Any,
371
+ is_error: bool = False,
372
+ ) -> Any:
373
+ """Format tool result for provider."""
374
+ ...
375
+
376
+ @abstractmethod
377
+ def format_tools(
378
+ self,
379
+ tools: list[Any],
380
+ ) -> list[dict[str, Any]]:
381
+ """Format tool definitions for provider."""
382
+ ...
383
+
384
+ def _serialize_result(self, result: Any) -> str:
385
+ """Serialize a result to string format."""
386
+ if isinstance(result, str):
387
+ return result
388
+ try:
389
+ return json.dumps(result, default=str)
390
+ except (TypeError, ValueError):
391
+ return str(result)
392
+
393
+
394
+ def detect_provider(response: Any) -> Provider:
395
+ """
396
+ Auto-detect provider from response object.
397
+
398
+ Examines the response type and module to determine
399
+ which provider generated it.
400
+
401
+ Args:
402
+ response: Provider response object.
403
+
404
+ Returns:
405
+ Detected Provider enum value.
406
+
407
+ Raises:
408
+ ValueError: If provider cannot be detected.
409
+
410
+ Example:
411
+ >>> from openai import OpenAI
412
+ >>> response = client.chat.completions.create(...)
413
+ >>> provider = detect_provider(response)
414
+ >>> assert provider == Provider.OPENAI
415
+ """
416
+ type_name = type(response).__name__
417
+ module = type(response).__module__
418
+
419
+ # Check module name
420
+ module_lower = module.lower()
421
+
422
+ if "openai" in module_lower:
423
+ return Provider.OPENAI
424
+ elif "anthropic" in module_lower:
425
+ return Provider.ANTHROPIC
426
+ elif (
427
+ "vertexai" in module_lower
428
+ or "google.generativeai" in module_lower
429
+ or ("google" in module_lower and "aiplatform" in module_lower)
430
+ ):
431
+ return Provider.GEMINI
432
+
433
+ # Check type name patterns
434
+ if "ChatCompletion" in type_name:
435
+ return Provider.OPENAI
436
+ elif "Message" in type_name and hasattr(response, "content"):
437
+ # Anthropic messages have content attribute that's a list
438
+ content = getattr(response, "content", None)
439
+ if isinstance(content, list):
440
+ return Provider.ANTHROPIC
441
+ elif "GenerateContentResponse" in type_name or "GenerationResponse" in type_name:
442
+ return Provider.GEMINI
443
+
444
+ # Check for specific attributes
445
+ if hasattr(response, "choices") and hasattr(response, "model"):
446
+ return Provider.OPENAI
447
+ elif hasattr(response, "candidates"):
448
+ return Provider.GEMINI
449
+ elif hasattr(response, "stop_reason"):
450
+ return Provider.ANTHROPIC
451
+
452
+ raise ValueError(f"Unknown provider for response type: {module}.{type_name}")
453
+
454
+
455
+ def detect_provider_safe(response: Any) -> Provider:
456
+ """
457
+ Safely detect provider, returning UNKNOWN if detection fails.
458
+
459
+ Args:
460
+ response: Provider response object.
461
+
462
+ Returns:
463
+ Detected Provider or Provider.UNKNOWN.
464
+ """
465
+ try:
466
+ return detect_provider(response)
467
+ except ValueError:
468
+ return Provider.UNKNOWN