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
@@ -1,8 +1,11 @@
1
1
  # chuk_tool_processor/registry/providers/memory.py
2
+ # chuk_tool_processor/registry/providers/memory.py
2
3
  """
3
4
  In-memory implementation of the tool registry.
4
5
  """
5
6
 
7
+ from __future__ import annotations
8
+
6
9
  import inspect
7
10
  from typing import Any, Dict, List, Optional, Tuple
8
11
 
@@ -14,152 +17,114 @@ from chuk_tool_processor.registry.metadata import ToolMetadata
14
17
  class InMemoryToolRegistry(ToolRegistryInterface):
15
18
  """
16
19
  In-memory implementation of ToolRegistryInterface with namespace support.
17
-
18
- This implementation stores tools and their metadata in memory,
19
- organized by namespace. It's suitable for single-process applications
20
- or for testing, but doesn't provide persistence or sharing across
21
- multiple processes.
20
+
21
+ Suitable for single-process apps or tests; not persisted across processes.
22
22
  """
23
- def __init__(self):
24
- """Initialize the in-memory registry."""
25
- # Store tools as {namespace: {name: tool}}
23
+
24
+ # ------------------------------------------------------------------ #
25
+ # construction
26
+ # ------------------------------------------------------------------ #
27
+
28
+ def __init__(self) -> None:
29
+ # {namespace: {tool_name: tool_obj}}
26
30
  self._tools: Dict[str, Dict[str, Any]] = {}
27
- # Store metadata as {namespace: {name: metadata}}
31
+ # {namespace: {tool_name: ToolMetadata}}
28
32
  self._metadata: Dict[str, Dict[str, ToolMetadata]] = {}
29
33
 
34
+ # ------------------------------------------------------------------ #
35
+ # registration
36
+ # ------------------------------------------------------------------ #
37
+
30
38
  def register_tool(
31
- self,
32
- tool: Any,
39
+ self,
40
+ tool: Any,
33
41
  name: Optional[str] = None,
34
42
  namespace: str = "default",
35
- metadata: Optional[Dict[str, Any]] = None
43
+ metadata: Optional[Dict[str, Any]] = None,
36
44
  ) -> None:
37
- """
38
- Register a tool implementation.
45
+ # ensure namespace buckets
46
+ self._tools.setdefault(namespace, {})
47
+ self._metadata.setdefault(namespace, {})
39
48
 
40
- Args:
41
- tool: The tool class or instance with an `execute` method.
42
- name: Optional explicit name; if omitted, uses tool.__name__.
43
- namespace: Namespace for the tool (default: "default").
44
- metadata: Optional additional metadata for the tool.
45
- """
46
- # Ensure the namespace exists
47
- if namespace not in self._tools:
48
- self._tools[namespace] = {}
49
- self._metadata[namespace] = {}
50
-
51
- # Determine tool name
52
49
  key = name or getattr(tool, "__name__", None) or repr(tool)
53
-
54
- # Register the tool
55
50
  self._tools[namespace][key] = tool
56
-
57
- # Create and store metadata
51
+
52
+ # build metadata -------------------------------------------------
58
53
  is_async = inspect.iscoroutinefunction(getattr(tool, "execute", None))
59
-
60
- # Get description from docstring if available
61
- description = None
62
- if hasattr(tool, "__doc__") and tool.__doc__:
63
- description = inspect.getdoc(tool)
64
-
65
- # Create metadata object
66
- meta_dict = {
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] = {
67
63
  "name": key,
68
64
  "namespace": namespace,
69
- "is_async": is_async
65
+ "is_async": is_async,
70
66
  }
71
-
72
- # Add description if available (but don't override metadata if provided)
73
- if description and not (metadata and "description" in metadata):
67
+ if description:
74
68
  meta_dict["description"] = description
75
-
76
- # Add any additional metadata
77
69
  if metadata:
78
70
  meta_dict.update(metadata)
79
-
80
- tool_metadata = ToolMetadata(**meta_dict)
81
-
82
- self._metadata[namespace][key] = tool_metadata
71
+
72
+ self._metadata[namespace][key] = ToolMetadata(**meta_dict)
73
+
74
+ # ------------------------------------------------------------------ #
75
+ # retrieval
76
+ # ------------------------------------------------------------------ #
83
77
 
84
78
  def get_tool(self, name: str, namespace: str = "default") -> Optional[Any]:
