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,646 @@
|
|
|
1
|
+
"""
|
|
2
|
+
OpenAI integration for Proxilion.
|
|
3
|
+
|
|
4
|
+
This module provides authorization wrappers for OpenAI function calling,
|
|
5
|
+
enabling secure function execution with user-context authorization.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- ProxilionFunctionHandler: Manages function registration and execution
|
|
9
|
+
- create_secure_function: Wraps individual functions with authorization
|
|
10
|
+
- Safe error handling (no internal details exposed)
|
|
11
|
+
|
|
12
|
+
Note:
|
|
13
|
+
OpenAI SDK is an optional dependency. This module works by wrapping
|
|
14
|
+
function definitions and implementations rather than modifying the
|
|
15
|
+
OpenAI client directly.
|
|
16
|
+
|
|
17
|
+
Example:
|
|
18
|
+
>>> from openai import OpenAI
|
|
19
|
+
>>> from proxilion import Proxilion
|
|
20
|
+
>>> from proxilion.contrib.openai import ProxilionFunctionHandler
|
|
21
|
+
>>>
|
|
22
|
+
>>> auth = Proxilion()
|
|
23
|
+
>>> handler = ProxilionFunctionHandler(auth)
|
|
24
|
+
>>>
|
|
25
|
+
>>> # Register a function
|
|
26
|
+
>>> handler.register_function(
|
|
27
|
+
... name="get_weather",
|
|
28
|
+
... schema=weather_schema,
|
|
29
|
+
... implementation=get_weather_impl,
|
|
30
|
+
... resource="weather_api",
|
|
31
|
+
... )
|
|
32
|
+
>>>
|
|
33
|
+
>>> # Process function call from OpenAI response
|
|
34
|
+
>>> if response.choices[0].message.function_call:
|
|
35
|
+
... result = handler.execute(
|
|
36
|
+
... function_call=response.choices[0].message.function_call,
|
|
37
|
+
... user=current_user,
|
|
38
|
+
... )
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
from __future__ import annotations
|
|
42
|
+
|
|
43
|
+
import asyncio
|
|
44
|
+
import inspect
|
|
45
|
+
import json
|
|
46
|
+
import logging
|
|
47
|
+
from collections.abc import Callable
|
|
48
|
+
from dataclasses import dataclass, field
|
|
49
|
+
from datetime import datetime, timezone
|
|
50
|
+
from typing import Any, TypeVar
|
|
51
|
+
|
|
52
|
+
from proxilion.exceptions import (
|
|
53
|
+
AuthorizationError,
|
|
54
|
+
ProxilionError,
|
|
55
|
+
)
|
|
56
|
+
from proxilion.types import AgentContext, UserContext
|
|
57
|
+
|
|
58
|
+
logger = logging.getLogger(__name__)
|
|
59
|
+
|
|
60
|
+
T = TypeVar("T")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class OpenAIIntegrationError(ProxilionError):
|
|
64
|
+
"""Error in OpenAI integration."""
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class FunctionNotFoundError(OpenAIIntegrationError):
|
|
69
|
+
"""Raised when a function is not registered."""
|
|
70
|
+
|
|
71
|
+
def __init__(self, function_name: str) -> None:
|
|
72
|
+
self.function_name = function_name
|
|
73
|
+
super().__init__(f"Function not registered: {function_name}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class FunctionExecutionError(OpenAIIntegrationError):
|
|
77
|
+
"""Raised when function execution fails."""
|
|
78
|
+
|
|
79
|
+
def __init__(self, function_name: str, safe_message: str) -> None:
|
|
80
|
+
self.function_name = function_name
|
|
81
|
+
self.safe_message = safe_message
|
|
82
|
+
super().__init__(f"Function execution failed: {safe_message}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class RegisteredFunction:
|
|
87
|
+
"""A registered function with its schema and implementation."""
|
|
88
|
+
name: str
|
|
89
|
+
schema: dict[str, Any]
|
|
90
|
+
implementation: Callable[..., Any]
|
|
91
|
+
resource: str
|
|
92
|
+
action: str
|
|
93
|
+
async_impl: bool
|
|
94
|
+
description: str
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class FunctionCallResult:
|
|
99
|
+
"""Result of a function call execution."""
|
|
100
|
+
function_name: str
|
|
101
|
+
success: bool
|
|
102
|
+
result: Any | None = None
|
|
103
|
+
error: str | None = None
|
|
104
|
+
authorized: bool = True
|
|
105
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class ProxilionFunctionHandler:
|
|
109
|
+
"""
|
|
110
|
+
Handler for OpenAI function calls with Proxilion authorization.
|
|
111
|
+
|
|
112
|
+
Manages function registration, schema validation, authorization,
|
|
113
|
+
and execution for OpenAI's function calling feature.
|
|
114
|
+
|
|
115
|
+
Example:
|
|
116
|
+
>>> from proxilion import Proxilion, Policy, UserContext
|
|
117
|
+
>>> from proxilion.contrib.openai import ProxilionFunctionHandler
|
|
118
|
+
>>>
|
|
119
|
+
>>> auth = Proxilion()
|
|
120
|
+
>>>
|
|
121
|
+
>>> @auth.policy("weather_api")
|
|
122
|
+
... class WeatherPolicy(Policy):
|
|
123
|
+
... def can_execute(self, context):
|
|
124
|
+
... return "weather_user" in self.user.roles
|
|
125
|
+
>>>
|
|
126
|
+
>>> handler = ProxilionFunctionHandler(auth)
|
|
127
|
+
>>>
|
|
128
|
+
>>> def get_weather(location: str, unit: str = "celsius") -> str:
|
|
129
|
+
... return f"Weather in {location}: 20{unit[0].upper()}"
|
|
130
|
+
>>>
|
|
131
|
+
>>> handler.register_function(
|
|
132
|
+
... name="get_weather",
|
|
133
|
+
... schema={
|
|
134
|
+
... "name": "get_weather",
|
|
135
|
+
... "description": "Get weather for a location",
|
|
136
|
+
... "parameters": {
|
|
137
|
+
... "type": "object",
|
|
138
|
+
... "properties": {
|
|
139
|
+
... "location": {"type": "string"},
|
|
140
|
+
... "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
|
|
141
|
+
... },
|
|
142
|
+
... "required": ["location"]
|
|
143
|
+
... }
|
|
144
|
+
... },
|
|
145
|
+
... implementation=get_weather,
|
|
146
|
+
... resource="weather_api",
|
|
147
|
+
... )
|
|
148
|
+
>>>
|
|
149
|
+
>>> # Execute function call
|
|
150
|
+
>>> user = UserContext(user_id="alice", roles=["weather_user"])
|
|
151
|
+
>>> result = handler.execute(
|
|
152
|
+
... function_name="get_weather",
|
|
153
|
+
... arguments={"location": "London"},
|
|
154
|
+
... user=user,
|
|
155
|
+
... )
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def __init__(
|
|
159
|
+
self,
|
|
160
|
+
proxilion: Any,
|
|
161
|
+
default_action: str = "execute",
|
|
162
|
+
safe_errors: bool = True,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Initialize the function handler.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
proxilion: Proxilion instance for authorization.
|
|
169
|
+
default_action: Default action for authorization checks.
|
|
170
|
+
safe_errors: If True, return safe error messages without internals.
|
|
171
|
+
"""
|
|
172
|
+
self.proxilion = proxilion
|
|
173
|
+
self.default_action = default_action
|
|
174
|
+
self.safe_errors = safe_errors
|
|
175
|
+
|
|
176
|
+
self._functions: dict[str, RegisteredFunction] = {}
|
|
177
|
+
self._call_history: list[FunctionCallResult] = []
|
|
178
|
+
|
|
179
|
+
@property
|
|
180
|
+
def functions(self) -> list[RegisteredFunction]:
|
|
181
|
+
"""Get list of registered functions."""
|
|
182
|
+
return list(self._functions.values())
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def function_schemas(self) -> list[dict[str, Any]]:
|
|
186
|
+
"""Get list of function schemas for OpenAI API."""
|
|
187
|
+
return [f.schema for f in self._functions.values()]
|
|
188
|
+
|
|
189
|
+
@property
|
|
190
|
+
def call_history(self) -> list[FunctionCallResult]:
|
|
191
|
+
"""Get history of function calls."""
|
|
192
|
+
return list(self._call_history)
|
|
193
|
+
|
|
194
|
+
def register_function(
|
|
195
|
+
self,
|
|
196
|
+
name: str,
|
|
197
|
+
schema: dict[str, Any],
|
|
198
|
+
implementation: Callable[..., Any],
|
|
199
|
+
resource: str | None = None,
|
|
200
|
+
action: str | None = None,
|
|
201
|
+
description: str | None = None,
|
|
202
|
+
) -> None:
|
|
203
|
+
"""
|
|
204
|
+
Register a function for execution.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
name: Function name (must match OpenAI function call).
|
|
208
|
+
schema: OpenAI function schema.
|
|
209
|
+
implementation: Python function to execute.
|
|
210
|
+
resource: Resource name for authorization (default: function name).
|
|
211
|
+
action: Action for authorization (default: handler default).
|
|
212
|
+
description: Optional description override.
|
|
213
|
+
"""
|
|
214
|
+
is_async = inspect.iscoroutinefunction(implementation)
|
|
215
|
+
|
|
216
|
+
self._functions[name] = RegisteredFunction(
|
|
217
|
+
name=name,
|
|
218
|
+
schema=schema,
|
|
219
|
+
implementation=implementation,
|
|
220
|
+
resource=resource or name,
|
|
221
|
+
action=action or self.default_action,
|
|
222
|
+
async_impl=is_async,
|
|
223
|
+
description=description or schema.get("description", ""),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
logger.debug(f"Registered function: {name} (resource: {resource or name})")
|
|
227
|
+
|
|
228
|
+
def unregister_function(self, name: str) -> bool:
|
|
229
|
+
"""
|
|
230
|
+
Unregister a function.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
name: Function name to unregister.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
True if function was registered and removed.
|
|
237
|
+
"""
|
|
238
|
+
if name in self._functions:
|
|
239
|
+
del self._functions[name]
|
|
240
|
+
return True
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
def get_function(self, name: str) -> RegisteredFunction | None:
|
|
244
|
+
"""Get a registered function by name."""
|
|
245
|
+
return self._functions.get(name)
|
|
246
|
+
|
|
247
|
+
def execute(
|
|
248
|
+
self,
|
|
249
|
+
function_name: str | None = None,
|
|
250
|
+
arguments: dict[str, Any] | str | None = None,
|
|
251
|
+
user: UserContext | None = None,
|
|
252
|
+
agent: AgentContext | None = None,
|
|
253
|
+
function_call: Any | None = None,
|
|
254
|
+
) -> FunctionCallResult:
|
|
255
|
+
"""
|
|
256
|
+
Execute a function call with authorization.
|
|
257
|
+
|
|
258
|
+
Can accept either explicit function_name/arguments or an OpenAI
|
|
259
|
+
function_call object.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
function_name: Name of the function to call.
|
|
263
|
+
arguments: Function arguments (dict or JSON string).
|
|
264
|
+
user: User context for authorization.
|
|
265
|
+
agent: Optional agent context.
|
|
266
|
+
function_call: OpenAI function_call object (alternative).
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
FunctionCallResult with execution result or error.
|
|
270
|
+
"""
|
|
271
|
+
# Extract from function_call object if provided
|
|
272
|
+
if function_call is not None:
|
|
273
|
+
function_name = getattr(function_call, "name", None)
|
|
274
|
+
raw_args = getattr(function_call, "arguments", "{}")
|
|
275
|
+
if isinstance(raw_args, str):
|
|
276
|
+
try:
|
|
277
|
+
arguments = json.loads(raw_args)
|
|
278
|
+
except json.JSONDecodeError:
|
|
279
|
+
arguments = {}
|
|
280
|
+
else:
|
|
281
|
+
arguments = raw_args or {}
|
|
282
|
+
|
|
283
|
+
if function_name is None:
|
|
284
|
+
return FunctionCallResult(
|
|
285
|
+
function_name="unknown",
|
|
286
|
+
success=False,
|
|
287
|
+
error="No function name provided",
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Parse arguments if string
|
|
291
|
+
if isinstance(arguments, str):
|
|
292
|
+
try:
|
|
293
|
+
arguments = json.loads(arguments)
|
|
294
|
+
except json.JSONDecodeError:
|
|
295
|
+
return FunctionCallResult(
|
|
296
|
+
function_name=function_name,
|
|
297
|
+
success=False,
|
|
298
|
+
error="Invalid JSON arguments",
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
arguments = arguments or {}
|
|
302
|
+
|
|
303
|
+
# Get registered function
|
|
304
|
+
func = self._functions.get(function_name)
|
|
305
|
+
if func is None:
|
|
306
|
+
result = FunctionCallResult(
|
|
307
|
+
function_name=function_name,
|
|
308
|
+
success=False,
|
|
309
|
+
error=f"Function not found: {function_name}",
|
|
310
|
+
)
|
|
311
|
+
self._call_history.append(result)
|
|
312
|
+
return result
|
|
313
|
+
|
|
314
|
+
# Check authorization
|
|
315
|
+
if user is not None:
|
|
316
|
+
context = {
|
|
317
|
+
"function_name": function_name,
|
|
318
|
+
"arguments": arguments,
|
|
319
|
+
**arguments,
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
auth_result = self.proxilion.check(user, func.action, func.resource, context)
|
|
323
|
+
|
|
324
|
+
if not auth_result.allowed:
|
|
325
|
+
result = FunctionCallResult(
|
|
326
|
+
function_name=function_name,
|
|
327
|
+
success=False,
|
|
328
|
+
error="Not authorized" if self.safe_errors else auth_result.reason,
|
|
329
|
+
authorized=False,
|
|
330
|
+
)
|
|
331
|
+
self._call_history.append(result)
|
|
332
|
+
return result
|
|
333
|
+
|
|
334
|
+
# Execute function
|
|
335
|
+
try:
|
|
336
|
+
if func.async_impl:
|
|
337
|
+
# Run async function synchronously
|
|
338
|
+
loop = asyncio.new_event_loop()
|
|
339
|
+
try:
|
|
340
|
+
output = loop.run_until_complete(func.implementation(**arguments))
|
|
341
|
+
finally:
|
|
342
|
+
loop.close()
|
|
343
|
+
else:
|
|
344
|
+
output = func.implementation(**arguments)
|
|
345
|
+
|
|
346
|
+
result = FunctionCallResult(
|
|
347
|
+
function_name=function_name,
|
|
348
|
+
success=True,
|
|
349
|
+
result=output,
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
except Exception as e:
|
|
353
|
+
logger.error(f"Function execution error: {function_name} - {e}")
|
|
354
|
+
|
|
355
|
+
error_msg = "Function execution failed"
|
|
356
|
+
if not self.safe_errors:
|
|
357
|
+
error_msg = str(e)
|
|
358
|
+
|
|
359
|
+
result = FunctionCallResult(
|
|
360
|
+
function_name=function_name,
|
|
361
|
+
success=False,
|
|
362
|
+
error=error_msg,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
self._call_history.append(result)
|
|
366
|
+
return result
|
|
367
|
+
|
|
368
|
+
async def execute_async(
|
|
369
|
+
self,
|
|
370
|
+
function_name: str | None = None,
|
|
371
|
+
arguments: dict[str, Any] | str | None = None,
|
|
372
|
+
user: UserContext | None = None,
|
|
373
|
+
agent: AgentContext | None = None,
|
|
374
|
+
function_call: Any | None = None,
|
|
375
|
+
) -> FunctionCallResult:
|
|
376
|
+
"""
|
|
377
|
+
Execute a function call asynchronously with authorization.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
function_name: Name of the function to call.
|
|
381
|
+
arguments: Function arguments.
|
|
382
|
+
user: User context for authorization.
|
|
383
|
+
agent: Optional agent context.
|
|
384
|
+
function_call: OpenAI function_call object.
|
|
385
|
+
|
|
386
|
+
Returns:
|
|
387
|
+
FunctionCallResult with execution result or error.
|
|
388
|
+
"""
|
|
389
|
+
# Extract from function_call object if provided
|
|
390
|
+
if function_call is not None:
|
|
391
|
+
function_name = getattr(function_call, "name", None)
|
|
392
|
+
raw_args = getattr(function_call, "arguments", "{}")
|
|
393
|
+
if isinstance(raw_args, str):
|
|
394
|
+
try:
|
|
395
|
+
arguments = json.loads(raw_args)
|
|
396
|
+
except json.JSONDecodeError:
|
|
397
|
+
arguments = {}
|
|
398
|
+
else:
|
|
399
|
+
arguments = raw_args or {}
|
|
400
|
+
|
|
401
|
+
if function_name is None:
|
|
402
|
+
return FunctionCallResult(
|
|
403
|
+
function_name="unknown",
|
|
404
|
+
success=False,
|
|
405
|
+
error="No function name provided",
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
if isinstance(arguments, str):
|
|
409
|
+
try:
|
|
410
|
+
arguments = json.loads(arguments)
|
|
411
|
+
except json.JSONDecodeError:
|
|
412
|
+
return FunctionCallResult(
|
|
413
|
+
function_name=function_name,
|
|
414
|
+
success=False,
|
|
415
|
+
error="Invalid JSON arguments",
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
arguments = arguments or {}
|
|
419
|
+
|
|
420
|
+
func = self._functions.get(function_name)
|
|
421
|
+
if func is None:
|
|
422
|
+
result = FunctionCallResult(
|
|
423
|
+
function_name=function_name,
|
|
424
|
+
success=False,
|
|
425
|
+
error=f"Function not found: {function_name}",
|
|
426
|
+
)
|
|
427
|
+
self._call_history.append(result)
|
|
428
|
+
return result
|
|
429
|
+
|
|
430
|
+
# Check authorization
|
|
431
|
+
if user is not None:
|
|
432
|
+
context = {
|
|
433
|
+
"function_name": function_name,
|
|
434
|
+
"arguments": arguments,
|
|
435
|
+
**arguments,
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
auth_result = self.proxilion.check(user, func.action, func.resource, context)
|
|
439
|
+
|
|
440
|
+
if not auth_result.allowed:
|
|
441
|
+
result = FunctionCallResult(
|
|
442
|
+
function_name=function_name,
|
|
443
|
+
success=False,
|
|
444
|
+
error="Not authorized" if self.safe_errors else auth_result.reason,
|
|
445
|
+
authorized=False,
|
|
446
|
+
)
|
|
447
|
+
self._call_history.append(result)
|
|
448
|
+
return result
|
|
449
|
+
|
|
450
|
+
# Execute function
|
|
451
|
+
try:
|
|
452
|
+
if func.async_impl:
|
|
453
|
+
output = await func.implementation(**arguments)
|
|
454
|
+
else:
|
|
455
|
+
loop = asyncio.get_event_loop()
|
|
456
|
+
output = await loop.run_in_executor(
|
|
457
|
+
None,
|
|
458
|
+
lambda: func.implementation(**arguments),
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
result = FunctionCallResult(
|
|
462
|
+
function_name=function_name,
|
|
463
|
+
success=True,
|
|
464
|
+
result=output,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
except Exception as e:
|
|
468
|
+
logger.error(f"Function execution error: {function_name} - {e}")
|
|
469
|
+
|
|
470
|
+
error_msg = "Function execution failed"
|
|
471
|
+
if not self.safe_errors:
|
|
472
|
+
error_msg = str(e)
|
|
473
|
+
|
|
474
|
+
result = FunctionCallResult(
|
|
475
|
+
function_name=function_name,
|
|
476
|
+
success=False,
|
|
477
|
+
error=error_msg,
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
self._call_history.append(result)
|
|
481
|
+
return result
|
|
482
|
+
|
|
483
|
+
def to_openai_tools(self) -> list[dict[str, Any]]:
|
|
484
|
+
"""
|
|
485
|
+
Get function schemas in OpenAI tools format.
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
List of tool definitions for OpenAI API.
|
|
489
|
+
"""
|
|
490
|
+
return [
|
|
491
|
+
{"type": "function", "function": func.schema}
|
|
492
|
+
for func in self._functions.values()
|
|
493
|
+
]
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def create_secure_function(
|
|
497
|
+
function_def: dict[str, Any],
|
|
498
|
+
implementation: Callable[..., Any],
|
|
499
|
+
proxilion: Any,
|
|
500
|
+
resource: str,
|
|
501
|
+
action: str = "execute",
|
|
502
|
+
safe_errors: bool = True,
|
|
503
|
+
) -> tuple[dict[str, Any], Callable[..., Any]]:
|
|
504
|
+
"""
|
|
505
|
+
Create a secured function wrapper for OpenAI function calling.
|
|
506
|
+
|
|
507
|
+
Returns both the function definition and a wrapped implementation
|
|
508
|
+
that includes authorization checks.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
function_def: OpenAI function definition schema.
|
|
512
|
+
implementation: The actual function implementation.
|
|
513
|
+
proxilion: Proxilion instance for authorization.
|
|
514
|
+
resource: Resource name for authorization.
|
|
515
|
+
action: Action for authorization checks.
|
|
516
|
+
safe_errors: If True, return safe error messages.
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
Tuple of (function_def, wrapped_implementation).
|
|
520
|
+
|
|
521
|
+
Example:
|
|
522
|
+
>>> schema = {
|
|
523
|
+
... "name": "get_weather",
|
|
524
|
+
... "description": "Get weather",
|
|
525
|
+
... "parameters": {...}
|
|
526
|
+
... }
|
|
527
|
+
>>>
|
|
528
|
+
>>> def get_weather_impl(location: str) -> str:
|
|
529
|
+
... return f"Weather in {location}: sunny"
|
|
530
|
+
>>>
|
|
531
|
+
>>> schema, secure_impl = create_secure_function(
|
|
532
|
+
... function_def=schema,
|
|
533
|
+
... implementation=get_weather_impl,
|
|
534
|
+
... proxilion=auth,
|
|
535
|
+
... resource="weather_api",
|
|
536
|
+
... )
|
|
537
|
+
"""
|
|
538
|
+
is_async = inspect.iscoroutinefunction(implementation)
|
|
539
|
+
|
|
540
|
+
if is_async:
|
|
541
|
+
async def async_wrapper(
|
|
542
|
+
user: UserContext | None = None,
|
|
543
|
+
**kwargs: Any,
|
|
544
|
+
) -> Any:
|
|
545
|
+
if user is not None:
|
|
546
|
+
context = {"function_name": function_def.get("name", resource), **kwargs}
|
|
547
|
+
result = proxilion.check(user, action, resource, context)
|
|
548
|
+
|
|
549
|
+
if not result.allowed:
|
|
550
|
+
if safe_errors:
|
|
551
|
+
raise AuthorizationError(
|
|
552
|
+
user=user.user_id,
|
|
553
|
+
action=action,
|
|
554
|
+
resource=resource,
|
|
555
|
+
reason="Not authorized",
|
|
556
|
+
)
|
|
557
|
+
raise AuthorizationError(
|
|
558
|
+
user=user.user_id,
|
|
559
|
+
action=action,
|
|
560
|
+
resource=resource,
|
|
561
|
+
reason=result.reason,
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
return await implementation(**kwargs)
|
|
565
|
+
|
|
566
|
+
return function_def, async_wrapper
|
|
567
|
+
else:
|
|
568
|
+
def sync_wrapper(
|
|
569
|
+
user: UserContext | None = None,
|
|
570
|
+
**kwargs: Any,
|
|
571
|
+
) -> Any:
|
|
572
|
+
if user is not None:
|
|
573
|
+
context = {"function_name": function_def.get("name", resource), **kwargs}
|
|
574
|
+
result = proxilion.check(user, action, resource, context)
|
|
575
|
+
|
|
576
|
+
if not result.allowed:
|
|
577
|
+
if safe_errors:
|
|
578
|
+
raise AuthorizationError(
|
|
579
|
+
user=user.user_id,
|
|
580
|
+
action=action,
|
|
581
|
+
resource=resource,
|
|
582
|
+
reason="Not authorized",
|
|
583
|
+
)
|
|
584
|
+
raise AuthorizationError(
|
|
585
|
+
user=user.user_id,
|
|
586
|
+
action=action,
|
|
587
|
+
resource=resource,
|
|
588
|
+
reason=result.reason,
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
return implementation(**kwargs)
|
|
592
|
+
|
|
593
|
+
return function_def, sync_wrapper
|
|
594
|
+
|
|
595
|
+
|
|
596
|
+
def process_openai_response(
|
|
597
|
+
response: Any,
|
|
598
|
+
handler: ProxilionFunctionHandler,
|
|
599
|
+
user: UserContext | None = None,
|
|
600
|
+
) -> list[FunctionCallResult]:
|
|
601
|
+
"""
|
|
602
|
+
Process an OpenAI response and execute any function calls.
|
|
603
|
+
|
|
604
|
+
Handles both single function calls and tool_calls format.
|
|
605
|
+
|
|
606
|
+
Args:
|
|
607
|
+
response: OpenAI API response object.
|
|
608
|
+
handler: ProxilionFunctionHandler for execution.
|
|
609
|
+
user: User context for authorization.
|
|
610
|
+
|
|
611
|
+
Returns:
|
|
612
|
+
List of FunctionCallResult for each function call.
|
|
613
|
+
"""
|
|
614
|
+
results: list[FunctionCallResult] = []
|
|
615
|
+
|
|
616
|
+
# Handle choices
|
|
617
|
+
if hasattr(response, "choices"):
|
|
618
|
+
for choice in response.choices:
|
|
619
|
+
message = getattr(choice, "message", None)
|
|
620
|
+
if message is None:
|
|
621
|
+
continue
|
|
622
|
+
|
|
623
|
+
# Handle function_call format (deprecated but still used)
|
|
624
|
+
function_call = getattr(message, "function_call", None)
|
|
625
|
+
if function_call is not None:
|
|
626
|
+
result = handler.execute(
|
|
627
|
+
function_call=function_call,
|
|
628
|
+
user=user,
|
|
629
|
+
)
|
|
630
|
+
results.append(result)
|
|
631
|
+
|
|
632
|
+
# Handle tool_calls format (newer)
|
|
633
|
+
tool_calls = getattr(message, "tool_calls", None)
|
|
634
|
+
if tool_calls:
|
|
635
|
+
for tool_call in tool_calls:
|
|
636
|
+
if getattr(tool_call, "type", None) == "function":
|
|
637
|
+
func = getattr(tool_call, "function", None)
|
|
638
|
+
if func:
|
|
639
|
+
result = handler.execute(
|
|
640
|
+
function_name=getattr(func, "name", None),
|
|
641
|
+
arguments=getattr(func, "arguments", "{}"),
|
|
642
|
+
user=user,
|
|
643
|
+
)
|
|
644
|
+
results.append(result)
|
|
645
|
+
|
|
646
|
+
return results
|