chuk-tool-processor 0.6.4__py3-none-any.whl → 0.9.7__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.

Potentially problematic release.


This version of chuk-tool-processor might be problematic. Click here for more details.

Files changed (66) hide show
  1. chuk_tool_processor/core/__init__.py +32 -1
  2. chuk_tool_processor/core/exceptions.py +225 -13
  3. chuk_tool_processor/core/processor.py +135 -104
  4. chuk_tool_processor/execution/strategies/__init__.py +6 -0
  5. chuk_tool_processor/execution/strategies/inprocess_strategy.py +142 -150
  6. chuk_tool_processor/execution/strategies/subprocess_strategy.py +202 -206
  7. chuk_tool_processor/execution/tool_executor.py +82 -84
  8. chuk_tool_processor/execution/wrappers/__init__.py +42 -0
  9. chuk_tool_processor/execution/wrappers/caching.py +150 -116
  10. chuk_tool_processor/execution/wrappers/circuit_breaker.py +370 -0
  11. chuk_tool_processor/execution/wrappers/rate_limiting.py +76 -43
  12. chuk_tool_processor/execution/wrappers/retry.py +116 -78
  13. chuk_tool_processor/logging/__init__.py +23 -17
  14. chuk_tool_processor/logging/context.py +40 -45
  15. chuk_tool_processor/logging/formatter.py +22 -21
  16. chuk_tool_processor/logging/helpers.py +28 -42
  17. chuk_tool_processor/logging/metrics.py +13 -15
  18. chuk_tool_processor/mcp/__init__.py +8 -12
  19. chuk_tool_processor/mcp/mcp_tool.py +158 -114
  20. chuk_tool_processor/mcp/register_mcp_tools.py +22 -22
  21. chuk_tool_processor/mcp/setup_mcp_http_streamable.py +57 -17
  22. chuk_tool_processor/mcp/setup_mcp_sse.py +57 -17
  23. chuk_tool_processor/mcp/setup_mcp_stdio.py +11 -11
  24. chuk_tool_processor/mcp/stream_manager.py +333 -276
  25. chuk_tool_processor/mcp/transport/__init__.py +22 -29
  26. chuk_tool_processor/mcp/transport/base_transport.py +180 -44
  27. chuk_tool_processor/mcp/transport/http_streamable_transport.py +505 -325
  28. chuk_tool_processor/mcp/transport/models.py +100 -0
  29. chuk_tool_processor/mcp/transport/sse_transport.py +607 -276
  30. chuk_tool_processor/mcp/transport/stdio_transport.py +597 -116
  31. chuk_tool_processor/models/__init__.py +21 -1
  32. chuk_tool_processor/models/execution_strategy.py +16 -21
  33. chuk_tool_processor/models/streaming_tool.py +28 -25
  34. chuk_tool_processor/models/tool_call.py +49 -31
  35. chuk_tool_processor/models/tool_export_mixin.py +22 -8
  36. chuk_tool_processor/models/tool_result.py +40 -77
  37. chuk_tool_processor/models/tool_spec.py +350 -0
  38. chuk_tool_processor/models/validated_tool.py +36 -18
  39. chuk_tool_processor/observability/__init__.py +30 -0
  40. chuk_tool_processor/observability/metrics.py +312 -0
  41. chuk_tool_processor/observability/setup.py +105 -0
  42. chuk_tool_processor/observability/tracing.py +345 -0
  43. chuk_tool_processor/plugins/__init__.py +1 -1
  44. chuk_tool_processor/plugins/discovery.py +11 -11
  45. chuk_tool_processor/plugins/parsers/__init__.py +1 -1
  46. chuk_tool_processor/plugins/parsers/base.py +1 -2
  47. chuk_tool_processor/plugins/parsers/function_call_tool.py +13 -8
  48. chuk_tool_processor/plugins/parsers/json_tool.py +4 -3
  49. chuk_tool_processor/plugins/parsers/openai_tool.py +12 -7
  50. chuk_tool_processor/plugins/parsers/xml_tool.py +4 -4
  51. chuk_tool_processor/registry/__init__.py +12 -12
  52. chuk_tool_processor/registry/auto_register.py +22 -30
  53. chuk_tool_processor/registry/decorators.py +127 -129
  54. chuk_tool_processor/registry/interface.py +26 -23
  55. chuk_tool_processor/registry/metadata.py +27 -22
  56. chuk_tool_processor/registry/provider.py +17 -18
  57. chuk_tool_processor/registry/providers/__init__.py +16 -19
  58. chuk_tool_processor/registry/providers/memory.py +18 -25
  59. chuk_tool_processor/registry/tool_export.py +42 -51
  60. chuk_tool_processor/utils/validation.py +15 -16
  61. chuk_tool_processor-0.9.7.dist-info/METADATA +1813 -0
  62. chuk_tool_processor-0.9.7.dist-info/RECORD +67 -0
  63. chuk_tool_processor-0.6.4.dist-info/METADATA +0 -697
  64. chuk_tool_processor-0.6.4.dist-info/RECORD +0 -60
  65. {chuk_tool_processor-0.6.4.dist-info → chuk_tool_processor-0.9.7.dist-info}/WHEEL +0 -0
  66. {chuk_tool_processor-0.6.4.dist-info → chuk_tool_processor-0.9.7.dist-info}/top_level.txt +0 -0