85
- """
86
- Retrieve a registered tool by name and namespace.
87
-
88
- Args:
89
- name: The name of the tool.
90
- namespace: The namespace of the tool (default: "default").
91
-
92
- Returns:
93
- The tool implementation or None if not found.
94
- """
95
- if namespace not in self._tools:
96
- return None
97
- return self._tools[namespace].get(name)
79
+ return self._tools.get(namespace, {}).get(name)
98
80
 
99
81
  def get_tool_strict(self, name: str, namespace: str = "default") -> Any:
100
- """
101
- Retrieve a registered tool by name and namespace, raising an exception if not found.
102
-
103
- Args:
104
- name: The name of the tool.
105
- namespace: The namespace of the tool (default: "default").
106
-
107
- Returns:
108
- The tool implementation.
109
-
110
- Raises:
111
- ToolNotFoundError: If the tool is not found.
112
- """
113
82
  tool = self.get_tool(name, namespace)
114
83
  if tool is None:
115
84
  raise ToolNotFoundError(f"{namespace}.{name}")
116
85
  return tool
117
86
 
118
- def get_metadata(self, name: str, namespace: str = "default") -> Optional[ToolMetadata]:
119
- """
120
- Retrieve metadata for a registered tool.
121
-
122
- Args:
123
- name: The name of the tool.
124
- namespace: The namespace of the tool (default: "default").
125
-
126
- Returns:
127
- ToolMetadata if found, None otherwise.
128
- """
129
- if namespace not in self._metadata:
130
- return None
131
- return self._metadata[namespace].get(name)
87
+ def get_metadata(
88
+ self, name: str, namespace: str = "default"
89
+ ) -> Optional[ToolMetadata]:
90
+ return self._metadata.get(namespace, {}).get(name)
91
+
92
+ # ------------------------------------------------------------------ #
93
+ # listing helpers
94
+ # ------------------------------------------------------------------ #
132
95
 
133
96
  def list_tools(self, namespace: Optional[str] = None) -> List[Tuple[str, str]]:
134
97
  """
135
- List all registered tool names, optionally filtered by namespace.
136
-
137
- Args:
138
- namespace: Optional namespace filter.
139
-
140
- Returns:
141
- List of (namespace, name) tuples.
98
+ Return a list of ``(namespace, name)`` tuples.
142
99
  """
143
- result = []
144
-
145
100
  if namespace:
146
- # List tools in specific namespace
147
- if namespace in self._tools:
148
- for name in self._tools[namespace].keys():
149
- result.append((namespace, name))
150
- else:
151
- # List all tools
152
- for ns, tools in self._tools.items():
153
- for name in tools.keys():
154
- result.append((ns, name))
155
-
101
+ return [
102
+ (namespace, n) for n in self._tools.get(namespace, {}).keys()
103
+ ]
104
+
105
+ result: List[Tuple[str, str]] = []
106
+ for ns, tools in self._tools.items():
107
+ result.extend((ns, n) for n in tools.keys())
156
108
  return result
157
109
 
158
110
  def list_namespaces(self) -> List[str]:
111
+ return list(self._tools.keys())
112
+
113
+ def list_metadata(self, namespace: str | None = None) -> List[ToolMetadata]:
159
114
  """
160
- List all registered namespaces.
161
-
162
- Returns:
163
- List of namespace names.
115
+ Return *all* :class:`ToolMetadata` objects.
116
+
117
+ Parameters
118
+ ----------
119
+ namespace
120
+ • ``None`` *(default)* – metadata from **all** namespaces
121
+ • ``"some_ns"`` – only that namespace
164
122
  """
