chuk-tool-processor 0.1.6__py3-none-any.whl → 0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of chuk-tool-processor might be problematic. Click here for more details.
- chuk_tool_processor/core/processor.py +345 -132
- chuk_tool_processor/execution/strategies/inprocess_strategy.py +522 -71
- chuk_tool_processor/execution/strategies/subprocess_strategy.py +559 -64
- chuk_tool_processor/execution/tool_executor.py +282 -24
- chuk_tool_processor/execution/wrappers/caching.py +465 -123
- chuk_tool_processor/execution/wrappers/rate_limiting.py +199 -86
- chuk_tool_processor/execution/wrappers/retry.py +133 -23
- chuk_tool_processor/logging/__init__.py +83 -10
- chuk_tool_processor/logging/context.py +218 -22
- chuk_tool_processor/logging/formatter.py +56 -13
- chuk_tool_processor/logging/helpers.py +91 -16
- chuk_tool_processor/logging/metrics.py +75 -6
- chuk_tool_processor/mcp/mcp_tool.py +80 -35
- chuk_tool_processor/mcp/register_mcp_tools.py +74 -56
- chuk_tool_processor/mcp/setup_mcp_sse.py +41 -36
- chuk_tool_processor/mcp/setup_mcp_stdio.py +39 -37
- chuk_tool_processor/mcp/transport/sse_transport.py +351 -105
- chuk_tool_processor/models/execution_strategy.py +52 -3
- chuk_tool_processor/models/streaming_tool.py +110 -0
- chuk_tool_processor/models/tool_call.py +56 -4
- chuk_tool_processor/models/tool_result.py +115 -9
- chuk_tool_processor/models/validated_tool.py +15 -13
- chuk_tool_processor/plugins/discovery.py +115 -70
- chuk_tool_processor/plugins/parsers/base.py +13 -5
- chuk_tool_processor/plugins/parsers/{function_call_tool_plugin.py → function_call_tool.py} +39 -20
- chuk_tool_processor/plugins/parsers/json_tool.py +50 -0
- chuk_tool_processor/plugins/parsers/openai_tool.py +88 -0
- chuk_tool_processor/plugins/parsers/xml_tool.py +74 -20
- chuk_tool_processor/registry/__init__.py +46 -7
- chuk_tool_processor/registry/auto_register.py +92 -28
- chuk_tool_processor/registry/decorators.py +134 -11
- chuk_tool_processor/registry/interface.py +48 -14
- chuk_tool_processor/registry/metadata.py +52 -6
- chuk_tool_processor/registry/provider.py +75 -36
- chuk_tool_processor/registry/providers/__init__.py +49 -10
- chuk_tool_processor/registry/providers/memory.py +59 -48
- chuk_tool_processor/registry/tool_export.py +208 -39
- chuk_tool_processor/utils/validation.py +18 -13
- chuk_tool_processor-0.2.dist-info/METADATA +401 -0
- chuk_tool_processor-0.2.dist-info/RECORD +58 -0
- {chuk_tool_processor-0.1.6.dist-info → chuk_tool_processor-0.2.dist-info}/WHEEL +1 -1
- chuk_tool_processor/plugins/parsers/json_tool_plugin.py +0 -38
- chuk_tool_processor/plugins/parsers/openai_tool_plugin.py +0 -76
- chuk_tool_processor-0.1.6.dist-info/METADATA +0 -462
- chuk_tool_processor-0.1.6.dist-info/RECORD +0 -57
- {chuk_tool_processor-0.1.6.dist-info → chuk_tool_processor-0.2.dist-info}/top_level.txt +0 -0
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
# chuk_tool_processor/parsers/base.py
|
|
2
|
+
"""Async-native parser-plugin base interface."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
2
6
|
from abc import ABC, abstractmethod
|
|
3
7
|
from typing import List
|
|
8
|
+
|
|
4
9
|
from chuk_tool_processor.models.tool_call import ToolCall
|
|
5
10
|
|
|
11
|
+
__all__ = ["ParserPlugin"]
|
|
12
|
+
|
|
6
13
|
|
|
7
14
|
class ParserPlugin(ABC):
|
|
8
15
|
"""
|
|
9
|
-
|
|
16
|
+
Every parser plugin **must** implement the async ``try_parse`` coroutine.
|
|
10
17
|
|
|
11
|
-
The processor
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
The processor awaits it and expects *a list* of :class:`ToolCall`
|
|
19
|
+
objects. If the plugin doesn’t recognise the input it should return an
|
|
20
|
+
empty list.
|
|
14
21
|
"""
|
|
15
22
|
|
|
16
23
|
@abstractmethod
|
|
17
|
-
def try_parse(self, raw: str) -> List[ToolCall]:
|
|
24
|
+
async def try_parse(self, raw: str | object) -> List[ToolCall]: # noqa: D401
|
|
25
|
+
"""Attempt to parse *raw* into one or more :class:`ToolCall` objects."""
|
|
18
26
|
...
|
|
@@ -1,33 +1,49 @@
|
|
|
1
|
-
# chuk_tool_processor/plugins/
|
|
2
|
-
"""
|
|
3
|
-
|
|
4
|
-
* Coerces non-dict `arguments` to `{}` instead of rejecting.
|
|
5
|
-
* Inherits from ``ParserPlugin`` so discovery categorises it as a *parser*.
|
|
6
|
-
"""
|
|
1
|
+
# chuk_tool_processor/plugins/parsers/function_call_tool.py
|
|
2
|
+
"""Async parser for OpenAI-style single `function_call` objects."""
|
|
3
|
+
|
|
7
4
|
from __future__ import annotations
|
|
8
5
|
|
|
9
6
|
import json
|
|
10
7
|
import re
|
|
11
8
|
from typing import Any, Dict, List
|
|
9
|
+
|
|
12
10
|
from pydantic import ValidationError
|
|
13
11
|
|
|
14
|
-
# imports
|
|
15
|
-
from .base import ParserPlugin
|
|
16
|
-
from chuk_tool_processor.models.tool_call import ToolCall
|
|
17
12
|
from chuk_tool_processor.logging import get_logger
|
|
13
|
+
from chuk_tool_processor.models.tool_call import ToolCall
|
|
14
|
+
from chuk_tool_processor.plugins.parsers.base import ParserPlugin
|
|
15
|
+
|
|
16
|
+
__all__ = ["FunctionCallPlugin"]
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
logger = get_logger("chuk_tool_processor.plugins.function_call_tool")
|
|
18
|
+
logger = get_logger(__name__)
|
|
21
19
|
|
|
22
|
-
# balanced
|
|
20
|
+
# One-level balanced JSON object (good enough for embedded argument blocks)
|
|
23
21
|
_JSON_OBJECT = re.compile(r"\{(?:[^{}]|(?:\{[^{}]*\}))*\}")
|
|
24
22
|
|
|
25
23
|
|
|
24
|
+
class PluginMeta:
|
|
25
|
+
"""Optional self-description used by the plugin-discovery system (if present)."""
|
|
26
|
+
|
|
27
|
+
name: str = "function_call"
|
|
28
|
+
description: str = (
|
|
29
|
+
"Parses a single OpenAI-style `function_call` JSON object (including "
|
|
30
|
+
"strings that embed such an object)."
|
|
31
|
+
)
|
|
32
|
+
version: str = "1.0.0"
|
|
33
|
+
author: str = "chuk_tool_processor"
|
|
34
|
+
|
|
35
|
+
|
|
26
36
|
class FunctionCallPlugin(ParserPlugin):
|
|
27
|
-
"""Parse
|
|
37
|
+
"""Parse messages containing a *single* `function_call` entry."""
|
|
38
|
+
|
|
39
|
+
# --------------------------------------------------------------------- #
|
|
40
|
+
# Public API
|
|
41
|
+
# --------------------------------------------------------------------- #
|
|
28
42
|
|
|
29
|
-
def try_parse(self, raw: str | Dict[str, Any]) -> List[ToolCall]:
|
|
43
|
+
async def try_parse(self, raw: str | Dict[str, Any]) -> List[ToolCall]:
|
|
30
44
|
payload: Dict[str, Any] | None
|
|
45
|
+
|
|
46
|
+
# 1️⃣ Primary path ─ whole payload is JSON
|
|
31
47
|
if isinstance(raw, dict):
|
|
32
48
|
payload = raw
|
|
33
49
|
else:
|
|
@@ -38,22 +54,24 @@ class FunctionCallPlugin(ParserPlugin):
|
|
|
38
54
|
|
|
39
55
|
calls: List[ToolCall] = []
|
|
40
56
|
|
|
41
|
-
# primary path -----------------------------------------------------
|
|
42
57
|
if isinstance(payload, dict):
|
|
43
58
|
calls.extend(self._extract_from_payload(payload))
|
|
44
59
|
|
|
45
|
-
#
|
|
60
|
+
# 2️⃣ Fallback path ─ scan for *nested* JSON objects inside a string
|
|
46
61
|
if not calls and isinstance(raw, str):
|
|
47
|
-
for
|
|
62
|
+
for match in _JSON_OBJECT.finditer(raw):
|
|
48
63
|
try:
|
|
49
|
-
sub = json.loads(
|
|
64
|
+
sub = json.loads(match.group(0))
|
|
50
65
|
except json.JSONDecodeError:
|
|
51
66
|
continue
|
|
52
67
|
calls.extend(self._extract_from_payload(sub))
|
|
53
68
|
|
|
54
69
|
return calls
|
|
55
70
|
|
|
56
|
-
# ------------------------------------------------------------------
|
|
71
|
+
# ------------------------------------------------------------------ #
|
|
72
|
+
# Helpers
|
|
73
|
+
# ------------------------------------------------------------------ #
|
|
74
|
+
|
|
57
75
|
def _extract_from_payload(self, payload: Dict[str, Any]) -> List[ToolCall]:
|
|
58
76
|
fc = payload.get("function_call")
|
|
59
77
|
if not isinstance(fc, dict):
|
|
@@ -62,12 +80,13 @@ class FunctionCallPlugin(ParserPlugin):
|
|
|
62
80
|
name = fc.get("name")
|
|
63
81
|
args = fc.get("arguments", {})
|
|
64
82
|
|
|
65
|
-
#
|
|
83
|
+
# Arguments may themselves be JSON in *string* form
|
|
66
84
|
if isinstance(args, str):
|
|
67
85
|
try:
|
|
68
86
|
args = json.loads(args)
|
|
69
87
|
except json.JSONDecodeError:
|
|
70
88
|
args = {}
|
|
89
|
+
|
|
71
90
|
if not isinstance(args, dict):
|
|
72
91
|
args = {}
|
|
73
92
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# chuk_tool_processor/plugins/parsers/json_tool.py
|
|
2
|
+
"""Async JSON `tool_calls` parser plugin."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any, List
|
|
8
|
+
|
|
9
|
+
from pydantic import ValidationError
|
|
10
|
+
|
|
11
|
+
from chuk_tool_processor.logging import get_logger
|
|
12
|
+
from chuk_tool_processor.models.tool_call import ToolCall
|
|
13
|
+
from chuk_tool_processor.plugins.parsers.base import ParserPlugin
|
|
14
|
+
|
|
15
|
+
__all__ = ["JsonToolPlugin"]
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PluginMeta:
|
|
21
|
+
"""Optional self-description consumed by the plugin-discovery subsystem."""
|
|
22
|
+
name: str = "json_tool_calls"
|
|
23
|
+
description: str = "Parses a JSON object containing a `tool_calls` array."
|
|
24
|
+
version: str = "1.0.0"
|
|
25
|
+
author: str = "chuk_tool_processor"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class JsonToolPlugin(ParserPlugin):
|
|
29
|
+
"""Extracts a *list* of :class:`ToolCall` objects from a `tool_calls` array."""
|
|
30
|
+
|
|
31
|
+
async def try_parse(self, raw: str | Any) -> List[ToolCall]: # noqa: D401
|
|
32
|
+
# Decode JSON if we were given a string
|
|
33
|
+
try:
|
|
34
|
+
data = json.loads(raw) if isinstance(raw, str) else raw
|
|
35
|
+
except json.JSONDecodeError:
|
|
36
|
+
logger.debug("json_tool: input is not valid JSON")
|
|
37
|
+
return []
|
|
38
|
+
|
|
39
|
+
if not isinstance(data, dict):
|
|
40
|
+
return []
|
|
41
|
+
|
|
42
|
+
calls: List[ToolCall] = []
|
|
43
|
+
for entry in data.get("tool_calls", []):
|
|
44
|
+
try:
|
|
45
|
+
calls.append(ToolCall(**entry))
|
|
46
|
+
except ValidationError:
|
|
47
|
+
logger.debug("json_tool: validation error on entry %s", entry)
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
return calls
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# chuk_tool_processor/plugins/parsers/openai_tool.py
|
|
2
|
+
"""Async parser for OpenAI-style `tool_calls` arrays."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from typing import Any, List
|
|
8
|
+
|
|
9
|
+
from pydantic import ValidationError
|
|
10
|
+
|
|
11
|
+
from chuk_tool_processor.logging import get_logger
|
|
12
|
+
from chuk_tool_processor.models.tool_call import ToolCall
|
|
13
|
+
from chuk_tool_processor.plugins.parsers.base import ParserPlugin
|
|
14
|
+
|
|
15
|
+
__all__ = ["OpenAIToolPlugin"]
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PluginMeta:
|
|
21
|
+
"""Optional descriptor consumed by the plugin-discovery system."""
|
|
22
|
+
name: str = "openai_tool_calls"
|
|
23
|
+
description: str = "Parses Chat-Completions responses containing `tool_calls`."
|
|
24
|
+
version: str = "1.0.0"
|
|
25
|
+
author: str = "chuk_tool_processor"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OpenAIToolPlugin(ParserPlugin):
|
|
29
|
+
"""
|
|
30
|
+
Understands structures like::
|
|
31
|
+
|
|
32
|
+
{
|
|
33
|
+
"tool_calls": [
|
|
34
|
+
{
|
|
35
|
+
"id": "call_abc",
|
|
36
|
+
"type": "function",
|
|
37
|
+
"function": {
|
|
38
|
+
"name": "weather",
|
|
39
|
+
"arguments": "{\"location\": \"New York\"}"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
async def try_parse(self, raw: str | Any) -> List[ToolCall]: # noqa: D401
|
|
47
|
+
# ------------------------------------------------------------------ #
|
|
48
|
+
# 1. Decode JSON when the input is a string
|
|
49
|
+
# ------------------------------------------------------------------ #
|
|
50
|
+
try:
|
|
51
|
+
data = json.loads(raw) if isinstance(raw, str) else raw
|
|
52
|
+
except json.JSONDecodeError:
|
|
53
|
+
logger.debug("openai_tool_plugin: input is not valid JSON")
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
if not isinstance(data, dict) or "tool_calls" not in data:
|
|
57
|
+
return []
|
|
58
|
+
|
|
59
|
+
# ------------------------------------------------------------------ #
|
|
60
|
+
# 2. Build ToolCall objects
|
|
61
|
+
# ------------------------------------------------------------------ #
|
|
62
|
+
calls: List[ToolCall] = []
|
|
63
|
+
for entry in data["tool_calls"]:
|
|
64
|
+
fn = entry.get("function", {})
|
|
65
|
+
name = fn.get("name")
|
|
66
|
+
args = fn.get("arguments", {})
|
|
67
|
+
|
|
68
|
+
# Arguments may be double-encoded JSON
|
|
69
|
+
if isinstance(args, str):
|
|
70
|
+
try:
|
|
71
|
+
args = json.loads(args)
|
|
72
|
+
except json.JSONDecodeError:
|
|
73
|
+
args = {}
|
|
74
|
+
|
|
75
|
+
if not isinstance(name, str) or not name:
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
calls.append(
|
|
80
|
+
ToolCall(tool=name, arguments=args if isinstance(args, dict) else {})
|
|
81
|
+
)
|
|
82
|
+
except ValidationError:
|
|
83
|
+
logger.debug(
|
|
84
|
+
"openai_tool_plugin: validation error while building ToolCall for %s",
|
|
85
|
+
name,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
return calls
|
|
@@ -1,45 +1,99 @@
|
|
|
1
|
-
# chuk_tool_processor/plugins/xml_tool.py
|
|
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.
|
|
1
|
+
# chuk_tool_processor/plugins/parsers/xml_tool.py
|
|
5
2
|
"""
|
|
3
|
+
Async-native parser for single-line XML-style tool-call tags, e.g.
|
|
4
|
+
|
|
5
|
+
<tool name="translate" args="{\"text\": \"Hello\", \"target\": \"es\"}"/>
|
|
6
|
+
|
|
7
|
+
The *args* attribute may be
|
|
8
|
+
|
|
9
|
+
1. A proper JSON object: args="{"x": 1}"
|
|
10
|
+
2. A JSON-encoded string (most common): args="{\"x\": 1}"
|
|
11
|
+
3. The empty string: args=""
|
|
12
|
+
|
|
13
|
+
All variants are normalised to a **dict** of arguments.
|
|
14
|
+
"""
|
|
15
|
+
|
|
6
16
|
from __future__ import annotations
|
|
7
17
|
|
|
8
18
|
import json
|
|
9
19
|
import re
|
|
10
20
|
from typing import List
|
|
21
|
+
|
|
11
22
|
from pydantic import ValidationError
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
from .base import ParserPlugin
|
|
24
|
+
from chuk_tool_processor.logging import get_logger
|
|
15
25
|
from chuk_tool_processor.models.tool_call import ToolCall
|
|
26
|
+
from chuk_tool_processor.plugins.parsers.base import ParserPlugin
|
|
27
|
+
|
|
28
|
+
__all__: list[str] = ["XmlToolPlugin"]
|
|
29
|
+
|
|
30
|
+
logger = get_logger(__name__)
|
|
16
31
|
|
|
17
32
|
|
|
33
|
+
class PluginMeta:
|
|
34
|
+
"""Optional descriptor that can be used by the plugin-discovery mechanism."""
|
|
35
|
+
name: str = "xml_tool_tag"
|
|
36
|
+
description: str = "Parses <tool …/> XML tags into ToolCall objects."
|
|
37
|
+
version: str = "1.0.0"
|
|
38
|
+
author: str = "chuk_tool_processor"
|
|
39
|
+
|
|
18
40
|
|
|
19
41
|
class XmlToolPlugin(ParserPlugin):
|
|
20
|
-
"""
|
|
42
|
+
"""Convert `<tool …/>` tags into :class:`ToolCall` objects."""
|
|
21
43
|
|
|
22
44
|
_TAG = re.compile(
|
|
23
45
|
r"<tool\s+"
|
|
24
|
-
r"name=(?P<q1>[\"
|
|
25
|
-
r"args=(?P<q2>[\"
|
|
46
|
+
r"name=(?P<q1>[\"'])(?P<tool>.+?)(?P=q1)\s+"
|
|
47
|
+
r"args=(?P<q2>[\"'])(?P<args>.*?)(?P=q2)\s*/>",
|
|
48
|
+
flags=re.IGNORECASE | re.DOTALL,
|
|
26
49
|
)
|
|
27
50
|
|
|
28
|
-
|
|
29
|
-
|
|
51
|
+
# ------------------------------------------------------------------ #
|
|
52
|
+
async def try_parse(self, raw: str | object) -> List[ToolCall]: # noqa: D401
|
|
53
|
+
if not isinstance(raw, str):
|
|
30
54
|
return []
|
|
31
55
|
|
|
32
56
|
calls: List[ToolCall] = []
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
except json.JSONDecodeError:
|
|
39
|
-
args = {}
|
|
57
|
+
|
|
58
|
+
for match in self._TAG.finditer(raw):
|
|
59
|
+
name = match.group("tool")
|
|
60
|
+
raw_args = match.group("args") or ""
|
|
61
|
+
args = self._decode_args(raw_args)
|
|
40
62
|
|
|
41
63
|
try:
|
|
42
|
-
calls.append(ToolCall(tool=
|
|
64
|
+
calls.append(ToolCall(tool=name, arguments=args))
|
|
43
65
|
except ValidationError:
|
|
44
|
-
|
|
66
|
+
logger.debug("xml_tool_plugin: validation error for <%s>", name)
|
|
67
|
+
|
|
45
68
|
return calls
|
|
69
|
+
|
|
70
|
+
# ------------------------------------------------------------------ #
|
|
71
|
+
# Helper – robust JSON decode for the args attribute
|
|
72
|
+
# ------------------------------------------------------------------ #
|
|
73
|
+
@staticmethod
|
|
74
|
+
def _decode_args(raw_args: str) -> dict:
|
|
75
|
+
"""Best-effort decoding of the *args* attribute to a dict."""
|
|
76
|
+
if not raw_args:
|
|
77
|
+
return {}
|
|
78
|
+
|
|
79
|
+
# 1️⃣ Try direct JSON
|
|
80
|
+
try:
|
|
81
|
+
parsed = json.loads(raw_args)
|
|
82
|
+
except json.JSONDecodeError:
|
|
83
|
+
parsed = None
|
|
84
|
+
|
|
85
|
+
# 2️⃣ If still None, the value might be a JSON-encoded string
|
|
86
|
+
if parsed is None:
|
|
87
|
+
try:
|
|
88
|
+
parsed = json.loads(raw_args.encode().decode("unicode_escape"))
|
|
89
|
+
except json.JSONDecodeError:
|
|
90
|
+
parsed = None
|
|
91
|
+
|
|
92
|
+
# 3️⃣ Last resort – naive unescaping of \" → "
|
|
93
|
+
if parsed is None:
|
|
94
|
+
try:
|
|
95
|
+
parsed = json.loads(raw_args.replace(r"\"", "\""))
|
|
96
|
+
except json.JSONDecodeError:
|
|
97
|
+
parsed = {}
|
|
98
|
+
|
|
99
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
@@ -1,21 +1,60 @@
|
|
|
1
|
+
# chuk_tool_processor/registry/__init__.py
|
|
1
2
|
"""
|
|
2
|
-
|
|
3
|
+
Async-native tool registry package for managing and accessing tool implementations.
|
|
3
4
|
"""
|
|
4
5
|
|
|
6
|
+
import asyncio
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
5
9
|
from chuk_tool_processor.registry.interface import ToolRegistryInterface
|
|
6
|
-
from chuk_tool_processor.registry.metadata import ToolMetadata
|
|
7
|
-
from chuk_tool_processor.registry.provider import ToolRegistryProvider
|
|
8
|
-
from chuk_tool_processor.registry.decorators import register_tool
|
|
10
|
+
from chuk_tool_processor.registry.metadata import ToolMetadata, StreamingToolMetadata
|
|
11
|
+
from chuk_tool_processor.registry.provider import ToolRegistryProvider, get_registry
|
|
12
|
+
from chuk_tool_processor.registry.decorators import register_tool, ensure_registrations, discover_decorated_tools
|
|
9
13
|
|
|
10
14
|
# --------------------------------------------------------------------------- #
|
|
11
|
-
#
|
|
15
|
+
# The default_registry is now an async function instead of direct property access
|
|
12
16
|
# --------------------------------------------------------------------------- #
|
|
13
|
-
|
|
17
|
+
async def get_default_registry() -> ToolRegistryInterface:
|
|
18
|
+
"""
|
|
19
|
+
Get the default registry instance.
|
|
20
|
+
|
|
21
|
+
This is a convenience function that calls ToolRegistryProvider.get_registry()
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
The default tool registry
|
|
25
|
+
"""
|
|
26
|
+
return await ToolRegistryProvider.get_registry()
|
|
14
27
|
|
|
15
28
|
__all__ = [
|
|
16
29
|
"ToolRegistryInterface",
|
|
17
30
|
"ToolMetadata",
|
|
31
|
+
"StreamingToolMetadata",
|
|
18
32
|
"ToolRegistryProvider",
|
|
19
33
|
"register_tool",
|
|
20
|
-
"
|
|
34
|
+
"ensure_registrations",
|
|
35
|
+
"discover_decorated_tools",
|
|
36
|
+
"get_default_registry",
|
|
37
|
+
"get_registry",
|
|
21
38
|
]
|
|
39
|
+
|
|
40
|
+
# --------------------------------------------------------------------------- #
|
|
41
|
+
# Initialization helper that should be called at application startup
|
|
42
|
+
# --------------------------------------------------------------------------- #
|
|
43
|
+
async def initialize():
|
|
44
|
+
"""
|
|
45
|
+
Initialize the registry system.
|
|
46
|
+
|
|
47
|
+
This function should be called during application startup to:
|
|
48
|
+
1. Ensure the registry is created
|
|
49
|
+
2. Register all tools decorated with @register_tool
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The initialized registry
|
|
53
|
+
"""
|
|
54
|
+
# Initialize registry
|
|
55
|
+
registry = await get_default_registry()
|
|
56
|
+
|
|
57
|
+
# Process all pending tool registrations
|
|
58
|
+
await ensure_registrations()
|
|
59
|
+
|
|
60
|
+
return registry
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# chuk_tool_processor/registry/auto_register.py
|
|
2
2
|
"""
|
|
3
|
-
|
|
3
|
+
Async auto-register helpers for registering functions and LangChain tools.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Usage:
|
|
6
|
+
await register_fn_tool(my_function)
|
|
7
|
+
await register_langchain_tool(my_langchain_tool)
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
These tools will immediately show up in the global registry.
|
|
9
10
|
"""
|
|
10
11
|
|
|
11
12
|
from __future__ import annotations
|
|
@@ -13,7 +14,7 @@ from __future__ import annotations
|
|
|
13
14
|
import asyncio
|
|
14
15
|
import inspect
|
|
15
16
|
import types
|
|
16
|
-
from typing import Callable, ForwardRef, Type, get_type_hints
|
|
17
|
+
from typing import Callable, ForwardRef, Type, get_type_hints, Any, Optional, Dict, Union
|
|
17
18
|
|
|
18
19
|
import anyio
|
|
19
20
|
from pydantic import BaseModel, create_model
|
|
@@ -23,7 +24,9 @@ try: # optional dependency
|
|
|
23
24
|
except ModuleNotFoundError: # pragma: no cover
|
|
24
25
|
BaseTool = None # noqa: N816 – keep the name for isinstance() checks
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
# registry
|
|
28
|
+
from .decorators import register_tool
|
|
29
|
+
from .provider import ToolRegistryProvider
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
# ────────────────────────────────────────────────────────────────────────────
|
|
@@ -37,7 +40,7 @@ def _auto_schema(func: Callable) -> Type[BaseModel]:
|
|
|
37
40
|
|
|
38
41
|
*Unknown* or *un-imported* annotations (common with third-party libs that
|
|
39
42
|
use forward-refs without importing the target – e.g. ``uuid.UUID`` in
|
|
40
|
-
LangChain
|
|
43
|
+
LangChain's `CallbackManagerForToolRun`) default to ``str`` instead of
|
|
41
44
|
crashing `get_type_hints()`.
|
|
42
45
|
"""
|
|
43
46
|
try:
|
|
@@ -49,14 +52,14 @@ def _auto_schema(func: Callable) -> Type[BaseModel]:
|
|
|
49
52
|
for param in inspect.signature(func).parameters.values():
|
|
50
53
|
raw_hint = hints.get(param.name, param.annotation)
|
|
51
54
|
# Default to ``str`` for ForwardRef / string annotations or if we
|
|
52
|
-
# couldn
|
|
55
|
+
# couldn't resolve the type.
|
|
53
56
|
hint: type = (
|
|
54
57
|
raw_hint
|
|
55
58
|
if raw_hint not in (inspect._empty, None, str)
|
|
56
59
|
and not isinstance(raw_hint, (str, ForwardRef))
|
|
57
60
|
else str
|
|
58
61
|
)
|
|
59
|
-
fields[param.name] = (hint, ...) #
|
|
62
|
+
fields[param.name] = (hint, ...) # "..." → required
|
|
60
63
|
|
|
61
64
|
return create_model(f"{func.__name__.title()}Args", **fields) # type: ignore
|
|
62
65
|
|
|
@@ -66,25 +69,54 @@ def _auto_schema(func: Callable) -> Type[BaseModel]:
|
|
|
66
69
|
# ────────────────────────────────────────────────────────────────────────────
|
|
67
70
|
|
|
68
71
|
|
|
69
|
-
def register_fn_tool(
|
|
72
|
+
async def register_fn_tool(
|
|
70
73
|
func: Callable,
|
|
71
74
|
*,
|
|
72
75
|
name: str | None = None,
|
|
73
76
|
description: str | None = None,
|
|
77
|
+
namespace: str = "default",
|
|
74
78
|
) -> None:
|
|
75
|
-
"""
|
|
76
|
-
|
|
79
|
+
"""
|
|
80
|
+
Register a plain function as a tool asynchronously.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
func: The function to register (can be sync or async)
|
|
84
|
+
name: Optional name for the tool (defaults to function name)
|
|
85
|
+
description: Optional description (defaults to function docstring)
|
|
86
|
+
namespace: Registry namespace (defaults to "default")
|
|
87
|
+
"""
|
|
77
88
|
schema = _auto_schema(func)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
89
|
+
tool_name = name or func.__name__
|
|
90
|
+
tool_description = (description or func.__doc__ or "").strip()
|
|
91
|
+
|
|
92
|
+
# Create the tool wrapper class
|
|
82
93
|
class _Tool: # noqa: D401, N801 – internal auto-wrapper
|
|
83
|
-
|
|
94
|
+
"""Auto-generated tool wrapper for function."""
|
|
95
|
+
|
|
96
|
+
async def execute(self, **kwargs: Any) -> Any:
|
|
97
|
+
"""Execute the wrapped function."""
|
|
84
98
|
if inspect.iscoroutinefunction(func):
|
|
85
99
|
return await func(**kwargs)
|
|
86
100
|
# off-load blocking sync work
|
|
87
101
|
return await anyio.to_thread.run_sync(func, **kwargs)
|
|
102
|
+
|
|
103
|
+
# Set the docstring
|
|
104
|
+
_Tool.__doc__ = tool_description
|
|
105
|
+
|
|
106
|
+
# Get the registry and register directly
|
|
107
|
+
registry = await ToolRegistryProvider.get_registry()
|
|
108
|
+
await registry.register_tool(
|
|
109
|
+
_Tool(),
|
|
110
|
+
name=tool_name,
|
|
111
|
+
namespace=namespace,
|
|
112
|
+
metadata={
|
|
113
|
+
"description": tool_description,
|
|
114
|
+
"is_async": True,
|
|
115
|
+
"argument_schema": schema.model_json_schema(),
|
|
116
|
+
"source": "function",
|
|
117
|
+
"source_name": func.__qualname__,
|
|
118
|
+
}
|
|
119
|
+
)
|
|
88
120
|
|
|
89
121
|
|
|
90
122
|
# ────────────────────────────────────────────────────────────────────────────
|
|
@@ -92,18 +124,27 @@ def register_fn_tool(
|
|
|
92
124
|
# ────────────────────────────────────────────────────────────────────────────
|
|
93
125
|
|
|
94
126
|
|
|
95
|
-
def register_langchain_tool(
|
|
96
|
-
tool,
|
|
127
|
+
async def register_langchain_tool(
|
|
128
|
+
tool: Any,
|
|
97
129
|
*,
|
|
98
130
|
name: str | None = None,
|
|
99
131
|
description: str | None = None,
|
|
132
|
+
namespace: str = "default",
|
|
100
133
|
) -> None:
|
|
101
134
|
"""
|
|
102
|
-
Register a **LangChain** `BaseTool` instance
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
135
|
+
Register a **LangChain** `BaseTool` instance asynchronously.
|
|
136
|
+
|
|
137
|
+
Works with any object exposing `.run` / `.arun` methods.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
tool: The LangChain tool to register
|
|
141
|
+
name: Optional name for the tool (defaults to tool.name)
|
|
142
|
+
description: Optional description (defaults to tool.description)
|
|
143
|
+
namespace: Registry namespace (defaults to "default")
|
|
144
|
+
|
|
145
|
+
Raises:
|
|
146
|
+
RuntimeError: If LangChain isn't installed
|
|
147
|
+
TypeError: If the object isn't a LangChain BaseTool
|
|
107
148
|
"""
|
|
108
149
|
if BaseTool is None:
|
|
109
150
|
raise RuntimeError(
|
|
@@ -117,9 +158,32 @@ def register_langchain_tool(
|
|
|
117
158
|
f"{type(tool).__name__}"
|
|
118
159
|
)
|
|
119
160
|
|
|
120
|
-
|
|
121
|
-
|
|
161
|
+
# Prefer async implementation if available
|
|
162
|
+
fn = tool.arun if hasattr(tool, "arun") else tool.run
|
|
163
|
+
|
|
164
|
+
tool_name = name or tool.name or tool.__class__.__name__
|
|
165
|
+
tool_description = description or tool.description or (tool.__doc__ or "")
|
|
166
|
+
|
|
167
|
+
await register_fn_tool(
|
|
122
168
|
fn,
|
|
123
|
-
name=
|
|
124
|
-
description=
|
|
169
|
+
name=tool_name,
|
|
170
|
+
description=tool_description,
|
|
171
|
+
namespace=namespace,
|
|
125
172
|
)
|
|
173
|
+
|
|
174
|
+
# Update the metadata to include LangChain info
|
|
175
|
+
registry = await ToolRegistryProvider.get_registry()
|
|
176
|
+
metadata = await registry.get_metadata(tool_name, namespace)
|
|
177
|
+
|
|
178
|
+
if metadata:
|
|
179
|
+
updated_metadata = metadata.model_copy()
|
|
180
|
+
# Update source info
|
|
181
|
+
updated_metadata.tags.add("langchain")
|
|
182
|
+
|
|
183
|
+
# Re-register with updated metadata
|
|
184
|
+
await registry.register_tool(
|
|
185
|
+
await registry.get_tool(tool_name, namespace),
|
|
186
|
+
name=tool_name,
|
|
187
|
+
namespace=namespace,
|
|
188
|
+
metadata=updated_metadata.model_dump(),
|
|
189
|
+
)
|