@@ -2,17 +2,19 @@
2
2
  """
3
3
  Tool metadata models for the registry with async-native support.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
- from typing import Any, Dict, Optional, Set, List, Union
8
8
  from datetime import datetime
9
+ from typing import Any
10
+
9
11
  from pydantic import BaseModel, Field, model_validator
10
12
 
11
13
 
12
14
  class ToolMetadata(BaseModel):
13
15
  """
14
16
  Metadata for registered tools.
15
-
17
+
16
18
  Attributes:
17
19
  name: The name of the tool.
18
20
  namespace: The namespace the tool belongs to.
@@ -31,38 +33,39 @@ class ToolMetadata(BaseModel):
31
33
  timeout: Optional default timeout in seconds.
32
34
  rate_limit: Optional rate limiting configuration.
33
35
  """
36
+
34
37
  name: str = Field(..., description="Tool name")
35
38
  namespace: str = Field("default", description="Namespace the tool belongs to")
36
- description: Optional[str] = Field(None, description="Tool description")
39
+ description: str | None = Field(None, description="Tool description")
37
40
  version: str = Field("1.0.0", description="Tool implementation version")
38
41
  is_async: bool = Field(True, description="Whether the tool's execute method is asynchronous")
39
- argument_schema: Optional[Dict[str, Any]] = Field(None, description="Schema for the tool's arguments")
40
- result_schema: Optional[Dict[str, Any]] = Field(None, description="Schema for the tool's result")
42
+ argument_schema: dict[str, Any] | None = Field(None, description="Schema for the tool's arguments")
43
+ result_schema: dict[str, Any] | None = Field(None, description="Schema for the tool's result")
41
44
  requires_auth: bool = Field(False, description="Whether the tool requires authentication")
42
- tags: Set[str] = Field(default_factory=set, description="Tags associated with the tool")
45
+ tags: set[str] = Field(default_factory=set, description="Tags associated with the tool")
43
46
  created_at: datetime = Field(default_factory=datetime.utcnow, description="When the tool was first registered")
44
47
  updated_at: datetime = Field(default_factory=datetime.utcnow, description="When the tool was last updated")
45
- source: Optional[str] = Field(None, description="Source of the tool (e.g., 'function', 'class', 'langchain')")
46
- source_name: Optional[str] = Field(None, description="Source identifier (e.g., function name, class name)")
47
- concurrency_limit: Optional[int] = Field(None, description="Maximum concurrent executions (None = unlimited)")
48
- timeout: Optional[float] = Field(None, description="Default timeout in seconds (None = no timeout)")
49
- rate_limit: Optional[Dict[str, Any]] = Field(None, description="Rate limiting configuration")
50
-
48
+ source: str | None = Field(None, description="Source of the tool (e.g., 'function', 'class', 'langchain')")
49
+ source_name: str | None = Field(None, description="Source identifier (e.g., function name, class name)")
50
+ concurrency_limit: int | None = Field(None, description="Maximum concurrent executions (None = unlimited)")
51
+ timeout: float | None = Field(None, description="Default timeout in seconds (None = no timeout)")
52
+ rate_limit: dict[str, Any] | None = Field(None, description="Rate limiting configuration")
53
+
51
54
  # Additional fields for async-native architecture
52
55
  supports_streaming: bool = Field(False, description="Whether the tool supports streaming responses")
53
- execution_options: Dict[str, Any] = Field(default_factory=dict, description="Additional execution options")
54
- dependencies: List[str] = Field(default_factory=list, description="Dependencies on other tools")
55
-
56
- @model_validator(mode='after')
57
- def ensure_async(self) -> 'ToolMetadata':
56
+ execution_options: dict[str, Any] = Field(default_factory=dict, description="Additional execution options")
57
+ dependencies: list[str] = Field(default_factory=list, description="Dependencies on other tools")
58
+
59
+ @model_validator(mode="after")
60
+ def ensure_async(self) -> ToolMetadata:
58
61
  """Ensure all tools are marked as async in the async-native architecture."""
59
62
  self.is_async = True
60
63
  return self
61
-
62
- def with_updated_timestamp(self) -> 'ToolMetadata':
64
+
65
+ def with_updated_timestamp(self) -> ToolMetadata:
63
66
  """Create a copy with updated timestamp."""
64
67
  return self.model_copy(update={"updated_at": datetime.utcnow()})
65
-
68
+
66
69
  def __str__(self) -> str:
67
70
  """String representation of the tool metadata."""
68
71
  return f"{self.namespace}.{self.name} (v{self.version})"
@@ -70,6 +73,7 @@ class ToolMetadata(BaseModel):
70
73
 
71
74
  class RateLimitConfig(BaseModel):
72
75
  """Rate limiting configuration for tools."""
76
+
73
77
  requests: int = Field(..., description="Maximum number of requests")
74
78
  period: float = Field(..., description="Time period in seconds")
75
79
  scope: str = Field("global", description="Scope of rate limiting: 'global', 'user', 'ip'")
@@ -77,6 +81,7 @@ class RateLimitConfig(BaseModel):
77
81
 
78
82
  class StreamingToolMetadata(ToolMetadata):
79
83
  """Extended metadata for tools that support streaming responses."""
84
+
80
85
  supports_streaming: bool = Field(True, description="Whether the tool supports streaming responses")
81
- chunk_size: Optional[int] = Field(None, description="Suggested chunk size for streaming")
82
- content_type: Optional[str] = Field(None, description="Content type for streaming responses")
86
+ chunk_size: int | None = Field(None, description="Suggested chunk size for streaming")
87
+ content_type: str | None = Field(None, description="Content type for streaming responses")
@@ -4,12 +4,12 @@ Global access to the async tool registry instance.
4
4
 
5
5
  There are two public faces:
6
6
 
7
- 1. **Module helpers**
7
+ 1. **Module helpers**
8
8
  • `get_registry()` lazily instantiates a default `InMemoryToolRegistry`
9
- and memoises it in the module-level variable ``_REGISTRY``.
9
+ and memoises it in the module-level variable ``_REGISTRY``.
10
10
  • `set_registry()` lets callers replace or reset that singleton.
11
11
 
12
- 2. **`ToolRegistryProvider` class**
12
+ 2. **`ToolRegistryProvider` class**
13
13
  Provides static methods for async-safe access to the registry.
14
14
 
15
15
  The contract verified by the test-suite is:
@@ -19,12 +19,12 @@ The contract verified by the test-suite is:
19
19
  * `await ToolRegistryProvider.set_registry(None)` resets the cache so the next
20
20
  `await get_registry()` call invokes (and honours any monkey-patched) factory.
21
21
  """