165
- return list(self._tools.keys())
123
+ if namespace is not None:
124
+ return list(self._metadata.get(namespace, {}).values())
125
+
126
+ # flatten
127
+ result: List[ToolMetadata] = []
128
+ for ns_meta in self._metadata.values():
129
+ result.extend(ns_meta.values())
130
+ return result
@@ -0,0 +1,76 @@
1
+ # chuk_tool_processor/registry/tool_export.py
2
+ """
3
+ Helpers that expose all registered tools in various formats and
4
+ translate an OpenAI `function.name` back to the matching tool.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ from typing import Dict, List
9
+
10
+ from .provider import ToolRegistryProvider
11
+
12
+ # --------------------------------------------------------------------------- #
13
+ # internal cache so tool-name lookup is O(1)
14
+ # --------------------------------------------------------------------------- #
15
+ _OPENAI_NAME_CACHE: dict[str, object] | None = None
16
+
17
+
18
+ def _build_openai_name_cache() -> None:
19
+ """Populate the global reverse-lookup table once."""
20
+ global _OPENAI_NAME_CACHE
21
+ if _OPENAI_NAME_CACHE is not None: # already built
22
+ return
23
+
24
+ _OPENAI_NAME_CACHE = {}
25
+ reg = ToolRegistryProvider.get_registry()
26
+
27
+ for ns, key in reg.list_tools():
28
+ tool = reg.get_tool(key, ns)
29
+
30
+ # ▸ registry key -> tool
31
+ _OPENAI_NAME_CACHE[key] = tool
32
+
33
+ # ▸ class name -> tool (legacy)
34
+ _OPENAI_NAME_CACHE[tool.__class__.__name__] = tool
35
+
36
+ # ▸ OpenAI name -> tool (may differ from both above)
37
+ _OPENAI_NAME_CACHE[tool.to_openai()["function"]["name"]] = tool
38
+
39
+
40
+ # --------------------------------------------------------------------------- #
41
+ # public helpers
42
+ # --------------------------------------------------------------------------- #
43
+ def openai_functions() -> List[Dict]:
44
+ """
45
+ Return **all** registered tools in the exact schema the Chat-Completions
46
+ API expects in its ``tools=[ … ]`` parameter.
47
+
48
+ The ``function.name`` is always the *registry key* so that the round-trip
49
+ (export → model → parser) stays consistent even when the class name and
50
+ the registered key differ.
51
+ """
52
+ reg = ToolRegistryProvider.get_registry()
53
+ specs: list[dict] = []
54
+
55
+ for ns, key in reg.list_tools():
56
+ tool = reg.get_tool(key, ns)
57
+ spec = tool.to_openai()
58
+ spec["function"]["name"] = key # ensure round-trip consistency
59
+ specs.append(spec)
60
+
61
+ # Ensure the cache is built the first time we export
62
+ _build_openai_name_cache()
63
+ return specs
64
+
65
+
66
+ def tool_by_openai_name(name: str):
67
+ """
68
+ Map an OpenAI ``function.name`` back to the registered tool.
69
+
70
+ Raises ``KeyError`` if the name is unknown.
71
+ """
72
+ _build_openai_name_cache()
73
+ try:
74
+ return _OPENAI_NAME_CACHE[name] # type: ignore[index]
75
+ except (KeyError, TypeError):
76
+ raise KeyError(f"No tool registered for OpenAI name {name!r}") from None
@@ -1,192 +1,121 @@
1
1
  # chuk_tool_processor/utils/validation.py
2
- from typing import Any, Dict, Optional, Type, get_type_hints, Union, List, Callable
3
- from pydantic import BaseModel, ValidationError, create_model
2
+ """
3
+ Runtime helpers for validating tool inputs / outputs with Pydantic.
4
+
5
+ Public API
6
+ ----------
7
+ validate_arguments(tool_name, fn, args) -> dict
8
+ validate_result(tool_name, fn, result) -> Any
9
+ @with_validation -> class decorator
10
+ """
11
+ from __future__ import annotations
4
12
  import inspect
5
- from functools import wraps
13
+ from functools import lru_cache, wraps
14
+ from typing import Any, Callable, Dict, get_type_hints
15
+ from pydantic import BaseModel, ValidationError, create_model, Extra
6
16
 
17
+ # excpetion
7
18
  from chuk_tool_processor.core.exceptions import ToolValidationError
8
19
 
20
+ __all__ = [
21
+ "validate_arguments",
22
+ "validate_result",
23
+ "with_validation",
24
+ ]
9
25
 
10
- def validate_arguments(tool_name: str, tool_func: Callable, args: Dict[str, Any]) -> Dict[str, Any]:
11
- """
12
- Validate tool arguments against function signature.
13
-
14
- Args:
15
- tool_name: Name of the tool for error reporting.
16
- tool_func: Tool function to validate against.
17
- args: Arguments to validate.
18
-
19
- Returns:
20
- Validated arguments dict.
21
-
22
- Raises:
23
- ToolValidationError: If validation fails.
24
- """
26
+ # --------------------------------------------------------------------------- #
27
+ # helpers – create & cache ad-hoc pydantic models
28
+ # --------------------------------------------------------------------------- #
29
+
30
+
31
+ @lru_cache(maxsize=256)
32
+ def _arg_model(tool_name: str, fn: Callable) -> type[BaseModel]:
33
+ """Return (and memoise) a pydantic model derived from *fn*'s signature."""
34
+ hints = get_type_hints(fn)
35
+ hints.pop("return", None)
36
+
37
+ sig = inspect.signature(fn)
38
+ fields: Dict[str, tuple[Any, Any]] = {}
39
+ for name, hint in hints.items():
40
+ param = sig.parameters[name]
41
+ default = param.default if param.default is not inspect.Parameter.empty else ...
42
+ fields[name] = (hint, default)
43
+
44
+ return create_model(
45
+ f"{tool_name}Args",
46
+ __config__=type(
47
+ "Cfg",
48
+ (),
49
+ {"extra": Extra.forbid}, # disallow unknown keys
50
+ ),
51
+ **fields,
52
+ )
53
+
54
+
55
+ @lru_cache(maxsize=256)
56
+ def _result_model(tool_name: str, fn: Callable) -> type[BaseModel] | None:
57
+ """Return a pydantic model for the annotated return type (or None)."""
58
+ return_hint = get_type_hints(fn).get("return")
59
+ if return_hint is None or return_hint is type(None): # noqa: E721
60
+ return None
61
+
62
+ return create_model(
63
+ f"{tool_name}Result",
64
+ result=(return_hint, ...),
65
+ )
66
+
67
+
68
+ # --------------------------------------------------------------------------- #
69
+ # public validation helpers
70
+ # --------------------------------------------------------------------------- #
71
+
72
+
73
+ def validate_arguments(tool_name: str, fn: Callable, args: Dict[str, Any]) -> Dict[str, Any]:
25
74
  try:
