chuk-tool-processor 0.1.6__py3-none-any.whl → 0.2__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 (46) hide show
  1. chuk_tool_processor/core/processor.py +345 -132
  2. chuk_tool_processor/execution/strategies/inprocess_strategy.py +522 -71
  3. chuk_tool_processor/execution/strategies/subprocess_strategy.py +559 -64
  4. chuk_tool_processor/execution/tool_executor.py +282 -24
  5. chuk_tool_processor/execution/wrappers/caching.py +465 -123
  6. chuk_tool_processor/execution/wrappers/rate_limiting.py +199 -86
  7. chuk_tool_processor/execution/wrappers/retry.py +133 -23
  8. chuk_tool_processor/logging/__init__.py +83 -10
  9. chuk_tool_processor/logging/context.py +218 -22
  10. chuk_tool_processor/logging/formatter.py +56 -13
  11. chuk_tool_processor/logging/helpers.py +91 -16
  12. chuk_tool_processor/logging/metrics.py +75 -6
  13. chuk_tool_processor/mcp/mcp_tool.py +80 -35
  14. chuk_tool_processor/mcp/register_mcp_tools.py +74 -56
  15. chuk_tool_processor/mcp/setup_mcp_sse.py +41 -36
  16. chuk_tool_processor/mcp/setup_mcp_stdio.py +39 -37
  17. chuk_tool_processor/mcp/transport/sse_transport.py +351 -105
  18. chuk_tool_processor/models/execution_strategy.py +52 -3
  19. chuk_tool_processor/models/streaming_tool.py +110 -0
  20. chuk_tool_processor/models/tool_call.py +56 -4
  21. chuk_tool_processor/models/tool_result.py +115 -9
  22. chuk_tool_processor/models/validated_tool.py +15 -13
  23. chuk_tool_processor/plugins/discovery.py +115 -70
  24. chuk_tool_processor/plugins/parsers/base.py +13 -5
  25. chuk_tool_processor/plugins/parsers/{function_call_tool_plugin.py → function_call_tool.py} +39 -20
  26. chuk_tool_processor/plugins/parsers/json_tool.py +50 -0
  27. chuk_tool_processor/plugins/parsers/openai_tool.py +88 -0
  28. chuk_tool_processor/plugins/parsers/xml_tool.py +74 -20
  29. chuk_tool_processor/registry/__init__.py +46 -7
  30. chuk_tool_processor/registry/auto_register.py +92 -28
  31. chuk_tool_processor/registry/decorators.py +134 -11
  32. chuk_tool_processor/registry/interface.py +48 -14
  33. chuk_tool_processor/registry/metadata.py +52 -6
  34. chuk_tool_processor/registry/provider.py +75 -36
  35. chuk_tool_processor/registry/providers/__init__.py +49 -10
  36. chuk_tool_processor/registry/providers/memory.py +59 -48
  37. chuk_tool_processor/registry/tool_export.py +208 -39
  38. chuk_tool_processor/utils/validation.py +18 -13
  39. chuk_tool_processor-0.2.dist-info/METADATA +401 -0
  40. chuk_tool_processor-0.2.dist-info/RECORD +58 -0
  41. {chuk_tool_processor-0.1.6.dist-info → chuk_tool_processor-0.2.dist-info}/WHEEL +1 -1
  42. chuk_tool_processor/plugins/parsers/json_tool_plugin.py +0 -38
  43. chuk_tool_processor/plugins/parsers/openai_tool_plugin.py +0 -76
  44. chuk_tool_processor-0.1.6.dist-info/METADATA +0 -462
  45. chuk_tool_processor-0.1.6.dist-info/RECORD +0 -57
  46. {chuk_tool_processor-0.1.6.dist-info → chuk_tool_processor-0.2.dist-info}/top_level.txt +0 -0
@@ -1,24 +1,42 @@
1
1
  # chuk_tool_processor/registry/decorators.py
2
2
  """
3
- Decorators for registering tools with the registry.
3
+ Decorators for registering tools with the registry asynchronously.
4
4
  """
5
5
 
