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