26
- # Get type hints from function
27
- type_hints = get_type_hints(tool_func)
28
-
29
- # Remove return type hint if present
30
- if 'return' in type_hints:
31
- type_hints.pop('return')
32
-
33
- # Create dynamic Pydantic model for validation
34
- field_definitions = {
35
- name: (type_hint, ...) for name, type_hint in type_hints.items()
36
- }
37
-
38
- # Add optional fields based on default values
39
- sig = inspect.signature(tool_func)
40
- for param_name, param in sig.parameters.items():
41
- if param.default is not inspect.Parameter.empty:
42
- if param_name in field_definitions:
43
- field_type, _ = field_definitions[param_name]
44
- field_definitions[param_name] = (field_type, param.default)
45
-
46
- # Create model
47
- model = create_model(f"{tool_name}Args", **field_definitions)
48
-
49
- # Validate args
50
- validated = model(**args)
51
- return validated.dict()
52
-
53
- except ValidationError as e:
54
- raise ToolValidationError(tool_name, e.errors())
55
- except Exception as e:
56
- raise ToolValidationError(tool_name, {"general": str(e)})
57
-
58
-
59
- def validate_result(tool_name: str, tool_func: Callable, result: Any) -> Any:
60
- """
61
- Validate tool result against function return type.
62
-
63
- Args:
64
- tool_name: Name of the tool for error reporting.
65
- tool_func: Tool function to validate against.
66
- result: Result to validate.
67
-
68
- Returns:
69
- Validated result.
70
-
71
- Raises:
72
- ToolValidationError: If validation fails.
73
- """
75
+ model = _arg_model(tool_name, fn)
76
+ return model(**args).dict()
77
+ except ValidationError as exc:
78
+ raise ToolValidationError(tool_name, exc.errors()) from exc
79
+
80
+
81
+ def validate_result(tool_name: str, fn: Callable, result: Any) -> Any:
82
+ model = _result_model(tool_name, fn)
83
+ if model is None: # no annotation ⇒ no validation
84
+ return result
74
85
  try:
75
- # Get return type hint
76
- type_hints = get_type_hints(tool_func)
77
- return_type = type_hints.get('return')
78
-
79
- if return_type is None:
80
- # No return type to validate against
81
- return result
82
-
83
- # Create dynamic Pydantic model for validation
84
- model = create_model(
85
- f"{tool_name}Result",
86
- result=(return_type, ...)
87
- )
88
-
89
- # Validate result
90
- validated = model(result=result)
91
- return validated.result
92
-
93
- except ValidationError as e:
94
- raise ToolValidationError(tool_name, e.errors())
95
- except Exception as e:
96
- raise ToolValidationError(tool_name, {"general": str(e)})
86
+ return model(result=result).result
87
+ except ValidationError as exc:
88
+ raise ToolValidationError(tool_name, exc.errors()) from exc
89
+
90
+
91
+ # --------------------------------------------------------------------------- #
92
+ # decorator for classic “imperative” tools
93
+ # --------------------------------------------------------------------------- #
97
94
 