6
- from functools import wraps
7
- from typing import Any, Callable, Dict, Optional, Type, TypeVar
6
+ import asyncio
7
+ import functools
8
+ import inspect
9
+ import sys
10
+ import weakref
11
+ import atexit
12
+ import warnings
13
+ from typing import Any, Callable, Dict, Optional, Type, TypeVar, cast, Set, List, Awaitable
8
14
 
9
15
  from chuk_tool_processor.registry.provider import ToolRegistryProvider
10
16
 
11
17
  T = TypeVar('T')
12
18
 
19
+ # Global tracking of classes to be registered
20
+ # Store coroutines rather than awaitables to avoid warnings
21
+ _PENDING_REGISTRATIONS: List[Callable[[], Awaitable]] = []
22
+ _REGISTERED_CLASSES = weakref.WeakSet()
23
+
24
+ # Keep track of whether we're shutting down
25
+ _SHUTTING_DOWN = False
26
+
13
27
 
14
28
  def register_tool(name: Optional[str] = None, namespace: str = "default", **metadata):
15
29
  """
16
30
  Decorator for registering tools with the global registry.
17
31
 
32
+ This decorator will queue the registration to happen asynchronously.
33
+ You must call `await ensure_registrations()` in your application startup
34
+ to complete all registrations.
35
+
18
36
  Example:
19
37
  @register_tool(name="my_tool", namespace="math", description="Performs math operations")
20
38
  class MyTool:
21
- def execute(self, x: int, y: int) -> int:
39
+ async def execute(self, x: int, y: int) -> int:
22
40
  return x + y
23
41
 
24
42
  Args:
@@ -30,13 +48,118 @@ def register_tool(name: Optional[str] = None, namespace: str = "default", **meta
30
48
  A decorator function that registers the class with the registry.
31
49
  """
32
50
  def decorator(cls: Type[T]) -> Type[T]:
33
- registry = ToolRegistryProvider.get_registry()
34
- registry.register_tool(cls, name=name, namespace=namespace, metadata=metadata)
51
+ # Skip if already registered
52
+ if cls in _REGISTERED_CLASSES:
53
+ return cls
54
+
55
+ # Skip if shutting down
56
+ if _SHUTTING_DOWN:
57
+ return cls
58
+
59
+ # Ensure execute method is async
60
+ if hasattr(cls, 'execute') and not inspect.iscoroutinefunction(cls.execute):
61
+ raise TypeError(f"Tool {cls.__name__} must have an async execute method")
62
+
63
+ # Create registration function (not coroutine)
64
+ async def do_register():
65
+ registry = await ToolRegistryProvider.get_registry()
66
+ await registry.register_tool(
67
+ cls,
68
+ name=name,
69
+ namespace=namespace,
70
+ metadata=metadata
71
+ )
35
72
 
36
- @wraps(cls)
37
- def wrapper(*args: Any, **kwargs: Dict[str, Any]) -> T:
38
- return cls(*args, **kwargs)
73
+ # Store the function, not the coroutine
74
+ _PENDING_REGISTRATIONS.append(do_register)
75
+ _REGISTERED_CLASSES.add(cls)
76
+
77
+ # Add class attribute so we can identify decorated classes
78
+ cls._tool_registration_info = {
79
+ 'name': name or cls.__name__,
80
+ 'namespace': namespace,
81
+ 'metadata': metadata
82
+ }
83
+
84
+ # Don't modify the original class
85
+ return cls
86
+
87
+ return decorator
88
+
89
+
90
+ async def ensure_registrations() -> None:
91
+ """
92
+ Process all pending tool registrations.
93
+
94
+ This must be called during application startup to register
95
+ all tools decorated with @register_tool.
96
+
97
+ Returns:
98
+ None
99
+ """
100
+ global _PENDING_REGISTRATIONS
101
+
102
+ if not _PENDING_REGISTRATIONS:
103
+ return
39
104
 
40
- return wrapper
105
+ # Create tasks from the stored functions
106
+ tasks = []
107
+ for registration_fn in _PENDING_REGISTRATIONS:
108
+ # Now we await the function to get the coroutine, then create a task
109
+ tasks.append(asyncio.create_task(registration_fn()))
110
+
111
+ # Clear the pending list
112
+ _PENDING_REGISTRATIONS.clear()
41
113
 
