chuk-tool-processor 0.1.0__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/__init__.py +1 -0
- chuk_tool_processor/core/__init__.py +1 -0
- chuk_tool_processor/core/exceptions.py +45 -0
- chuk_tool_processor/core/processor.py +268 -0
- chuk_tool_processor/execution/__init__.py +0 -0
- chuk_tool_processor/execution/strategies/__init__.py +0 -0
- chuk_tool_processor/execution/strategies/inprocess_strategy.py +206 -0
- chuk_tool_processor/execution/strategies/subprocess_strategy.py +103 -0
- chuk_tool_processor/execution/tool_executor.py +46 -0
- chuk_tool_processor/execution/wrappers/__init__.py +0 -0
- chuk_tool_processor/execution/wrappers/caching.py +234 -0
- chuk_tool_processor/execution/wrappers/rate_limiting.py +149 -0
- chuk_tool_processor/execution/wrappers/retry.py +176 -0
- chuk_tool_processor/models/__init__.py +1 -0
- chuk_tool_processor/models/execution_strategy.py +19 -0
- chuk_tool_processor/models/tool_call.py +7 -0
- chuk_tool_processor/models/tool_result.py +49 -0
- chuk_tool_processor/plugins/__init__.py +1 -0
- chuk_tool_processor/plugins/discovery.py +205 -0
- chuk_tool_processor/plugins/parsers/__init__.py +1 -0
- chuk_tool_processor/plugins/parsers/function_call_tool.py +105 -0
- chuk_tool_processor/plugins/parsers/json_tool.py +17 -0
- chuk_tool_processor/plugins/parsers/xml_tool.py +41 -0
- chuk_tool_processor/registry/__init__.py +20 -0
- chuk_tool_processor/registry/decorators.py +42 -0
- chuk_tool_processor/registry/interface.py +79 -0
- chuk_tool_processor/registry/metadata.py +36 -0
- chuk_tool_processor/registry/provider.py +44 -0
- chuk_tool_processor/registry/providers/__init__.py +41 -0
- chuk_tool_processor/registry/providers/memory.py +165 -0
- chuk_tool_processor/utils/__init__.py +0 -0
- chuk_tool_processor/utils/logging.py +260 -0
- chuk_tool_processor/utils/validation.py +192 -0
- chuk_tool_processor-0.1.0.dist-info/METADATA +293 -0
- chuk_tool_processor-0.1.0.dist-info/RECORD +37 -0
- chuk_tool_processor-0.1.0.dist-info/WHEEL +5 -0
- chuk_tool_processor-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# chuk_tool_processor/plugins/discovery.py
|
|
2
|
+
import importlib
|
|
3
|
+
import inspect
|
|
4
|
+
import pkgutil
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Dict, List, Optional, Set, Type, Any
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PluginRegistry:
|
|
13
|
+
"""
|
|
14
|
+
Registry for discovered plugins.
|
|
15
|
+
"""
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self._plugins: Dict[str, Dict[str, Any]] = {}
|
|
18
|
+
|
|
19
|
+
def register_plugin(self, category: str, name: str, plugin: Any) -> None:
|
|
20
|
+
"""
|
|
21
|
+
Register a plugin in the registry.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
category: Plugin category (e.g., "parser", "executor").
|
|
25
|
+
name: Plugin name.
|
|
26
|
+
plugin: Plugin implementation.
|
|
27
|
+
"""
|
|
28
|
+
# Ensure category exists
|
|
29
|
+
if category not in self._plugins:
|
|
30
|
+
self._plugins[category] = {}
|
|
31
|
+
|
|
32
|
+
# Register plugin
|
|
33
|
+
self._plugins[category][name] = plugin
|
|
34
|
+
logger.debug(f"Registered plugin: {category}.{name}")
|
|
35
|
+
|
|
36
|
+
def get_plugin(self, category: str, name: str) -> Optional[Any]:
|
|
37
|
+
"""
|
|
38
|
+
Get a plugin from the registry.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
category: Plugin category.
|
|
42
|
+
name: Plugin name.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Plugin implementation or None if not found.
|
|
46
|
+
"""
|
|
47
|
+
return self._plugins.get(category, {}).get(name)
|
|
48
|
+
|
|
49
|
+
def list_plugins(self, category: Optional[str] = None) -> Dict[str, List[str]]:
|
|
50
|
+
"""
|
|
51
|
+
List registered plugins.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
category: Optional category filter.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Dict mapping categories to lists of plugin names.
|
|
58
|
+
"""
|
|
59
|
+
if category:
|
|
60
|
+
return {category: list(self._plugins.get(category, {}).keys())}
|
|
61
|
+
else:
|
|
62
|
+
return {cat: list(plugins.keys()) for cat, plugins in self._plugins.items()}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class PluginDiscovery:
|
|
66
|
+
"""
|
|
67
|
+
Discovers and loads plugins from specified packages.
|
|
68
|
+
"""
|
|
69
|
+
def __init__(self, registry: PluginRegistry):
|
|
70
|
+
"""
|
|
71
|
+
Initialize the plugin discovery.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
registry: Plugin registry to register discovered plugins.
|
|
75
|
+
"""
|
|
76
|
+
self.registry = registry
|
|
77
|
+
self._discovered_modules: Set[str] = set()
|
|
78
|
+
|
|
79
|
+
def discover_plugins(self, package_paths: List[str]) -> None:
|
|
80
|
+
"""
|
|
81
|
+
Discover plugins in the specified packages.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
package_paths: List of package paths to search for plugins.
|
|
85
|
+
"""
|
|
86
|
+
for package_path in package_paths:
|
|
87
|
+
self._discover_in_package(package_path)
|
|
88
|
+
|
|
89
|
+
def _discover_in_package(self, package_path: str) -> None:
|
|
90
|
+
"""
|
|
91
|
+
Discover plugins in a single package.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
package_path: Package path to search.
|
|
95
|
+
"""
|
|
96
|
+
try:
|
|
97
|
+
# Import the package
|
|
98
|
+
package = importlib.import_module(package_path)
|
|
99
|
+
|
|
100
|
+
# Walk through package modules
|
|
101
|
+
for _, name, is_pkg in pkgutil.iter_modules(package.__path__, package.__name__ + "."):
|
|
102
|
+
# Skip if already processed
|
|
103
|
+
if name in self._discovered_modules:
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
self._discovered_modules.add(name)
|
|
107
|
+
|
|
108
|
+
# Process module
|
|
109
|
+
self._process_module(name)
|
|
110
|
+
|
|
111
|
+
# Recurse into subpackages
|
|
112
|
+
if is_pkg:
|
|
113
|
+
self._discover_in_package(name)
|
|
114
|
+
|
|
115
|
+
except ImportError as e:
|
|
116
|
+
logger.warning(f"Failed to import package {package_path}: {e}")
|
|
117
|
+
|
|
118
|
+
def _process_module(self, module_name: str) -> None:
|
|
119
|
+
"""
|
|
120
|
+
Process a module for plugins.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
module_name: Module name to process.
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
# Import the module
|
|
127
|
+
module = importlib.import_module(module_name)
|
|
128
|
+
|
|
129
|
+
# Find all classes in the module
|
|
130
|
+
for attr_name in dir(module):
|
|
131
|
+
attr = getattr(module, attr_name)
|
|
132
|
+
|
|
133
|
+
# Skip non-classes
|
|
134
|
+
if not inspect.isclass(attr):
|
|
135
|
+
continue
|
|
136
|
+
|
|
137
|
+
# Check if it's a plugin
|
|
138
|
+
self._register_if_plugin(attr)
|
|
139
|
+
|
|
140
|
+
except ImportError as e:
|
|
141
|
+
logger.warning(f"Failed to import module {module_name}: {e}")
|
|
142
|
+
|
|
143
|
+
def _register_if_plugin(self, cls: Type) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Register a class if it's a plugin.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
cls: Class to check.
|
|
149
|
+
"""
|
|
150
|
+
# Check if it's a parser plugin
|
|
151
|
+
if hasattr(cls, "try_parse") and callable(getattr(cls, "try_parse")):
|
|
152
|
+
self.registry.register_plugin("parser", cls.__name__, cls())
|
|
153
|
+
|
|
154
|
+
# Check if it's an execution strategy
|
|
155
|
+
if "ExecutionStrategy" in [base.__name__ for base in cls.__mro__]:
|
|
156
|
+
self.registry.register_plugin("execution_strategy", cls.__name__, cls)
|
|
157
|
+
|
|
158
|
+
# Check if it has plugin metadata
|
|
159
|
+
if hasattr(cls, "_plugin_meta"):
|
|
160
|
+
meta = getattr(cls, "_plugin_meta")
|
|
161
|
+
self.registry.register_plugin(meta.get("category", "unknown"), meta.get("name", cls.__name__), cls())
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def plugin(category: str, name: Optional[str] = None):
|
|
165
|
+
"""
|
|
166
|
+
Decorator to mark a class as a plugin.
|
|
167
|
+
|
|
168
|
+
Example:
|
|
169
|
+
@plugin(category="parser", name="custom_format")
|
|
170
|
+
class CustomFormatParser:
|
|
171
|
+
def try_parse(self, raw: str):
|
|
172
|
+
...
|
|
173
|
+
"""
|
|
174
|
+
def decorator(cls):
|
|
175
|
+
cls._plugin_meta = {
|
|
176
|
+
"category": category,
|
|
177
|
+
"name": name or cls.__name__
|
|
178
|
+
}
|
|
179
|
+
return cls
|
|
180
|
+
return decorator
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# Initialize the global plugin registry
|
|
184
|
+
plugin_registry = PluginRegistry()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# Function to discover plugins in the default package
|
|
188
|
+
def discover_default_plugins():
|
|
189
|
+
"""
|
|
190
|
+
Discover plugins in the default package.
|
|
191
|
+
"""
|
|
192
|
+
discovery = PluginDiscovery(plugin_registry)
|
|
193
|
+
discovery.discover_plugins(["chuk_tool_processor.plugins"])
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# Function to discover plugins in custom packages
|
|
197
|
+
def discover_plugins(package_paths: List[str]):
|
|
198
|
+
"""
|
|
199
|
+
Discover plugins in custom packages.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
package_paths: List of package paths to search for plugins.
|
|
203
|
+
"""
|
|
204
|
+
discovery = PluginDiscovery(plugin_registry)
|
|
205
|
+
discovery.discover_plugins(package_paths)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# chuk_tool_processor/plugins/parsers__init__.py
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# chuk_tool_processor/plugins/function_call_tool.py
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from typing import List, Any, Dict
|
|
5
|
+
from pydantic import ValidationError
|
|
6
|
+
|
|
7
|
+
# imports
|
|
8
|
+
from chuk_tool_processor.models.tool_call import ToolCall
|
|
9
|
+
from chuk_tool_processor.utils.logging import get_logger
|
|
10
|
+
|
|
11
|
+
# logger
|
|
12
|
+
logger = get_logger("chuk_tool_processor.plugins.function_call_tool")
|
|
13
|
+
|
|
14
|
+
class FunctionCallPlugin:
|
|
15
|
+
"""
|
|
16
|
+
Parse OpenAI-style `function_call` payloads embedded in the LLM response.
|
|
17
|
+
|
|
18
|
+
Supports two formats:
|
|
19
|
+
1. JSON object with function_call field:
|
|
20
|
+
{
|
|
21
|
+
"function_call": {
|
|
22
|
+
"name": "my_tool",
|
|
23
|
+
"arguments": '{"x":1,"y":"two"}'
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
2. JSON object with function_call field and already parsed arguments:
|
|
28
|
+
{
|
|
29
|
+
"function_call": {
|
|
30
|
+
"name": "my_tool",
|
|
31
|
+
"arguments": {"x":1, "y":"two"}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
"""
|
|
35
|
+
def try_parse(self, raw: str) -> List[ToolCall]:
|
|
36
|
+
calls: List[ToolCall] = []
|
|
37
|
+
|
|
38
|
+
# First, try to parse as a complete JSON object
|
|
39
|
+
try:
|
|
40
|
+
payload = json.loads(raw)
|
|
41
|
+
|
|
42
|
+
# Check if this is a function call payload
|
|
43
|
+
if isinstance(payload, dict) and "function_call" in payload:
|
|
44
|
+
fc = payload.get("function_call")
|
|
45
|
+
if not isinstance(fc, dict):
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
name = fc.get("name")
|
|
49
|
+
args = fc.get("arguments", {})
|
|
50
|
+
|
|
51
|
+
# Arguments sometimes come back as a JSON-encoded string
|
|
52
|
+
if isinstance(args, str):
|
|
53
|
+
try:
|
|
54
|
+
args = json.loads(args)
|
|
55
|
+
except json.JSONDecodeError:
|
|
56
|
+
# Leave as empty dict if malformed but still create the call
|
|
57
|
+
args = {}
|
|
58
|
+
|
|
59
|
+
# Only proceed if we have a valid name
|
|
60
|
+
if not isinstance(name, str) or not name:
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
call = ToolCall(tool=name, arguments=args if isinstance(args, Dict) else {})
|
|
65
|
+
calls.append(call)
|
|
66
|
+
logger.debug(f"Found function call to {name}")
|
|
67
|
+
except ValidationError:
|
|
68
|
+
# invalid tool name or args shape
|
|
69
|
+
logger.warning(f"Invalid function call: {name}")
|
|
70
|
+
|
|
71
|
+
# Look for nested function calls
|
|
72
|
+
if not calls:
|
|
73
|
+
# Try to find function calls in nested objects
|
|
74
|
+
json_str = json.dumps(payload)
|
|
75
|
+
json_pattern = r'\{(?:[^{}]|(?:\{[^{}]*\}))*\}'
|
|
76
|
+
matches = re.finditer(json_pattern, json_str)
|
|
77
|
+
|
|
78
|
+
for match in matches:
|
|
79
|
+
# Skip if it's the complete string we already parsed
|
|
80
|
+
json_substr = match.group(0)
|
|
81
|
+
if json_substr == json_str:
|
|
82
|
+
continue
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
nested_payload = json.loads(json_substr)
|
|
86
|
+
if isinstance(nested_payload, dict) and "function_call" in nested_payload:
|
|
87
|
+
nested_calls = self.try_parse(json_substr)
|
|
88
|
+
calls.extend(nested_calls)
|
|
89
|
+
except json.JSONDecodeError:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
except json.JSONDecodeError:
|
|
93
|
+
# If it's not valid JSON, try to extract function calls using regex
|
|
94
|
+
json_pattern = r'\{(?:[^{}]|(?:\{[^{}]*\}))*\}'
|
|
95
|
+
matches = re.finditer(json_pattern, raw)
|
|
96
|
+
|
|
97
|
+
for match in matches:
|
|
98
|
+
json_str = match.group(0)
|
|
99
|
+
try:
|
|
100
|
+
nested_calls = self.try_parse(json_str)
|
|
101
|
+
calls.extend(nested_calls)
|
|
102
|
+
except Exception:
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
return calls
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# chuk_tool_processor/plugins/json_tool.py
|
|
2
|
+
import json
|
|
3
|
+
from typing import List
|
|
4
|
+
from pydantic import ValidationError
|
|
5
|
+
|
|
6
|
+
# tool processor
|
|
7
|
+
from chuk_tool_processor.models.tool_call import ToolCall
|
|
8
|
+
|
|
9
|
+
class JsonToolPlugin:
|
|
10
|
+
"""Parse JSON-encoded `tool_calls` field."""
|
|
11
|
+
def try_parse(self, raw: str) -> List[ToolCall]:
|
|
12
|
+
try:
|
|
13
|
+
data = json.loads(raw)
|
|
14
|
+
calls = data.get('tool_calls', []) if isinstance(data, dict) else []
|
|
15
|
+
return [ToolCall(**c) for c in calls]
|
|
16
|
+
except (json.JSONDecodeError, ValidationError):
|
|
17
|
+
return []
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# chuk_tool_processor/plugins/xml_tool.py
|
|
2
|
+
import re
|
|
3
|
+
import json
|
|
4
|
+
from typing import List
|
|
5
|
+
from pydantic import ValidationError
|
|
6
|
+
|
|
7
|
+
# tool processor
|
|
8
|
+
from chuk_tool_processor.models.tool_call import ToolCall
|
|
9
|
+
|
|
10
|
+
class XmlToolPlugin:
|
|
11
|
+
"""
|
|
12
|
+
Parse XML-like `<tool name="..." args='{"x":1}'/>` constructs,
|
|
13
|
+
supporting both single- and double-quoted attributes.
|
|
14
|
+
"""
|
|
15
|
+
_pattern = re.compile(
|
|
16
|
+
r'<tool\s+'
|
|
17
|
+
r'name=(?P<q1>["\'])(?P<tool>.+?)(?P=q1)\s+'
|
|
18
|
+
r'args=(?P<q2>["\'])(?P<args>.*?)(?P=q2)\s*/>'
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
def try_parse(self, raw: str) -> List[ToolCall]:
|
|
22
|
+
calls: List[ToolCall] = []
|
|
23
|
+
for m in self._pattern.finditer(raw):
|
|
24
|
+
tool_name = m.group('tool')
|
|
25
|
+
raw_args = m.group('args')
|
|
26
|
+
# Decode the JSON payload in the args attribute
|
|
27
|
+
try:
|
|
28
|
+
args = json.loads(raw_args) if raw_args else {}
|
|
29
|
+
except (json.JSONDecodeError, ValidationError):
|
|
30
|
+
args = {}
|
|
31
|
+
|
|
32
|
+
# Validate & construct the ToolCall
|
|
33
|
+
try:
|
|
34
|
+
call = ToolCall(tool=tool_name, arguments=args)
|
|
35
|
+
calls.append(call)
|
|
36
|
+
except ValidationError:
|
|
37
|
+
# Skip malformed calls
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
return calls
|
|
41
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tool registry package for managing and accessing tool implementations.
|
|
3
|
+
"""
|
|
4
|
+
from chuk_tool_processor.registry.interface import ToolRegistryInterface
|
|
5
|
+
from chuk_tool_processor.registry.metadata import ToolMetadata
|
|
6
|
+
from chuk_tool_processor.registry.provider import ToolRegistryProvider
|
|
7
|
+
from chuk_tool_processor.registry.decorators import register_tool
|
|
8
|
+
from chuk_tool_processor.registry.provider import get_registry
|
|
9
|
+
|
|
10
|
+
# Create and expose the default registry
|
|
11
|
+
default_registry = get_registry()
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
'ToolRegistryInterface',
|
|
15
|
+
'ToolMetadata',
|
|
16
|
+
'ToolRegistryProvider',
|
|
17
|
+
'register_tool',
|
|
18
|
+
'default_registry',
|
|
19
|
+
'get_registry',
|
|
20
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# chuk_tool_processor/registry/decorators.py
|
|
2
|
+
"""
|
|
3
|
+
Decorators for registering tools with the registry.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from typing import Any, Callable, Dict, Optional, Type, TypeVar
|
|
8
|
+
|
|
9
|
+
from chuk_tool_processor.registry.provider import ToolRegistryProvider
|
|
10
|
+
|
|
11
|
+
T = TypeVar('T')
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register_tool(name: Optional[str] = None, namespace: str = "default", **metadata):
|
|
15
|
+
"""
|
|
16
|
+
Decorator for registering tools with the global registry.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
@register_tool(name="my_tool", namespace="math", description="Performs math operations")
|
|
20
|
+
class MyTool:
|
|
21
|
+
def execute(self, x: int, y: int) -> int:
|
|
22
|
+
return x + y
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
name: Optional explicit name; if omitted, uses class.__name__.
|
|
26
|
+
namespace: Namespace for the tool (default: "default").
|
|
27
|
+
**metadata: Additional metadata for the tool.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
A decorator function that registers the class with the registry.
|
|
31
|
+
"""
|
|
32
|
+
def decorator(cls: Type[T]) -> Type[T]:
|
|
33
|
+
registry = ToolRegistryProvider.get_registry()
|
|
34
|
+
registry.register_tool(cls, name=name, namespace=namespace, metadata=metadata)
|
|
35
|
+
|
|
36
|
+
@wraps(cls)
|
|
37
|
+
def wrapper(*args: Any, **kwargs: Dict[str, Any]) -> T:
|
|
38
|
+
return cls(*args, **kwargs)
|
|
39
|
+
|
|
40
|
+
return wrapper
|
|
41
|
+
|
|
42
|
+
return decorator
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# chuk_tool_processor/registry/interface.py
|
|
2
|
+
"""
|
|
3
|
+
Defines the interface for tool registries.
|
|
4
|
+
"""
|
|
5
|
+
from typing import Protocol, Any, Dict, List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
# imports
|
|
8
|
+
from chuk_tool_processor.registry.metadata import ToolMetadata
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ToolRegistryInterface(Protocol):
|
|
12
|
+
"""
|
|
13
|
+
Protocol for a tool registry. Implementations should allow registering tools
|
|
14
|
+
and retrieving them by name and namespace.
|
|
15
|
+
"""
|
|
16
|
+
def register_tool(
|
|
17
|
+
self,
|
|
18
|
+
tool: Any,
|
|
19
|
+
name: Optional[str] = None,
|
|
20
|
+
namespace: str = "default",
|
|
21
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
22
|
+
) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Register a tool implementation.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
tool: The tool class or instance with an `execute` method.
|
|
28
|
+
name: Optional explicit name; if omitted, uses tool.__name__.
|
|
29
|
+
namespace: Namespace for the tool (default: "default").
|
|
30
|
+
metadata: Optional additional metadata for the tool.
|
|
31
|
+
"""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
def get_tool(self, name: str, namespace: str = "default") -> Optional[Any]:
|
|
35
|
+
"""
|
|
36
|
+
Retrieve a registered tool by name and namespace.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
name: The name of the tool.
|
|
40
|
+
namespace: The namespace of the tool (default: "default").
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The tool implementation or None if not found.
|
|
44
|
+
"""
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
def get_metadata(self, name: str, namespace: str = "default") -> Optional[ToolMetadata]:
|
|
48
|
+
"""
|
|
49
|
+
Retrieve metadata for a registered tool.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
name: The name of the tool.
|
|
53
|
+
namespace: The namespace of the tool (default: "default").
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
ToolMetadata if found, None otherwise.
|
|
57
|
+
"""
|
|
58
|
+
...
|
|
59
|
+
|
|
60
|
+
def list_tools(self, namespace: Optional[str] = None) -> List[Tuple[str, str]]:
|
|
61
|
+
"""
|
|
62
|
+
List all registered tool names, optionally filtered by namespace.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
namespace: Optional namespace filter.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
List of (namespace, name) tuples.
|
|
69
|
+
"""
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
def list_namespaces(self) -> List[str]:
|
|
73
|
+
"""
|
|
74
|
+
List all registered namespaces.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
List of namespace names.
|
|
78
|
+
"""
|
|
79
|
+
...
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# chuk_tool_processor/registry/metadata.py
|
|
2
|
+
"""
|
|
3
|
+
Tool metadata models for the registry.
|
|
4
|
+
"""
|
|
5
|
+
from typing import Any, Dict, Optional, Set
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ToolMetadata(BaseModel):
|
|
10
|
+
"""
|
|
11
|
+
Metadata for registered tools.
|
|
12
|
+
|
|
13
|
+
Attributes:
|
|
14
|
+
name: The name of the tool.
|
|
15
|
+
namespace: The namespace the tool belongs to.
|
|
16
|
+
description: Optional description of the tool's functionality.
|
|
17
|
+
version: Version of the tool implementation.
|
|
18
|
+
is_async: Whether the tool's execute method is asynchronous.
|
|
19
|
+
argument_schema: Optional schema for the tool's arguments.
|
|
20
|
+
result_schema: Optional schema for the tool's result.
|
|
21
|
+
requires_auth: Whether the tool requires authentication.
|
|
22
|
+
tags: Set of tags associated with the tool.
|
|
23
|
+
"""
|
|
24
|
+
name: str = Field(..., description="Tool name")
|
|
25
|
+
namespace: str = Field("default", description="Namespace the tool belongs to")
|
|
26
|
+
description: Optional[str] = Field(None, description="Tool description")
|
|
27
|
+
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")
|
|
29
|
+
argument_schema: Optional[Dict[str, Any]] = Field(None, description="Schema for the tool's arguments")
|
|
30
|
+
result_schema: Optional[Dict[str, Any]] = Field(None, description="Schema for the tool's result")
|
|
31
|
+
requires_auth: bool = Field(False, description="Whether the tool requires authentication")
|
|
32
|
+
tags: Set[str] = Field(default_factory=set, description="Tags associated with the tool")
|
|
33
|
+
|
|
34
|
+
def __str__(self) -> str:
|
|
35
|
+
"""String representation of the tool metadata."""
|
|
36
|
+
return f"{self.namespace}.{self.name} (v{self.version})"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# chuk_tool_processor/registry/provider.py
|
|
2
|
+
"""
|
|
3
|
+
Registry provider that maintains a global tool registry.
|
|
4
|
+
"""
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
# imports
|
|
8
|
+
from chuk_tool_processor.registry.interface import ToolRegistryInterface
|
|
9
|
+
from chuk_tool_processor.registry.providers import get_registry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ToolRegistryProvider:
|
|
13
|
+
"""
|
|
14
|
+
Global provider for a ToolRegistryInterface implementation.
|
|
15
|
+
Use `set_registry` to override (e.g., for testing).
|
|
16
|
+
|
|
17
|
+
This class provides a singleton-like access to a registry implementation,
|
|
18
|
+
allowing components throughout the application to access the same registry
|
|
19
|
+
without having to pass it explicitly.
|
|
20
|
+
"""
|
|
21
|
+
# Initialize with default registry
|
|
22
|
+
_registry: Optional[ToolRegistryInterface] = None
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def get_registry(cls) -> ToolRegistryInterface:
|
|
26
|
+
"""
|
|
27
|
+
Get the current registry instance.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The current registry instance.
|
|
31
|
+
"""
|
|
32
|
+
if cls._registry is None:
|
|
33
|
+
cls._registry = get_registry()
|
|
34
|
+
return cls._registry
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def set_registry(cls, registry: ToolRegistryInterface) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Set the global registry instance.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
registry: The registry instance to use.
|
|
43
|
+
"""
|
|
44
|
+
cls._registry = registry
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Registry provider implementations and factory functions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
from chuk_tool_processor.registry.interface import ToolRegistryInterface
|
|
9
|
+
from chuk_tool_processor.registry.providers.memory import InMemoryToolRegistry
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_registry(
|
|
13
|
+
provider_type: Optional[str] = None,
|
|
14
|
+
**kwargs
|
|
15
|
+
) -> ToolRegistryInterface:
|
|
16
|
+
"""
|
|
17
|
+
Factory function to get a registry implementation.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
provider_type: Type of registry provider to use. Options:
|
|
21
|
+
- "memory" (default): In-memory implementation
|
|
22
|
+
- "redis": Redis-backed implementation (if available)
|
|
23
|
+
- "sqlalchemy": Database-backed implementation (if available)
|
|
24
|
+
**kwargs: Additional configuration for the provider.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
A registry implementation.
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
ImportError: If the requested provider is not available.
|
|
31
|
+
ValueError: If the provider type is not recognized.
|
|
32
|
+
"""
|
|
33
|
+
# Use environment variable if not specified
|
|
34
|
+
if provider_type is None:
|
|
35
|
+
provider_type = os.environ.get("CHUK_TOOL_REGISTRY_PROVIDER", "memory")
|
|
36
|
+
|
|
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}")
|