98
95
 
99
96
  def with_validation(cls):
100
97
  """
101
- Class decorator to add type validation to tool classes.
102
-
103
- Example:
104
- @with_validation
105
- class MyTool:
106
- def execute(self, x: int, y: str) -> float:
107
- return float(x) + float(y)
98
+ Wrap *execute* / *_execute* so that their arguments & return values
99
+ are type-checked each call.
100
+
101
+ ```
102
+ @with_validation
103
+ class MyTool:
104
+ def execute(self, x: int, y: int) -> int:
105
+ return x + y
106
+ ```
108
107
  """
109
- original_execute = cls.execute
110
-
111
- @wraps(original_execute)
112
- def execute_with_validation(self, **kwargs):
113
- # Get tool name
114
- tool_name = getattr(cls, "__name__", repr(cls))
115
-
116
- # Validate arguments
117
- validated_args = validate_arguments(tool_name, original_execute, kwargs)
118
-
119
- # Execute the tool
120
- result = original_execute(self, **validated_args)
121
-
122
- # Validate result
123
- return validate_result(tool_name, original_execute, result)
124
-
125
- cls.execute = execute_with_validation
126
- return cls
127
108
 
109
+ # Which method did the user provide?
110
+ fn_name = "_execute" if hasattr(cls, "_execute") else "execute"
111
+ original = getattr(cls, fn_name)
128
112
 
129
- class ValidatedTool(BaseModel):
130
- """
131
- Base class for tools with built-in validation.
132
-
133
- Example:
134
- class AddTool(ValidatedTool):
135
- class Arguments(BaseModel):
136
- x: int
137
- y: int
138
-
139
- class Result(BaseModel):
140
- sum: int
141
-
142
- def execute(self, x: int, y: int) -> Result:
143
- return self.Result(sum=x + y)
144
- """
145
- class Arguments(BaseModel):
146
- """Base arguments model to be overridden by subclasses."""
147
- pass
148
-
149
- class Result(BaseModel):
150
- """Base result model to be overridden by subclasses."""
151
- pass
152
-
153
- def execute(self, **kwargs) -> Any:
154
- """
155
- Execute the tool with validated arguments.
156
-
157
- Args:
158
- **kwargs: Arguments to validate against Arguments model.
159
-
160
- Returns:
161
- Validated result according to Result model.
162
-
163
- Raises:
164
- ToolValidationError: If validation fails.
165
- """
166
- try:
167
- # Validate arguments
168
- validated_args = self.Arguments(**kwargs)
169
-
170
- # Execute implementation
171
- result = self._execute(**validated_args.dict())
172
-
173
- # Validate result if it's not already a Result instance
174
- if not isinstance(result, self.Result):
175
- result = self.Result(**result if isinstance(result, dict) else {"value": result})
176
-
177
- return result
178
-
179
- except ValidationError as e:
180
- raise ToolValidationError(self.__class__.__name__, e.errors())
181
-
182
- def _execute(self, **kwargs) -> Any:
183
- """
184
- Implementation method to be overridden by subclasses.
185
-
186
- Args:
187
- **kwargs: Validated arguments.
188
-
189
- Returns:
190
- Result that will be validated against Result model.
191
- """
192
- raise NotImplementedError("Subclasses must implement _execute")
113
+ @wraps(original)
114
+ def _validated(self, **kwargs):
115
+ name = cls.__name__
116
+ kwargs = validate_arguments(name, original, kwargs)
117
+ res = original(self, **kwargs)
118
+ return validate_result(name, original, res)
119
+
120
+ setattr(cls, fn_name, _validated)
121
+ return cls
@@ -1,10 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chuk-tool-processor
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Add your description here
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
7
+ Requires-Dist: dotenv>=0.9.9
8
+ Requires-Dist: openai>=1.76.0
7
9
  Requires-Dist: pydantic>=2.11.3
10
+ Requires-Dist: uuid>=1.30
8
11
 
9
12
  # CHUK Tool Processor
10
13
 
@@ -222,7 +225,7 @@ plugin_registry.register_plugin("parser", "BracketToolParser", BracketToolParser
222
225
  ### Structured Logging
223
226
 
224
227
  ```python
225
- from chuk_tool_processor.utils.logging import get_logger, log_context_span
228
+ from chuk_tool_processor.logging import get_logger, log_context_span
226
229
 
227
230
  logger = get_logger("my_module")
228
231