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.
- chuk_tool_processor/core/processor.py +345 -132
- chuk_tool_processor/execution/strategies/inprocess_strategy.py +522 -71
- chuk_tool_processor/execution/strategies/subprocess_strategy.py +559 -64
- chuk_tool_processor/execution/tool_executor.py +282 -24
- chuk_tool_processor/execution/wrappers/caching.py +465 -123
- chuk_tool_processor/execution/wrappers/rate_limiting.py +199 -86
- chuk_tool_processor/execution/wrappers/retry.py +133 -23
- chuk_tool_processor/logging/__init__.py +83 -10
- chuk_tool_processor/logging/context.py +218 -22
- chuk_tool_processor/logging/formatter.py +56 -13
- chuk_tool_processor/logging/helpers.py +91 -16
- chuk_tool_processor/logging/metrics.py +75 -6
- chuk_tool_processor/mcp/mcp_tool.py +80 -35
- chuk_tool_processor/mcp/register_mcp_tools.py +74 -56
- chuk_tool_processor/mcp/setup_mcp_sse.py +41 -36
- chuk_tool_processor/mcp/setup_mcp_stdio.py +39 -37
- chuk_tool_processor/mcp/transport/sse_transport.py +351 -105
- chuk_tool_processor/models/execution_strategy.py +52 -3
- chuk_tool_processor/models/streaming_tool.py +110 -0
- chuk_tool_processor/models/tool_call.py +56 -4
- chuk_tool_processor/models/tool_result.py +115 -9
- chuk_tool_processor/models/validated_tool.py +15 -13
- chuk_tool_processor/plugins/discovery.py +115 -70
- chuk_tool_processor/plugins/parsers/base.py +13 -5
- chuk_tool_processor/plugins/parsers/{function_call_tool_plugin.py → function_call_tool.py} +39 -20
- chuk_tool_processor/plugins/parsers/json_tool.py +50 -0
- chuk_tool_processor/plugins/parsers/openai_tool.py +88 -0
- chuk_tool_processor/plugins/parsers/xml_tool.py +74 -20
- chuk_tool_processor/registry/__init__.py +46 -7
- chuk_tool_processor/registry/auto_register.py +92 -28
- chuk_tool_processor/registry/decorators.py +134 -11
- chuk_tool_processor/registry/interface.py +48 -14
- chuk_tool_processor/registry/metadata.py +52 -6
- chuk_tool_processor/registry/provider.py +75 -36
- chuk_tool_processor/registry/providers/__init__.py +49 -10
- chuk_tool_processor/registry/providers/memory.py +59 -48
- chuk_tool_processor/registry/tool_export.py +208 -39
- chuk_tool_processor/utils/validation.py +18 -13
- chuk_tool_processor-0.2.dist-info/METADATA +401 -0
- chuk_tool_processor-0.2.dist-info/RECORD +58 -0
- {chuk_tool_processor-0.1.6.dist-info → chuk_tool_processor-0.2.dist-info}/WHEEL +1 -1
- chuk_tool_processor/plugins/parsers/json_tool_plugin.py +0 -38
- chuk_tool_processor/plugins/parsers/openai_tool_plugin.py +0 -76
- chuk_tool_processor-0.1.6.dist-info/METADATA +0 -462
- chuk_tool_processor-0.1.6.dist-info/RECORD +0 -57
- {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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5
|
+
from __future__ import annotations
|
|
6
6
|
|
|
7
|
-
|
|
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
|
|
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
|
|
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
|
|
6
|
-
|
|
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(
|
|
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
|
|
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`
|
|
12
|
-
|
|
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
|
-
|
|
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
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
71
|
+
async with _REGISTRY_LOCK:
|
|
72
|
+
_REGISTRY = registry
|
|
66
73
|
|
|
67
74
|
|
|
68
75
|
# --------------------------------------------------------------------------- #
|
|
69
|
-
#
|
|
76
|
+
# Provider class for consistent access to the registry
|
|
70
77
|
# --------------------------------------------------------------------------- #
|
|
71
|
-
class ToolRegistryProvider:
|
|
72
|
-
"""
|
|
78
|
+
class ToolRegistryProvider:
|
|
79
|
+
"""Async static wrapper for registry access."""
|
|
73
80
|
|
|
74
|
-
#
|
|
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
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
|
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
|
-
|
|
138
|
+
return await get_registry()
|
|
@@ -1,20 +1,29 @@
|
|
|
1
|
+
# chuk_tool_processor/registry/providers/__init__.py
|
|
1
2
|
"""
|
|
2
|
-
|
|
3
|
+
Async registry provider implementations and factory functions.
|
|
3
4
|
"""
|
|
4
5
|
|
|
5
6
|
import os
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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()
|