22
+
22
23
  from __future__ import annotations
23
24
 
24
25
  import asyncio
25
- import importlib
26
26
  import sys
27
- from typing import Optional, Callable, Awaitable, Dict, Any
27
+ from collections.abc import Awaitable, Callable
28
28
 
29
29
  # registry
30
30
  from .interface import ToolRegistryInterface
@@ -32,7 +32,7 @@ from .interface import ToolRegistryInterface
32
32
  # --------------------------------------------------------------------------- #
33
33
  # Module-level singleton used by the helper functions
34
34
  # --------------------------------------------------------------------------- #
35
- _REGISTRY: Optional[ToolRegistryInterface] = None
35
+ _REGISTRY: ToolRegistryInterface | None = None
36
36
  _REGISTRY_LOCK = asyncio.Lock()
37
37
  # --------------------------------------------------------------------------- #
38
38
 
@@ -41,13 +41,14 @@ async def _default_registry() -> ToolRegistryInterface:
41
41
  """Create the default in-memory registry asynchronously."""
42
42
  # Import here to avoid circular import
43
43
  from .providers.memory import InMemoryToolRegistry
44
+
44
45
  return InMemoryToolRegistry()
45
46
 
46
47
 
47
48
  async def get_registry() -> ToolRegistryInterface:
