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,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