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.
Files changed (33) hide show
  1. chuk_tool_processor/core/processor.py +8 -8
  2. chuk_tool_processor/execution/strategies/inprocess_strategy.py +110 -148
  3. chuk_tool_processor/execution/strategies/subprocess_strategy.py +1 -1
  4. chuk_tool_processor/execution/wrappers/retry.py +1 -1
  5. chuk_tool_processor/logging/__init__.py +33 -0
  6. chuk_tool_processor/logging/context.py +47 -0
  7. chuk_tool_processor/logging/formatter.py +55 -0
  8. chuk_tool_processor/logging/helpers.py +112 -0
  9. chuk_tool_processor/logging/metrics.py +59 -0
  10. chuk_tool_processor/models/execution_strategy.py +1 -1
  11. chuk_tool_processor/models/tool_export_mixin.py +29 -0
  12. chuk_tool_processor/models/validated_tool.py +155 -0
  13. chuk_tool_processor/plugins/discovery.py +105 -172
  14. chuk_tool_processor/plugins/parsers/__init__.py +1 -1
  15. chuk_tool_processor/plugins/parsers/base.py +18 -0
  16. chuk_tool_processor/plugins/parsers/function_call_tool_plugin.py +81 -0
  17. chuk_tool_processor/plugins/parsers/json_tool_plugin.py +38 -0
  18. chuk_tool_processor/plugins/parsers/openai_tool_plugin.py +76 -0
  19. chuk_tool_processor/plugins/parsers/xml_tool.py +28 -24
  20. chuk_tool_processor/registry/__init__.py +11 -10
  21. chuk_tool_processor/registry/auto_register.py +125 -0
  22. chuk_tool_processor/registry/provider.py +84 -29
  23. chuk_tool_processor/registry/providers/memory.py +77 -112
  24. chuk_tool_processor/registry/tool_export.py +76 -0
  25. chuk_tool_processor/utils/validation.py +106 -177
  26. {chuk_tool_processor-0.1.0.dist-info → chuk_tool_processor-0.1.2.dist-info}/METADATA +5 -2
  27. chuk_tool_processor-0.1.2.dist-info/RECORD +47 -0
  28. chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -105
  29. chuk_tool_processor/plugins/parsers/json_tool.py +0 -17
  30. chuk_tool_processor/utils/logging.py +0 -260
  31. chuk_tool_processor-0.1.0.dist-info/RECORD +0 -37
  32. {chuk_tool_processor-0.1.0.dist-info → chuk_tool_processor-0.1.2.dist-info}/WHEEL +0 -0
  33. {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
- import re
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
- # tool processor
13
+ # imports
14
+ from .base import ParserPlugin
8
15
  from chuk_tool_processor.models.tool_call import ToolCall
9
16
 
10
- class XmlToolPlugin:
11
- """
12
- Parse XML-like `<tool name="..." args='{"x":1}'/>` constructs,
13
- supporting both single- and double-quoted attributes.
14
- """
15
- _pattern = re.compile(
16
- r'<tool\s+'
17
- r'name=(?P<q1>["\'])(?P<tool>.+?)(?P=q1)\s+'
18
- r'args=(?P<q2>["\'])(?P<args>.*?)(?P=q2)\s*/>'
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: str) -> List[ToolCall]:
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._pattern.finditer(raw):
24
- tool_name = m.group('tool')
25
- raw_args = m.group('args')
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 (json.JSONDecodeError, ValidationError):
38
+ except json.JSONDecodeError:
30
39
  args = {}
31
40
 
32
- # Validate & construct the ToolCall
33
41
  try:
34
- call = ToolCall(tool=tool_name, arguments=args)
35
- calls.append(call)
42
+ calls.append(ToolCall(tool=tool_name, arguments=args))
36
43
  except ValidationError:
37
- # Skip malformed calls
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
- # Create and expose the default registry
11
- default_registry = get_registry()
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
- 'ToolRegistryInterface',
15
- 'ToolMetadata',
16
- 'ToolRegistryProvider',
17
- 'register_tool',
18
- 'default_registry',
19
- 'get_registry',
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
- Registry provider that maintains a global tool registry.
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
- # imports
8
- from chuk_tool_processor.registry.interface import ToolRegistryInterface
9
- from chuk_tool_processor.registry.providers import get_registry
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
- class ToolRegistryProvider:
43
+
44
+ def get_registry() -> ToolRegistryInterface:
13
45
  """
14
- Global provider for a ToolRegistryInterface implementation.
15
- Use `set_registry` to override (e.g., for testing).
16
-
17
- This class provides a singleton-like access to a registry implementation,
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
- # Initialize with default registry
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
- @classmethod
25
- def get_registry(cls) -> ToolRegistryInterface:
77
+ # ------------------------ public API ------------------------ #
78
+ @staticmethod
79
+ def get_registry() -> ToolRegistryInterface:
26
80
  """
27
- Get the current registry instance.
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 cls._registry is None:
33
- cls._registry = get_registry()
34
- return cls._registry
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
- @classmethod
37
- def set_registry(cls, registry: ToolRegistryInterface) -> None:
89
+ @staticmethod
90
+ def set_registry(registry: ToolRegistryInterface | None) -> None:
38
91
  """
39
- Set the global registry instance.
40
-
41
- Args:
42
- registry: The registry instance to use.
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
- cls._registry = registry
99
+ ToolRegistryProvider._registry = registry