48
49
  """
49
50
  Return the process-wide registry asynchronously, creating it on first use.
50
-
51
+
51
52
  This function is thread-safe and will only create the registry once,
52
53
  even with concurrent calls.
53
54
  """
@@ -79,7 +80,7 @@ class ToolRegistryProvider:
79
80
  """Async static wrapper for registry access."""
80
81
 
81
82
  # Thread-safe singleton management
82
- _registry: Optional[ToolRegistryInterface] = None
83
+ _registry: ToolRegistryInterface | None = None
83
84
  _lock = asyncio.Lock()
84
85
 
85
86
  # ------------------------ public API ------------------------ #
@@ -87,7 +88,7 @@ class ToolRegistryProvider:
87
88
  async def get_registry() -> ToolRegistryInterface:
88
89
  """
89
90
  Return the cached instance or initialize a new one asynchronously.
90
-
91
+
91
92
  This method ensures thread-safety when initializing the registry.
92
93
  """
93
94
  if ToolRegistryProvider._registry is None:
@@ -96,12 +97,10 @@ class ToolRegistryProvider:
96
97
  if ToolRegistryProvider._registry is None:
97
98
  # Dynamically import to get the latest definition
98
99
  module = sys.modules[__name__]
99
- get_registry_func: Callable[[], Awaitable[ToolRegistryInterface]] = getattr(
100
- module, "get_registry"
101
- )
100
+ get_registry_func: Callable[[], Awaitable[ToolRegistryInterface]] = module.get_registry
102
101
  # Call it to get the registry
103
102
  ToolRegistryProvider._registry = await get_registry_func()
104
-
103
+
105
104
  return ToolRegistryProvider._registry
106
105
 
107
106
  @staticmethod
@@ -116,23 +115,23 @@ class ToolRegistryProvider:
116
115
  """
117
116
  async with ToolRegistryProvider._lock:
118
117
  ToolRegistryProvider._registry = registry
119
-
118
+
120
119
  @staticmethod
121
120
  async def reset() -> None:
122
121
  """
123
122
  Reset both the module-level and class-level registry caches.
124
-
123
+
125
124
  This is primarily used in tests to ensure a clean state.
126
125
  """
127
126
  async with ToolRegistryProvider._lock:
128
127
  ToolRegistryProvider._registry = None
129
128
  await set_registry(None)
130
-
129
+
131
130
  @staticmethod
132
131
  async def get_global_registry() -> ToolRegistryInterface:
133
132
  """
134
133
  Get the module-level registry directly.
135
-
134
+
136
135
  This bypasses the class-level cache and always returns the module-level registry.
137
136
  """
138
- return await get_registry()
137
+ return await get_registry()
@@ -3,38 +3,34 @@
3
3
  Async registry provider implementations and factory functions.
4
4
  """
5
5
 
6
- import os
7
6
  import asyncio
8
- from typing import Optional, Dict, Any
7
+ import os
9
8
 
10
9
  from chuk_tool_processor.registry.interface import ToolRegistryInterface
11
10
 
12
11
  # Cache for initialized registries
13
- _REGISTRY_CACHE: Dict[str, ToolRegistryInterface] = {}
14
- _REGISTRY_LOCKS: Dict[str, asyncio.Lock] = {}
12
+ _REGISTRY_CACHE: dict[str, ToolRegistryInterface] = {}
13
+ _REGISTRY_LOCKS: dict[str, asyncio.Lock] = {}
15
14
 
16
15
 
