chuk-tool-processor 0.1.6__py3-none-any.whl → 0.1.7__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.

Files changed (45) hide show
  1. chuk_tool_processor/core/processor.py +345 -132
  2. chuk_tool_processor/execution/strategies/inprocess_strategy.py +512 -68
  3. chuk_tool_processor/execution/strategies/subprocess_strategy.py +523 -63
  4. chuk_tool_processor/execution/tool_executor.py +282 -24
  5. chuk_tool_processor/execution/wrappers/caching.py +465 -123
  6. chuk_tool_processor/execution/wrappers/rate_limiting.py +199 -86
  7. chuk_tool_processor/execution/wrappers/retry.py +133 -23
  8. chuk_tool_processor/logging/__init__.py +83 -10
  9. chuk_tool_processor/logging/context.py +218 -22
  10. chuk_tool_processor/logging/formatter.py +56 -13
  11. chuk_tool_processor/logging/helpers.py +91 -16
  12. chuk_tool_processor/logging/metrics.py +75 -6
  13. chuk_tool_processor/mcp/mcp_tool.py +80 -35
  14. chuk_tool_processor/mcp/register_mcp_tools.py +74 -56
  15. chuk_tool_processor/mcp/setup_mcp_sse.py +41 -36
  16. chuk_tool_processor/mcp/setup_mcp_stdio.py +39 -37
  17. chuk_tool_processor/models/execution_strategy.py +52 -3
  18. chuk_tool_processor/models/streaming_tool.py +110 -0
  19. chuk_tool_processor/models/tool_call.py +56 -4
  20. chuk_tool_processor/models/tool_result.py +115 -9
  21. chuk_tool_processor/models/validated_tool.py +15 -13
  22. chuk_tool_processor/plugins/discovery.py +115 -70
  23. chuk_tool_processor/plugins/parsers/base.py +13 -5
  24. chuk_tool_processor/plugins/parsers/{function_call_tool_plugin.py → function_call_tool.py} +39 -20
  25. chuk_tool_processor/plugins/parsers/json_tool.py +50 -0
  26. chuk_tool_processor/plugins/parsers/openai_tool.py +88 -0
  27. chuk_tool_processor/plugins/parsers/xml_tool.py +74 -20
  28. chuk_tool_processor/registry/__init__.py +46 -7
  29. chuk_tool_processor/registry/auto_register.py +92 -28
  30. chuk_tool_processor/registry/decorators.py +134 -11
  31. chuk_tool_processor/registry/interface.py +48 -14
  32. chuk_tool_processor/registry/metadata.py +52 -6
  33. chuk_tool_processor/registry/provider.py +75 -36
  34. chuk_tool_processor/registry/providers/__init__.py +49 -10
  35. chuk_tool_processor/registry/providers/memory.py +59 -48
  36. chuk_tool_processor/registry/tool_export.py +208 -39
  37. chuk_tool_processor/utils/validation.py +18 -13
  38. chuk_tool_processor-0.1.7.dist-info/METADATA +401 -0
  39. chuk_tool_processor-0.1.7.dist-info/RECORD +58 -0
  40. {chuk_tool_processor-0.1.6.dist-info → chuk_tool_processor-0.1.7.dist-info}/WHEEL +1 -1
  41. chuk_tool_processor/plugins/parsers/json_tool_plugin.py +0 -38
  42. chuk_tool_processor/plugins/parsers/openai_tool_plugin.py +0 -76
  43. chuk_tool_processor-0.1.6.dist-info/METADATA +0 -462
  44. chuk_tool_processor-0.1.6.dist-info/RECORD +0 -57
  45. {chuk_tool_processor-0.1.6.dist-info → chuk_tool_processor-0.1.7.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,6 @@
1
+ # chuk_tool_processor/registry/provider.py
1
2
  """
2
- Global access to *the* tool registry instance.
3
+ Global access to the async tool registry instance.
3
4
 
4
5
  There are two public faces:
5
6
 
@@ -8,92 +9,130 @@ There are two public faces:
8
9
  and memoises it in the module-level variable ``_REGISTRY``.
9
10
  • `set_registry()` lets callers replace or reset that singleton.
10
11
 
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**.
12
+ 2. **`ToolRegistryProvider` class**
13
+ Provides static methods for async-safe access to the registry.
17
14
 
18
15
  The contract verified by the test-suite is:
19
16
 
20
17
  * 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.
18
+ * `await ToolRegistryProvider.set_registry(obj)` overrides subsequent retrievals.
19
+ * `await ToolRegistryProvider.set_registry(None)` resets the cache so the next
20
+ `await get_registry()` call invokes (and honours any monkey-patched) factory.
24
21
  """
25
22
  from __future__ import annotations
26
23
 
27
- from typing import Optional
24
+ import asyncio
25
+ import importlib
26
+ import sys
27
+ from typing import Optional, Callable, Awaitable, Dict, Any
28
28
 
29
+ # registry
29
30
  from .interface import ToolRegistryInterface
30
- from .providers.memory import InMemoryToolRegistry
31
31
 
32
32
  # --------------------------------------------------------------------------- #
33
33
  # Module-level singleton used by the helper functions
34
34
  # --------------------------------------------------------------------------- #
35
35
  _REGISTRY: Optional[ToolRegistryInterface] = None
36
+ _REGISTRY_LOCK = asyncio.Lock()
36
37
  # --------------------------------------------------------------------------- #
37
38
 
38
39
 
39
- def _default_registry() -> ToolRegistryInterface:
40
- """Create the default in-memory registry."""
40
+ async def _default_registry() -> ToolRegistryInterface:
41
+ """Create the default in-memory registry asynchronously."""
42
+ # Import here to avoid circular import
43
+ from .providers.memory import InMemoryToolRegistry
41
44
  return InMemoryToolRegistry()
42
45
 
43
46
 
44
- def get_registry() -> ToolRegistryInterface:
47
+ async def get_registry() -> ToolRegistryInterface:
45
48
  """
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.
49
+ Return the process-wide registry asynchronously, creating it on first use.
50
+
51
+ This function is thread-safe and will only create the registry once,
52
+ even with concurrent calls.
50
53
  """
51
54
  global _REGISTRY
52
55
  if _REGISTRY is None:
53
- _REGISTRY = _default_registry()
56
+ async with _REGISTRY_LOCK:
57
+ # Double-check pattern: check again after acquiring the lock
58
+ if _REGISTRY is None:
59
+ _REGISTRY = await _default_registry()
54
60
  return _REGISTRY
55
61
 
56
62
 
57
- def set_registry(registry: ToolRegistryInterface | None) -> None:
63
+ async def set_registry(registry: ToolRegistryInterface | None) -> None:
58
64
  """
59
- Replace or clear the global registry.
65
+ Replace or clear the global registry asynchronously.
60
66
 
61
67
  Passing ``None`` resets the singleton so that the next `get_registry()`
62
68
  call recreates it (useful in tests).
63
69
  """
64
70
  global _REGISTRY
65
- _REGISTRY = registry
71
+ async with _REGISTRY_LOCK:
72
+ _REGISTRY = registry
66
73
 
67
74
 
68
75
  # --------------------------------------------------------------------------- #
69
- # Back-compat shim used by legacy import paths and the test-suite
76
+ # Provider class for consistent access to the registry
70
77
  # --------------------------------------------------------------------------- #
71
- class ToolRegistryProvider: # noqa: D401
72
- """Legacy static wrapper retaining historical semantics."""
78
+ class ToolRegistryProvider:
79
+ """Async static wrapper for registry access."""
73
80
 
74
- # The test-suite directly mutates this attribute, so we keep it.
81
+ # Thread-safe singleton management
75
82
  _registry: Optional[ToolRegistryInterface] = None
83
+ _lock = asyncio.Lock()
76
84
 
77
85
  # ------------------------ public API ------------------------ #
78
86
  @staticmethod
79
- def get_registry() -> ToolRegistryInterface:
87
+ async def get_registry() -> ToolRegistryInterface:
80
88
  """
81
- Return the cached instance or, if absent, call the *current*
82
- module-level `get_registry()` exactly once to populate it.
89
+ Return the cached instance or initialize a new one asynchronously.
90
+
91
+ This method ensures thread-safety when initializing the registry.
83
92
  """
84
93
  if ToolRegistryProvider._registry is None:
85
- # Honour any runtime monkey-patching of the factory.
86
- ToolRegistryProvider._registry = globals()["get_registry"]()
94
+ async with ToolRegistryProvider._lock:
95
+ # Check again after acquiring the lock
96
+ if ToolRegistryProvider._registry is None:
97
+ # Dynamically import to get the latest definition
98
+ module = sys.modules[__name__]
99
+ get_registry_func: Callable[[], Awaitable[ToolRegistryInterface]] = getattr(
100
+ module, "get_registry"
101
+ )
102
+ # Call it to get the registry
103
+ ToolRegistryProvider._registry = await get_registry_func()
104
+
87
105
  return ToolRegistryProvider._registry
88
106
 
89
107
  @staticmethod
90
- def set_registry(registry: ToolRegistryInterface | None) -> None:
108
+ async def set_registry(registry: ToolRegistryInterface | None) -> None:
91
109
  """
92
- Override the cached registry.
110
+ Override the cached registry asynchronously.
93
111
 
94
112
  * If ``registry`` is an object, all subsequent `get_registry()`
95
113
  calls return it without touching the factory.
96
114
  * If ``registry`` is ``None``, the cache is cleared so the next
97
- `get_registry()` call invokes the (possibly patched) factory.
115
+ `get_registry()` call invokes the factory.
116
+ """
117
+ async with ToolRegistryProvider._lock:
118
+ ToolRegistryProvider._registry = registry
119
+
120
+ @staticmethod
121
+ async def reset() -> None:
122
+ """
123
+ Reset both the module-level and class-level registry caches.
124
+
125
+ This is primarily used in tests to ensure a clean state.
126
+ """
127
+ async with ToolRegistryProvider._lock:
128
+ ToolRegistryProvider._registry = None
129
+ await set_registry(None)
130
+
131
+ @staticmethod
132
+ async def get_global_registry() -> ToolRegistryInterface:
133
+ """
134
+ Get the module-level registry directly.
135
+
136
+ This bypasses the class-level cache and always returns the module-level registry.
98
137
  """
99
- ToolRegistryProvider._registry = registry
138
+ return await get_registry()
@@ -1,20 +1,29 @@
1
+ # chuk_tool_processor/registry/providers/__init__.py
1
2
  """
2
- Registry provider implementations and factory functions.
3
+ Async registry provider implementations and factory functions.
3
4
  """
4
5
 
5
6
  import os
6
- from typing import Optional
7
+ import asyncio
8
+ from typing import Optional, Dict, Any
7
9
 
8
10
  from chuk_tool_processor.registry.interface import ToolRegistryInterface
9
- from chuk_tool_processor.registry.providers.memory import InMemoryToolRegistry
10
11
 
12
+ # Cache for initialized registries
13
+ _REGISTRY_CACHE: Dict[str, ToolRegistryInterface] = {}
14
+ _REGISTRY_LOCKS: Dict[str, asyncio.Lock] = {}
11
15
 
12
- def get_registry(
16
+
17
+ async def get_registry(
13
18
  provider_type: Optional[str] = None,
14
19
  **kwargs
15
20
  ) -> ToolRegistryInterface:
16
21
  """
17
- Factory function to get a registry implementation.
22
+ Factory function to get a registry implementation asynchronously.
23
+
24
+ This function caches registry instances by provider_type to avoid
25
+ creating multiple instances unnecessarily. The cache is protected
26
+ by locks to ensure thread safety.
18
27
 
19
28
  Args:
20
29
  provider_type: Type of registry provider to use. Options:
@@ -34,8 +43,38 @@ def get_registry(
34
43
  if provider_type is None:
35
44
  provider_type = os.environ.get("CHUK_TOOL_REGISTRY_PROVIDER", "memory")
36
45
 
37
- # Create the appropriate provider
38
- if provider_type == "memory":
39
- return InMemoryToolRegistry()
40
- else:
41
- raise ValueError(f"Unknown registry provider type: {provider_type}")
46
+ # Check cache first
47
+ cache_key = f"{provider_type}:{hash(frozenset(kwargs.items()))}"
48
+ if cache_key in _REGISTRY_CACHE:
49
+ return _REGISTRY_CACHE[cache_key]
50
+
51
+ # Create lock if needed
52
+ if cache_key not in _REGISTRY_LOCKS:
53
+ _REGISTRY_LOCKS[cache_key] = asyncio.Lock()
54
+
55
+ # Acquire lock to ensure only one registry is created
56
+ async with _REGISTRY_LOCKS[cache_key]:
57
+ # Double-check pattern: check cache again after acquiring lock
58
+ if cache_key in _REGISTRY_CACHE:
59
+ return _REGISTRY_CACHE[cache_key]
60
+
61
+ # Create the appropriate provider
62
+ if provider_type == "memory":
63
+ # Import here to avoid circular imports
64
+ from chuk_tool_processor.registry.providers.memory import InMemoryToolRegistry
65
+ registry = InMemoryToolRegistry()
66
+ else:
67
+ raise ValueError(f"Unknown registry provider type: {provider_type}")
68
+
69
+ # Cache the registry
70
+ _REGISTRY_CACHE[cache_key] = registry
71
+ return registry
72
+
73
+
74
+ async def clear_registry_cache() -> None:
75
+ """
76
+ Clear the registry cache.
77
+
78
+ This is useful in tests or when configuration changes.
79
+ """
80
+ _REGISTRY_CACHE.clear()
@@ -1,11 +1,10 @@
1
1
  # chuk_tool_processor/registry/providers/memory.py
2
- # chuk_tool_processor/registry/providers/memory.py
3
2
  """
4
- In-memory implementation of the tool registry.
3
+ In-memory implementation of the asynchronous tool registry.
5
4
  """
6
-
7
5
  from __future__ import annotations
8
6
 
7
+ import asyncio
9
8
  import inspect
10
9
  from typing import Any, Dict, List, Optional, Tuple
11
10
 
@@ -16,9 +15,10 @@ from chuk_tool_processor.registry.metadata import ToolMetadata
16
15
 
17
16
  class InMemoryToolRegistry(ToolRegistryInterface):
18
17
  """
19
- In-memory implementation of ToolRegistryInterface with namespace support.
18
+ In-memory implementation of the async ToolRegistryInterface with namespace support.
20
19
 
21
20
  Suitable for single-process apps or tests; not persisted across processes.
21
+ Thread-safe with asyncio locking.
22
22
  """
23
23
 
24
24
  # ------------------------------------------------------------------ #
@@ -30,72 +30,80 @@ class InMemoryToolRegistry(ToolRegistryInterface):
30
30
  self._tools: Dict[str, Dict[str, Any]] = {}
31
31
  # {namespace: {tool_name: ToolMetadata}}
32
32
  self._metadata: Dict[str, Dict[str, ToolMetadata]] = {}
33
+ # Lock for thread safety
34
+ self._lock = asyncio.Lock()
33
35
 
34
36
  # ------------------------------------------------------------------ #
35
37
  # registration
36
38
  # ------------------------------------------------------------------ #
37
39
 
38
- def register_tool(
40
+ async def register_tool(
39
41
  self,
40
42
  tool: Any,
41
43
  name: Optional[str] = None,
42
44
  namespace: str = "default",
43
45
  metadata: Optional[Dict[str, Any]] = None,
44
46
  ) -> None:
45
- # ensure namespace buckets
46
- self._tools.setdefault(namespace, {})
47
- self._metadata.setdefault(namespace, {})
48
-
49
- key = name or getattr(tool, "__name__", None) or repr(tool)
50
- self._tools[namespace][key] = tool
51
-
52
- # build metadata -------------------------------------------------
53
- is_async = inspect.iscoroutinefunction(getattr(tool, "execute", None))
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] = {
63
- "name": key,
64
- "namespace": namespace,
65
- "is_async": is_async,
66
- }
67
- if description:
68
- meta_dict["description"] = description
69
- if metadata:
70
- meta_dict.update(metadata)
71
-
72
- self._metadata[namespace][key] = ToolMetadata(**meta_dict)
47
+ """Register a tool in the registry asynchronously."""
48
+ async with self._lock:
49
+ # ensure namespace buckets
50
+ self._tools.setdefault(namespace, {})
51
+ self._metadata.setdefault(namespace, {})
52
+
53
+ key = name or getattr(tool, "__name__", None) or repr(tool)
54
+ self._tools[namespace][key] = tool
55
+
56
+ # build metadata -------------------------------------------------
57
+ is_async = inspect.iscoroutinefunction(getattr(tool, "execute", None))
58
+
59
+ # default description -> docstring
60
+ description = (
61
+ (inspect.getdoc(tool) or "").strip()
62
+ if not (metadata and "description" in metadata)
63
+ else None
64
+ )
65
+
66
+ meta_dict: Dict[str, Any] = {
67
+ "name": key,
68
+ "namespace": namespace,
69
+ "is_async": is_async,
70
+ }
71
+ if description:
72
+ meta_dict["description"] = description
73
+ if metadata:
74
+ meta_dict.update(metadata)
75
+
76
+ self._metadata[namespace][key] = ToolMetadata(**meta_dict)
73
77
 
74
78
  # ------------------------------------------------------------------ #
75
79
  # retrieval
76
80
  # ------------------------------------------------------------------ #
77
81
 
78
- def get_tool(self, name: str, namespace: str = "default") -> Optional[Any]:
82
+ async def get_tool(self, name: str, namespace: str = "default") -> Optional[Any]:
83
+ """Retrieve a tool by name and namespace asynchronously."""
84
+ # Read operations don't need locking for better concurrency
79
85
  return self._tools.get(namespace, {}).get(name)
80
86
 
81
- def get_tool_strict(self, name: str, namespace: str = "default") -> Any:
82
- tool = self.get_tool(name, namespace)
87
+ async def get_tool_strict(self, name: str, namespace: str = "default") -> Any:
88
+ """Get a tool with strict validation, raising if not found."""
89
+ tool = await self.get_tool(name, namespace)
83
90
  if tool is None:
84
91
  raise ToolNotFoundError(f"{namespace}.{name}")
85
92
  return tool
86
93
 
87
- def get_metadata(
94
+ async def get_metadata(
88
95
  self, name: str, namespace: str = "default"
89
96
  ) -> Optional[ToolMetadata]:
97
+ """Get metadata for a tool asynchronously."""
90
98
  return self._metadata.get(namespace, {}).get(name)
91
99
 
92
100
  # ------------------------------------------------------------------ #
93
101
  # listing helpers
94
102
  # ------------------------------------------------------------------ #
95
103
 
96
- def list_tools(self, namespace: Optional[str] = None) -> List[Tuple[str, str]]:
104
+ async def list_tools(self, namespace: Optional[str] = None) -> List[Tuple[str, str]]:
97
105
  """
98
- Return a list of ``(namespace, name)`` tuples.
106
+ Return a list of ``(namespace, name)`` tuples asynchronously.
99
107
  """
100
108
  if namespace:
101
109
  return [
@@ -107,18 +115,21 @@ class InMemoryToolRegistry(ToolRegistryInterface):
107
115
  result.extend((ns, n) for n in tools.keys())
108
116
  return result
109
117
 
110
- def list_namespaces(self) -> List[str]:
118
+ async def list_namespaces(self) -> List[str]:
119
+ """List all namespaces asynchronously."""
111
120
  return list(self._tools.keys())
112
121
 
113
- def list_metadata(self, namespace: str | None = None) -> List[ToolMetadata]:
122
+ async def list_metadata(self, namespace: Optional[str] = None) -> List[ToolMetadata]:
114
123
  """
115
- Return *all* :class:`ToolMetadata` objects.
124
+ Return all ToolMetadata objects asynchronously.
116
125
 
117
- Parameters
118
- ----------
119
- namespace
120
- ``None`` *(default)* metadata from **all** namespaces
121
- • ``"some_ns"`` – only that namespace
126
+ Args:
127
+ namespace: Optional filter by namespace.
128
+ • None (default) – metadata from all namespaces
129
+ "some_ns"only that namespace
130
+
131
+ Returns:
132
+ List of ToolMetadata objects.
122
133
  """
123
134
  if namespace is not None:
124
135
  return list(self._metadata.get(namespace, {}).values())
@@ -127,4 +138,4 @@ class InMemoryToolRegistry(ToolRegistryInterface):
127
138
  result: List[ToolMetadata] = []
128
139
  for ns_meta in self._metadata.values():
129
140
  result.extend(ns_meta.values())
130
- return result
141
+ return result