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,376 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Policy registry for Proxilion.
|
|
3
|
+
|
|
4
|
+
This module provides the PolicyRegistry class for registering, discovering,
|
|
5
|
+
and looking up policy classes. It supports both explicit registration via
|
|
6
|
+
decorators and automatic discovery based on naming conventions.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import threading
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from proxilion.exceptions import PolicyNotFoundError
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from proxilion.policies.base import Policy
|
|
19
|
+
from proxilion.types import UserContext
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PolicyRegistry:
|
|
25
|
+
"""
|
|
26
|
+
Registry for policy classes.
|
|
27
|
+
|
|
28
|
+
The PolicyRegistry manages policy class registration and lookup,
|
|
29
|
+
enabling the authorization system to find the appropriate policy
|
|
30
|
+
for any given resource.
|
|
31
|
+
|
|
32
|
+
Features:
|
|
33
|
+
- Decorator-based registration (@registry.policy("resource_name"))
|
|
34
|
+
- Auto-discovery by naming convention (ResourcePolicy -> "resource")
|
|
35
|
+
- Thread-safe operations
|
|
36
|
+
- Policy caching for performance
|
|
37
|
+
- Verification that all resources have been authorized
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
>>> registry = PolicyRegistry()
|
|
41
|
+
>>>
|
|
42
|
+
>>> @registry.policy("calculator")
|
|
43
|
+
>>> class CalculatorPolicy(Policy):
|
|
44
|
+
... def can_execute(self, context: dict) -> bool:
|
|
45
|
+
... return "calculator_user" in self.user.roles
|
|
46
|
+
>>>
|
|
47
|
+
>>> # Lookup and use
|
|
48
|
+
>>> policy_class = registry.get_policy("calculator")
|
|
49
|
+
>>> policy = policy_class(user, resource)
|
|
50
|
+
>>> if policy.can("execute"):
|
|
51
|
+
... # proceed
|
|
52
|
+
|
|
53
|
+
Thread Safety:
|
|
54
|
+
All operations are thread-safe via internal locking.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(self, default_policy: type[Policy] | None = None) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Initialize the registry.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
default_policy: Optional default policy class to use when no
|
|
63
|
+
policy is registered for a resource. If None, lookups for
|
|
64
|
+
unregistered resources will raise PolicyNotFoundError.
|
|
65
|
+
"""
|
|
66
|
+
self._policies: dict[str, type[Policy]] = {}
|
|
67
|
+
self._default_policy = default_policy
|
|
68
|
+
self._lock = threading.RLock()
|
|
69
|
+
self._authorized_resources: set[str] = set()
|
|
70
|
+
|
|
71
|
+
def policy(self, resource_name: str) -> Any:
|
|
72
|
+
"""
|
|
73
|
+
Decorator for registering a policy class.
|
|
74
|
+
|
|
75
|
+
This is the primary way to register policies. The decorator
|
|
76
|
+
associates a policy class with a resource name.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
resource_name: The name of the resource this policy handles.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
A decorator function that registers the policy class.
|
|
83
|
+
|
|
84
|
+
Example:
|
|
85
|
+
>>> @registry.policy("database")
|
|
86
|
+
>>> class DatabasePolicy(Policy):
|
|
87
|
+
... def can_query(self, context: dict) -> bool:
|
|
88
|
+
... return "analyst" in self.user.roles
|
|
89
|
+
"""
|
|
90
|
+
def decorator(policy_class: type[Policy]) -> type[Policy]:
|
|
91
|
+
self.register(resource_name, policy_class)
|
|
92
|
+
return policy_class
|
|
93
|
+
return decorator
|
|
94
|
+
|
|
95
|
+
def register(self, resource_name: str, policy_class: type[Policy]) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Register a policy class for a resource.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
resource_name: The name of the resource.
|
|
101
|
+
policy_class: The policy class to register.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValueError: If a policy is already registered for this resource.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
>>> registry.register("documents", DocumentPolicy)
|
|
108
|
+
"""
|
|
109
|
+
with self._lock:
|
|
110
|
+
if resource_name in self._policies:
|
|
111
|
+
existing = self._policies[resource_name].__name__
|
|
112
|
+
logger.warning(
|
|
113
|
+
f"Overwriting policy for '{resource_name}': "
|
|
114
|
+
f"{existing} -> {policy_class.__name__}"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
self._policies[resource_name] = policy_class
|
|
118
|
+
# Set the resource name on the policy class
|
|
119
|
+
policy_class._resource_name = resource_name
|
|
120
|
+
|
|
121
|
+
logger.debug(
|
|
122
|
+
f"Registered policy '{policy_class.__name__}' for resource '{resource_name}'"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def register_by_convention(self, policy_class: type[Policy]) -> None:
|
|
126
|
+
"""
|
|
127
|
+
Register a policy class using naming convention.
|
|
128
|
+
|
|
129
|
+
Derives the resource name from the class name:
|
|
130
|
+
- DatabaseQueryPolicy -> "database_query"
|
|
131
|
+
- UserPolicy -> "user"
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
policy_class: The policy class to register.
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
>>> class FileSystemPolicy(Policy):
|
|
138
|
+
... pass
|
|
139
|
+
>>> registry.register_by_convention(FileSystemPolicy)
|
|
140
|
+
>>> # Registered as "file_system"
|
|
141
|
+
"""
|
|
142
|
+
resource_name = policy_class.get_resource_name()
|
|
143
|
+
self.register(resource_name, policy_class)
|
|
144
|
+
|
|
145
|
+
def get_policy(self, resource_name: str) -> type[Policy]:
|
|
146
|
+
"""
|
|
147
|
+
Get the policy class for a resource.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
resource_name: The name of the resource.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
The registered policy class.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
PolicyNotFoundError: If no policy is registered and no default is set.
|
|
157
|
+
|
|
158
|
+
Example:
|
|
159
|
+
>>> policy_class = registry.get_policy("database")
|
|
160
|
+
>>> policy = policy_class(user, db_connection)
|
|
161
|
+
"""
|
|
162
|
+
with self._lock:
|
|
163
|
+
if resource_name in self._policies:
|
|
164
|
+
return self._policies[resource_name]
|
|
165
|
+
|
|
166
|
+
if self._default_policy is not None:
|
|
167
|
+
logger.debug(
|
|
168
|
+
f"No policy for '{resource_name}', using default: "
|
|
169
|
+
f"{self._default_policy.__name__}"
|
|
170
|
+
)
|
|
171
|
+
return self._default_policy
|
|
172
|
+
|
|
173
|
+
available = list(self._policies.keys())
|
|
174
|
+
raise PolicyNotFoundError(resource_name, available)
|
|
175
|
+
|
|
176
|
+
def get_policy_instance(
|
|
177
|
+
self,
|
|
178
|
+
resource_name: str,
|
|
179
|
+
user: UserContext,
|
|
180
|
+
resource: Any = None,
|
|
181
|
+
) -> Policy:
|
|
182
|
+
"""
|
|
183
|
+
Get an instantiated policy for a resource.
|
|
184
|
+
|
|
185
|
+
Convenience method that looks up the policy class and
|
|
186
|
+
creates an instance with the provided user and resource.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
resource_name: The name of the resource.
|
|
190
|
+
user: The user context for the policy.
|
|
191
|
+
resource: The resource instance (optional).
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
An instantiated policy object.
|
|
195
|
+
|
|
196
|
+
Example:
|
|
197
|
+
>>> policy = registry.get_policy_instance("document", user, doc)
|
|
198
|
+
>>> if policy.can("read"):
|
|
199
|
+
... return doc.content
|
|
200
|
+
"""
|
|
201
|
+
policy_class = self.get_policy(resource_name)
|
|
202
|
+
return policy_class(user, resource)
|
|
203
|
+
|
|
204
|
+
def has_policy(self, resource_name: str) -> bool:
|
|
205
|
+
"""
|
|
206
|
+
Check if a policy is registered for a resource.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
resource_name: The name of the resource.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
True if a policy is registered, False otherwise.
|
|
213
|
+
"""
|
|
214
|
+
with self._lock:
|
|
215
|
+
return resource_name in self._policies
|
|
216
|
+
|
|
217
|
+
def list_policies(self) -> dict[str, str]:
|
|
218
|
+
"""
|
|
219
|
+
List all registered policies.
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Dictionary mapping resource names to policy class names.
|
|
223
|
+
|
|
224
|
+
Example:
|
|
225
|
+
>>> registry.list_policies()
|
|
226
|
+
{'database': 'DatabasePolicy', 'file': 'FilePolicy'}
|
|
227
|
+
"""
|
|
228
|
+
with self._lock:
|
|
229
|
+
return {
|
|
230
|
+
resource: policy.__name__
|
|
231
|
+
for resource, policy in self._policies.items()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
def unregister(self, resource_name: str) -> bool:
|
|
235
|
+
"""
|
|
236
|
+
Unregister a policy for a resource.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
resource_name: The name of the resource.
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
True if a policy was unregistered, False if none was registered.
|
|
243
|
+
"""
|
|
244
|
+
with self._lock:
|
|
245
|
+
if resource_name in self._policies:
|
|
246
|
+
del self._policies[resource_name]
|
|
247
|
+
logger.debug(f"Unregistered policy for resource '{resource_name}'")
|
|
248
|
+
return True
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
def clear(self) -> None:
|
|
252
|
+
"""
|
|
253
|
+
Clear all registered policies.
|
|
254
|
+
|
|
255
|
+
Useful for testing or reconfiguration.
|
|
256
|
+
"""
|
|
257
|
+
with self._lock:
|
|
258
|
+
self._policies.clear()
|
|
259
|
+
self._authorized_resources.clear()
|
|
260
|
+
logger.debug("Cleared all registered policies")
|
|
261
|
+
|
|
262
|
+
def set_default_policy(self, policy_class: type[Policy] | None) -> None:
|
|
263
|
+
"""
|
|
264
|
+
Set or clear the default policy.
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
policy_class: The default policy class, or None to clear.
|
|
268
|
+
"""
|
|
269
|
+
with self._lock:
|
|
270
|
+
self._default_policy = policy_class
|
|
271
|
+
if policy_class:
|
|
272
|
+
logger.debug(f"Set default policy to '{policy_class.__name__}'")
|
|
273
|
+
else:
|
|
274
|
+
logger.debug("Cleared default policy")
|
|
275
|
+
|
|
276
|
+
# Authorization tracking methods
|
|
277
|
+
|
|
278
|
+
def mark_authorized(self, resource_name: str) -> None:
|
|
279
|
+
"""
|
|
280
|
+
Mark a resource as having been authorized.
|
|
281
|
+
|
|
282
|
+
This is called automatically by the authorization system
|
|
283
|
+
when a policy check is performed.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
resource_name: The name of the authorized resource.
|
|
287
|
+
"""
|
|
288
|
+
with self._lock:
|
|
289
|
+
self._authorized_resources.add(resource_name)
|
|
290
|
+
|
|
291
|
+
def clear_authorized(self) -> None:
|
|
292
|
+
"""
|
|
293
|
+
Clear the set of authorized resources.
|
|
294
|
+
|
|
295
|
+
Should be called at the start of each request to track
|
|
296
|
+
which resources were checked during that request.
|
|
297
|
+
"""
|
|
298
|
+
with self._lock:
|
|
299
|
+
self._authorized_resources.clear()
|
|
300
|
+
|
|
301
|
+
def verify_all_authorized(self, expected_resources: list[str]) -> tuple[bool, list[str]]:
|
|
302
|
+
"""
|
|
303
|
+
Verify that all expected resources were authorized.
|
|
304
|
+
|
|
305
|
+
This method helps catch cases where authorization was
|
|
306
|
+
accidentally skipped for a resource that should have been checked.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
expected_resources: List of resource names that should have been authorized.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
Tuple of (all_authorized, missing_resources).
|
|
313
|
+
- all_authorized: True if all resources were checked.
|
|
314
|
+
- missing_resources: List of resources that were not authorized.
|
|
315
|
+
|
|
316
|
+
Example:
|
|
317
|
+
>>> # At end of request handling
|
|
318
|
+
>>> ok, missing = registry.verify_all_authorized(["document", "user"])
|
|
319
|
+
>>> if not ok:
|
|
320
|
+
... logger.error(f"Authorization skipped for: {missing}")
|
|
321
|
+
"""
|
|
322
|
+
with self._lock:
|
|
323
|
+
missing = [
|
|
324
|
+
resource for resource in expected_resources
|
|
325
|
+
if resource not in self._authorized_resources
|
|
326
|
+
]
|
|
327
|
+
return len(missing) == 0, missing
|
|
328
|
+
|
|
329
|
+
def get_authorized_resources(self) -> set[str]:
|
|
330
|
+
"""
|
|
331
|
+
Get the set of resources that have been authorized.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
Set of resource names that were checked.
|
|
335
|
+
"""
|
|
336
|
+
with self._lock:
|
|
337
|
+
return self._authorized_resources.copy()
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# Global registry instance for convenience
|
|
341
|
+
_global_registry: PolicyRegistry | None = None
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def get_global_registry() -> PolicyRegistry:
|
|
345
|
+
"""
|
|
346
|
+
Get the global policy registry instance.
|
|
347
|
+
|
|
348
|
+
Creates one if it doesn't exist. This provides a convenient
|
|
349
|
+
singleton for simple use cases.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
The global PolicyRegistry instance.
|
|
353
|
+
|
|
354
|
+
Example:
|
|
355
|
+
>>> from proxilion.policies.registry import get_global_registry
|
|
356
|
+
>>> registry = get_global_registry()
|
|
357
|
+
>>> @registry.policy("my_resource")
|
|
358
|
+
>>> class MyPolicy(Policy):
|
|
359
|
+
... pass
|
|
360
|
+
"""
|
|
361
|
+
global _global_registry
|
|
362
|
+
if _global_registry is None:
|
|
363
|
+
_global_registry = PolicyRegistry()
|
|
364
|
+
return _global_registry
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def reset_global_registry() -> None:
|
|
368
|
+
"""
|
|
369
|
+
Reset the global registry.
|
|
370
|
+
|
|
371
|
+
Clears the global registry instance. Primarily useful for testing.
|
|
372
|
+
"""
|
|
373
|
+
global _global_registry
|
|
374
|
+
if _global_registry is not None:
|
|
375
|
+
_global_registry.clear()
|
|
376
|
+
_global_registry = None
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Provider-agnostic helpers for AI integrations.
|
|
3
|
+
|
|
4
|
+
Supports:
|
|
5
|
+
- OpenAI / Azure OpenAI
|
|
6
|
+
- Anthropic Claude
|
|
7
|
+
- Google Vertex AI / Gemini
|
|
8
|
+
- AWS Bedrock (via OpenAI-compatible interface)
|
|
9
|
+
- Local models (Ollama)
|
|
10
|
+
|
|
11
|
+
This module provides a unified interface for working with tool calls
|
|
12
|
+
across different LLM providers, enabling provider-agnostic authorization
|
|
13
|
+
and execution.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
>>> from proxilion.providers import (
|
|
17
|
+
... get_adapter, detect_provider, UnifiedToolCall,
|
|
18
|
+
... OpenAIAdapter, AnthropicAdapter, GeminiAdapter,
|
|
19
|
+
... )
|
|
20
|
+
>>>
|
|
21
|
+
>>> # Auto-detect provider from response
|
|
22
|
+
>>> adapter = get_adapter(response=llm_response)
|
|
23
|
+
>>> tool_calls = adapter.extract_tool_calls(llm_response)
|
|
24
|
+
>>>
|
|
25
|
+
>>> # Or specify provider explicitly
|
|
26
|
+
>>> adapter = get_adapter(provider="openai")
|
|
27
|
+
>>> openai_tools = adapter.format_tools(registry.list_enabled())
|
|
28
|
+
>>>
|
|
29
|
+
>>> # Work with unified tool calls
|
|
30
|
+
>>> for call in tool_calls:
|
|
31
|
+
... if auth.can(user, "execute", call.name):
|
|
32
|
+
... result = execute_tool(call.name, **call.arguments)
|
|
33
|
+
... response_msg = adapter.format_tool_result(call, result)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from proxilion.providers.adapter import (
|
|
37
|
+
BaseAdapter,
|
|
38
|
+
Provider,
|
|
39
|
+
ProviderAdapter,
|
|
40
|
+
UnifiedResponse,
|
|
41
|
+
UnifiedToolCall,
|
|
42
|
+
UnifiedToolResult,
|
|
43
|
+
detect_provider,
|
|
44
|
+
detect_provider_safe,
|
|
45
|
+
)
|
|
46
|
+
from proxilion.providers.anthropic_adapter import AnthropicAdapter
|
|
47
|
+
from proxilion.providers.gemini_adapter import GeminiAdapter
|
|
48
|
+
from proxilion.providers.openai_adapter import OpenAIAdapter
|
|
49
|
+
|
|
50
|
+
# Adapter registry
|
|
51
|
+
_ADAPTERS: dict[str, type[BaseAdapter]] = {
|
|
52
|
+
"openai": OpenAIAdapter,
|
|
53
|
+
"anthropic": AnthropicAdapter,
|
|
54
|
+
"gemini": GeminiAdapter,
|
|
55
|
+
"vertexai": GeminiAdapter, # Alias
|
|
56
|
+
"google": GeminiAdapter, # Alias
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Singleton instances
|
|
60
|
+
_adapter_instances: dict[str, BaseAdapter] = {}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_adapter(
|
|
64
|
+
provider: str | Provider | None = None,
|
|
65
|
+
response: object | None = None,
|
|
66
|
+
) -> BaseAdapter:
|
|
67
|
+
"""
|
|
68
|
+
Get the appropriate adapter for a provider.
|
|
69
|
+
|
|
70
|
+
Can auto-detect the provider from a response object or
|
|
71
|
+
accept an explicit provider name.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
provider: Provider name or enum (e.g., "openai", Provider.ANTHROPIC).
|
|
75
|
+
response: Optional response object for auto-detection.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Appropriate adapter instance.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
ValueError: If provider cannot be determined or is unknown.
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
>>> # Auto-detect from response
|
|
85
|
+
>>> adapter = get_adapter(response=openai_response)
|
|
86
|
+
>>>
|
|
87
|
+
>>> # Explicit provider
|
|
88
|
+
>>> adapter = get_adapter(provider="anthropic")
|
|
89
|
+
>>> adapter = get_adapter(provider=Provider.GEMINI)
|
|
90
|
+
"""
|
|
91
|
+
# Auto-detect if no provider specified
|
|
92
|
+
if provider is None:
|
|
93
|
+
if response is not None:
|
|
94
|
+
provider = detect_provider(response)
|
|
95
|
+
else:
|
|
96
|
+
raise ValueError("Must specify provider or response for auto-detection")
|
|
97
|
+
|
|
98
|
+
# Convert Provider enum to string
|
|
99
|
+
provider_key = provider.value if isinstance(provider, Provider) else provider.lower()
|
|
100
|
+
|
|
101
|
+
# Get or create adapter instance
|
|
102
|
+
if provider_key not in _adapter_instances:
|
|
103
|
+
if provider_key not in _ADAPTERS:
|
|
104
|
+
raise ValueError(
|
|
105
|
+
f"Unknown provider: {provider_key}. "
|
|
106
|
+
f"Supported: {list(_ADAPTERS.keys())}"
|
|
107
|
+
)
|
|
108
|
+
_adapter_instances[provider_key] = _ADAPTERS[provider_key]()
|
|
109
|
+
|
|
110
|
+
return _adapter_instances[provider_key]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def register_adapter(name: str, adapter_class: type[BaseAdapter]) -> None:
|
|
114
|
+
"""
|
|
115
|
+
Register a custom adapter.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
name: Provider name to register under.
|
|
119
|
+
adapter_class: Adapter class (must inherit from BaseAdapter).
|
|
120
|
+
|
|
121
|
+
Example:
|
|
122
|
+
>>> class MyCustomAdapter(BaseAdapter):
|
|
123
|
+
... ...
|
|
124
|
+
>>> register_adapter("custom", MyCustomAdapter)
|
|
125
|
+
>>> adapter = get_adapter("custom")
|
|
126
|
+
"""
|
|
127
|
+
_ADAPTERS[name.lower()] = adapter_class
|
|
128
|
+
# Clear cached instance if exists
|
|
129
|
+
if name.lower() in _adapter_instances:
|
|
130
|
+
del _adapter_instances[name.lower()]
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def list_providers() -> list[str]:
|
|
134
|
+
"""
|
|
135
|
+
List all registered provider names.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
List of provider names.
|
|
139
|
+
"""
|
|
140
|
+
return list(_ADAPTERS.keys())
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def extract_tool_calls(response: object) -> list[UnifiedToolCall]:
|
|
144
|
+
"""
|
|
145
|
+
Convenience function to extract tool calls from any supported response.
|
|
146
|
+
|
|
147
|
+
Auto-detects the provider and extracts tool calls.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
response: LLM response object.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
List of unified tool calls.
|
|
154
|
+
|
|
155
|
+
Example:
|
|
156
|
+
>>> tool_calls = extract_tool_calls(llm_response)
|
|
157
|
+
>>> for call in tool_calls:
|
|
158
|
+
... print(f"{call.name}: {call.arguments}")
|
|
159
|
+
"""
|
|
160
|
+
adapter = get_adapter(response=response)
|
|
161
|
+
return adapter.extract_tool_calls(response)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def extract_response(response: object) -> UnifiedResponse:
|
|
165
|
+
"""
|
|
166
|
+
Convenience function to extract full response from any supported response.
|
|
167
|
+
|
|
168
|
+
Auto-detects the provider and extracts the response.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
response: LLM response object.
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
UnifiedResponse instance.
|
|
175
|
+
"""
|
|
176
|
+
adapter = get_adapter(response=response)
|
|
177
|
+
return adapter.extract_response(response)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
__all__ = [
|
|
181
|
+
# Core types
|
|
182
|
+
"Provider",
|
|
183
|
+
"ProviderAdapter",
|
|
184
|
+
"BaseAdapter",
|
|
185
|
+
"UnifiedToolCall",
|
|
186
|
+
"UnifiedToolResult",
|
|
187
|
+
"UnifiedResponse",
|
|
188
|
+
# Adapters
|
|
189
|
+
"OpenAIAdapter",
|
|
190
|
+
"AnthropicAdapter",
|
|
191
|
+
"GeminiAdapter",
|
|
192
|
+
# Factory functions
|
|
193
|
+
"get_adapter",
|
|
194
|
+
"register_adapter",
|
|
195
|
+
"list_providers",
|
|
196
|
+
"detect_provider",
|
|
197
|
+
"detect_provider_safe",
|
|
198
|
+
# Convenience functions
|
|
199
|
+
"extract_tool_calls",
|
|
200
|
+
"extract_response",
|
|
201
|
+
]
|