17
- async def get_registry(
18
- provider_type: Optional[str] = None,
19
- **kwargs
20
- ) -> ToolRegistryInterface:
16
+ async def get_registry(provider_type: str | None = None, **kwargs) -> ToolRegistryInterface:
21
17
  """
22
18
  Factory function to get a registry implementation asynchronously.
23
-
19
+
24
20
  This function caches registry instances by provider_type to avoid
25
21
  creating multiple instances unnecessarily. The cache is protected
26
22
  by locks to ensure thread safety.
27
-
23
+
28
24
  Args:
29
25
  provider_type: Type of registry provider to use. Options:
30
26
  - "memory" (default): In-memory implementation
31
27
  - "redis": Redis-backed implementation (if available)
32
28
  - "sqlalchemy": Database-backed implementation (if available)
33
29
  **kwargs: Additional configuration for the provider.
34
-
30
+
35
31
  Returns:
36
32
  A registry implementation.
37
-
33
+
38
34
  Raises:
39
35
  ImportError: If the requested provider is not available.
40
36
  ValueError: If the provider type is not recognized.
@@ -42,30 +38,31 @@ async def get_registry(
42
38
  # Use environment variable if not specified
43
39
  if provider_type is None:
44
40
  provider_type = os.environ.get("CHUK_TOOL_REGISTRY_PROVIDER", "memory")
45
-
41
+
46
42
  # Check cache first
47
43
  cache_key = f"{provider_type}:{hash(frozenset(kwargs.items()))}"
48
44
  if cache_key in _REGISTRY_CACHE:
49
45
  return _REGISTRY_CACHE[cache_key]
50
-
46
+
51
47
  # Create lock if needed
52
48
  if cache_key not in _REGISTRY_LOCKS:
53
49
  _REGISTRY_LOCKS[cache_key] = asyncio.Lock()
54
-
50
+
55
51
  # Acquire lock to ensure only one registry is created
56
52
  async with _REGISTRY_LOCKS[cache_key]:
57
53
  # Double-check pattern: check cache again after acquiring lock
58
54
  if cache_key in _REGISTRY_CACHE:
59
55
  return _REGISTRY_CACHE[cache_key]
60
-
56
+
61
57
  # Create the appropriate provider
62
58
  if provider_type == "memory":
63
59
  # Import here to avoid circular imports
64
60
  from chuk_tool_processor.registry.providers.memory import InMemoryToolRegistry
61
+
65
62
  registry = InMemoryToolRegistry()
66
63
  else:
67
64
  raise ValueError(f"Unknown registry provider type: {provider_type}")
68
-
65
+
69
66
  # Cache the registry
70
67
  _REGISTRY_CACHE[cache_key] = registry
71
68
  return registry
@@ -74,7 +71,7 @@ async def get_registry(
74
71
  async def clear_registry_cache() -> None:
75
72
  """
76
73
  Clear the registry cache.
77
-
74
+
78
75
  This is useful in tests or when configuration changes.
79
76
  """
80
- _REGISTRY_CACHE.clear()
77
+ _REGISTRY_CACHE.clear()
@@ -2,11 +2,12 @@
2
2
  """
3
3
  In-memory implementation of the asynchronous tool registry.
