chuk-tool-processor 0.1.0__py3-none-any.whl → 0.1.1__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 +1 -1
- chuk_tool_processor/execution/strategies/inprocess_strategy.py +110 -148
- chuk_tool_processor/execution/strategies/subprocess_strategy.py +1 -1
- chuk_tool_processor/logging/__init__.py +35 -0
- chuk_tool_processor/logging/context.py +47 -0
- chuk_tool_processor/logging/formatter.py +55 -0
- chuk_tool_processor/logging/helpers.py +112 -0
- chuk_tool_processor/logging/metrics.py +59 -0
- chuk_tool_processor/models/execution_strategy.py +1 -1
- chuk_tool_processor/models/tool_export_mixin.py +29 -0
- chuk_tool_processor/models/validated_tool.py +155 -0
- chuk_tool_processor/plugins/discovery.py +105 -172
- chuk_tool_processor/plugins/parsers/__init__.py +1 -1
- chuk_tool_processor/plugins/parsers/base.py +18 -0
- chuk_tool_processor/plugins/parsers/function_call_tool_plugin.py +81 -0
- chuk_tool_processor/plugins/parsers/json_tool_plugin.py +38 -0
- chuk_tool_processor/plugins/parsers/openai_tool_plugin.py +76 -0
- chuk_tool_processor/plugins/parsers/xml_tool.py +28 -24
- chuk_tool_processor/registry/__init__.py +11 -10
- chuk_tool_processor/registry/auto_register.py +125 -0
- chuk_tool_processor/registry/provider.py +84 -29
- chuk_tool_processor/registry/providers/memory.py +77 -112
- chuk_tool_processor/registry/tool_export.py +76 -0
- chuk_tool_processor/utils/validation.py +106 -177
- {chuk_tool_processor-0.1.0.dist-info → chuk_tool_processor-0.1.1.dist-info}/METADATA +5 -2
- chuk_tool_processor-0.1.1.dist-info/RECORD +47 -0
- chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -105
- chuk_tool_processor/plugins/parsers/json_tool.py +0 -17
- chuk_tool_processor/utils/logging.py +0 -260
- chuk_tool_processor-0.1.0.dist-info/RECORD +0 -37
- {chuk_tool_processor-0.1.0.dist-info → chuk_tool_processor-0.1.1.dist-info}/WHEEL +0 -0
- {chuk_tool_processor-0.1.0.dist-info → chuk_tool_processor-0.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# chuk_tool_processor/parsers/openai_tool_plugin.py
|
|
2
|
+
"""
|
|
3
|
+
Parser for OpenAI Chat-Completions responses that contain a `tool_calls` array.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any, List
|
|
10
|
+
|
|
11
|
+
from pydantic import ValidationError
|
|
12
|
+
|
|
13
|
+
from .base import ParserPlugin
|
|
14
|
+
from chuk_tool_processor.models.tool_call import ToolCall
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class OpenAIToolPlugin(ParserPlugin):
|
|
18
|
+
"""
|
|
19
|
+
Understands responses that look like:
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
"tool_calls": [
|
|
23
|
+
{
|
|
24
|
+
"id": "call_abc",
|
|
25
|
+
"type": "function",
|
|
26
|
+
"function": {
|
|
27
|
+
"name": "weather",
|
|
28
|
+
"arguments": "{\"location\": \"New York\"}"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
…
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def try_parse(self, raw: str | Any) -> List[ToolCall]:
|
|
37
|
+
# ------------------------------------------------------------------ #
|
|
38
|
+
# Parse the incoming JSON (string or already-dict)
|
|
39
|
+
# ------------------------------------------------------------------ #
|
|
40
|
+
try:
|
|
41
|
+
data = json.loads(raw) if isinstance(raw, str) else raw
|
|
42
|
+
except (TypeError, json.JSONDecodeError):
|
|
43
|
+
return []
|
|
44
|
+
|
|
45
|
+
if not isinstance(data, dict) or "tool_calls" not in data:
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
# ------------------------------------------------------------------ #
|
|
49
|
+
# Convert each entry into a ToolCall
|
|
50
|
+
# ------------------------------------------------------------------ #
|
|
51
|
+
calls: List[ToolCall] = []
|
|
52
|
+
for tc in data["tool_calls"]:
|
|
53
|
+
fn = tc.get("function", {})
|
|
54
|
+
name = fn.get("name")
|
|
55
|
+
args = fn.get("arguments", {})
|
|
56
|
+
|
|
57
|
+
if not isinstance(name, str) or not name:
|
|
58
|
+
continue
|
|
59
|
+
|
|
60
|
+
# arguments come back as a JSON-encoded string – decode if needed
|
|
61
|
+
if isinstance(args, str):
|
|
62
|
+
try:
|
|
63
|
+
args = json.loads(args)
|
|
64
|
+
except json.JSONDecodeError:
|
|
65
|
+
args = {}
|
|
66
|
+
|
|
67
|
+
if not isinstance(args, dict):
|
|
68
|
+
args = {}
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
# `tool` must match the **registry key** (e.g. "weather")
|
|
72
|
+
calls.append(ToolCall(tool=name, arguments=args))
|
|
73
|
+
except ValidationError:
|
|
74
|
+
continue # skip malformed entries
|
|
75
|
+
|
|
76
|
+
return calls
|
|
@@ -1,41 +1,45 @@
|
|
|
1
1
|
# chuk_tool_processor/plugins/xml_tool.py
|
|
2
|
-
|
|
2
|
+
"""XML tool-call parser plugin.
|
|
3
|
+
Understands `<tool name="..." args='{"x":1}'/>` single-line constructs –
|
|
4
|
+
format used by many examples and test-fixtures.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
3
8
|
import json
|
|
9
|
+
import re
|
|
4
10
|
from typing import List
|
|
5
11
|
from pydantic import ValidationError
|
|
6
12
|
|
|
7
|
-
#
|
|
13
|
+
# imports
|
|
14
|
+
from .base import ParserPlugin
|
|
8
15
|
from chuk_tool_processor.models.tool_call import ToolCall
|
|
9
16
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
r
|
|
17
|
-
r
|
|
18
|
-
r
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class XmlToolPlugin(ParserPlugin):
|
|
20
|
+
"""Parse XML-like tool-call tags."""
|
|
21
|
+
|
|
22
|
+
_TAG = re.compile(
|
|
23
|
+
r"<tool\s+"
|
|
24
|
+
r"name=(?P<q1>[\"\'])(?P<tool>.+?)(?P=q1)\s+"
|
|
25
|
+
r"args=(?P<q2>[\"\'])(?P<args>.*?)(?P=q2)\s*/>"
|
|
19
26
|
)
|
|
20
27
|
|
|
21
|
-
def try_parse(self, raw:
|
|
28
|
+
def try_parse(self, raw): # type: ignore[override]
|
|
29
|
+
if not isinstance(raw, str): # XML form only exists in strings
|
|
30
|
+
return []
|
|
31
|
+
|
|
22
32
|
calls: List[ToolCall] = []
|
|
23
|
-
for m in self.
|
|
24
|
-
tool_name = m.group(
|
|
25
|
-
raw_args = m.group(
|
|
26
|
-
# Decode the JSON payload in the args attribute
|
|
33
|
+
for m in self._TAG.finditer(raw):
|
|
34
|
+
tool_name = m.group("tool")
|
|
35
|
+
raw_args = m.group("args")
|
|
27
36
|
try:
|
|
28
37
|
args = json.loads(raw_args) if raw_args else {}
|
|
29
|
-
except
|
|
38
|
+
except json.JSONDecodeError:
|
|
30
39
|
args = {}
|
|
31
40
|
|
|
32
|
-
# Validate & construct the ToolCall
|
|
33
41
|
try:
|
|
34
|
-
|
|
35
|
-
calls.append(call)
|
|
42
|
+
calls.append(ToolCall(tool=tool_name, arguments=args))
|
|
36
43
|
except ValidationError:
|
|
37
|
-
#
|
|
38
|
-
continue
|
|
39
|
-
|
|
44
|
+
continue # skip malformed
|
|
40
45
|
return calls
|
|
41
|
-
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Tool registry package for managing and accessing tool implementations.
|
|
3
3
|
"""
|
|
4
|
+
|
|
4
5
|
from chuk_tool_processor.registry.interface import ToolRegistryInterface
|
|
5
6
|
from chuk_tool_processor.registry.metadata import ToolMetadata
|
|
6
7
|
from chuk_tool_processor.registry.provider import ToolRegistryProvider
|
|
7
8
|
from chuk_tool_processor.registry.decorators import register_tool
|
|
8
|
-
from chuk_tool_processor.registry.provider import get_registry
|
|
9
9
|
|
|
10
|
-
#
|
|
11
|
-
|
|
10
|
+
# --------------------------------------------------------------------------- #
|
|
11
|
+
# Expose the *singleton* registry that every part of the library should use
|
|
12
|
+
# --------------------------------------------------------------------------- #
|
|
13
|
+
default_registry = ToolRegistryProvider.get_registry()
|
|
12
14
|
|
|
13
15
|
__all__ = [
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
]
|
|
16
|
+
"ToolRegistryInterface",
|
|
17
|
+
"ToolMetadata",
|
|
18
|
+
"ToolRegistryProvider",
|
|
19
|
+
"register_tool",
|
|
20
|
+
"default_registry",
|
|
21
|
+
]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# chuk_tool_processor/registry/auto_register.py
|
|
2
|
+
"""
|
|
3
|
+
Tiny “auto-register” helpers so you can do
|
|
4
|
+
|
|
5
|
+
register_fn_tool(my_function)
|
|
6
|
+
register_langchain_tool(my_langchain_tool)
|
|
7
|
+
|
|
8
|
+
and they immediately show up in the global registry.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import asyncio
|
|
14
|
+
import inspect
|
|
15
|
+
import types
|
|
16
|
+
from typing import Callable, ForwardRef, Type, get_type_hints
|
|
17
|
+
|
|
18
|
+
import anyio
|
|
19
|
+
from pydantic import BaseModel, create_model
|
|
20
|
+
|
|
21
|
+
try: # optional dependency
|
|
22
|
+
from langchain.tools.base import BaseTool # type: ignore
|
|
23
|
+
except ModuleNotFoundError: # pragma: no cover
|
|
24
|
+
BaseTool = None # noqa: N816 – keep the name for isinstance() checks
|
|
25
|
+
|
|
26
|
+
from chuk_tool_processor.registry.decorators import register_tool
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
# internals – build a Pydantic schema from an arbitrary callable
|
|
31
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _auto_schema(func: Callable) -> Type[BaseModel]:
|
|
35
|
+
"""
|
|
36
|
+
Turn a function signature into a `pydantic.BaseModel` subclass.
|
|
37
|
+
|
|
38
|
+
*Unknown* or *un-imported* annotations (common with third-party libs that
|
|
39
|
+
use forward-refs without importing the target – e.g. ``uuid.UUID`` in
|
|
40
|
+
LangChain’s `CallbackManagerForToolRun`) default to ``str`` instead of
|
|
41
|
+
crashing `get_type_hints()`.
|
|
42
|
+
"""
|
|
43
|
+
try:
|
|
44
|
+
hints = get_type_hints(func)
|
|
45
|
+
except Exception:
|
|
46
|
+
hints = {}
|
|
47
|
+
|
|
48
|
+
fields: dict[str, tuple[type, object]] = {}
|
|
49
|
+
for param in inspect.signature(func).parameters.values():
|
|
50
|
+
raw_hint = hints.get(param.name, param.annotation)
|
|
51
|
+
# Default to ``str`` for ForwardRef / string annotations or if we
|
|
52
|
+
# couldn’t resolve the type.
|
|
53
|
+
hint: type = (
|
|
54
|
+
raw_hint
|
|
55
|
+
if raw_hint not in (inspect._empty, None, str)
|
|
56
|
+
and not isinstance(raw_hint, (str, ForwardRef))
|
|
57
|
+
else str
|
|
58
|
+
)
|
|
59
|
+
fields[param.name] = (hint, ...) # “...” → required
|
|
60
|
+
|
|
61
|
+
return create_model(f"{func.__name__.title()}Args", **fields) # type: ignore
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
65
|
+
# 1️⃣ plain Python function (sync **or** async)
|
|
66
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def register_fn_tool(
|
|
70
|
+
func: Callable,
|
|
71
|
+
*,
|
|
72
|
+
name: str | None = None,
|
|
73
|
+
description: str | None = None,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Register a plain function as a tool – one line is all you need."""
|
|
76
|
+
|
|
77
|
+
schema = _auto_schema(func)
|
|
78
|
+
name = name or func.__name__
|
|
79
|
+
description = (description or func.__doc__ or "").strip()
|
|
80
|
+
|
|
81
|
+
@register_tool(name=name, description=description, arg_schema=schema)
|
|
82
|
+
class _Tool: # noqa: D401, N801 – internal auto-wrapper
|
|
83
|
+
async def _execute(self, **kwargs):
|
|
84
|
+
if inspect.iscoroutinefunction(func):
|
|
85
|
+
return await func(**kwargs)
|
|
86
|
+
# off-load blocking sync work
|
|
87
|
+
return await anyio.to_thread.run_sync(func, **kwargs)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
91
|
+
# 2️⃣ LangChain BaseTool (or anything that quacks like it)
|
|
92
|
+
# ────────────────────────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def register_langchain_tool(
|
|
96
|
+
tool,
|
|
97
|
+
*,
|
|
98
|
+
name: str | None = None,
|
|
99
|
+
description: str | None = None,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""
|
|
102
|
+
Register a **LangChain** `BaseTool` instance (or anything exposing
|
|
103
|
+
``.run`` / ``.arun``).
|
|
104
|
+
|
|
105
|
+
If LangChain isn’t installed you’ll get a clear error instead of an import
|
|
106
|
+
failure deep in the stack.
|
|
107
|
+
"""
|
|
108
|
+
if BaseTool is None:
|
|
109
|
+
raise RuntimeError(
|
|
110
|
+
"register_langchain_tool() requires LangChain - "
|
|
111
|
+
"install with `pip install langchain`"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if not isinstance(tool, BaseTool): # pragma: no cover
|
|
115
|
+
raise TypeError(
|
|
116
|
+
"Expected a langchain.tools.base.BaseTool instance – got "
|
|
117
|
+
f"{type(tool).__name__}"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
fn = tool.arun if hasattr(tool, "arun") else tool.run # prefer async
|
|
121
|
+
register_fn_tool(
|
|
122
|
+
fn,
|
|
123
|
+
name=name or tool.name or tool.__class__.__name__,
|
|
124
|
+
description=description or tool.description or (tool.__doc__ or ""),
|
|
125
|
+
)
|
|
@@ -1,44 +1,99 @@
|
|
|
1
|
-
# chuk_tool_processor/registry/provider.py
|
|
2
1
|
"""
|
|
3
|
-
|
|
2
|
+
Global access to *the* tool registry instance.
|
|
3
|
+
|
|
4
|
+
There are two public faces:
|
|
5
|
+
|
|
6
|
+
1. **Module helpers**
|
|
7
|
+
• `get_registry()` lazily instantiates a default `InMemoryToolRegistry`
|
|
8
|
+
and memoises it in the module-level variable ``_REGISTRY``.
|
|
9
|
+
• `set_registry()` lets callers replace or reset that singleton.
|
|
10
|
+
|
|
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**.
|
|
17
|
+
|
|
18
|
+
The contract verified by the test-suite is:
|
|
19
|
+
|
|
20
|
+
* 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.
|
|
4
24
|
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
5
27
|
from typing import Optional
|
|
6
28
|
|
|
7
|
-
|
|
8
|
-
from
|
|
9
|
-
|
|
29
|
+
from .interface import ToolRegistryInterface
|
|
30
|
+
from .providers.memory import InMemoryToolRegistry
|
|
31
|
+
|
|
32
|
+
# --------------------------------------------------------------------------- #
|
|
33
|
+
# Module-level singleton used by the helper functions
|
|
34
|
+
# --------------------------------------------------------------------------- #
|
|
35
|
+
_REGISTRY: Optional[ToolRegistryInterface] = None
|
|
36
|
+
# --------------------------------------------------------------------------- #
|
|
37
|
+
|
|
10
38
|
|
|
39
|
+
def _default_registry() -> ToolRegistryInterface:
|
|
40
|
+
"""Create the default in-memory registry."""
|
|
41
|
+
return InMemoryToolRegistry()
|
|
11
42
|
|
|
12
|
-
|
|
43
|
+
|
|
44
|
+
def get_registry() -> ToolRegistryInterface:
|
|
13
45
|
"""
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
allowing components throughout the application to access the same registry
|
|
19
|
-
without having to pass it explicitly.
|
|
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.
|
|
20
50
|
"""
|
|
21
|
-
|
|
51
|
+
global _REGISTRY
|
|
52
|
+
if _REGISTRY is None:
|
|
53
|
+
_REGISTRY = _default_registry()
|
|
54
|
+
return _REGISTRY
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def set_registry(registry: ToolRegistryInterface | None) -> None:
|
|
58
|
+
"""
|
|
59
|
+
Replace or clear the global registry.
|
|
60
|
+
|
|
61
|
+
Passing ``None`` resets the singleton so that the next `get_registry()`
|
|
62
|
+
call recreates it (useful in tests).
|
|
63
|
+
"""
|
|
64
|
+
global _REGISTRY
|
|
65
|
+
_REGISTRY = registry
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# --------------------------------------------------------------------------- #
|
|
69
|
+
# Back-compat shim used by legacy import paths and the test-suite
|
|
70
|
+
# --------------------------------------------------------------------------- #
|
|
71
|
+
class ToolRegistryProvider: # noqa: D401
|
|
72
|
+
"""Legacy static wrapper retaining historical semantics."""
|
|
73
|
+
|
|
74
|
+
# The test-suite directly mutates this attribute, so we keep it.
|
|
22
75
|
_registry: Optional[ToolRegistryInterface] = None
|
|
23
76
|
|
|
24
|
-
|
|
25
|
-
|
|
77
|
+
# ------------------------ public API ------------------------ #
|
|
78
|
+
@staticmethod
|
|
79
|
+
def get_registry() -> ToolRegistryInterface:
|
|
26
80
|
"""
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
Returns:
|
|
30
|
-
The current registry instance.
|
|
81
|
+
Return the cached instance or, if absent, call the *current*
|
|
82
|
+
module-level `get_registry()` exactly once to populate it.
|
|
31
83
|
"""
|
|
32
|
-
if
|
|
33
|
-
|
|
34
|
-
|
|
84
|
+
if ToolRegistryProvider._registry is None:
|
|
85
|
+
# Honour any runtime monkey-patching of the factory.
|
|
86
|
+
ToolRegistryProvider._registry = globals()["get_registry"]()
|
|
87
|
+
return ToolRegistryProvider._registry
|
|
35
88
|
|
|
36
|
-
@
|
|
37
|
-
def set_registry(
|
|
89
|
+
@staticmethod
|
|
90
|
+
def set_registry(registry: ToolRegistryInterface | None) -> None:
|
|
38
91
|
"""
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
92
|
+
Override the cached registry.
|
|
93
|
+
|
|
94
|
+
* If ``registry`` is an object, all subsequent `get_registry()`
|
|
95
|
+
calls return it without touching the factory.
|
|
96
|
+
* If ``registry`` is ``None``, the cache is cleared so the next
|
|
97
|
+
`get_registry()` call invokes the (possibly patched) factory.
|
|
43
98
|
"""
|
|
44
|
-
|
|
99
|
+
ToolRegistryProvider._registry = registry
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
# chuk_tool_processor/registry/providers/memory.py
|
|
2
|
+
# chuk_tool_processor/registry/providers/memory.py
|
|
2
3
|
"""
|
|
3
4
|
In-memory implementation of the tool registry.
|
|
4
5
|
"""
|
|
5
6
|
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
6
9
|
import inspect
|
|
7
10
|
from typing import Any, Dict, List, Optional, Tuple
|
|
8
11
|
|
|
@@ -14,152 +17,114 @@ from chuk_tool_processor.registry.metadata import ToolMetadata
|
|
|
14
17
|
class InMemoryToolRegistry(ToolRegistryInterface):
|
|
15
18
|
"""
|
|
16
19
|
In-memory implementation of ToolRegistryInterface with namespace support.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
organized by namespace. It's suitable for single-process applications
|
|
20
|
-
or for testing, but doesn't provide persistence or sharing across
|
|
21
|
-
multiple processes.
|
|
20
|
+
|
|
21
|
+
Suitable for single-process apps or tests; not persisted across processes.
|
|
22
22
|
"""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
|
|
24
|
+
# ------------------------------------------------------------------ #
|
|
25
|
+
# construction
|
|
26
|
+
# ------------------------------------------------------------------ #
|
|
27
|
+
|
|
28
|
+
def __init__(self) -> None:
|
|
29
|
+
# {namespace: {tool_name: tool_obj}}
|
|
26
30
|
self._tools: Dict[str, Dict[str, Any]] = {}
|
|
27
|
-
#
|
|
31
|
+
# {namespace: {tool_name: ToolMetadata}}
|
|
28
32
|
self._metadata: Dict[str, Dict[str, ToolMetadata]] = {}
|
|
29
33
|
|
|
34
|
+
# ------------------------------------------------------------------ #
|
|
35
|
+
# registration
|
|
36
|
+
# ------------------------------------------------------------------ #
|
|
37
|
+
|
|
30
38
|
def register_tool(
|
|
31
|
-
self,
|
|
32
|
-
tool: Any,
|
|
39
|
+
self,
|
|
40
|
+
tool: Any,
|
|
33
41
|
name: Optional[str] = None,
|
|
34
42
|
namespace: str = "default",
|
|
35
|
-
metadata: Optional[Dict[str, Any]] = None
|
|
43
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
36
44
|
) -> None:
|
|
37
|
-
|
|
38
|
-
|
|
45
|
+
# ensure namespace buckets
|
|
46
|
+
self._tools.setdefault(namespace, {})
|
|
47
|
+
self._metadata.setdefault(namespace, {})
|
|
39
48
|
|
|
40
|
-
Args:
|
|
41
|
-
tool: The tool class or instance with an `execute` method.
|
|
42
|
-
name: Optional explicit name; if omitted, uses tool.__name__.
|
|
43
|
-
namespace: Namespace for the tool (default: "default").
|
|
44
|
-
metadata: Optional additional metadata for the tool.
|
|
45
|
-
"""
|
|
46
|
-
# Ensure the namespace exists
|
|
47
|
-
if namespace not in self._tools:
|
|
48
|
-
self._tools[namespace] = {}
|
|
49
|
-
self._metadata[namespace] = {}
|
|
50
|
-
|
|
51
|
-
# Determine tool name
|
|
52
49
|
key = name or getattr(tool, "__name__", None) or repr(tool)
|
|
53
|
-
|
|
54
|
-
# Register the tool
|
|
55
50
|
self._tools[namespace][key] = tool
|
|
56
|
-
|
|
57
|
-
#
|
|
51
|
+
|
|
52
|
+
# build metadata -------------------------------------------------
|
|
58
53
|
is_async = inspect.iscoroutinefunction(getattr(tool, "execute", None))
|
|
59
|
-
|
|
60
|
-
#
|
|
61
|
-
description =
|
|
62
|
-
|
|
63
|
-
description
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
54
|
+
|
|
55
|
+
# default description -> docstring
|
|
56
|
+
description = (
|
|
57
|
+
(inspect.getdoc(tool) or "").strip()
|
|
58
|
+
if not (metadata and "description" in metadata)
|
|
59
|
+
else None
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
meta_dict: Dict[str, Any] = {
|
|
67
63
|
"name": key,
|
|
68
64
|
"namespace": namespace,
|
|
69
|
-
"is_async": is_async
|
|
65
|
+
"is_async": is_async,
|
|
70
66
|
}
|
|
71
|
-
|
|
72
|
-
# Add description if available (but don't override metadata if provided)
|
|
73
|
-
if description and not (metadata and "description" in metadata):
|
|
67
|
+
if description:
|
|
74
68
|
meta_dict["description"] = description
|
|
75
|
-
|
|
76
|
-
# Add any additional metadata
|
|
77
69
|
if metadata:
|
|
78
70
|
meta_dict.update(metadata)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
|
|
72
|
+
self._metadata[namespace][key] = ToolMetadata(**meta_dict)
|
|
73
|
+
|
|
74
|
+
# ------------------------------------------------------------------ #
|
|
75
|
+
# retrieval
|
|
76
|
+
# ------------------------------------------------------------------ #
|
|
83
77
|
|
|
84
78
|
def get_tool(self, name: str, namespace: str = "default") -> Optional[Any]:
|
|
85
|
-
|
|
86
|
-
Retrieve a registered tool by name and namespace.
|
|
87
|
-
|
|
88
|
-
Args:
|
|
89
|
-
name: The name of the tool.
|
|
90
|
-
namespace: The namespace of the tool (default: "default").
|
|
91
|
-
|
|
92
|
-
Returns:
|
|
93
|
-
The tool implementation or None if not found.
|
|
94
|
-
"""
|
|
95
|
-
if namespace not in self._tools:
|
|
96
|
-
return None
|
|
97
|
-
return self._tools[namespace].get(name)
|
|
79
|
+
return self._tools.get(namespace, {}).get(name)
|
|
98
80
|
|
|
99
81
|
def get_tool_strict(self, name: str, namespace: str = "default") -> Any:
|
|
100
|
-
"""
|
|
101
|
-
Retrieve a registered tool by name and namespace, raising an exception if not found.
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
name: The name of the tool.
|
|
105
|
-
namespace: The namespace of the tool (default: "default").
|
|
106
|
-
|
|
107
|
-
Returns:
|
|
108
|
-
The tool implementation.
|
|
109
|
-
|
|
110
|
-
Raises:
|
|
111
|
-
ToolNotFoundError: If the tool is not found.
|
|
112
|
-
"""
|
|
113
82
|
tool = self.get_tool(name, namespace)
|
|
114
83
|
if tool is None:
|
|
115
84
|
raise ToolNotFoundError(f"{namespace}.{name}")
|
|
116
85
|
return tool
|
|
117
86
|
|
|
118
|
-
def get_metadata(
|
|
119
|
-
""
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
Returns:
|
|
127
|
-
ToolMetadata if found, None otherwise.
|
|
128
|
-
"""
|
|
129
|
-
if namespace not in self._metadata:
|
|
130
|
-
return None
|
|
131
|
-
return self._metadata[namespace].get(name)
|
|
87
|
+
def get_metadata(
|
|
88
|
+
self, name: str, namespace: str = "default"
|
|
89
|
+
) -> Optional[ToolMetadata]:
|
|
90
|
+
return self._metadata.get(namespace, {}).get(name)
|
|
91
|
+
|
|
92
|
+
# ------------------------------------------------------------------ #
|
|
93
|
+
# listing helpers
|
|
94
|
+
# ------------------------------------------------------------------ #
|
|
132
95
|
|
|
133
96
|
def list_tools(self, namespace: Optional[str] = None) -> List[Tuple[str, str]]:
|
|
134
97
|
"""
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
Args:
|
|
138
|
-
namespace: Optional namespace filter.
|
|
139
|
-
|
|
140
|
-
Returns:
|
|
141
|
-
List of (namespace, name) tuples.
|
|
98
|
+
Return a list of ``(namespace, name)`` tuples.
|
|
142
99
|
"""
|
|
143
|
-
result = []
|
|
144
|
-
|
|
145
100
|
if namespace:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
for name in tools.keys():
|
|
154
|
-
result.append((ns, name))
|
|
155
|
-
|
|
101
|
+
return [
|
|
102
|
+
(namespace, n) for n in self._tools.get(namespace, {}).keys()
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
result: List[Tuple[str, str]] = []
|
|
106
|
+
for ns, tools in self._tools.items():
|
|
107
|
+
result.extend((ns, n) for n in tools.keys())
|
|
156
108
|
return result
|
|
157
109
|
|
|
158
110
|
def list_namespaces(self) -> List[str]:
|
|
111
|
+
return list(self._tools.keys())
|
|
112
|
+
|
|
113
|
+
def list_metadata(self, namespace: str | None = None) -> List[ToolMetadata]:
|
|
159
114
|
"""
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
115
|
+
Return *all* :class:`ToolMetadata` objects.
|
|
116
|
+
|
|
117
|
+
Parameters
|
|
118
|
+
----------
|
|
119
|
+
namespace
|
|
120
|
+
• ``None`` *(default)* – metadata from **all** namespaces
|
|
121
|
+
• ``"some_ns"`` – only that namespace
|
|
164
122
|
"""
|
|
165
|
-
|
|
123
|
+
if namespace is not None:
|
|
124
|
+
return list(self._metadata.get(namespace, {}).values())
|
|
125
|
+
|
|
126
|
+
# flatten
|
|
127
|
+
result: List[ToolMetadata] = []
|
|
128
|
+
for ns_meta in self._metadata.values():
|
|
129
|
+
result.extend(ns_meta.values())
|
|
130
|
+
return result
|