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