4
4
  """
5
+
5
6
  from __future__ import annotations
6
7
 
7
8
  import asyncio
8
9
  import inspect
9
- from typing import Any, Dict, List, Optional, Tuple
10
+ from typing import Any
10
11
 
11
12
  from chuk_tool_processor.core.exceptions import ToolNotFoundError
12
13
  from chuk_tool_processor.registry.interface import ToolRegistryInterface
@@ -27,9 +28,9 @@ class InMemoryToolRegistry(ToolRegistryInterface):
27
28
 
28
29
  def __init__(self) -> None:
29
30
  # {namespace: {tool_name: tool_obj}}
30
- self._tools: Dict[str, Dict[str, Any]] = {}
31
+ self._tools: dict[str, dict[str, Any]] = {}
31
32
  # {namespace: {tool_name: ToolMetadata}}
32
- self._metadata: Dict[str, Dict[str, ToolMetadata]] = {}
33
+ self._metadata: dict[str, dict[str, ToolMetadata]] = {}
33
34
  # Lock for thread safety
34
35
  self._lock = asyncio.Lock()
35
36
 
@@ -40,9 +41,9 @@ class InMemoryToolRegistry(ToolRegistryInterface):
40
41
  async def register_tool(
41
42
  self,
42
43
  tool: Any,
43
- name: Optional[str] = None,
44
+ name: str | None = None,
44
45
  namespace: str = "default",
45
- metadata: Optional[Dict[str, Any]] = None,
46
+ metadata: dict[str, Any] | None = None,
46
47
  ) -> None:
47
48
  """Register a tool in the registry asynchronously."""
48
49
  async with self._lock:
@@ -57,13 +58,9 @@ class InMemoryToolRegistry(ToolRegistryInterface):
57
58
  is_async = inspect.iscoroutinefunction(getattr(tool, "execute", None))
58
59
 
59
60
  # default description -> docstring
60
- description = (
61
- (inspect.getdoc(tool) or "").strip()
62
- if not (metadata and "description" in metadata)
63
- else None
64
- )
61
+ description = (inspect.getdoc(tool) or "").strip() if not (metadata and "description" in metadata) else None
65
62
 
66
- meta_dict: Dict[str, Any] = {
63
+ meta_dict: dict[str, Any] = {
67
64
  "name": key,
68
65
  "namespace": namespace,
69
66
  "is_async": is_async,
@@ -79,7 +76,7 @@ class InMemoryToolRegistry(ToolRegistryInterface):
79
76
  # retrieval
80
77
  # ------------------------------------------------------------------ #
81
78
 
82
- async def get_tool(self, name: str, namespace: str = "default") -> Optional[Any]:
79
+ async def get_tool(self, name: str, namespace: str = "default") -> Any | None:
83
80
  """Retrieve a tool by name and namespace asynchronously."""
84
81
  # Read operations don't need locking for better concurrency
85
82
  return self._tools.get(namespace, {}).get(name)
@@ -91,9 +88,7 @@ class InMemoryToolRegistry(ToolRegistryInterface):
91
88
  raise ToolNotFoundError(f"{namespace}.{name}")
92
89
  return tool
93
90
 
94
- async def get_metadata(
95
- self, name: str, namespace: str = "default"
96
- ) -> Optional[ToolMetadata]:
91
+ async def get_metadata(self, name: str, namespace: str = "default") -> ToolMetadata | None:
97
92
  """Get metadata for a tool asynchronously."""
98
93
  return self._metadata.get(namespace, {}).get(name)
99
94
 
@@ -101,25 +96,23 @@ class InMemoryToolRegistry(ToolRegistryInterface):
101
96
  # listing helpers
102
97
  # ------------------------------------------------------------------ #
103
98
 
104
- async def list_tools(self, namespace: Optional[str] = None) -> List[Tuple[str, str]]:
99
+ async def list_tools(self, namespace: str | None = None) -> list[tuple[str, str]]:
105
100
  """
106
101
  Return a list of ``(namespace, name)`` tuples asynchronously.
107
102
  """
108
103
  if namespace:
109
- return [
110
- (namespace, n) for n in self._tools.get(namespace, {}).keys()
111
- ]
104
+ return [(namespace, n) for n in self._tools.get(namespace, {})]
112
105
 
113
- result: List[Tuple[str, str]] = []
106
+ result: list[tuple[str, str]] = []
114
107
  for ns, tools in self._tools.items():
115
- result.extend((ns, n) for n in tools.keys())
108
+ result.extend((ns, n) for n in tools)
116
109
  return result
117
110
 
118
- async def list_namespaces(self) -> List[str]:
111
+ async def list_namespaces(self) -> list[str]:
119
112
  """List all namespaces asynchronously."""
120
113
  return list(self._tools.keys())
121
114
 
122
- async def list_metadata(self, namespace: Optional[str] = None) -> List[ToolMetadata]:
115
+ async def list_metadata(self, namespace: str | None = None) -> list[ToolMetadata]:
123
116
  """
124
117
  Return all ToolMetadata objects asynchronously.
125
118
 
@@ -135,7 +128,7 @@ class InMemoryToolRegistry(ToolRegistryInterface):
135
128
  return list(self._metadata.get(namespace, {}).values())
136
129
 
137
130
  # flatten
138
- result: List[ToolMetadata] = []
131
+ result: list[ToolMetadata] = []
139
132
  for ns_meta in self._metadata.values():
140
133
  result.extend(ns_meta.values())
