chuk-tool-processor 0.6.12__py3-none-any.whl → 0.6.13__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 +1 -1
- chuk_tool_processor/core/exceptions.py +10 -4
- chuk_tool_processor/core/processor.py +97 -97
- chuk_tool_processor/execution/strategies/inprocess_strategy.py +142 -150
- chuk_tool_processor/execution/strategies/subprocess_strategy.py +200 -205
- chuk_tool_processor/execution/tool_executor.py +82 -84
- chuk_tool_processor/execution/wrappers/caching.py +102 -103
- chuk_tool_processor/execution/wrappers/rate_limiting.py +45 -42
- chuk_tool_processor/execution/wrappers/retry.py +23 -25
- 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 +24 -38
- chuk_tool_processor/logging/metrics.py +11 -13
- chuk_tool_processor/mcp/__init__.py +8 -12
- chuk_tool_processor/mcp/mcp_tool.py +124 -112
- chuk_tool_processor/mcp/register_mcp_tools.py +17 -17
- chuk_tool_processor/mcp/setup_mcp_http_streamable.py +11 -13
- chuk_tool_processor/mcp/setup_mcp_sse.py +11 -13
- chuk_tool_processor/mcp/setup_mcp_stdio.py +7 -9
- chuk_tool_processor/mcp/stream_manager.py +168 -204
- chuk_tool_processor/mcp/transport/__init__.py +4 -4
- chuk_tool_processor/mcp/transport/base_transport.py +43 -58
- chuk_tool_processor/mcp/transport/http_streamable_transport.py +145 -163
- chuk_tool_processor/mcp/transport/sse_transport.py +217 -255
- chuk_tool_processor/mcp/transport/stdio_transport.py +171 -189
- chuk_tool_processor/models/__init__.py +1 -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 +19 -34
- chuk_tool_processor/models/tool_export_mixin.py +22 -8
- chuk_tool_processor/models/tool_result.py +40 -77
- chuk_tool_processor/models/validated_tool.py +14 -16
- chuk_tool_processor/plugins/__init__.py +1 -1
- chuk_tool_processor/plugins/discovery.py +10 -10
- 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.6.12.dist-info → chuk_tool_processor-0.6.13.dist-info}/METADATA +1 -1
- chuk_tool_processor-0.6.13.dist-info/RECORD +60 -0
- chuk_tool_processor-0.6.12.dist-info/RECORD +0 -60
- {chuk_tool_processor-0.6.12.dist-info → chuk_tool_processor-0.6.13.dist-info}/WHEEL +0 -0
- {chuk_tool_processor-0.6.12.dist-info → chuk_tool_processor-0.6.13.dist-info}/top_level.txt +0 -0
|
@@ -8,10 +8,10 @@ import inspect
|
|
|
8
8
|
import logging
|
|
9
9
|
import pkgutil
|
|
10
10
|
from types import ModuleType
|
|
11
|
-
from typing import Any
|
|
11
|
+
from typing import Any
|
|
12
12
|
|
|
13
|
-
from chuk_tool_processor.plugins.parsers.base import ParserPlugin
|
|
14
13
|
from chuk_tool_processor.models.execution_strategy import ExecutionStrategy
|
|
14
|
+
from chuk_tool_processor.plugins.parsers.base import ParserPlugin
|
|
15
15
|
|
|
16
16
|
__all__ = [
|
|
17
17
|
"plugin_registry",
|
|
@@ -33,7 +33,7 @@ class PluginRegistry:
|
|
|
33
33
|
|
|
34
34
|
def __init__(self) -> None:
|
|
35
35
|
# category → {name → object}
|
|
36
|
-
self._plugins:
|
|
36
|
+
self._plugins: dict[str, dict[str, Any]] = {}
|
|
37
37
|
|
|
38
38
|
# --------------------------------------------------------------------- #
|
|
39
39
|
# Public API
|
|
@@ -42,10 +42,10 @@ class PluginRegistry:
|
|
|
42
42
|
self._plugins.setdefault(category, {})[name] = plugin
|
|
43
43
|
logger.debug("Registered plugin %s.%s", category, name)
|
|
44
44
|
|
|
45
|
-
def get_plugin(self, category: str, name: str) ->
|
|
45
|
+
def get_plugin(self, category: str, name: str) -> Any | None: # noqa: D401
|
|
46
46
|
return self._plugins.get(category, {}).get(name)
|
|
47
47
|
|
|
48
|
-
def list_plugins(self, category: str | None = None) ->
|
|
48
|
+
def list_plugins(self, category: str | None = None) -> dict[str, list[str]]:
|
|
49
49
|
if category is not None:
|
|
50
50
|
return {category: sorted(self._plugins.get(category, {}))}
|
|
51
51
|
return {cat: sorted(names) for cat, names in self._plugins.items()}
|
|
@@ -70,10 +70,10 @@ class PluginDiscovery:
|
|
|
70
70
|
# ------------------------------------------------------------------ #
|
|
71
71
|
def __init__(self, registry: PluginRegistry) -> None:
|
|
72
72
|
self._registry = registry
|
|
73
|
-
self._seen_modules:
|
|
73
|
+
self._seen_modules: set[str] = set()
|
|
74
74
|
|
|
75
75
|
# ------------------------------------------------------------------ #
|
|
76
|
-
def discover_plugins(self, package_paths:
|
|
76
|
+
def discover_plugins(self, package_paths: list[str]) -> None:
|
|
77
77
|
"""Import every package in *package_paths* and walk its subtree."""
|
|
78
78
|
for pkg_path in package_paths:
|
|
79
79
|
self._walk(pkg_path)
|
|
@@ -113,7 +113,7 @@ class PluginDiscovery:
|
|
|
113
113
|
self._maybe_register(attr)
|
|
114
114
|
|
|
115
115
|
# ------------------------------------------------------------------ #
|
|
116
|
-
def _maybe_register(self, cls:
|
|
116
|
+
def _maybe_register(self, cls: type) -> None:
|
|
117
117
|
"""Register *cls* in all matching plugin categories."""
|
|
118
118
|
if inspect.isabstract(cls):
|
|
119
119
|
return
|
|
@@ -133,7 +133,7 @@ class PluginDiscovery:
|
|
|
133
133
|
self._registry.register_plugin("execution_strategy", cls.__name__, cls)
|
|
134
134
|
|
|
135
135
|
# ------------- Explicit @plugin decorator ------------------
|
|
136
|
-
meta:
|
|
136
|
+
meta: dict | None = getattr(cls, "_plugin_meta", None)
|
|
137
137
|
if meta:
|
|
138
138
|
category = meta.get("category", "unknown")
|
|
139
139
|
name = meta.get("name", cls.__name__)
|
|
@@ -178,6 +178,6 @@ def discover_default_plugins() -> None:
|
|
|
178
178
|
PluginDiscovery(plugin_registry).discover_plugins(["chuk_tool_processor.plugins"])
|
|
179
179
|
|
|
180
180
|
|
|
181
|
-
def discover_plugins(package_paths:
|
|
181
|
+
def discover_plugins(package_paths: list[str]) -> None:
|
|
182
182
|
"""Discover plugins from arbitrary external *package_paths*."""
|
|
183
183
|
PluginDiscovery(plugin_registry).discover_plugins(package_paths)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
# chuk_tool_processor/plugins/parsers/__init__.py
|
|
1
|
+
# chuk_tool_processor/plugins/parsers/__init__.py
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
from abc import ABC, abstractmethod
|
|
7
|
-
from typing import List
|
|
8
7
|
|
|
9
8
|
from chuk_tool_processor.models.tool_call import ToolCall
|
|
10
9
|
|
|
@@ -21,6 +20,6 @@ class ParserPlugin(ABC):
|
|
|
21
20
|
"""
|
|
22
21
|
|
|
23
22
|
@abstractmethod
|
|
24
|
-
async def try_parse(self, raw: str | object) ->
|
|
23
|
+
async def try_parse(self, raw: str | object) -> list[ToolCall]: # noqa: D401
|
|
25
24
|
"""Attempt to parse *raw* into one or more :class:`ToolCall` objects."""
|
|
26
25
|
...
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
7
|
import re
|
|
8
|
-
from typing import Any
|
|
8
|
+
from typing import Any
|
|
9
9
|
|
|
10
10
|
from pydantic import ValidationError
|
|
11
11
|
|
|
@@ -26,8 +26,7 @@ class PluginMeta:
|
|
|
26
26
|
|
|
27
27
|
name: str = "function_call"
|
|
28
28
|
description: str = (
|
|
29
|
-
"Parses a single OpenAI-style `function_call` JSON object (including "
|
|
30
|
-
"strings that embed such an object)."
|
|
29
|
+
"Parses a single OpenAI-style `function_call` JSON object (including strings that embed such an object)."
|
|
31
30
|
)
|
|
32
31
|
version: str = "1.0.0"
|
|
33
32
|
author: str = "chuk_tool_processor"
|
|
@@ -40,19 +39,25 @@ class FunctionCallPlugin(ParserPlugin):
|
|
|
40
39
|
# Public API
|
|
41
40
|
# --------------------------------------------------------------------- #
|
|
42
41
|
|
|
43
|
-
async def try_parse(self, raw:
|
|
44
|
-
|
|
42
|
+
async def try_parse(self, raw: Any) -> list[ToolCall]:
|
|
43
|
+
# Handle non-string, non-dict inputs gracefully
|
|
44
|
+
if not isinstance(raw, str | dict):
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
payload: dict[str, Any] | None
|
|
45
48
|
|
|
46
49
|
# 1️⃣ Primary path ─ whole payload is JSON
|
|
47
50
|
if isinstance(raw, dict):
|
|
48
51
|
payload = raw
|
|
49
|
-
|
|
52
|
+
elif isinstance(raw, str):
|
|
50
53
|
try:
|
|
51
54
|
payload = json.loads(raw)
|
|
52
55
|
except json.JSONDecodeError:
|
|
53
56
|
payload = None
|
|
57
|
+
else:
|
|
58
|
+
return []
|
|
54
59
|
|
|
55
|
-
calls:
|
|
60
|
+
calls: list[ToolCall] = []
|
|
56
61
|
|
|
57
62
|
if isinstance(payload, dict):
|
|
58
63
|
calls.extend(self._extract_from_payload(payload))
|
|
@@ -72,7 +77,7 @@ class FunctionCallPlugin(ParserPlugin):
|
|
|
72
77
|
# Helpers
|
|
73
78
|
# ------------------------------------------------------------------ #
|
|
74
79
|
|
|
75
|
-
def _extract_from_payload(self, payload:
|
|
80
|
+
def _extract_from_payload(self, payload: dict[str, Any]) -> list[ToolCall]:
|
|
76
81
|
fc = payload.get("function_call")
|
|
77
82
|
if not isinstance(fc, dict):
|
|
78
83
|
return []
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any
|
|
8
8
|
|
|
9
9
|
from pydantic import ValidationError
|
|
10
10
|
|
|
@@ -19,6 +19,7 @@ logger = get_logger(__name__)
|
|
|
19
19
|
|
|
20
20
|
class PluginMeta:
|
|
21
21
|
"""Optional self-description consumed by the plugin-discovery subsystem."""
|
|
22
|
+
|
|
22
23
|
name: str = "json_tool_calls"
|
|
23
24
|
description: str = "Parses a JSON object containing a `tool_calls` array."
|
|
24
25
|
version: str = "1.0.0"
|
|
@@ -28,7 +29,7 @@ class PluginMeta:
|
|
|
28
29
|
class JsonToolPlugin(ParserPlugin):
|
|
29
30
|
"""Extracts a *list* of :class:`ToolCall` objects from a `tool_calls` array."""
|
|
30
31
|
|
|
31
|
-
async def try_parse(self, raw: str | Any) ->
|
|
32
|
+
async def try_parse(self, raw: str | Any) -> list[ToolCall]: # noqa: D401
|
|
32
33
|
# Decode JSON if we were given a string
|
|
33
34
|
try:
|
|
34
35
|
data = json.loads(raw) if isinstance(raw, str) else raw
|
|
@@ -39,7 +40,7 @@ class JsonToolPlugin(ParserPlugin):
|
|
|
39
40
|
if not isinstance(data, dict):
|
|
40
41
|
return []
|
|
41
42
|
|
|
42
|
-
calls:
|
|
43
|
+
calls: list[ToolCall] = []
|
|
43
44
|
for entry in data.get("tool_calls", []):
|
|
44
45
|
try:
|
|
45
46
|
calls.append(ToolCall(**entry))
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import json
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import Any
|
|
8
8
|
|
|
9
9
|
from pydantic import ValidationError
|
|
10
10
|
|
|
@@ -19,6 +19,7 @@ logger = get_logger(__name__)
|
|
|
19
19
|
|
|
20
20
|
class PluginMeta:
|
|
21
21
|
"""Optional descriptor consumed by the plugin-discovery system."""
|
|
22
|
+
|
|
22
23
|
name: str = "openai_tool_calls"
|
|
23
24
|
description: str = "Parses Chat-Completions responses containing `tool_calls`."
|
|
24
25
|
version: str = "1.0.0"
|
|
@@ -43,7 +44,7 @@ class OpenAIToolPlugin(ParserPlugin):
|
|
|
43
44
|
}
|
|
44
45
|
"""
|
|
45
46
|
|
|
46
|
-
async def try_parse(self, raw: str | Any) ->
|
|
47
|
+
async def try_parse(self, raw: str | Any) -> list[ToolCall]: # noqa: D401
|
|
47
48
|
# ------------------------------------------------------------------ #
|
|
48
49
|
# 1. Decode JSON when the input is a string
|
|
49
50
|
# ------------------------------------------------------------------ #
|
|
@@ -59,8 +60,14 @@ class OpenAIToolPlugin(ParserPlugin):
|
|
|
59
60
|
# ------------------------------------------------------------------ #
|
|
60
61
|
# 2. Build ToolCall objects
|
|
61
62
|
# ------------------------------------------------------------------ #
|
|
62
|
-
calls:
|
|
63
|
-
|
|
63
|
+
calls: list[ToolCall] = []
|
|
64
|
+
|
|
65
|
+
# Ensure tool_calls is a list
|
|
66
|
+
tool_calls = data.get("tool_calls", [])
|
|
67
|
+
if not isinstance(tool_calls, list):
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
for entry in tool_calls:
|
|
64
71
|
fn = entry.get("function", {})
|
|
65
72
|
name = fn.get("name")
|
|
66
73
|
args = fn.get("arguments", {})
|
|
@@ -76,9 +83,7 @@ class OpenAIToolPlugin(ParserPlugin):
|
|
|
76
83
|
continue
|
|
77
84
|
|
|
78
85
|
try:
|
|
79
|
-
calls.append(
|
|
80
|
-
ToolCall(tool=name, arguments=args if isinstance(args, dict) else {})
|
|
81
|
-
)
|
|
86
|
+
calls.append(ToolCall(tool=name, arguments=args if isinstance(args, dict) else {}))
|
|
82
87
|
except ValidationError:
|
|
83
88
|
logger.debug(
|
|
84
89
|
"openai_tool_plugin: validation error while building ToolCall for %s",
|
|
@@ -17,7 +17,6 @@ from __future__ import annotations
|
|
|
17
17
|
|
|
18
18
|
import json
|
|
19
19
|
import re
|
|
20
|
-
from typing import List
|
|
21
20
|
|
|
22
21
|
from pydantic import ValidationError
|
|
23
22
|
|
|
@@ -32,6 +31,7 @@ logger = get_logger(__name__)
|
|
|
32
31
|
|
|
33
32
|
class PluginMeta:
|
|
34
33
|
"""Optional descriptor that can be used by the plugin-discovery mechanism."""
|
|
34
|
+
|
|
35
35
|
name: str = "xml_tool_tag"
|
|
36
36
|
description: str = "Parses <tool …/> XML tags into ToolCall objects."
|
|
37
37
|
version: str = "1.0.0"
|
|
@@ -49,11 +49,11 @@ class XmlToolPlugin(ParserPlugin):
|
|
|
49
49
|
)
|
|
50
50
|
|
|
51
51
|
# ------------------------------------------------------------------ #
|
|
52
|
-
async def try_parse(self, raw: str | object) ->
|
|
52
|
+
async def try_parse(self, raw: str | object) -> list[ToolCall]: # noqa: D401
|
|
53
53
|
if not isinstance(raw, str):
|
|
54
54
|
return []
|
|
55
55
|
|
|
56
|
-
calls:
|
|
56
|
+
calls: list[ToolCall] = []
|
|
57
57
|
|
|
58
58
|
for match in self._TAG.finditer(raw):
|
|
59
59
|
name = match.group("tool")
|
|
@@ -92,7 +92,7 @@ class XmlToolPlugin(ParserPlugin):
|
|
|
92
92
|
# 3️⃣ Last resort - naive unescaping of \" → "
|
|
93
93
|
if parsed is None:
|
|
94
94
|
try:
|
|
95
|
-
parsed = json.loads(raw_args.replace(r"\"", "
|
|
95
|
+
parsed = json.loads(raw_args.replace(r"\"", '"'))
|
|
96
96
|
except json.JSONDecodeError:
|
|
97
97
|
parsed = {}
|
|
98
98
|
|
|
@@ -3,13 +3,11 @@
|
|
|
3
3
|
Async-native tool registry package for managing and accessing tool implementations.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
import
|
|
7
|
-
from typing import Optional
|
|
8
|
-
|
|
6
|
+
from chuk_tool_processor.registry.decorators import discover_decorated_tools, ensure_registrations, register_tool
|
|
9
7
|
from chuk_tool_processor.registry.interface import ToolRegistryInterface
|
|
10
|
-
from chuk_tool_processor.registry.metadata import
|
|
8
|
+
from chuk_tool_processor.registry.metadata import StreamingToolMetadata, ToolMetadata
|
|
11
9
|
from chuk_tool_processor.registry.provider import ToolRegistryProvider, get_registry
|
|
12
|
-
|
|
10
|
+
|
|
13
11
|
|
|
14
12
|
# --------------------------------------------------------------------------- #
|
|
15
13
|
# The default_registry is now an async function instead of direct property access
|
|
@@ -17,14 +15,15 @@ from chuk_tool_processor.registry.decorators import register_tool, ensure_regist
|
|
|
17
15
|
async def get_default_registry() -> ToolRegistryInterface:
|
|
18
16
|
"""
|
|
19
17
|
Get the default registry instance.
|
|
20
|
-
|
|
18
|
+
|
|
21
19
|
This is a convenience function that calls ToolRegistryProvider.get_registry()
|
|
22
|
-
|
|
20
|
+
|
|
23
21
|
Returns:
|
|
24
22
|
The default tool registry
|
|
25
23
|
"""
|
|
26
24
|
return await ToolRegistryProvider.get_registry()
|
|
27
25
|
|
|
26
|
+
|
|
28
27
|
__all__ = [
|
|
29
28
|
"ToolRegistryInterface",
|
|
30
29
|
"ToolMetadata",
|
|
@@ -37,24 +36,25 @@ __all__ = [
|
|
|
37
36
|
"get_registry",
|
|
38
37
|
]
|
|
39
38
|
|
|
39
|
+
|
|
40
40
|
# --------------------------------------------------------------------------- #
|
|
41
41
|
# Initialization helper that should be called at application startup
|
|
42
42
|
# --------------------------------------------------------------------------- #
|
|
43
43
|
async def initialize():
|
|
44
44
|
"""
|
|
45
45
|
Initialize the registry system.
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
This function should be called during application startup to:
|
|
48
48
|
1. Ensure the registry is created
|
|
49
49
|
2. Register all tools decorated with @register_tool
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
Returns:
|
|
52
52
|
The initialized registry
|
|
53
53
|
"""
|
|
54
54
|
# Initialize registry
|
|
55
55
|
registry = await get_default_registry()
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
# Process all pending tool registrations
|
|
58
58
|
await ensure_registrations()
|
|
59
|
-
|
|
60
|
-
return registry
|
|
59
|
+
|
|
60
|
+
return registry
|
|
@@ -11,10 +11,9 @@ These tools will immediately show up in the global registry.
|
|
|
11
11
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
|
-
import asyncio
|
|
15
14
|
import inspect
|
|
16
|
-
import
|
|
17
|
-
from typing import
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from typing import Any, ForwardRef, get_type_hints
|
|
18
17
|
|
|
19
18
|
import anyio
|
|
20
19
|
from pydantic import BaseModel, create_model
|
|
@@ -25,16 +24,14 @@ except ModuleNotFoundError: # pragma: no cover
|
|
|
25
24
|
BaseTool = None # noqa: N816 - keep the name for isinstance() checks
|
|
26
25
|
|
|
27
26
|
# registry
|
|
28
|
-
from .decorators import register_tool
|
|
29
27
|
from .provider import ToolRegistryProvider
|
|
30
28
|
|
|
31
|
-
|
|
32
29
|
# ────────────────────────────────────────────────────────────────────────────
|
|
33
30
|
# internals - build a Pydantic schema from an arbitrary callable
|
|
34
31
|
# ────────────────────────────────────────────────────────────────────────────
|
|
35
32
|
|
|
36
33
|
|
|
37
|
-
def _auto_schema(func: Callable) ->
|
|
34
|
+
def _auto_schema(func: Callable) -> type[BaseModel]:
|
|
38
35
|
"""
|
|
39
36
|
Turn a function signature into a `pydantic.BaseModel` subclass.
|
|
40
37
|
|
|
@@ -55,8 +52,7 @@ def _auto_schema(func: Callable) -> Type[BaseModel]:
|
|
|
55
52
|
# couldn't resolve the type.
|
|
56
53
|
hint: type = (
|
|
57
54
|
raw_hint
|
|
58
|
-
if raw_hint not in (inspect._empty, None, str)
|
|
59
|
-
and not isinstance(raw_hint, (str, ForwardRef))
|
|
55
|
+
if raw_hint not in (inspect._empty, None, str) and not isinstance(raw_hint, str | ForwardRef)
|
|
60
56
|
else str
|
|
61
57
|
)
|
|
62
58
|
fields[param.name] = (hint, ...) # "..." → required
|
|
@@ -78,7 +74,7 @@ async def register_fn_tool(
|
|
|
78
74
|
) -> None:
|
|
79
75
|
"""
|
|
80
76
|
Register a plain function as a tool asynchronously.
|
|
81
|
-
|
|
77
|
+
|
|
82
78
|
Args:
|
|
83
79
|
func: The function to register (can be sync or async)
|
|
84
80
|
name: Optional name for the tool (defaults to function name)
|
|
@@ -88,21 +84,23 @@ async def register_fn_tool(
|
|
|
88
84
|
schema = _auto_schema(func)
|
|
89
85
|
tool_name = name or func.__name__
|
|
90
86
|
tool_description = (description or func.__doc__ or "").strip()
|
|
91
|
-
|
|
87
|
+
|
|
92
88
|
# Create the tool wrapper class
|
|
93
89
|
class _Tool: # noqa: D401, N801 - internal auto-wrapper
|
|
94
90
|
"""Auto-generated tool wrapper for function."""
|
|
95
|
-
|
|
91
|
+
|
|
96
92
|
async def execute(self, **kwargs: Any) -> Any:
|
|
97
93
|
"""Execute the wrapped function."""
|
|
98
94
|
if inspect.iscoroutinefunction(func):
|
|
99
95
|
return await func(**kwargs)
|
|
100
96
|
# off-load blocking sync work
|
|
101
|
-
|
|
102
|
-
|
|
97
|
+
import functools
|
|
98
|
+
|
|
99
|
+
return await anyio.to_thread.run_sync(functools.partial(func, **kwargs))
|
|
100
|
+
|
|
103
101
|
# Set the docstring
|
|
104
102
|
_Tool.__doc__ = tool_description
|
|
105
|
-
|
|
103
|
+
|
|
106
104
|
# Get the registry and register directly
|
|
107
105
|
registry = await ToolRegistryProvider.get_registry()
|
|
108
106
|
await registry.register_tool(
|
|
@@ -115,7 +113,7 @@ async def register_fn_tool(
|
|
|
115
113
|
"argument_schema": schema.model_json_schema(),
|
|
116
114
|
"source": "function",
|
|
117
115
|
"source_name": func.__qualname__,
|
|
118
|
-
}
|
|
116
|
+
},
|
|
119
117
|
)
|
|
120
118
|
|
|
121
119
|
|
|
@@ -133,7 +131,7 @@ async def register_langchain_tool(
|
|
|
133
131
|
) -> None:
|
|
134
132
|
"""
|
|
135
133
|
Register a **LangChain** `BaseTool` instance asynchronously.
|
|
136
|
-
|
|
134
|
+
|
|
137
135
|
Works with any object exposing `.run` / `.arun` methods.
|
|
138
136
|
|
|
139
137
|
Args:
|
|
@@ -141,45 +139,39 @@ async def register_langchain_tool(
|
|
|
141
139
|
name: Optional name for the tool (defaults to tool.name)
|
|
142
140
|
description: Optional description (defaults to tool.description)
|
|
143
141
|
namespace: Registry namespace (defaults to "default")
|
|
144
|
-
|
|
142
|
+
|
|
145
143
|
Raises:
|
|
146
144
|
RuntimeError: If LangChain isn't installed
|
|
147
145
|
TypeError: If the object isn't a LangChain BaseTool
|
|
148
146
|
"""
|
|
149
147
|
if BaseTool is None:
|
|
150
|
-
raise RuntimeError(
|
|
151
|
-
"register_langchain_tool() requires LangChain - "
|
|
152
|
-
"install with `pip install langchain`"
|
|
153
|
-
)
|
|
148
|
+
raise RuntimeError("register_langchain_tool() requires LangChain - install with `pip install langchain`")
|
|
154
149
|
|
|
155
150
|
if not isinstance(tool, BaseTool): # pragma: no cover
|
|
156
|
-
raise TypeError(
|
|
157
|
-
"Expected a langchain.tools.base.BaseTool instance - got "
|
|
158
|
-
f"{type(tool).__name__}"
|
|
159
|
-
)
|
|
151
|
+
raise TypeError(f"Expected a langchain.tools.base.BaseTool instance - got {type(tool).__name__}")
|
|
160
152
|
|
|
161
153
|
# Prefer async implementation if available
|
|
162
154
|
fn = tool.arun if hasattr(tool, "arun") else tool.run
|
|
163
|
-
|
|
155
|
+
|
|
164
156
|
tool_name = name or tool.name or tool.__class__.__name__
|
|
165
157
|
tool_description = description or tool.description or (tool.__doc__ or "")
|
|
166
|
-
|
|
158
|
+
|
|
167
159
|
await register_fn_tool(
|
|
168
160
|
fn,
|
|
169
161
|
name=tool_name,
|
|
170
162
|
description=tool_description,
|
|
171
163
|
namespace=namespace,
|
|
172
164
|
)
|
|
173
|
-
|
|
165
|
+
|
|
174
166
|
# Update the metadata to include LangChain info
|
|
175
167
|
registry = await ToolRegistryProvider.get_registry()
|
|
176
168
|
metadata = await registry.get_metadata(tool_name, namespace)
|
|
177
|
-
|
|
169
|
+
|
|
178
170
|
if metadata:
|
|
179
171
|
updated_metadata = metadata.model_copy()
|
|
180
172
|
# Update source info
|
|
181
173
|
updated_metadata.tags.add("langchain")
|
|
182
|
-
|
|
174
|
+
|
|
183
175
|
# Re-register with updated metadata
|
|
184
176
|
await registry.register_tool(
|
|
185
177
|
await registry.get_tool(tool_name, namespace),
|