chuk-tool-processor 0.1.0__py3-none-any.whl → 0.1.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.
- chuk_tool_processor/core/processor.py +8 -8
- chuk_tool_processor/execution/strategies/inprocess_strategy.py +110 -148
- chuk_tool_processor/execution/strategies/subprocess_strategy.py +1 -1
- chuk_tool_processor/execution/wrappers/retry.py +1 -1
- chuk_tool_processor/logging/__init__.py +33 -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.2.dist-info}/METADATA +5 -2
- chuk_tool_processor-0.1.2.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.2.dist-info}/WHEEL +0 -0
- {chuk_tool_processor-0.1.0.dist-info → chuk_tool_processor-0.1.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# chuk_tool_processor/parsers/json_tool.py
|
|
2
|
+
"""JSON *tool_calls* parser plugin (drop-in).
|
|
3
|
+
|
|
4
|
+
Accepts raw‐string or dict input where the top-level object includes a
|
|
5
|
+
``tool_calls`` array – an early OpenAI Chat Completions schema.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any, List
|
|
11
|
+
from pydantic import ValidationError
|
|
12
|
+
|
|
13
|
+
# imports
|
|
14
|
+
from .base import ParserPlugin
|
|
15
|
+
from chuk_tool_processor.models.tool_call import ToolCall
|
|
16
|
+
|
|
17
|
+
class JsonToolPlugin(ParserPlugin):
|
|
18
|
+
"""Extracts ``tool_calls`` array from a JSON response."""
|
|
19
|
+
|
|
20
|
+
def try_parse(self, raw: str | Any) -> List[ToolCall]:
|
|
21
|
+
try:
|
|
22
|
+
data = json.loads(raw) if isinstance(raw, str) else raw
|
|
23
|
+
except json.JSONDecodeError:
|
|
24
|
+
return []
|
|
25
|
+
|
|
26
|
+
if not isinstance(data, dict):
|
|
27
|
+
return []
|
|
28
|
+
|
|
29
|
+
calls = data.get("tool_calls", [])
|
|
30
|
+
out: List[ToolCall] = []
|
|
31
|
+
|
|
32
|
+
for c in calls:
|
|
33
|
+
try:
|
|
34
|
+
out.append(ToolCall(**c))
|
|
35
|
+
except ValidationError:
|
|
36
|
+
continue
|
|
37
|
+
return out
|
|
38
|
+
|
|
@@ -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
|