141
- return result
134
+ return result
@@ -3,10 +3,11 @@
3
3
  Async helpers that expose all registered tools in various formats and
4
4
  translate an OpenAI `function.name` back to the matching tool.
5
5
  """
6
+
6
7
  from __future__ import annotations
7
8
 
8
9
  import asyncio
9
- from typing import Dict, List, Any, Optional, Mapping
10
+ from typing import Any
10
11
 
11
12
  # registry
12
13
  from .provider import ToolRegistryProvider
@@ -14,19 +15,19 @@ from .provider import ToolRegistryProvider
14
15
  # --------------------------------------------------------------------------- #
15
16
  # internal cache so tool-name lookup is O(1) with async protection
16
17
  # --------------------------------------------------------------------------- #
17
- _OPENAI_NAME_CACHE: Optional[Dict[str, Any]] = None
18
+ _OPENAI_NAME_CACHE: dict[str, Any] | None = None
18
19
  _CACHE_LOCK = asyncio.Lock()
19
20
 
20
21
 
21
22
  async def _build_openai_name_cache() -> None:
22
23
  """
23
24
  Populate the global reverse-lookup table once asynchronously.
24
-
25
+
25
26
  This function is thread-safe and will only build the cache once,
26
27
  even with concurrent calls.
27
28
  """
28
29
  global _OPENAI_NAME_CACHE
29
-
30
+
30
31
  # Fast path - cache already exists
31
32
  if _OPENAI_NAME_CACHE is not None:
32
33
  return
@@ -36,16 +37,16 @@ async def _build_openai_name_cache() -> None:
36
37
  # Double-check pattern: check again after acquiring the lock
37
38
  if _OPENAI_NAME_CACHE is not None:
38
39
  return
39
-
40
+
40
41
  # Initialize an empty cache
41
42
  _OPENAI_NAME_CACHE = {}
42
-
43
+
43
44
  # Get the registry
44
45
  reg = await ToolRegistryProvider.get_registry()
45
46
 
46
47
  # Get all tools and their names
47
48
  tools_list = await reg.list_tools()
48
-
49
+
49
50
  for ns, key in tools_list:
50
51
  # Get the tool
51
52
  tool = await reg.get_tool(key, ns)
@@ -71,7 +72,7 @@ async def _build_openai_name_cache() -> None:
71
72
  # --------------------------------------------------------------------------- #
72
73
  # public helpers
73
74
  # --------------------------------------------------------------------------- #
74
- async def openai_functions() -> List[Dict]:
75
+ async def openai_functions() -> list[dict]:
75
76
  """
76
77
  Return **all** registered tools in the exact schema the Chat-Completions
77
78
  API expects in its ``tools=[ … ]`` parameter.
@@ -79,23 +80,23 @@ async def openai_functions() -> List[Dict]:
79
80
  The ``function.name`` is always the *registry key* so that the round-trip
80
81
  (export → model → parser) stays consistent even when the class name and
81
82
  the registered key differ.
82
-
83
+
83
84
  Returns:
84
85
  List of OpenAI function specifications
85
86
  """
86
87
  # Get the registry
87
88
  reg = await ToolRegistryProvider.get_registry()
88
- specs: List[Dict[str, Any]] = []
89
+ specs: list[dict[str, Any]] = []
89
90
 
90
91
  # List all tools
91
92
  tools_list = await reg.list_tools()
92
-
93
+
93
94
  for ns, key in tools_list:
94
95
  # Get each tool
95
96
  tool = await reg.get_tool(key, ns)
96
97
  if tool is None:
97
98
  continue
98
-
99
+
99
100
  try:
100
101
  # Get the OpenAI spec
101
102
  spec = tool.to_openai()
@@ -117,21 +118,21 @@ async def tool_by_openai_name(name: str) -> Any:
117
118
 
118
119
  Args:
119
120
  name: The OpenAI function name
120
-
121
+
121
122
  Returns:
122
123
  The tool implementation
123
-
124
+
124
125
  Raises:
125
126
  KeyError: If the name is unknown
126
127
  """
127
128
  # Ensure the cache is built
128
129
  await _build_openai_name_cache()
129
-
130
+
130
131
  # Look up the tool
131
132
  try:
132
133
  if _OPENAI_NAME_CACHE is None:
