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.
- chuk_tool_processor/core/__init__.py +32 -1
- chuk_tool_processor/core/exceptions.py +225 -13
- chuk_tool_processor/core/processor.py +135 -104
- chuk_tool_processor/execution/strategies/__init__.py +6 -0
- chuk_tool_processor/execution/strategies/inprocess_strategy.py +142 -150
- chuk_tool_processor/execution/strategies/subprocess_strategy.py +202 -206
- chuk_tool_processor/execution/tool_executor.py +82 -84
- chuk_tool_processor/execution/wrappers/__init__.py +42 -0
- chuk_tool_processor/execution/wrappers/caching.py +150 -116
- chuk_tool_processor/execution/wrappers/circuit_breaker.py +370 -0
- chuk_tool_processor/execution/wrappers/rate_limiting.py +76 -43
- chuk_tool_processor/execution/wrappers/retry.py +116 -78
- chuk_tool_processor/logging/__init__.py +23 -17
- chuk_tool_processor/logging/context.py +40 -45
- chuk_tool_processor/logging/formatter.py +22 -21
- chuk_tool_processor/logging/helpers.py +28 -42
- chuk_tool_processor/logging/metrics.py +13 -15
- chuk_tool_processor/mcp/__init__.py +8 -12
- chuk_tool_processor/mcp/mcp_tool.py +158 -114
- chuk_tool_processor/mcp/register_mcp_tools.py +22 -22
- chuk_tool_processor/mcp/setup_mcp_http_streamable.py +57 -17
- chuk_tool_processor/mcp/setup_mcp_sse.py +57 -17
- chuk_tool_processor/mcp/setup_mcp_stdio.py +11 -11
- chuk_tool_processor/mcp/stream_manager.py +333 -276
- chuk_tool_processor/mcp/transport/__init__.py +22 -29
- chuk_tool_processor/mcp/transport/base_transport.py +180 -44
- chuk_tool_processor/mcp/transport/http_streamable_transport.py +505 -325
- chuk_tool_processor/mcp/transport/models.py +100 -0
- chuk_tool_processor/mcp/transport/sse_transport.py +607 -276
- chuk_tool_processor/mcp/transport/stdio_transport.py +597 -116
- chuk_tool_processor/models/__init__.py +21 -1
- chuk_tool_processor/models/execution_strategy.py +16 -21
- chuk_tool_processor/models/streaming_tool.py +28 -25
- chuk_tool_processor/models/tool_call.py +49 -31
- chuk_tool_processor/models/tool_export_mixin.py +22 -8
- chuk_tool_processor/models/tool_result.py +40 -77
- chuk_tool_processor/models/tool_spec.py +350 -0
- chuk_tool_processor/models/validated_tool.py +36 -18
- chuk_tool_processor/observability/__init__.py +30 -0
- chuk_tool_processor/observability/metrics.py +312 -0
- chuk_tool_processor/observability/setup.py +105 -0
- chuk_tool_processor/observability/tracing.py +345 -0
- chuk_tool_processor/plugins/__init__.py +1 -1
- chuk_tool_processor/plugins/discovery.py +11 -11
- chuk_tool_processor/plugins/parsers/__init__.py +1 -1
- chuk_tool_processor/plugins/parsers/base.py +1 -2
- chuk_tool_processor/plugins/parsers/function_call_tool.py +13 -8
- chuk_tool_processor/plugins/parsers/json_tool.py +4 -3
- chuk_tool_processor/plugins/parsers/openai_tool.py +12 -7
- chuk_tool_processor/plugins/parsers/xml_tool.py +4 -4
- chuk_tool_processor/registry/__init__.py +12 -12
- chuk_tool_processor/registry/auto_register.py +22 -30
- chuk_tool_processor/registry/decorators.py +127 -129
- chuk_tool_processor/registry/interface.py +26 -23
- chuk_tool_processor/registry/metadata.py +27 -22
- chuk_tool_processor/registry/provider.py +17 -18
- chuk_tool_processor/registry/providers/__init__.py +16 -19
- chuk_tool_processor/registry/providers/memory.py +18 -25
- chuk_tool_processor/registry/tool_export.py +42 -51
- chuk_tool_processor/utils/validation.py +15 -16
- chuk_tool_processor-0.9.7.dist-info/METADATA +1813 -0
- chuk_tool_processor-0.9.7.dist-info/RECORD +67 -0
- chuk_tool_processor-0.6.4.dist-info/METADATA +0 -697
- chuk_tool_processor-0.6.4.dist-info/RECORD +0 -60
- {chuk_tool_processor-0.6.4.dist-info → chuk_tool_processor-0.9.7.dist-info}/WHEEL +0 -0
- {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:
|
|
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:
|
|
40
|
-
result_schema:
|
|
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:
|
|
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:
|
|
46
|
-
source_name:
|
|
47
|
-
concurrency_limit:
|
|
48
|
-
timeout:
|
|
49
|
-
rate_limit:
|
|
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:
|
|
54
|
-
dependencies:
|
|
55
|
-
|
|
56
|
-
@model_validator(mode=
|
|
57
|
-
def ensure_async(self) ->
|
|
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) ->
|
|
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:
|
|
82
|
-
content_type:
|
|
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
|
|
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:
|
|
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:
|
|
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]] =
|
|
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
|
-
|
|
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:
|
|
14
|
-
_REGISTRY_LOCKS:
|
|
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
|
|
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:
|
|
31
|
+
self._tools: dict[str, dict[str, Any]] = {}
|
|
31
32
|
# {namespace: {tool_name: ToolMetadata}}
|
|
32
|
-
self._metadata:
|
|
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:
|
|
44
|
+
name: str | None = None,
|
|
44
45
|
namespace: str = "default",
|
|
45
|
-
metadata:
|
|
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:
|
|
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") ->
|
|
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:
|
|
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:
|
|
106
|
+
result: list[tuple[str, str]] = []
|
|
114
107
|
for ns, tools in self._tools.items():
|
|
115
|
-
result.extend((ns, n) for n in tools
|
|
108
|
+
result.extend((ns, n) for n in tools)
|
|
116
109
|
return result
|
|
117
110
|
|
|
118
|
-
async def list_namespaces(self) ->
|
|
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:
|
|
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:
|
|
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
|
|
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:
|
|
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() ->
|
|
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:
|
|
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(
|
|
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
|
-
) ->
|
|
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:
|
|
172
|
-
schemas:
|
|
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
|
-
"
|
|
227
|
-
|
|
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
|
-
|
|
244
|
-
}
|
|
245
|
-
}
|
|
235
|
+
"components": {"schemas": schemas},
|
|
236
|
+
}
|