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,1012 @@
1
+ """
2
+ Google Vertex AI / Gemini integration for Proxilion.
3
+
4
+ Provides authorization wrappers for Gemini's function calling feature,
5
+ enabling secure tool execution with user-context authorization.
6
+
7
+ Features:
8
+ - ProxilionVertexHandler: Manages tool registration and execution for Vertex AI
9
+ - GeminiFunctionCall: Represents a function call from Gemini response
10
+ - GeminiToolResult: Result of a tool execution
11
+ - extract_function_calls: Extract function calls from Gemini responses
12
+ - format_tool_response: Format results for sending back to Gemini
13
+
14
+ Note:
15
+ The vertexai library (google-cloud-aiplatform) is an optional dependency.
16
+ This module works by wrapping tool definitions and implementations rather
17
+ than modifying the Vertex AI client directly.
18
+
19
+ Example:
20
+ >>> import vertexai
21
+ >>> from vertexai.generative_models import GenerativeModel, Tool
22
+ >>> from proxilion import Proxilion, UserContext
23
+ >>> from proxilion.contrib.google import ProxilionVertexHandler
24
+ >>>
25
+ >>> auth = Proxilion()
26
+ >>> handler = ProxilionVertexHandler(auth)
27
+ >>>
28
+ >>> # Register tools
29
+ >>> handler.register_tool(
30
+ ... name="search_database",
31
+ ... declaration={
32
+ ... "name": "search_database",
33
+ ... "description": "Search the database",
34
+ ... "parameters": {
35
+ ... "type": "object",
36
+ ... "properties": {
37
+ ... "query": {"type": "string"}
38
+ ... },
39
+ ... "required": ["query"]
40
+ ... }
41
+ ... },
42
+ ... implementation=search_database_fn,
43
+ ... resource="database",
44
+ ... )
45
+ >>>
46
+ >>> # Get Gemini-formatted tools
47
+ >>> gemini_tools = handler.to_gemini_tools()
48
+ >>>
49
+ >>> # Create model with tools
50
+ >>> model = GenerativeModel("gemini-1.5-pro", tools=gemini_tools)
51
+ >>> response = model.generate_content("Find users named John")
52
+ >>>
53
+ >>> # Process function calls with authorization
54
+ >>> results = handler.process_response(response, user=current_user)
55
+ """
56
+
57
+ from __future__ import annotations
58
+
59
+ import asyncio
60
+ import contextlib
61
+ import inspect
62
+ import logging
63
+ from collections.abc import Callable
64
+ from dataclasses import dataclass, field
65
+ from datetime import datetime, timezone
66
+ from typing import Any, TypeVar
67
+
68
+ from proxilion.exceptions import ProxilionError
69
+ from proxilion.types import AgentContext, UserContext
70
+
71
+ logger = logging.getLogger(__name__)
72
+
73
+ T = TypeVar("T")
74
+
75
+
76
+ class GoogleIntegrationError(ProxilionError):
77
+ """Error in Google Vertex AI / Gemini integration."""
78
+ pass
79
+
80
+
81
+ class ToolNotFoundError(GoogleIntegrationError):
82
+ """Raised when a tool is not registered."""
83
+
84
+ def __init__(self, tool_name: str) -> None:
85
+ self.tool_name = tool_name
86
+ super().__init__(f"Tool not registered: {tool_name}")
87
+
88
+
89
+ class ToolExecutionError(GoogleIntegrationError):
90
+ """Raised when tool execution fails."""
91
+
92
+ def __init__(self, tool_name: str, safe_message: str) -> None:
93
+ self.tool_name = tool_name
94
+ self.safe_message = safe_message
95
+ super().__init__(f"Tool execution failed: {safe_message}")
96
+
97
+
98
+ @dataclass
99
+ class GeminiFunctionCall:
100
+ """
101
+ Represents a Gemini function call.
102
+
103
+ Attributes:
104
+ name: Name of the function to call.
105
+ args: Arguments passed to the function.
106
+ raw: Original function call object from Gemini.
107
+
108
+ Example:
109
+ >>> call = GeminiFunctionCall(
110
+ ... name="get_weather",
111
+ ... args={"location": "San Francisco"},
112
+ ... raw=gemini_function_call,
113
+ ... )
114
+ """
115
+ name: str
116
+ args: dict[str, Any]
117
+ raw: Any = None
118
+
119
+ def to_dict(self) -> dict[str, Any]:
120
+ """Convert to dictionary."""
121
+ return {
122
+ "name": self.name,
123
+ "args": self.args,
124
+ }
125
+
126
+
127
+ @dataclass
128
+ class GeminiToolResult:
129
+ """
130
+ Result of a Gemini tool execution.
131
+
132
+ Attributes:
133
+ name: Name of the tool that was executed.
134
+ success: Whether the execution succeeded.
135
+ result: The result value if successful.
136
+ error: Error message if failed.
137
+ authorized: Whether the call was authorized.
138
+ timestamp: When the execution occurred.
139
+
140
+ Example:
141
+ >>> result = GeminiToolResult(
142
+ ... name="get_weather",
143
+ ... success=True,
144
+ ... result={"temperature": 72, "condition": "sunny"},
145
+ ... )
146
+ """
147
+ name: str
148
+ success: bool
149
+ result: Any | None = None
150
+ error: str | None = None
151
+ authorized: bool = True
152
+ timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
153
+
154
+ def to_dict(self) -> dict[str, Any]:
155
+ """Convert to dictionary."""
156
+ return {
157
+ "name": self.name,
158
+ "success": self.success,
159
+ "result": self.result,
160
+ "error": self.error,
161
+ "authorized": self.authorized,
162
+ "timestamp": self.timestamp.isoformat(),
163
+ }
164
+
165
+ def to_function_response(self) -> dict[str, Any]:
166
+ """
167
+ Convert to Gemini function_response format.
168
+
169
+ Returns:
170
+ Dictionary suitable for Part.from_function_response().
171
+ """
172
+ if self.success:
173
+ response_data = self.result
174
+ if not isinstance(response_data, dict):
175
+ response_data = {"result": response_data}
176
+ else:
177
+ response_data = {"error": self.error or "Execution failed"}
178
+
179
+ return {
180
+ "name": self.name,
181
+ "response": response_data,
182
+ }
183
+
184
+
185
+ @dataclass
186
+ class RegisteredGeminiTool:
187
+ """A registered tool with its declaration and implementation."""
188
+ name: str
189
+ declaration: dict[str, Any]
190
+ implementation: Callable[..., Any]
191
+ resource: str
192
+ action: str
193
+ async_impl: bool
194
+ description: str
195
+
196
+
197
+ class ProxilionVertexHandler:
198
+ """
199
+ Handler for Google Vertex AI / Gemini function calling with Proxilion.
200
+
201
+ Manages tool registration, authorization, and execution for
202
+ Gemini's function calling feature.
203
+
204
+ Example:
205
+ >>> from proxilion import Proxilion, Policy, UserContext
206
+ >>> from proxilion.contrib.google import ProxilionVertexHandler
207
+ >>>
208
+ >>> auth = Proxilion()
209
+ >>>
210
+ >>> @auth.policy("weather_api")
211
+ ... class WeatherPolicy(Policy):
212
+ ... def can_execute(self, context):
213
+ ... return True
214
+ >>>
215
+ >>> handler = ProxilionVertexHandler(auth)
216
+ >>>
217
+ >>> def get_weather(location: str) -> dict:
218
+ ... return {"temp": 72, "condition": "sunny"}
219
+ >>>
220
+ >>> handler.register_tool(
221
+ ... name="get_weather",
222
+ ... declaration={
223
+ ... "name": "get_weather",
224
+ ... "description": "Get weather for a location",
225
+ ... "parameters": {
226
+ ... "type": "object",
227
+ ... "properties": {
228
+ ... "location": {"type": "string", "description": "City name"}
229
+ ... },
230
+ ... "required": ["location"]
231
+ ... }
232
+ ... },
233
+ ... implementation=get_weather,
234
+ ... resource="weather_api",
235
+ ... )
236
+ >>>
237
+ >>> # Get tools for Gemini model
238
+ >>> tools = handler.to_gemini_tools()
239
+ >>> model = GenerativeModel("gemini-1.5-pro", tools=tools)
240
+ """
241
+
242
+ def __init__(
243
+ self,
244
+ proxilion: Any,
245
+ default_action: str = "execute",
246
+ safe_errors: bool = True,
247
+ ) -> None:
248
+ """
249
+ Initialize the Vertex AI handler.
250
+
251
+ Args:
252
+ proxilion: Proxilion instance for authorization.
253
+ default_action: Default action for authorization checks.
254
+ safe_errors: If True, return safe error messages to the model.
255
+ """
256
+ self.proxilion = proxilion
257
+ self.default_action = default_action
258
+ self.safe_errors = safe_errors
259
+
260
+ self._tools: dict[str, RegisteredGeminiTool] = {}
261
+ self._execution_history: list[GeminiToolResult] = []
262
+
263
+ @property
264
+ def tools(self) -> list[RegisteredGeminiTool]:
265
+ """Get list of registered tools."""
266
+ return list(self._tools.values())
267
+
268
+ @property
269
+ def tool_declarations(self) -> list[dict[str, Any]]:
270
+ """Get list of tool declarations for Gemini."""
271
+ return [t.declaration for t in self._tools.values()]
272
+
273
+ @property
274
+ def execution_history(self) -> list[GeminiToolResult]:
275
+ """Get history of tool executions."""
276
+ return list(self._execution_history)
277
+
278
+ def register_tool(
279
+ self,
280
+ name: str,
281
+ declaration: dict[str, Any],
282
+ implementation: Callable[..., Any],
283
+ resource: str | None = None,
284
+ action: str | None = None,
285
+ description: str | None = None,
286
+ ) -> None:
287
+ """
288
+ Register a tool for Gemini function calling.
289
+
290
+ Args:
291
+ name: Tool name (must match function call name from Gemini).
292
+ declaration: Gemini function declaration dict.
293
+ implementation: Python function to execute.
294
+ resource: Resource name for authorization (default: tool name).
295
+ action: Action for authorization (default: handler default).
296
+ description: Optional description override.
297
+
298
+ Example:
299
+ >>> handler.register_tool(
300
+ ... name="search_db",
301
+ ... declaration={
302
+ ... "name": "search_db",
303
+ ... "description": "Search the database",
304
+ ... "parameters": {
305
+ ... "type": "object",
306
+ ... "properties": {
307
+ ... "query": {"type": "string"}
308
+ ... },
309
+ ... "required": ["query"]
310
+ ... }
311
+ ... },
312
+ ... implementation=search_database,
313
+ ... resource="database",
314
+ ... )
315
+ """
316
+ is_async = inspect.iscoroutinefunction(implementation)
317
+
318
+ self._tools[name] = RegisteredGeminiTool(
319
+ name=name,
320
+ declaration=declaration,
321
+ implementation=implementation,
322
+ resource=resource or name,
323
+ action=action or self.default_action,
324
+ async_impl=is_async,
325
+ description=description or declaration.get("description", ""),
326
+ )
327
+
328
+ logger.debug(f"Registered Gemini tool: {name} (resource: {resource or name})")
329
+
330
+ def register_tool_from_function(
331
+ self,
332
+ func: Callable[..., Any],
333
+ name: str | None = None,
334
+ resource: str | None = None,
335
+ action: str | None = None,
336
+ ) -> None:
337
+ """
338
+ Register a tool by inferring the declaration from a function.
339
+
340
+ Uses type hints and docstring to build the function declaration.
341
+
342
+ Args:
343
+ func: Python function to register.
344
+ name: Optional name override (defaults to function name).
345
+ resource: Resource for authorization.
346
+ action: Action for authorization.
347
+
348
+ Example:
349
+ >>> def get_user(user_id: str) -> dict:
350
+ ... '''Get user by ID.'''
351
+ ... return {"id": user_id, "name": "John"}
352
+ >>>
353
+ >>> handler.register_tool_from_function(
354
+ ... get_user,
355
+ ... resource="users",
356
+ ... )
357
+ """
358
+ tool_name = name or func.__name__
359
+
360
+ # Infer description from docstring
361
+ description = ""
362
+ if func.__doc__:
363
+ description = func.__doc__.strip().split("\n")[0]
364
+
365
+ # Build parameters schema from type hints
366
+ parameters = self._infer_parameters_from_function(func)
367
+
368
+ declaration = {
369
+ "name": tool_name,
370
+ "description": description or f"Execute {tool_name}",
371
+ "parameters": parameters,
372
+ }
373
+
374
+ self.register_tool(
375
+ name=tool_name,
376
+ declaration=declaration,
377
+ implementation=func,
378
+ resource=resource,
379
+ action=action,
380
+ )
381
+
382
+ def _infer_parameters_from_function(self, func: Callable[..., Any]) -> dict[str, Any]:
383
+ """Infer parameters schema from function signature."""
384
+ from typing import get_type_hints
385
+
386
+ sig = inspect.signature(func)
387
+ hints = {}
388
+ try:
389
+ # Use get_type_hints to resolve forward references and string annotations
390
+ all_hints = get_type_hints(func)
391
+ hints = {k: v for k, v in all_hints.items() if k != "return"}
392
+ except Exception:
393
+ # Fall back to raw annotations
394
+ with contextlib.suppress(AttributeError):
395
+ hints = {k: v for k, v in func.__annotations__.items() if k != "return"}
396
+
397
+ properties: dict[str, Any] = {}
398
+ required: list[str] = []
399
+
400
+ for param_name, param in sig.parameters.items():
401
+ if param_name in ("self", "cls"):
402
+ continue
403
+ if param.kind in (
404
+ inspect.Parameter.VAR_POSITIONAL,
405
+ inspect.Parameter.VAR_KEYWORD,
406
+ ):
407
+ continue
408
+
409
+ # Get type hint
410
+ type_hint = hints.get(param_name, Any)
411
+
412
+ # Convert to JSON Schema type
413
+ prop_schema = self._type_to_schema(type_hint)
414
+ properties[param_name] = prop_schema
415
+
416
+ # Check if required
417
+ if param.default is inspect.Parameter.empty:
418
+ required.append(param_name)
419
+
420
+ schema: dict[str, Any] = {
421
+ "type": "object",
422
+ "properties": properties,
423
+ }
424
+ if required:
425
+ schema["required"] = required
426
+
427
+ return schema
428
+
429
+ def _type_to_schema(self, type_hint: Any) -> dict[str, Any]:
430
+ """Convert Python type hint to JSON Schema."""
431
+ if type_hint is str:
432
+ return {"type": "string"}
433
+ if type_hint is int:
434
+ return {"type": "integer"}
435
+ if type_hint is float:
436
+ return {"type": "number"}
437
+ if type_hint is bool:
438
+ return {"type": "boolean"}
439
+ if type_hint is list or (hasattr(type_hint, "__origin__") and type_hint.__origin__ is list):
440
+ return {"type": "array"}
441
+ if type_hint is dict or (hasattr(type_hint, "__origin__") and type_hint.__origin__ is dict):
442
+ return {"type": "object"}
443
+ # Default to string for unknown types
444
+ return {"type": "string"}
445
+
446
+ def unregister_tool(self, name: str) -> bool:
447
+ """
448
+ Unregister a tool.
449
+
450
+ Args:
451
+ name: Tool name to unregister.
452
+
453
+ Returns:
454
+ True if tool was registered and removed.
455
+ """
456
+ if name in self._tools:
457
+ del self._tools[name]
458
+ return True
459
+ return False
460
+
461
+ def get_tool(self, name: str) -> RegisteredGeminiTool | None:
462
+ """Get a registered tool by name."""
463
+ return self._tools.get(name)
464
+
465
+ def extract_function_calls(self, response: Any) -> list[GeminiFunctionCall]:
466
+ """
467
+ Extract function calls from a Gemini response.
468
+
469
+ Parses the response to find all function_call parts.
470
+
471
+ Args:
472
+ response: Gemini GenerateContentResponse object.
473
+
474
+ Returns:
475
+ List of GeminiFunctionCall objects.
476
+
477
+ Example:
478
+ >>> response = model.generate_content("What's the weather?")
479
+ >>> calls = handler.extract_function_calls(response)
480
+ >>> for call in calls:
481
+ ... print(f"{call.name}: {call.args}")
482
+ """
483
+ calls: list[GeminiFunctionCall] = []
484
+
485
+ # Handle dictionary response
486
+ if isinstance(response, dict):
487
+ return self._extract_from_dict(response)
488
+
489
+ # Handle object response
490
+ candidates = getattr(response, "candidates", None)
491
+ if not candidates:
492
+ return calls
493
+
494
+ for candidate in candidates:
495
+ content = getattr(candidate, "content", None)
496
+ if not content:
497
+ continue
498
+
499
+ parts = getattr(content, "parts", None)
500
+ if not parts:
501
+ continue
502
+
503
+ for part in parts:
504
+ function_call = getattr(part, "function_call", None)
505
+ if function_call:
506
+ # Handle protobuf-style args
507
+ args = {}
508
+ raw_args = getattr(function_call, "args", None)
509
+ if raw_args:
510
+ # Convert protobuf Struct to dict if needed
511
+ if hasattr(raw_args, "items"):
512
+ args = dict(raw_args.items())
513
+ elif hasattr(raw_args, "__iter__"):
514
+ args = dict(raw_args)
515
+ else:
516
+ try:
517
+ # Try to iterate as Struct
518
+ args = {
519
+ k: self._convert_protobuf_value(v)
520
+ for k, v in raw_args.items()
521
+ }
522
+ except (TypeError, AttributeError):
523
+ args = {}
524
+
525
+ calls.append(GeminiFunctionCall(
526
+ name=function_call.name,
527
+ args=args,
528
+ raw=function_call,
529
+ ))
530
+
531
+ return calls
532
+
533
+ def _extract_from_dict(self, response: dict) -> list[GeminiFunctionCall]:
534
+ """Extract function calls from dictionary response."""
535
+ calls: list[GeminiFunctionCall] = []
536
+ candidates = response.get("candidates", [])
537
+
538
+ for candidate in candidates:
539
+ content = candidate.get("content", {})
540
+ parts = content.get("parts", [])
541
+
542
+ for part in parts:
543
+ # Handle both camelCase and snake_case
544
+ fc = part.get("functionCall") or part.get("function_call")
545
+ if fc:
546
+ calls.append(GeminiFunctionCall(
547
+ name=fc.get("name", ""),
548
+ args=fc.get("args", {}),
549
+ raw=fc,
550
+ ))
551
+
552
+ return calls
553
+
554
+ def _convert_protobuf_value(self, value: Any) -> Any:
555
+ """Convert protobuf Value to Python native type."""
556
+ if hasattr(value, "string_value"):
557
+ return value.string_value
558
+ if hasattr(value, "number_value"):
559
+ return value.number_value
560
+ if hasattr(value, "bool_value"):
561
+ return value.bool_value
562
+ if hasattr(value, "struct_value"):
563
+ return {
564
+ k: self._convert_protobuf_value(v)
565
+ for k, v in value.struct_value.fields.items()
566
+ }
567
+ if hasattr(value, "list_value"):
568
+ return [self._convert_protobuf_value(v) for v in value.list_value.values]
569
+ return value
570
+
571
+ def execute(
572
+ self,
573
+ function_call: GeminiFunctionCall,
574
+ user: UserContext | None = None,
575
+ agent: AgentContext | None = None,
576
+ ) -> GeminiToolResult:
577
+ """
578
+ Execute a function call with authorization.
579
+
580
+ Args:
581
+ function_call: The function call to execute.
582
+ user: User context for authorization.
583
+ agent: Optional agent context.
584
+
585
+ Returns:
586
+ GeminiToolResult with execution result or error.
587
+
588
+ Example:
589
+ >>> calls = handler.extract_function_calls(response)
590
+ >>> for call in calls:
591
+ ... result = handler.execute(call, user=current_user)
592
+ ... if result.authorized:
593
+ ... print(f"Result: {result.result}")
594
+ ... else:
595
+ ... print("Unauthorized")
596
+ """
597
+ tool_name = function_call.name
598
+
599
+ # Get registered tool
600
+ tool = self._tools.get(tool_name)
601
+ if tool is None:
602
+ result = GeminiToolResult(
603
+ name=tool_name,
604
+ success=False,
605
+ error=f"Tool not found: {tool_name}",
606
+ )
607
+ self._execution_history.append(result)
608
+ return result
609
+
610
+ # Check authorization
611
+ if user is not None:
612
+ context = {
613
+ "tool_name": tool_name,
614
+ "args": function_call.args,
615
+ **function_call.args,
616
+ }
617
+
618
+ auth_result = self.proxilion.check(
619
+ user, tool.action, tool.resource, context
620
+ )
621
+
622
+ if not auth_result.allowed:
623
+ result = GeminiToolResult(
624
+ name=tool_name,
625
+ success=False,
626
+ error="Not authorized" if self.safe_errors else auth_result.reason,
627
+ authorized=False,
628
+ )
629
+ self._execution_history.append(result)
630
+ return result
631
+
632
+ # Execute tool
633
+ try:
634
+ if tool.async_impl:
635
+ loop = asyncio.new_event_loop()
636
+ try:
637
+ output = loop.run_until_complete(
638
+ tool.implementation(**function_call.args)
639
+ )
640
+ finally:
641
+ loop.close()
642
+ else:
643
+ output = tool.implementation(**function_call.args)
644
+
645
+ result = GeminiToolResult(
646
+ name=tool_name,
647
+ success=True,
648
+ result=output,
649
+ )
650
+
651
+ except Exception as e:
652
+ logger.error(f"Tool execution error: {tool_name} - {e}")
653
+
654
+ error_msg = "Tool execution failed"
655
+ if not self.safe_errors:
656
+ error_msg = str(e)
657
+
658
+ result = GeminiToolResult(
659
+ name=tool_name,
660
+ success=False,
661
+ error=error_msg,
662
+ )
663
+
664
+ self._execution_history.append(result)
665
+ return result
666
+
667
+ async def execute_async(
668
+ self,
669
+ function_call: GeminiFunctionCall,
670
+ user: UserContext | None = None,
671
+ agent: AgentContext | None = None,
672
+ ) -> GeminiToolResult:
673
+ """
674
+ Execute a function call asynchronously with authorization.
675
+
676
+ Args:
677
+ function_call: The function call to execute.
678
+ user: User context for authorization.
679
+ agent: Optional agent context.
680
+
681
+ Returns:
682
+ GeminiToolResult with execution result or error.
683
+ """
684
+ tool_name = function_call.name
685
+
686
+ tool = self._tools.get(tool_name)
687
+ if tool is None:
688
+ result = GeminiToolResult(
689
+ name=tool_name,
690
+ success=False,
691
+ error=f"Tool not found: {tool_name}",
692
+ )
693
+ self._execution_history.append(result)
694
+ return result
695
+
696
+ # Check authorization
697
+ if user is not None:
698
+ context = {
699
+ "tool_name": tool_name,
700
+ "args": function_call.args,
701
+ **function_call.args,
702
+ }
703
+
704
+ auth_result = self.proxilion.check(
705
+ user, tool.action, tool.resource, context
706
+ )
707
+
708
+ if not auth_result.allowed:
709
+ result = GeminiToolResult(
710
+ name=tool_name,
711
+ success=False,
712
+ error="Not authorized" if self.safe_errors else auth_result.reason,
713
+ authorized=False,
714
+ )
715
+ self._execution_history.append(result)
716
+ return result
717
+
718
+ # Execute tool
719
+ try:
720
+ if tool.async_impl:
721
+ output = await tool.implementation(**function_call.args)
722
+ else:
723
+ loop = asyncio.get_event_loop()
724
+ output = await loop.run_in_executor(
725
+ None,
726
+ lambda: tool.implementation(**function_call.args),
727
+ )
728
+
729
+ result = GeminiToolResult(
730
+ name=tool_name,
731
+ success=True,
732
+ result=output,
733
+ )
734
+
735
+ except Exception as e:
736
+ logger.error(f"Tool execution error: {tool_name} - {e}")
737
+
738
+ error_msg = "Tool execution failed"
739
+ if not self.safe_errors:
740
+ error_msg = str(e)
741
+
742
+ result = GeminiToolResult(
743
+ name=tool_name,
744
+ success=False,
745
+ error=error_msg,
746
+ )
747
+
748
+ self._execution_history.append(result)
749
+ return result
750
+
751
+ def process_response(
752
+ self,
753
+ response: Any,
754
+ user: UserContext | None = None,
755
+ agent: AgentContext | None = None,
756
+ ) -> list[GeminiToolResult]:
757
+ """
758
+ Process all function calls in a Gemini response.
759
+
760
+ Extracts and executes all function calls with authorization.
761
+
762
+ Args:
763
+ response: Gemini GenerateContentResponse.
764
+ user: User context for authorization.
765
+ agent: Optional agent context.
766
+
767
+ Returns:
768
+ List of GeminiToolResult for each function call.
769
+
770
+ Example:
771
+ >>> response = model.generate_content("Search for products")
772
+ >>> results = handler.process_response(response, user=current_user)
773
+ >>> for result in results:
774
+ ... if result.success:
775
+ ... print(f"{result.name}: {result.result}")
776
+ """
777
+ calls = self.extract_function_calls(response)
778
+ return [self.execute(call, user=user, agent=agent) for call in calls]
779
+
780
+ async def process_response_async(
781
+ self,
782
+ response: Any,
783
+ user: UserContext | None = None,
784
+ agent: AgentContext | None = None,
785
+ ) -> list[GeminiToolResult]:
786
+ """
787
+ Process all function calls asynchronously.
788
+
789
+ Args:
790
+ response: Gemini GenerateContentResponse.
791
+ user: User context for authorization.
792
+ agent: Optional agent context.
793
+
794
+ Returns:
795
+ List of GeminiToolResult for each function call.
796
+ """
797
+ calls = self.extract_function_calls(response)
798
+ results = []
799
+ for call in calls:
800
+ result = await self.execute_async(call, user=user, agent=agent)
801
+ results.append(result)
802
+ return results
803
+
804
+ def to_gemini_tools(self) -> list[Any]:
805
+ """
806
+ Get tool declarations in Gemini format.
807
+
808
+ Returns tools suitable for GenerativeModel(tools=...).
809
+
810
+ Returns:
811
+ List containing a Tool object (if vertexai is available)
812
+ or a list of raw declarations.
813
+
814
+ Example:
815
+ >>> tools = handler.to_gemini_tools()
816
+ >>> model = GenerativeModel("gemini-1.5-pro", tools=tools)
817
+ """
818
+ try:
819
+ from vertexai.generative_models import FunctionDeclaration, Tool
820
+
821
+ declarations = [
822
+ FunctionDeclaration(**tool.declaration)
823
+ for tool in self._tools.values()
824
+ ]
825
+ return [Tool(function_declarations=declarations)]
826
+ except ImportError:
827
+ # Return raw dicts if vertexai not installed
828
+ logger.debug("vertexai not installed, returning raw declarations")
829
+ return [{"function_declarations": self.tool_declarations}]
830
+
831
+ def to_gemini_tool_config(
832
+ self,
833
+ mode: str = "AUTO",
834
+ allowed_functions: list[str] | None = None,
835
+ ) -> Any:
836
+ """
837
+ Create a ToolConfig for Gemini.
838
+
839
+ Args:
840
+ mode: Function calling mode - "AUTO", "ANY", or "NONE".
841
+ allowed_functions: List of function names to allow (for "ANY" mode).
842
+
843
+ Returns:
844
+ ToolConfig object or dict.
845
+ """
846
+ try:
847
+ from vertexai.generative_models import ToolConfig
848
+
849
+ if allowed_functions:
850
+ return ToolConfig(
851
+ function_calling_config=ToolConfig.FunctionCallingConfig(
852
+ mode=ToolConfig.FunctionCallingConfig.Mode[mode],
853
+ allowed_function_names=allowed_functions,
854
+ )
855
+ )
856
+ else:
857
+ return ToolConfig(
858
+ function_calling_config=ToolConfig.FunctionCallingConfig(
859
+ mode=ToolConfig.FunctionCallingConfig.Mode[mode],
860
+ )
861
+ )
862
+ except ImportError:
863
+ # Return raw dict if vertexai not installed
864
+ config: dict[str, Any] = {
865
+ "function_calling_config": {
866
+ "mode": mode,
867
+ }
868
+ }
869
+ if allowed_functions:
870
+ config["function_calling_config"]["allowed_function_names"] = allowed_functions
871
+ return config
872
+
873
+ def format_tool_response(
874
+ self,
875
+ results: list[GeminiToolResult],
876
+ ) -> list[dict[str, Any]]:
877
+ """
878
+ Format tool results for sending back to Gemini.
879
+
880
+ Creates function_response parts for the model.
881
+
882
+ Args:
883
+ results: List of GeminiToolResult objects.
884
+
885
+ Returns:
886
+ List of function_response dictionaries.
887
+
888
+ Example:
889
+ >>> results = handler.process_response(response, user=user)
890
+ >>> tool_responses = handler.format_tool_response(results)
891
+ >>> # Continue conversation
892
+ >>> next_response = chat.send_message(tool_responses)
893
+ """
894
+ return [
895
+ {
896
+ "function_response": r.to_function_response()
897
+ }
898
+ for r in results
899
+ ]
900
+
901
+ def create_response_parts(
902
+ self,
903
+ results: list[GeminiToolResult],
904
+ ) -> list[Any]:
905
+ """
906
+ Create Vertex AI Part objects for function responses.
907
+
908
+ Requires vertexai library.
909
+
910
+ Args:
911
+ results: List of GeminiToolResult objects.
912
+
913
+ Returns:
914
+ List of Part objects.
915
+
916
+ Raises:
917
+ ImportError: If vertexai is not installed.
918
+ """
919
+ try:
920
+ from vertexai.generative_models import Part
921
+ except ImportError:
922
+ raise ImportError(
923
+ "vertexai library required. Install with: pip install google-cloud-aiplatform"
924
+ ) from None
925
+
926
+ parts = []
927
+ for result in results:
928
+ response_data = result.to_function_response()
929
+ parts.append(
930
+ Part.from_function_response(
931
+ name=response_data["name"],
932
+ response=response_data["response"],
933
+ )
934
+ )
935
+ return parts
936
+
937
+ def clear_history(self) -> None:
938
+ """Clear the execution history."""
939
+ self._execution_history.clear()
940
+
941
+
942
+ def extract_function_calls(response: Any) -> list[GeminiFunctionCall]:
943
+ """
944
+ Extract function calls from a Gemini response.
945
+
946
+ Standalone function for quick extraction without handler.
947
+
948
+ Args:
949
+ response: Gemini GenerateContentResponse.
950
+
951
+ Returns:
952
+ List of GeminiFunctionCall objects.
953
+
954
+ Example:
955
+ >>> from proxilion.contrib.google import extract_function_calls
956
+ >>> calls = extract_function_calls(response)
957
+ >>> for call in calls:
958
+ ... print(f"{call.name}: {call.args}")
959
+ """
960
+ handler = ProxilionVertexHandler(None) # type: ignore
961
+ return handler.extract_function_calls(response)
962
+
963
+
964
+ def format_tool_response(results: list[GeminiToolResult]) -> list[dict[str, Any]]:
965
+ """
966
+ Format tool results for Gemini.
967
+
968
+ Standalone function for formatting results.
969
+
970
+ Args:
971
+ results: List of GeminiToolResult objects.
972
+
973
+ Returns:
974
+ List of function_response dictionaries.
975
+
976
+ Example:
977
+ >>> from proxilion.contrib.google import format_tool_response
978
+ >>> responses = format_tool_response(results)
979
+ >>> next_response = chat.send_message(responses)
980
+ """
981
+ return [
982
+ {
983
+ "function_response": r.to_function_response()
984
+ }
985
+ for r in results
986
+ ]
987
+
988
+
989
+ def to_gemini_tools(declarations: list[dict[str, Any]]) -> list[Any]:
990
+ """
991
+ Convert function declarations to Gemini Tool format.
992
+
993
+ Args:
994
+ declarations: List of function declaration dicts.
995
+
996
+ Returns:
997
+ List containing a Tool object or raw format.
998
+
999
+ Example:
1000
+ >>> declarations = [
1001
+ ... {"name": "search", "description": "Search", "parameters": {...}}
1002
+ ... ]
1003
+ >>> tools = to_gemini_tools(declarations)
1004
+ >>> model = GenerativeModel("gemini-1.5-pro", tools=tools)
1005
+ """
1006
+ try:
1007
+ from vertexai.generative_models import FunctionDeclaration, Tool
1008
+
1009
+ func_declarations = [FunctionDeclaration(**d) for d in declarations]
1010
+ return [Tool(function_declarations=func_declarations)]
1011
+ except ImportError:
1012
+ return [{"function_declarations": declarations}]