133
- raise KeyError(f"Tool cache not initialized")
134
-
134
+ raise KeyError("Tool cache not initialized")
135
+
135
136
  return _OPENAI_NAME_CACHE[name]
136
137
  except (KeyError, TypeError):
137
138
  raise KeyError(f"No tool registered for OpenAI name {name!r}") from None
@@ -140,7 +141,7 @@ async def tool_by_openai_name(name: str) -> Any:
140
141
  async def clear_name_cache() -> None:
141
142
  """
142
143
  Clear the OpenAI name cache.
143
-
144
+
144
145
  This is useful in tests or when the registry changes significantly.
145
146
  """
146
147
  global _OPENAI_NAME_CACHE
@@ -152,51 +153,51 @@ async def export_tools_as_openapi(
152
153
  title: str = "Tool API",
153
154
  version: str = "1.0.0",
154
155
  description: str = "API for registered tools",
155
- ) -> Dict[str, Any]:
156
+ ) -> dict[str, Any]:
156
157
  """
157
158
  Export all registered tools as an OpenAPI specification.
158
-
159
+
159
160
  Args:
160
161
  title: API title
161
162
  version: API version
162
163
  description: API description
163
-
164
+
164
165
  Returns:
165
166
  OpenAPI specification as a dictionary
166
167
  """
167
168
  # Get the registry
168
169
  reg = await ToolRegistryProvider.get_registry()
169
-
170
+
170
171
  # Build paths and components
171
- paths: Dict[str, Any] = {}
172
- schemas: Dict[str, Any] = {}
173
-
172
+ paths: dict[str, Any] = {}
173
+ schemas: dict[str, Any] = {}
174
+
174
175
  # List all tools
175
176
  tools_list = await reg.list_tools()
176
-
177
+
177
178
  for ns, key in tools_list:
178
179
  # Get tool and metadata
179
180
  tool = await reg.get_tool(key, ns)
180
181
  metadata = await reg.get_metadata(key, ns)
181
-
182
+
182
183
  if tool is None or metadata is None:
183
184
  continue
184
-
185
+
185
186
  # Create path
186
187
  path = f"/{ns}/{key}"
187
-
188
+
188
189
  # Get schemas from tool if available
189
190
  arg_schema = None
190
191
  result_schema = None
191
-
192
+
192
193
  if hasattr(tool, "Arguments") and hasattr(tool.Arguments, "model_json_schema"):
193
194
  arg_schema = tool.Arguments.model_json_schema()
194
195
  schemas[f"{key}Args"] = arg_schema
195
-
196
+
196
197
  if hasattr(tool, "Result") and hasattr(tool.Result, "model_json_schema"):
197
198
  result_schema = tool.Result.model_json_schema()
198
199
  schemas[f"{key}Result"] = result_schema
199
-
200
+
200
201
  # Add path
201
202
  paths[path] = {
202
203
  "post": {
@@ -209,7 +210,7 @@ async def export_tools_as_openapi(
209
210
  "application/json": {
210
211
  "schema": {"$ref": f"#/components/schemas/{key}Args"} if arg_schema else {}
211
212
  }
212
- }
213
+ },
213
214
  },
214
215
  "responses": {
215
216
  "200": {
@@ -218,28 +219,18 @@ async def export_tools_as_openapi(
218
219
  "application/json": {
219
220
  "schema": {"$ref": f"#/components/schemas/{key}Result"} if result_schema else {}
220
221
  }
221
- }
222
- },
223
- "400": {
224
- "description": "Bad request"
222
+ },
225
223
  },
226
- "500": {
227
- "description": "Internal server error"
228
- }
229
- }
224
+ "400": {"description": "Bad request"},
225
+ "500": {"description": "Internal server error"},
226
+ },
230
227
  }
231
228
  }
232
-
229
+
233
230
  # Build the OpenAPI spec
234
231
  return {
235
232
  "openapi": "3.0.0",
236
- "info": {
237
- "title": title,
238
- "version": version,
239
- "description": description
240
- },
233
+ "info": {"title": title, "version": version, "description": description},
241
234
  "paths": paths,
242
- "components": {
243
- "schemas": schemas
244
- }
245
- }
235
+ "components": {"schemas": schemas},
236
+ }