42
- return decorator
114
+ # Wait for all registrations to complete
115
+ if tasks:
116
+ await asyncio.gather(*tasks)
117
+
118
+
119
+ def discover_decorated_tools() -> List[Type]:
120
+ """
121
+ Discover all tool classes decorated with @register_tool.
122
+
123
+ This can be used to inspect what tools have been registered
124
+ without awaiting ensure_registrations().
125
+
126
+ Returns:
127
+ List of tool classes that have been decorated
128
+ """
129
+ tools = []
130
+
131
+ # Search all loaded modules
132
+ for module_name, module in list(sys.modules.items()):
133
+ if not module_name.startswith('chuk_tool_processor'):
134
+ continue
135
+
136
+ for attr_name in dir(module):
137
+ try:
138
+ attr = getattr(module, attr_name)
139
+ if hasattr(attr, '_tool_registration_info'):
140
+ tools.append(attr)
141
+ except (AttributeError, ImportError):
142
+ pass
143
+
144
+ return tools
145
+
146
+
147
+ # Register atexit handler to prevent warnings at shutdown
148
+ def _handle_shutdown():
149
+ """
150
+ Handle shutdown by marking shutdown flag and clearing pending registrations.
151
+ This prevents warnings about unawaited coroutines.
152
+ """
153
+ global _SHUTTING_DOWN, _PENDING_REGISTRATIONS
154
+
155
+ # Set the shutdown flag
156
+ _SHUTTING_DOWN = True
157
+
158
+ # Clear without creating any coroutines
159
+ _PENDING_REGISTRATIONS = []
160
+
161
+ # Register the shutdown handler
162
+ atexit.register(_handle_shutdown)
163
+
164
+ # Filter the coroutine never awaited warning
165
+ warnings.filterwarnings("ignore", message="coroutine.*was never awaited")
@@ -1,19 +1,23 @@
1
1
  # chuk_tool_processor/registry/interface.py
2
2
  """
3
- Defines the interface for tool registries.
3
+ Defines the interface for asynchronous tool registries.
4
4
  """
5
- from typing import Protocol, Any, Dict, List, Optional, Tuple
5
+ from __future__ import annotations
6
6
 
7
- # imports
7
+ from typing import Protocol, Any, Dict, List, Optional, Tuple, TypeVar, runtime_checkable
8
+
9
+ # imports
8
10
  from chuk_tool_processor.registry.metadata import ToolMetadata
9
11
 
12
+ T = TypeVar('T')
10
13
 
14
+ @runtime_checkable
11
15
  class ToolRegistryInterface(Protocol):
12
16
  """
13
- Protocol for a tool registry. Implementations should allow registering tools
17
+ Protocol for an async tool registry. Implementations should allow registering tools
14
18
  and retrieving them by name and namespace.
15
19
  """
