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,732 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool registry for centralized tool management.
|
|
3
|
+
|
|
4
|
+
Provides a registry for tools with metadata, schemas, and
|
|
5
|
+
export capabilities for different LLM providers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import inspect
|
|
12
|
+
import logging
|
|
13
|
+
import threading
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
from enum import Enum, auto
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ToolCategory(Enum):
|
|
24
|
+
"""Categories of tools for organization and filtering."""
|
|
25
|
+
|
|
26
|
+
FILE_SYSTEM = auto()
|
|
27
|
+
DATABASE = auto()
|
|
28
|
+
API = auto()
|
|
29
|
+
SEARCH = auto()
|
|
30
|
+
COMPUTE = auto()
|
|
31
|
+
COMMUNICATION = auto()
|
|
32
|
+
CODE_EXECUTION = auto()
|
|
33
|
+
DATA_PROCESSING = auto()
|
|
34
|
+
AUTHENTICATION = auto()
|
|
35
|
+
CUSTOM = auto()
|
|
36
|
+
|
|
37
|
+
def __str__(self) -> str:
|
|
38
|
+
return self.name.lower()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class RiskLevel(Enum):
|
|
42
|
+
"""Risk levels for tool operations."""
|
|
43
|
+
|
|
44
|
+
LOW = 1 # Read-only, no sensitive data
|
|
45
|
+
MEDIUM = 2 # May modify non-critical data
|
|
46
|
+
HIGH = 3 # May modify critical data or access sensitive info
|
|
47
|
+
CRITICAL = 4 # Destructive operations, requires approval
|
|
48
|
+
|
|
49
|
+
def __lt__(self, other: RiskLevel) -> bool:
|
|
50
|
+
if not isinstance(other, RiskLevel):
|
|
51
|
+
return NotImplemented
|
|
52
|
+
return self.value < other.value
|
|
53
|
+
|
|
54
|
+
def __le__(self, other: RiskLevel) -> bool:
|
|
55
|
+
return self == other or self < other
|
|
56
|
+
|
|
57
|
+
def __gt__(self, other: RiskLevel) -> bool:
|
|
58
|
+
return not self <= other
|
|
59
|
+
|
|
60
|
+
def __ge__(self, other: RiskLevel) -> bool:
|
|
61
|
+
return not self < other
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ToolDefinition:
|
|
66
|
+
"""
|
|
67
|
+
Definition of a tool that can be called by an AI agent.
|
|
68
|
+
|
|
69
|
+
Attributes:
|
|
70
|
+
name: Unique identifier for the tool.
|
|
71
|
+
description: Human-readable description of what the tool does.
|
|
72
|
+
parameters: JSON Schema describing the tool's parameters.
|
|
73
|
+
category: Category for organization and filtering.
|
|
74
|
+
risk_level: Risk level for authorization decisions.
|
|
75
|
+
requires_approval: Whether human approval is required.
|
|
76
|
+
timeout: Timeout for tool execution in seconds.
|
|
77
|
+
handler: The function that implements the tool.
|
|
78
|
+
metadata: Additional metadata about the tool.
|
|
79
|
+
enabled: Whether the tool is currently enabled.
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
>>> tool_def = ToolDefinition(
|
|
83
|
+
... name="search_web",
|
|
84
|
+
... description="Search the web for information",
|
|
85
|
+
... parameters={
|
|
86
|
+
... "type": "object",
|
|
87
|
+
... "properties": {
|
|
88
|
+
... "query": {"type": "string", "description": "Search query"},
|
|
89
|
+
... "max_results": {"type": "integer", "default": 10},
|
|
90
|
+
... },
|
|
91
|
+
... "required": ["query"],
|
|
92
|
+
... },
|
|
93
|
+
... category=ToolCategory.SEARCH,
|
|
94
|
+
... risk_level=RiskLevel.LOW,
|
|
95
|
+
... handler=search_function,
|
|
96
|
+
... )
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
name: str
|
|
100
|
+
description: str
|
|
101
|
+
parameters: dict[str, Any] = field(default_factory=lambda: {
|
|
102
|
+
"type": "object",
|
|
103
|
+
"properties": {},
|
|
104
|
+
"required": [],
|
|
105
|
+
})
|
|
106
|
+
category: ToolCategory = ToolCategory.CUSTOM
|
|
107
|
+
risk_level: RiskLevel = RiskLevel.LOW
|
|
108
|
+
requires_approval: bool = False
|
|
109
|
+
timeout: float | None = None
|
|
110
|
+
handler: Callable[..., Any] | None = None
|
|
111
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
112
|
+
enabled: bool = True
|
|
113
|
+
|
|
114
|
+
def __post_init__(self) -> None:
|
|
115
|
+
"""Validate the tool definition."""
|
|
116
|
+
if not self.name:
|
|
117
|
+
raise ValueError("Tool name cannot be empty")
|
|
118
|
+
if not self.description:
|
|
119
|
+
raise ValueError("Tool description cannot be empty")
|
|
120
|
+
|
|
121
|
+
def to_openai_format(self) -> dict[str, Any]:
|
|
122
|
+
"""
|
|
123
|
+
Export as OpenAI function definition.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
Dictionary in OpenAI tools format.
|
|
127
|
+
"""
|
|
128
|
+
return {
|
|
129
|
+
"type": "function",
|
|
130
|
+
"function": {
|
|
131
|
+
"name": self.name,
|
|
132
|
+
"description": self.description,
|
|
133
|
+
"parameters": self.parameters,
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
def to_anthropic_format(self) -> dict[str, Any]:
|
|
138
|
+
"""
|
|
139
|
+
Export as Anthropic tool definition.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Dictionary in Anthropic tools format.
|
|
143
|
+
"""
|
|
144
|
+
return {
|
|
145
|
+
"name": self.name,
|
|
146
|
+
"description": self.description,
|
|
147
|
+
"input_schema": self.parameters,
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
def to_gemini_format(self) -> dict[str, Any]:
|
|
151
|
+
"""
|
|
152
|
+
Export as Google Gemini function declaration.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Dictionary in Gemini function declaration format.
|
|
156
|
+
"""
|
|
157
|
+
return {
|
|
158
|
+
"name": self.name,
|
|
159
|
+
"description": self.description,
|
|
160
|
+
"parameters": self.parameters,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
def to_dict(self) -> dict[str, Any]:
|
|
164
|
+
"""Convert to dictionary representation."""
|
|
165
|
+
return {
|
|
166
|
+
"name": self.name,
|
|
167
|
+
"description": self.description,
|
|
168
|
+
"parameters": self.parameters,
|
|
169
|
+
"category": self.category.name,
|
|
170
|
+
"risk_level": self.risk_level.name,
|
|
171
|
+
"requires_approval": self.requires_approval,
|
|
172
|
+
"timeout": self.timeout,
|
|
173
|
+
"enabled": self.enabled,
|
|
174
|
+
"metadata": self.metadata,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@classmethod
|
|
178
|
+
def from_dict(cls, data: dict[str, Any]) -> ToolDefinition:
|
|
179
|
+
"""Create from dictionary."""
|
|
180
|
+
return cls(
|
|
181
|
+
name=data["name"],
|
|
182
|
+
description=data["description"],
|
|
183
|
+
parameters=data.get("parameters", {}),
|
|
184
|
+
category=ToolCategory[data.get("category", "CUSTOM")],
|
|
185
|
+
risk_level=RiskLevel[data.get("risk_level", "LOW")],
|
|
186
|
+
requires_approval=data.get("requires_approval", False),
|
|
187
|
+
timeout=data.get("timeout"),
|
|
188
|
+
metadata=data.get("metadata", {}),
|
|
189
|
+
enabled=data.get("enabled", True),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass
|
|
194
|
+
class ToolExecutionResult:
|
|
195
|
+
"""
|
|
196
|
+
Result of a tool execution.
|
|
197
|
+
|
|
198
|
+
Attributes:
|
|
199
|
+
tool_name: Name of the executed tool.
|
|
200
|
+
success: Whether execution succeeded.
|
|
201
|
+
result: The result value if successful.
|
|
202
|
+
error: Error message if failed.
|
|
203
|
+
execution_time: Time taken to execute in seconds.
|
|
204
|
+
timestamp: When execution occurred.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
tool_name: str
|
|
208
|
+
success: bool
|
|
209
|
+
result: Any = None
|
|
210
|
+
error: str | None = None
|
|
211
|
+
execution_time: float = 0.0
|
|
212
|
+
timestamp: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
|
213
|
+
|
|
214
|
+
def to_dict(self) -> dict[str, Any]:
|
|
215
|
+
"""Convert to dictionary."""
|
|
216
|
+
return {
|
|
217
|
+
"tool_name": self.tool_name,
|
|
218
|
+
"success": self.success,
|
|
219
|
+
"result": self.result if self.success else None,
|
|
220
|
+
"error": self.error,
|
|
221
|
+
"execution_time": self.execution_time,
|
|
222
|
+
"timestamp": self.timestamp.isoformat(),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class ToolRegistry:
|
|
227
|
+
"""
|
|
228
|
+
Central registry for tool management.
|
|
229
|
+
|
|
230
|
+
Provides registration, discovery, and execution of tools
|
|
231
|
+
with support for different export formats.
|
|
232
|
+
|
|
233
|
+
Example:
|
|
234
|
+
>>> registry = ToolRegistry()
|
|
235
|
+
>>> registry.register(ToolDefinition(
|
|
236
|
+
... name="calculator",
|
|
237
|
+
... description="Perform calculations",
|
|
238
|
+
... parameters={"type": "object", "properties": {...}},
|
|
239
|
+
... handler=calculate,
|
|
240
|
+
... ))
|
|
241
|
+
>>>
|
|
242
|
+
>>> # Get tool
|
|
243
|
+
>>> tool = registry.get("calculator")
|
|
244
|
+
>>>
|
|
245
|
+
>>> # Export to OpenAI format
|
|
246
|
+
>>> tools = registry.export_all(format="openai")
|
|
247
|
+
>>>
|
|
248
|
+
>>> # Execute tool
|
|
249
|
+
>>> result = registry.execute("calculator", expression="2+2")
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
SUPPORTED_FORMATS = ("openai", "anthropic", "gemini", "dict")
|
|
253
|
+
|
|
254
|
+
def __init__(self) -> None:
|
|
255
|
+
"""Initialize the registry."""
|
|
256
|
+
self._tools: dict[str, ToolDefinition] = {}
|
|
257
|
+
self._lock = threading.RLock()
|
|
258
|
+
self._execution_hooks: list[Callable[[str, dict[str, Any]], None]] = []
|
|
259
|
+
|
|
260
|
+
def register(self, tool: ToolDefinition) -> ToolRegistry:
|
|
261
|
+
"""
|
|
262
|
+
Register a tool.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
tool: The tool definition to register.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Self for chaining.
|
|
269
|
+
|
|
270
|
+
Raises:
|
|
271
|
+
ValueError: If a tool with the same name already exists.
|
|
272
|
+
"""
|
|
273
|
+
with self._lock:
|
|
274
|
+
if tool.name in self._tools:
|
|
275
|
+
raise ValueError(f"Tool '{tool.name}' is already registered")
|
|
276
|
+
self._tools[tool.name] = tool
|
|
277
|
+
logger.debug(f"Registered tool: {tool.name}")
|
|
278
|
+
return self
|
|
279
|
+
|
|
280
|
+
def register_or_replace(self, tool: ToolDefinition) -> ToolRegistry:
|
|
281
|
+
"""
|
|
282
|
+
Register a tool, replacing if it already exists.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
tool: The tool definition to register.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Self for chaining.
|
|
289
|
+
"""
|
|
290
|
+
with self._lock:
|
|
291
|
+
existed = tool.name in self._tools
|
|
292
|
+
self._tools[tool.name] = tool
|
|
293
|
+
action = "Replaced" if existed else "Registered"
|
|
294
|
+
logger.debug(f"{action} tool: {tool.name}")
|
|
295
|
+
return self
|
|
296
|
+
|
|
297
|
+
def unregister(self, name: str) -> bool:
|
|
298
|
+
"""
|
|
299
|
+
Unregister a tool.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
name: Name of the tool to unregister.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
True if tool was found and removed.
|
|
306
|
+
"""
|
|
307
|
+
with self._lock:
|
|
308
|
+
if name in self._tools:
|
|
309
|
+
del self._tools[name]
|
|
310
|
+
logger.debug(f"Unregistered tool: {name}")
|
|
311
|
+
return True
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
def get(self, name: str) -> ToolDefinition | None:
|
|
315
|
+
"""
|
|
316
|
+
Get a tool by name.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
name: Name of the tool.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
ToolDefinition or None if not found.
|
|
323
|
+
"""
|
|
324
|
+
return self._tools.get(name)
|
|
325
|
+
|
|
326
|
+
def get_required(self, name: str) -> ToolDefinition:
|
|
327
|
+
"""
|
|
328
|
+
Get a tool by name, raising if not found.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
name: Name of the tool.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
ToolDefinition.
|
|
335
|
+
|
|
336
|
+
Raises:
|
|
337
|
+
KeyError: If tool not found.
|
|
338
|
+
"""
|
|
339
|
+
tool = self.get(name)
|
|
340
|
+
if tool is None:
|
|
341
|
+
raise KeyError(f"Tool '{name}' not found")
|
|
342
|
+
return tool
|
|
343
|
+
|
|
344
|
+
def has(self, name: str) -> bool:
|
|
345
|
+
"""Check if a tool is registered."""
|
|
346
|
+
return name in self._tools
|
|
347
|
+
|
|
348
|
+
def list_all(self) -> list[ToolDefinition]:
|
|
349
|
+
"""
|
|
350
|
+
List all registered tools.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
List of all tool definitions.
|
|
354
|
+
"""
|
|
355
|
+
with self._lock:
|
|
356
|
+
return list(self._tools.values())
|
|
357
|
+
|
|
358
|
+
def list_enabled(self) -> list[ToolDefinition]:
|
|
359
|
+
"""
|
|
360
|
+
List all enabled tools.
|
|
361
|
+
|
|
362
|
+
Returns:
|
|
363
|
+
List of enabled tool definitions.
|
|
364
|
+
"""
|
|
365
|
+
with self._lock:
|
|
366
|
+
return [t for t in self._tools.values() if t.enabled]
|
|
367
|
+
|
|
368
|
+
def list_by_category(self, category: ToolCategory) -> list[ToolDefinition]:
|
|
369
|
+
"""
|
|
370
|
+
List tools by category.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
category: The category to filter by.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
List of matching tool definitions.
|
|
377
|
+
"""
|
|
378
|
+
with self._lock:
|
|
379
|
+
return [t for t in self._tools.values() if t.category == category]
|
|
380
|
+
|
|
381
|
+
def list_by_risk_level(
|
|
382
|
+
self, max_risk: RiskLevel, include_higher: bool = False
|
|
383
|
+
) -> list[ToolDefinition]:
|
|
384
|
+
"""
|
|
385
|
+
List tools up to a given risk level.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
max_risk: Maximum risk level to include.
|
|
389
|
+
include_higher: If True, include higher risk levels too.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
List of matching tool definitions.
|
|
393
|
+
"""
|
|
394
|
+
with self._lock:
|
|
395
|
+
if include_higher:
|
|
396
|
+
return [t for t in self._tools.values() if t.risk_level >= max_risk]
|
|
397
|
+
return [t for t in self._tools.values() if t.risk_level <= max_risk]
|
|
398
|
+
|
|
399
|
+
def list_requiring_approval(self) -> list[ToolDefinition]:
|
|
400
|
+
"""
|
|
401
|
+
List tools that require approval.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
List of tools requiring human approval.
|
|
405
|
+
"""
|
|
406
|
+
with self._lock:
|
|
407
|
+
return [t for t in self._tools.values() if t.requires_approval]
|
|
408
|
+
|
|
409
|
+
def list_names(self) -> list[str]:
|
|
410
|
+
"""Get list of all tool names."""
|
|
411
|
+
with self._lock:
|
|
412
|
+
return list(self._tools.keys())
|
|
413
|
+
|
|
414
|
+
def enable(self, name: str) -> bool:
|
|
415
|
+
"""
|
|
416
|
+
Enable a tool.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
name: Name of the tool.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
True if tool was found.
|
|
423
|
+
"""
|
|
424
|
+
tool = self.get(name)
|
|
425
|
+
if tool:
|
|
426
|
+
tool.enabled = True
|
|
427
|
+
return True
|
|
428
|
+
return False
|
|
429
|
+
|
|
430
|
+
def disable(self, name: str) -> bool:
|
|
431
|
+
"""
|
|
432
|
+
Disable a tool.
|
|
433
|
+
|
|
434
|
+
Args:
|
|
435
|
+
name: Name of the tool.
|
|
436
|
+
|
|
437
|
+
Returns:
|
|
438
|
+
True if tool was found.
|
|
439
|
+
"""
|
|
440
|
+
tool = self.get(name)
|
|
441
|
+
if tool:
|
|
442
|
+
tool.enabled = False
|
|
443
|
+
return True
|
|
444
|
+
return False
|
|
445
|
+
|
|
446
|
+
def export_all(
|
|
447
|
+
self,
|
|
448
|
+
format: str = "openai",
|
|
449
|
+
enabled_only: bool = True,
|
|
450
|
+
) -> list[dict[str, Any]]:
|
|
451
|
+
"""
|
|
452
|
+
Export all tools to a specific format.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
format: Export format ("openai", "anthropic", "gemini", "dict").
|
|
456
|
+
enabled_only: Only export enabled tools.
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
List of tool definitions in requested format.
|
|
460
|
+
|
|
461
|
+
Raises:
|
|
462
|
+
ValueError: If format is not supported.
|
|
463
|
+
"""
|
|
464
|
+
if format not in self.SUPPORTED_FORMATS:
|
|
465
|
+
raise ValueError(
|
|
466
|
+
f"Unsupported format: {format}. Supported: {self.SUPPORTED_FORMATS}"
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
with self._lock:
|
|
470
|
+
tools = self.list_enabled() if enabled_only else self.list_all()
|
|
471
|
+
|
|
472
|
+
if format == "openai":
|
|
473
|
+
return [t.to_openai_format() for t in tools]
|
|
474
|
+
elif format == "anthropic":
|
|
475
|
+
return [t.to_anthropic_format() for t in tools]
|
|
476
|
+
elif format == "gemini":
|
|
477
|
+
return [t.to_gemini_format() for t in tools]
|
|
478
|
+
else: # dict
|
|
479
|
+
return [t.to_dict() for t in tools]
|
|
480
|
+
|
|
481
|
+
def export_one(self, name: str, format: str = "openai") -> dict[str, Any] | None:
|
|
482
|
+
"""
|
|
483
|
+
Export a single tool.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
name: Name of the tool.
|
|
487
|
+
format: Export format.
|
|
488
|
+
|
|
489
|
+
Returns:
|
|
490
|
+
Tool definition in requested format, or None if not found.
|
|
491
|
+
"""
|
|
492
|
+
tool = self.get(name)
|
|
493
|
+
if tool is None:
|
|
494
|
+
return None
|
|
495
|
+
|
|
496
|
+
if format == "openai":
|
|
497
|
+
return tool.to_openai_format()
|
|
498
|
+
elif format == "anthropic":
|
|
499
|
+
return tool.to_anthropic_format()
|
|
500
|
+
elif format == "gemini":
|
|
501
|
+
return tool.to_gemini_format()
|
|
502
|
+
else:
|
|
503
|
+
return tool.to_dict()
|
|
504
|
+
|
|
505
|
+
def execute(
|
|
506
|
+
self,
|
|
507
|
+
name: str,
|
|
508
|
+
**kwargs: Any,
|
|
509
|
+
) -> ToolExecutionResult:
|
|
510
|
+
"""
|
|
511
|
+
Execute a tool by name.
|
|
512
|
+
|
|
513
|
+
Args:
|
|
514
|
+
name: Name of the tool to execute.
|
|
515
|
+
**kwargs: Arguments to pass to the tool handler.
|
|
516
|
+
|
|
517
|
+
Returns:
|
|
518
|
+
ToolExecutionResult with execution details.
|
|
519
|
+
|
|
520
|
+
Raises:
|
|
521
|
+
KeyError: If tool not found.
|
|
522
|
+
ValueError: If tool has no handler.
|
|
523
|
+
"""
|
|
524
|
+
tool = self.get_required(name)
|
|
525
|
+
|
|
526
|
+
if not tool.enabled:
|
|
527
|
+
return ToolExecutionResult(
|
|
528
|
+
tool_name=name,
|
|
529
|
+
success=False,
|
|
530
|
+
error=f"Tool '{name}' is disabled",
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
if tool.handler is None:
|
|
534
|
+
raise ValueError(f"Tool '{name}' has no handler")
|
|
535
|
+
|
|
536
|
+
# Invoke execution hooks
|
|
537
|
+
for hook in self._execution_hooks:
|
|
538
|
+
try:
|
|
539
|
+
hook(name, kwargs)
|
|
540
|
+
except Exception as e:
|
|
541
|
+
logger.warning(f"Execution hook error: {e}")
|
|
542
|
+
|
|
543
|
+
start_time = datetime.now(timezone.utc)
|
|
544
|
+
|
|
545
|
+
try:
|
|
546
|
+
# Check if handler is async
|
|
547
|
+
if inspect.iscoroutinefunction(tool.handler):
|
|
548
|
+
# Run async handler in event loop
|
|
549
|
+
loop = asyncio.new_event_loop()
|
|
550
|
+
try:
|
|
551
|
+
result = loop.run_until_complete(tool.handler(**kwargs))
|
|
552
|
+
finally:
|
|
553
|
+
loop.close()
|
|
554
|
+
else:
|
|
555
|
+
result = tool.handler(**kwargs)
|
|
556
|
+
|
|
557
|
+
execution_time = (datetime.now(timezone.utc) - start_time).total_seconds()
|
|
558
|
+
|
|
559
|
+
return ToolExecutionResult(
|
|
560
|
+
tool_name=name,
|
|
561
|
+
success=True,
|
|
562
|
+
result=result,
|
|
563
|
+
execution_time=execution_time,
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
except Exception as e:
|
|
567
|
+
execution_time = (datetime.now(timezone.utc) - start_time).total_seconds()
|
|
568
|
+
logger.error(f"Tool '{name}' execution failed: {e}")
|
|
569
|
+
|
|
570
|
+
return ToolExecutionResult(
|
|
571
|
+
tool_name=name,
|
|
572
|
+
success=False,
|
|
573
|
+
error=str(e),
|
|
574
|
+
execution_time=execution_time,
|
|
575
|
+
)
|
|
576
|
+
|
|
577
|
+
async def execute_async(
|
|
578
|
+
self,
|
|
579
|
+
name: str,
|
|
580
|
+
**kwargs: Any,
|
|
581
|
+
) -> ToolExecutionResult:
|
|
582
|
+
"""
|
|
583
|
+
Execute a tool by name asynchronously.
|
|
584
|
+
|
|
585
|
+
Args:
|
|
586
|
+
name: Name of the tool to execute.
|
|
587
|
+
**kwargs: Arguments to pass to the tool handler.
|
|
588
|
+
|
|
589
|
+
Returns:
|
|
590
|
+
ToolExecutionResult with execution details.
|
|
591
|
+
"""
|
|
592
|
+
tool = self.get_required(name)
|
|
593
|
+
|
|
594
|
+
if not tool.enabled:
|
|
595
|
+
return ToolExecutionResult(
|
|
596
|
+
tool_name=name,
|
|
597
|
+
success=False,
|
|
598
|
+
error=f"Tool '{name}' is disabled",
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
if tool.handler is None:
|
|
602
|
+
raise ValueError(f"Tool '{name}' has no handler")
|
|
603
|
+
|
|
604
|
+
# Invoke execution hooks
|
|
605
|
+
for hook in self._execution_hooks:
|
|
606
|
+
try:
|
|
607
|
+
hook(name, kwargs)
|
|
608
|
+
except Exception as e:
|
|
609
|
+
logger.warning(f"Execution hook error: {e}")
|
|
610
|
+
|
|
611
|
+
start_time = datetime.now(timezone.utc)
|
|
612
|
+
|
|
613
|
+
try:
|
|
614
|
+
# Check if handler is async
|
|
615
|
+
if inspect.iscoroutinefunction(tool.handler):
|
|
616
|
+
result = await tool.handler(**kwargs)
|
|
617
|
+
else:
|
|
618
|
+
# Run sync handler in thread pool
|
|
619
|
+
loop = asyncio.get_event_loop()
|
|
620
|
+
result = await loop.run_in_executor(
|
|
621
|
+
None, lambda: tool.handler(**kwargs)
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
execution_time = (datetime.now(timezone.utc) - start_time).total_seconds()
|
|
625
|
+
|
|
626
|
+
return ToolExecutionResult(
|
|
627
|
+
tool_name=name,
|
|
628
|
+
success=True,
|
|
629
|
+
result=result,
|
|
630
|
+
execution_time=execution_time,
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
except Exception as e:
|
|
634
|
+
execution_time = (datetime.now(timezone.utc) - start_time).total_seconds()
|
|
635
|
+
logger.error(f"Tool '{name}' execution failed: {e}")
|
|
636
|
+
|
|
637
|
+
return ToolExecutionResult(
|
|
638
|
+
tool_name=name,
|
|
639
|
+
success=False,
|
|
640
|
+
error=str(e),
|
|
641
|
+
execution_time=execution_time,
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
def add_execution_hook(
|
|
645
|
+
self, hook: Callable[[str, dict[str, Any]], None]
|
|
646
|
+
) -> ToolRegistry:
|
|
647
|
+
"""
|
|
648
|
+
Add a hook to be called before tool execution.
|
|
649
|
+
|
|
650
|
+
Args:
|
|
651
|
+
hook: Function called with (tool_name, kwargs).
|
|
652
|
+
|
|
653
|
+
Returns:
|
|
654
|
+
Self for chaining.
|
|
655
|
+
"""
|
|
656
|
+
self._execution_hooks.append(hook)
|
|
657
|
+
return self
|
|
658
|
+
|
|
659
|
+
def remove_execution_hook(
|
|
660
|
+
self, hook: Callable[[str, dict[str, Any]], None]
|
|
661
|
+
) -> bool:
|
|
662
|
+
"""
|
|
663
|
+
Remove an execution hook.
|
|
664
|
+
|
|
665
|
+
Args:
|
|
666
|
+
hook: The hook to remove.
|
|
667
|
+
|
|
668
|
+
Returns:
|
|
669
|
+
True if hook was found and removed.
|
|
670
|
+
"""
|
|
671
|
+
try:
|
|
672
|
+
self._execution_hooks.remove(hook)
|
|
673
|
+
return True
|
|
674
|
+
except ValueError:
|
|
675
|
+
return False
|
|
676
|
+
|
|
677
|
+
def clear(self) -> None:
|
|
678
|
+
"""Remove all registered tools."""
|
|
679
|
+
with self._lock:
|
|
680
|
+
self._tools.clear()
|
|
681
|
+
|
|
682
|
+
def __len__(self) -> int:
|
|
683
|
+
"""Get number of registered tools."""
|
|
684
|
+
return len(self._tools)
|
|
685
|
+
|
|
686
|
+
def __contains__(self, name: str) -> bool:
|
|
687
|
+
"""Check if tool is registered."""
|
|
688
|
+
return name in self._tools
|
|
689
|
+
|
|
690
|
+
def __iter__(self):
|
|
691
|
+
"""Iterate over tool definitions."""
|
|
692
|
+
return iter(self._tools.values())
|
|
693
|
+
|
|
694
|
+
def to_dict(self) -> dict[str, Any]:
|
|
695
|
+
"""Convert registry state to dictionary."""
|
|
696
|
+
return {
|
|
697
|
+
"tools": [t.to_dict() for t in self._tools.values()],
|
|
698
|
+
"count": len(self._tools),
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
# Global registry instance
|
|
703
|
+
_global_registry: ToolRegistry | None = None
|
|
704
|
+
_global_lock = threading.Lock()
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def get_global_registry() -> ToolRegistry:
|
|
708
|
+
"""
|
|
709
|
+
Get the global tool registry.
|
|
710
|
+
|
|
711
|
+
Creates one if it doesn't exist.
|
|
712
|
+
|
|
713
|
+
Returns:
|
|
714
|
+
The global ToolRegistry instance.
|
|
715
|
+
"""
|
|
716
|
+
global _global_registry
|
|
717
|
+
with _global_lock:
|
|
718
|
+
if _global_registry is None:
|
|
719
|
+
_global_registry = ToolRegistry()
|
|
720
|
+
return _global_registry
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def set_global_registry(registry: ToolRegistry | None) -> None:
|
|
724
|
+
"""
|
|
725
|
+
Set the global tool registry.
|
|
726
|
+
|
|
727
|
+
Args:
|
|
728
|
+
registry: The registry to use, or None to clear.
|
|
729
|
+
"""
|
|
730
|
+
global _global_registry
|
|
731
|
+
with _global_lock:
|
|
732
|
+
_global_registry = registry
|