16
- def register_tool(
20
+ async def register_tool(
17
21
  self,
18
22
  tool: Any,
19
23
  name: Optional[str] = None,
@@ -21,7 +25,7 @@ class ToolRegistryInterface(Protocol):
21
25
  metadata: Optional[Dict[str, Any]] = None
22
26
  ) -> None:
23
27
  """
24
- Register a tool implementation.
28
+ Register a tool implementation asynchronously.
25
29
 
26
30
  Args:
27
31
  tool: The tool class or instance with an `execute` method.
@@ -31,9 +35,9 @@ class ToolRegistryInterface(Protocol):
31
35
  """
32
36
  ...
33
37
 
34
- def get_tool(self, name: str, namespace: str = "default") -> Optional[Any]:
38
+ async def get_tool(self, name: str, namespace: str = "default") -> Optional[Any]:
35
39
  """
36
- Retrieve a registered tool by name and namespace.
40
+ Retrieve a registered tool by name and namespace asynchronously.
37
41
 
38
42
  Args:
39
43
  name: The name of the tool.
@@ -44,9 +48,25 @@ class ToolRegistryInterface(Protocol):
44
48
  """
45
49
  ...
46
50
 
47
- def get_metadata(self, name: str, namespace: str = "default") -> Optional[ToolMetadata]:
51
+ async def get_tool_strict(self, name: str, namespace: str = "default") -> Any:
52
+ """
53
+ Retrieve a registered tool by name and namespace, raising if not found.
54
+
55
+ Args:
56
+ name: The name of the tool.
57
+ namespace: The namespace of the tool (default: "default").
58
+
59
+ Returns:
60
+ The tool implementation.
61
+
62
+ Raises:
63
+ ToolNotFoundError: If the tool is not found in the registry.
64
+ """
65
+ ...
66
+
67
+ async def get_metadata(self, name: str, namespace: str = "default") -> Optional[ToolMetadata]:
48
68
  """
49
- Retrieve metadata for a registered tool.
69
+ Retrieve metadata for a registered tool asynchronously.
50
70
 
51
71
  Args:
52
72
  name: The name of the tool.
@@ -57,9 +77,9 @@ class ToolRegistryInterface(Protocol):
57
77
  """
58
78
  ...
59
79
 
60
- def list_tools(self, namespace: Optional[str] = None) -> List[Tuple[str, str]]:
80
+ async def list_tools(self, namespace: Optional[str] = None) -> List[Tuple[str, str]]:
61
81
  """
62
- List all registered tool names, optionally filtered by namespace.
82
+ List all registered tool names asynchronously, optionally filtered by namespace.
63
83
 
64
84
  Args:
65
85
  namespace: Optional namespace filter.
@@ -69,11 +89,25 @@ class ToolRegistryInterface(Protocol):
69
89
  """
70
90
  ...
71
91
 
72
- def list_namespaces(self) -> List[str]:
92
+ async def list_namespaces(self) -> List[str]:
73
93
  """
74
- List all registered namespaces.
94
+ List all registered namespaces asynchronously.
75
95
 
76
96
  Returns:
77
97
  List of namespace names.
78
98
  """
99
+ ...
100
+
101
+ async def list_metadata(self, namespace: Optional[str] = None) -> List[ToolMetadata]:
102
+ """
103
+ Return all ToolMetadata objects asynchronously.
104
+
105
+ Args:
106
+ namespace: Optional filter by namespace.
107
+ • None (default) – metadata from all namespaces
108
+ • "some_ns" – only that namespace
109
+
110
+ Returns:
111
+ List of ToolMetadata objects.
112
+ """
79
113
  ...
@@ -1,9 +1,12 @@
1
1
  # chuk_tool_processor/registry/metadata.py
2
2
  """
3
- Tool metadata models for the registry.
3
+ Tool metadata models for the registry with async-native support.
4
4
  """
5
- from typing import Any, Dict, Optional, Set
6
- from pydantic import BaseModel, Field
5
+ from __future__ import annotations
6
+
7
+ from typing import Any, Dict, Optional, Set, List, Union
8
+ from datetime import datetime
9
+ from pydantic import BaseModel, Field, model_validator
7
10
 
8
11
 
9
12
  class ToolMetadata(BaseModel):
@@ -20,17 +23,60 @@ class ToolMetadata(BaseModel):
20
23
  result_schema: Optional schema for the tool's result.
21
24
  requires_auth: Whether the tool requires authentication.
22
25
  tags: Set of tags associated with the tool.
26
+ created_at: When the tool was first registered.
27
+ updated_at: When the tool was last updated.
28
+ source: Optional source information (e.g., "function", "class", "langchain").
29
+ source_name: Optional source identifier.
30
+ concurrency_limit: Optional maximum concurrent executions.
31
+ timeout: Optional default timeout in seconds.
32
+ rate_limit: Optional rate limiting configuration.
23
33
  """
24
34
  name: str = Field(..., description="Tool name")
25
35
  namespace: str = Field("default", description="Namespace the tool belongs to")
26
36
  description: Optional[str] = Field(None, description="Tool description")
27
37
  version: str = Field("1.0.0", description="Tool implementation version")
28
- is_async: bool = Field(False, description="Whether the tool's execute method is asynchronous")
38
+ is_async: bool = Field(True, description="Whether the tool's execute method is asynchronous")
29
39
  argument_schema: Optional[Dict[str, Any]] = Field(None, description="Schema for the tool's arguments")
30
40
  result_schema: Optional[Dict[str, Any]] = Field(None, description="Schema for the tool's result")
31
41
  requires_auth: bool = Field(False, description="Whether the tool requires authentication")
32
42
  tags: Set[str] = Field(default_factory=set, description="Tags associated with the tool")
33
-
43
+ created_at: datetime = Field(default_factory=datetime.utcnow, description="When the tool was first registered")
44
+ 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
+
51
+ # Additional fields for async-native architecture
52
+ 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':
58
+ """Ensure all tools are marked as async in the async-native architecture."""
59
+ self.is_async = True
60
+ return self
61
+
62
+ def with_updated_timestamp(self) -> 'ToolMetadata':
63
+ """Create a copy with updated timestamp."""
64
+ return self.model_copy(update={"updated_at": datetime.utcnow()})
65
+
34
66
  def __str__(self) -> str:
35
67
  """String representation of the tool metadata."""
36
- return f"{self.namespace}.{self.name} (v{self.version})"
68
+ return f"{self.namespace}.{self.name} (v{self.version})"
69
+
70
+
71
+ class RateLimitConfig(BaseModel):
72
+ """Rate limiting configuration for tools."""
73
+ requests: int = Field(..., description="Maximum number of requests")
74
+ period: float = Field(..., description="Time period in seconds")
75
+ scope: str = Field("global", description="Scope of rate limiting: 'global', 'user', 'ip'")
76
+
77
+
78
+ class StreamingToolMetadata(ToolMetadata):
79
+ """Extended metadata for tools that support streaming responses."""
80
+ 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")
@@ -1,5 +1,6 @@
1
+ # chuk_tool_processor/registry/provider.py
1
2
  """
2
- Global access to *the* tool registry instance.
3
+ Global access to the async tool registry instance.
3
4
 
4
5
  There are two public faces:
5
6
 
@@ -8,92 +9,130 @@ There are two public faces:
8
9
  and memoises it in the module-level variable ``_REGISTRY``.
9
10
  • `set_registry()` lets callers replace or reset that singleton.
10
11
 
11
- 2. **`ToolRegistryProvider` shim**
12
- Earlier versions exposed a static wrapper. Tests rely on being able to
13
- monkey-patch the *module-level* factory and to clear the cached instance
14
- by setting `ToolRegistryProvider._registry = None`. We therefore keep a
15
- **separate class-level cache** (`_registry`) and call the *current*
16
- module-level `get_registry()` **only when the cache is empty**.
12
+ 2. **`ToolRegistryProvider` class**
13
+ Provides static methods for async-safe access to the registry.
17
14
 
18
15
  The contract verified by the test-suite is:
19
16
 
20
17
  * The module-level factory is invoked **exactly once** per fresh cache.
21
- * `ToolRegistryProvider.set_registry(obj)` overrides subsequent retrievals.
22
- * `ToolRegistryProvider.set_registry(None)` resets the cache so the next
23
- `get_registry()` call invokes (and honours any monkey-patched) factory.
18
+ * `await ToolRegistryProvider.set_registry(obj)` overrides subsequent retrievals.
19
+ * `await ToolRegistryProvider.set_registry(None)` resets the cache so the next
20
+ `await get_registry()` call invokes (and honours any monkey-patched) factory.
24
21
  """
25
22
  from __future__ import annotations
26
23
 
27
- from typing import Optional
24
+ import asyncio
25
+ import importlib
26
+ import sys
27
+ from typing import Optional, Callable, Awaitable, Dict, Any
28
28
 
29
+ # registry
29
30
  from .interface import ToolRegistryInterface
30
- from .providers.memory import InMemoryToolRegistry
31
31
 
32
32
  # --------------------------------------------------------------------------- #
33
33
  # Module-level singleton used by the helper functions
34
34
  # --------------------------------------------------------------------------- #
35
35
  _REGISTRY: Optional[ToolRegistryInterface] = None
36
+ _REGISTRY_LOCK = asyncio.Lock()
36
37
  # --------------------------------------------------------------------------- #
37
38
 
38
39
 
39
- def _default_registry() -> ToolRegistryInterface:
40
- """Create the default in-memory registry."""
40
+ async def _default_registry() -> ToolRegistryInterface:
41
+ """Create the default in-memory registry asynchronously."""
42
+ # Import here to avoid circular import
43
+ from .providers.memory import InMemoryToolRegistry
41
44
  return InMemoryToolRegistry()
42
45
 
43
46
 
44
- def get_registry() -> ToolRegistryInterface:
47
+ async def get_registry() -> ToolRegistryInterface:
45
48
  """
46
- Return the process-wide registry, creating it on first use.
47
-
48
- This function *may* be monkey-patched in tests; call it via
49
- ``globals()["get_registry"]()`` if you need the latest binding.
49
+ Return the process-wide registry asynchronously, creating it on first use.
50
+
51
+ This function is thread-safe and will only create the registry once,
52
+ even with concurrent calls.
50
53
  """
51
54
  global _REGISTRY
52
55
  if _REGISTRY is None:
53
- _REGISTRY = _default_registry()
56
+ async with _REGISTRY_LOCK:
57
+ # Double-check pattern: check again after acquiring the lock
58
+ if _REGISTRY is None:
59
+ _REGISTRY = await _default_registry()
54
60
  return _REGISTRY
55
61
 
56
62
 
57
- def set_registry(registry: ToolRegistryInterface | None) -> None:
63
+ async def set_registry(registry: ToolRegistryInterface | None) -> None:
58
64
  """
59
- Replace or clear the global registry.
65
+ Replace or clear the global registry asynchronously.
60
66
 
61
67
  Passing ``None`` resets the singleton so that the next `get_registry()`
62
68
  call recreates it (useful in tests).
63
69
  """
64
70
  global _REGISTRY
65
- _REGISTRY = registry
71
+ async with _REGISTRY_LOCK:
72
+ _REGISTRY = registry
66
73
 
67
74
 
68
75
  # --------------------------------------------------------------------------- #
69
- # Back-compat shim used by legacy import paths and the test-suite
76
+ # Provider class for consistent access to the registry
70
77
  # --------------------------------------------------------------------------- #
71
- class ToolRegistryProvider: # noqa: D401
72
- """Legacy static wrapper retaining historical semantics."""
78
+ class ToolRegistryProvider:
79
+ """Async static wrapper for registry access."""
73
80
 
74
- # The test-suite directly mutates this attribute, so we keep it.
81
+ # Thread-safe singleton management
75
82
  _registry: Optional[ToolRegistryInterface] = None
83
+ _lock = asyncio.Lock()
76
84
 
77
85
  # ------------------------ public API ------------------------ #
78
86
  @staticmethod
79
- def get_registry() -> ToolRegistryInterface:
87
+ async def get_registry() -> ToolRegistryInterface:
80
88
  """
81
- Return the cached instance or, if absent, call the *current*
82
- module-level `get_registry()` exactly once to populate it.
89
+ Return the cached instance or initialize a new one asynchronously.
90
+
91
+ This method ensures thread-safety when initializing the registry.
83
92
  """
84
93
  if ToolRegistryProvider._registry is None:
85
- # Honour any runtime monkey-patching of the factory.
86
- ToolRegistryProvider._registry = globals()["get_registry"]()
94
+ async with ToolRegistryProvider._lock:
95
+ # Check again after acquiring the lock
96
+ if ToolRegistryProvider._registry is None:
97
+ # Dynamically import to get the latest definition
98
+ module = sys.modules[__name__]
99
+ get_registry_func: Callable[[], Awaitable[ToolRegistryInterface]] = getattr(
100
+ module, "get_registry"
101
+ )
102
+ # Call it to get the registry
103
+ ToolRegistryProvider._registry = await get_registry_func()
104
+
87
105
  return ToolRegistryProvider._registry
88
106
 
89
107
  @staticmethod
90
- def set_registry(registry: ToolRegistryInterface | None) -> None:
108
+ async def set_registry(registry: ToolRegistryInterface | None) -> None:
91
109
  """
92
- Override the cached registry.
110
+ Override the cached registry asynchronously.
93
111
 
94
112
  * If ``registry`` is an object, all subsequent `get_registry()`
95
113
  calls return it without touching the factory.
96
114
  * If ``registry`` is ``None``, the cache is cleared so the next
97
- `get_registry()` call invokes the (possibly patched) factory.
115
+ `get_registry()` call invokes the factory.
116
+ """
117
+ async with ToolRegistryProvider._lock:
118
+ ToolRegistryProvider._registry = registry
119
+
120
+ @staticmethod
121
+ async def reset() -> None:
122
+ """
123
+ Reset both the module-level and class-level registry caches.
124
+
125
+ This is primarily used in tests to ensure a clean state.
126
+ """
127
+ async with ToolRegistryProvider._lock:
128
+ ToolRegistryProvider._registry = None
129
+ await set_registry(None)
130
+
131
+ @staticmethod
132
+ async def get_global_registry() -> ToolRegistryInterface:
133
+ """
134
+ Get the module-level registry directly.
135
+
136
+ This bypasses the class-level cache and always returns the module-level registry.
98
137
  """
99
- ToolRegistryProvider._registry = registry
138
+ return await get_registry()
@@ -1,20 +1,29 @@
1
+ # chuk_tool_processor/registry/providers/__init__.py
1
2
  """
2
- Registry provider implementations and factory functions.
3
+ Async registry provider implementations and factory functions.
3
4
  """
4
5
 
5
6
  import os
6
- from typing import Optional
7
+ import asyncio
8
+ from typing import Optional, Dict, Any
7
9
 
8
10
  from chuk_tool_processor.registry.interface import ToolRegistryInterface
9
- from chuk_tool_processor.registry.providers.memory import InMemoryToolRegistry
10
11
 
12
+ # Cache for initialized registries
13
+ _REGISTRY_CACHE: Dict[str, ToolRegistryInterface] = {}
14
+ _REGISTRY_LOCKS: Dict[str, asyncio.Lock] = {}
11
15
 
12
- def get_registry(
16
+
17
+ async def get_registry(
13
18
  provider_type: Optional[str] = None,
14
19
  **kwargs
15
20
  ) -> ToolRegistryInterface:
16
21
  """
17
- Factory function to get a registry implementation.
22
+ Factory function to get a registry implementation asynchronously.
23
+
24
+ This function caches registry instances by provider_type to avoid
25
+ creating multiple instances unnecessarily. The cache is protected
26
+ by locks to ensure thread safety.
18
27
 
19
28
  Args:
20
29
  provider_type: Type of registry provider to use. Options:
@@ -34,8 +43,38 @@ def get_registry(
34
43
  if provider_type is None:
35
44
  provider_type = os.environ.get("CHUK_TOOL_REGISTRY_PROVIDER", "memory")
36
45
 
37
- # Create the appropriate provider
38
- if provider_type == "memory":
39
- return InMemoryToolRegistry()
40
- else:
41
- raise ValueError(f"Unknown registry provider type: {provider_type}")
46
+ # Check cache first
47
+ cache_key = f"{provider_type}:{hash(frozenset(kwargs.items()))}"
48
+ if cache_key in _REGISTRY_CACHE:
49
+ return _REGISTRY_CACHE[cache_key]
50
+
51
+ # Create lock if needed
52
+ if cache_key not in _REGISTRY_LOCKS:
53
+ _REGISTRY_LOCKS[cache_key] = asyncio.Lock()
54
+
55
+ # Acquire lock to ensure only one registry is created
56
+ async with _REGISTRY_LOCKS[cache_key]:
57
+ # Double-check pattern: check cache again after acquiring lock
58
+ if cache_key in _REGISTRY_CACHE:
59
+ return _REGISTRY_CACHE[cache_key]
60
+
61
+ # Create the appropriate provider
62
+ if provider_type == "memory":
63
+ # Import here to avoid circular imports
64
+ from chuk_tool_processor.registry.providers.memory import InMemoryToolRegistry
65
+ registry = InMemoryToolRegistry()
66
+ else:
67
+ raise ValueError(f"Unknown registry provider type: {provider_type}")
68
+
69
+ # Cache the registry
70
+ _REGISTRY_CACHE[cache_key] = registry
71
+ return registry
72
+
73
+
74
+ async def clear_registry_cache() -> None:
75
+ """
76
+ Clear the registry cache.
77
+
78
+ This is useful in tests or when configuration changes.
79
+ """
80
+ _REGISTRY_CACHE.clear()