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

@@ -0,0 +1,114 @@
1
+ """
2
+ CHUK Tool Processor - Async-native framework for processing LLM tool calls.
3
+
4
+ This package provides a production-ready framework for:
5
+ - Processing tool calls from various LLM output formats
6
+ - Executing tools with timeouts, retries, and rate limiting
7
+ - Connecting to remote MCP servers
8
+ - Caching results and circuit breaking
9
+
10
+ Quick Start:
11
+ >>> import asyncio
12
+ >>> from chuk_tool_processor import ToolProcessor
13
+ >>>
14
+ >>> async def main():
15
+ ... async with ToolProcessor() as processor:
16
+ ... llm_output = '<tool name="calculator" args=\'{"a": 5, "b": 3}\'/>'
17
+ ... results = await processor.process(llm_output)
18
+ ... print(results[0].result)
19
+ >>>
20
+ >>> asyncio.run(main())
21
+ """
22
+
23
+ from typing import TYPE_CHECKING
24
+
25
+ # Version
26
+ __version__ = "0.9.7"
27
+
28
+ # Core processor
29
+ from chuk_tool_processor.core.processor import ToolProcessor
30
+
31
+ # Execution strategies
32
+ from chuk_tool_processor.execution.strategies.inprocess_strategy import InProcessStrategy
33
+ from chuk_tool_processor.execution.strategies.subprocess_strategy import SubprocessStrategy
34
+ from chuk_tool_processor.execution.strategies.subprocess_strategy import SubprocessStrategy as IsolatedStrategy
35
+
36
+ # MCP setup helpers
37
+ from chuk_tool_processor.mcp import (
38
+ setup_mcp_http_streamable,
39
+ setup_mcp_sse,
40
+ setup_mcp_stdio,
41
+ )
42
+
43
+ # Stream manager for advanced MCP usage
44
+ from chuk_tool_processor.mcp.stream_manager import StreamManager
45
+
46
+ # Models (commonly used)
47
+ from chuk_tool_processor.models.tool_call import ToolCall
48
+ from chuk_tool_processor.models.tool_result import ToolResult
49
+
50
+ # Registry functions
51
+ from chuk_tool_processor.registry import (
52
+ ToolRegistryProvider,
53
+ get_default_registry,
54
+ initialize,
55
+ )
56
+ from chuk_tool_processor.registry.auto_register import register_fn_tool
57
+
58
+ # Decorators for registering tools
59
+ from chuk_tool_processor.registry.decorators import register_tool
60
+
61
+ # Type checking imports (not available at runtime)
62
+ if TYPE_CHECKING:
63
+ # Advanced models for type hints
64
+ # Execution strategies
65
+ from chuk_tool_processor.execution.strategies.inprocess_strategy import InProcessStrategy
66
+ from chuk_tool_processor.execution.strategies.subprocess_strategy import SubprocessStrategy
67
+
68
+ # Retry config
69
+ from chuk_tool_processor.execution.wrappers.retry import RetryConfig
70
+ from chuk_tool_processor.models.streaming_tool import StreamingTool
71
+ from chuk_tool_processor.models.tool_spec import ToolSpec
72
+ from chuk_tool_processor.models.validated_tool import ValidatedTool
73
+
74
+ # Registry interface
75
+ from chuk_tool_processor.registry.interface import ToolRegistryInterface
76
+
77
+ # Public API
78
+ __all__ = [
79
+ # Version
80
+ "__version__",
81
+ # Core classes
82
+ "ToolProcessor",
83
+ "StreamManager",
84
+ # Models
85
+ "ToolCall",
86
+ "ToolResult",
87
+ # Registry
88
+ "initialize",
89
+ "get_default_registry",
90
+ "ToolRegistryProvider",
91
+ # Decorators
92
+ "register_tool",
93
+ "register_fn_tool",
94
+ # Execution strategies
95
+ "InProcessStrategy",
96
+ "IsolatedStrategy",
97
+ "SubprocessStrategy",
98
+ # MCP setup
99
+ "setup_mcp_stdio",
100
+ "setup_mcp_sse",
101
+ "setup_mcp_http_streamable",
102
+ ]
103
+
104
+ # Type checking exports (documentation only)
105
+ if TYPE_CHECKING:
106
+ __all__ += [
107
+ "ValidatedTool",
108
+ "StreamingTool",
109
+ "ToolSpec",
110
+ "InProcessStrategy",
111
+ "SubprocessStrategy",
112
+ "ToolRegistryInterface",
113
+ "RetryConfig",
114
+ ]
@@ -34,13 +34,62 @@ from chuk_tool_processor.registry import ToolRegistryInterface, ToolRegistryProv
34
34
  class ToolProcessor:
35
35
  """
36
36
  Main class for processing tool calls from LLM responses.
37
- Combines parsing, execution, and result handling with full async support.
37
+
38
+ ToolProcessor combines parsing, execution, and result handling with full async support.
39
+ It provides production-ready features including timeouts, retries, caching, rate limiting,
40
+ and circuit breaking.
41
+
42
+ Examples:
43
+ Basic usage with context manager:
44
+
45
+ >>> import asyncio
46
+ >>> from chuk_tool_processor import ToolProcessor, register_tool
47
+ >>>
48
+ >>> @register_tool(name="calculator")
49
+ ... class Calculator:
50
+ ... async def execute(self, a: int, b: int) -> dict:
51
+ ... return {"result": a + b}
52
+ >>>
53
+ >>> async def main():
54
+ ... async with ToolProcessor() as processor:
55
+ ... llm_output = '<tool name="calculator" args=\'{"a": 5, "b": 3}\'/>'
56
+ ... results = await processor.process(llm_output)
57
+ ... print(results[0].result) # {'result': 8}
58
+ >>>
59
+ >>> asyncio.run(main())
60
+
61
+ Production configuration:
62
+
63
+ >>> processor = ToolProcessor(
64
+ ... default_timeout=30.0,
65
+ ... enable_caching=True,
66
+ ... cache_ttl=600,
67
+ ... enable_rate_limiting=True,
68
+ ... global_rate_limit=100, # 100 requests/minute
69
+ ... enable_retries=True,
70
+ ... max_retries=3,
71
+ ... enable_circuit_breaker=True,
72
+ ... )
73
+
74
+ Manual cleanup:
75
+
76
+ >>> processor = ToolProcessor()
77
+ >>> try:
78
+ ... results = await processor.process(llm_output)
79
+ ... finally:
80
+ ... await processor.close()
81
+
82
+ Attributes:
83
+ registry: Tool registry containing registered tools
84
+ strategy: Execution strategy (InProcess or Subprocess)
85
+ executor: Wrapped executor with caching, retries, etc.
86
+ parsers: List of parser plugins for extracting tool calls
38
87
  """
39
88
 
40
89
  def __init__(
41
90
  self,
42
91
  registry: ToolRegistryInterface | None = None,
43
- strategy=None,
92
+ strategy: Any | None = None, # Strategy can be InProcessStrategy or SubprocessStrategy
44
93
  default_timeout: float = 10.0,
45
94
  max_concurrency: int | None = None,
46
95
  enable_caching: bool = True,
@@ -61,21 +110,60 @@ class ToolProcessor:
61
110
 
62
111
  Args:
63
112
  registry: Tool registry to use. If None, uses the global registry.
64
- strategy: Optional execution strategy (default: InProcessStrategy)
113
+ strategy: Optional execution strategy (default: InProcessStrategy).
114
+ Use SubprocessStrategy for isolated execution of untrusted code.
65
115
  default_timeout: Default timeout for tool execution in seconds.
116
+ Individual tools can override this. Default: 10.0
66
117
  max_concurrency: Maximum number of concurrent tool executions.
67
- enable_caching: Whether to enable result caching.
68
- cache_ttl: Default cache TTL in seconds.
69
- enable_rate_limiting: Whether to enable rate limiting.
118
+ If None, uses unlimited concurrency. Default: None
119
+ enable_caching: Whether to enable result caching. Caches results
120
+ based on tool name and arguments. Default: True
121
+ cache_ttl: Default cache TTL in seconds. Results older than this
122
+ are evicted. Default: 300 (5 minutes)
123
+ enable_rate_limiting: Whether to enable rate limiting. Prevents
124
+ API abuse and quota exhaustion. Default: False
70
125
  global_rate_limit: Optional global rate limit (requests per minute).
126
+ Applies to all tools unless overridden. Default: None
71
127
  tool_rate_limits: Dict mapping tool names to (limit, period) tuples.
72
- enable_retries: Whether to enable automatic retries.
73
- max_retries: Maximum number of retry attempts.
128
+ Example: {"api_call": (10, 60)} = 10 requests per 60 seconds
129
+ enable_retries: Whether to enable automatic retries on transient
130
+ failures. Uses exponential backoff. Default: True
131
+ max_retries: Maximum number of retry attempts. Total attempts will
132
+ be max_retries + 1 (initial + retries). Default: 3
133
+ retry_config: Optional custom retry configuration. If provided,
134
+ overrides max_retries. See RetryConfig for details.
74
135
  enable_circuit_breaker: Whether to enable circuit breaker pattern.
75
- circuit_breaker_threshold: Number of failures before opening circuit.
76
- circuit_breaker_timeout: Seconds to wait before testing recovery.
77
- parser_plugins: List of parser plugin names to use.
78
- If None, uses all available parsers.
136
+ Opens circuit after repeated failures to prevent cascading
137
+ failures. Default: False
138
+ circuit_breaker_threshold: Number of consecutive failures before
139
+ opening circuit. Default: 5
140
+ circuit_breaker_timeout: Seconds to wait before attempting recovery
141
+ (transition from OPEN to HALF_OPEN). Default: 60.0
142
+ parser_plugins: List of parser plugin names to use. If None, uses
143
+ all available parsers (XML, OpenAI, JSON). Default: None
144
+
145
+ Raises:
146
+ ImportError: If required dependencies are not installed.
147
+
148
+ Example:
149
+ >>> # Production configuration with all features
150
+ >>> processor = ToolProcessor(
151
+ ... default_timeout=30.0,
152
+ ... max_concurrency=20,
153
+ ... enable_caching=True,
154
+ ... cache_ttl=600,
155
+ ... enable_rate_limiting=True,
156
+ ... global_rate_limit=100,
157
+ ... tool_rate_limits={
158
+ ... "expensive_api": (10, 60),
159
+ ... "free_api": (100, 60),
160
+ ... },
161
+ ... enable_retries=True,
162
+ ... max_retries=3,
163
+ ... enable_circuit_breaker=True,
164
+ ... circuit_breaker_threshold=5,
165
+ ... circuit_breaker_timeout=60.0,
166
+ ... )
79
167
  """
80
168
  self.logger = get_logger("chuk_tool_processor.processor")
81
169
 
@@ -97,11 +185,11 @@ class ToolProcessor:
97
185
  self.circuit_breaker_timeout = circuit_breaker_timeout
98
186
  self.parser_plugin_names = parser_plugins
99
187
 
100
- # Placeholder for initialized components
101
- self.registry = None
102
- self.strategy = None
103
- self.executor = None
104
- self.parsers = []
188
+ # Placeholder for initialized components (typed as Optional for type safety)
189
+ self.registry: ToolRegistryInterface | None = None
190
+ self.strategy: Any | None = None # Strategy type is complex, use Any for now
191
+ self.executor: Any | None = None # Executor type is complex, use Any for now
192
+ self.parsers: list[Any] = [] # Parser types vary, use Any for now
105
193
 
106
194
  # Flag for tracking initialization state
107
195
  self._initialized = False
@@ -114,13 +202,8 @@ class ToolProcessor:
114
202
  This method ensures all components are properly initialized before use.
115
203
  It is called automatically by other methods if needed.
116
204
  """
117
- # Fast path if already initialized
118
- if self._initialized:
119
- return
120
-
121
205
  # Ensure only one initialization happens at a time
122
206
  async with self._init_lock:
123
- # Double-check pattern after acquiring lock
124
207
  if self._initialized:
125
208
  return
126
209
 
@@ -217,21 +300,87 @@ class ToolProcessor:
217
300
  request_id: str | None = None,
218
301
  ) -> list[ToolResult]:
219
302
  """
220
- Process tool calls from various input formats.
221
-
222
- This method handles different input types:
223
- - String: Parses tool calls from text using registered parsers
224
- - Dict: Processes an OpenAI-style tool_calls object
225
- - List[Dict]: Processes a list of individual tool calls
303
+ Process tool calls from various LLM output formats.
304
+
305
+ This method handles different input types from various LLM providers:
306
+
307
+ **String Input (Anthropic Claude style)**:
308
+ Parses tool calls from XML-like text using registered parsers.
309
+
310
+ Example:
311
+ >>> llm_output = '<tool name="calculator" args=\'{"a": 5, "b": 3}\'/>'
312
+ >>> results = await processor.process(llm_output)
313
+
314
+ **Dict Input (OpenAI style)**:
315
+ Processes an OpenAI-style tool_calls object.
316
+
317
+ Example:
318
+ >>> openai_output = {
319
+ ... "tool_calls": [
320
+ ... {
321
+ ... "type": "function",
322
+ ... "function": {
323
+ ... "name": "calculator",
324
+ ... "arguments": '{"a": 5, "b": 3}'
325
+ ... }
326
+ ... }
327
+ ... ]
328
+ ... }
329
+ >>> results = await processor.process(openai_output)
330
+
331
+ **List[Dict] Input (Direct tool calls)**:
332
+ Processes a list of individual tool call dictionaries.
333
+
334
+ Example:
335
+ >>> direct_calls = [
336
+ ... {"tool": "calculator", "arguments": {"a": 5, "b": 3}},
337
+ ... {"tool": "weather", "arguments": {"city": "London"}}
338
+ ... ]
339
+ >>> results = await processor.process(direct_calls)
226
340
 
227
341
  Args:
228
- data: Input data containing tool calls
229
- timeout: Optional timeout for execution
230
- use_cache: Whether to use cached results
231
- request_id: Optional request ID for logging
342
+ data: Input data containing tool calls. Can be:
343
+ - String: XML/text format (e.g., Anthropic Claude)
344
+ - Dict: OpenAI tool_calls format
345
+ - List[Dict]: Direct tool call list
346
+ timeout: Optional timeout in seconds for tool execution.
347
+ Overrides default_timeout if provided. Default: None
348
+ use_cache: Whether to use cached results. If False, forces
349
+ fresh execution even if cached results exist. Default: True
350
+ request_id: Optional request ID for tracing and logging.
351
+ If not provided, a UUID will be generated. Default: None
232
352
 
233
353
  Returns:
234
- List of tool results
354
+ List of ToolResult objects. Each result contains:
355
+ - tool: Name of the tool that was executed
356
+ - result: The tool's output (None if error)
357
+ - error: Error message if execution failed (None if success)
358
+ - duration: Execution time in seconds
359
+ - cached: Whether result was retrieved from cache
360
+
361
+ **Always returns a list** (never None), even if empty.
362
+
363
+ Raises:
364
+ ToolNotFoundError: If a tool is not registered in the registry
365
+ ToolTimeoutError: If tool execution exceeds timeout
366
+ ToolCircuitOpenError: If circuit breaker is open for a tool
367
+ ToolRateLimitedError: If rate limit is exceeded
368
+
369
+ Example:
370
+ >>> async with ToolProcessor() as processor:
371
+ ... # Process Claude-style XML
372
+ ... results = await processor.process(
373
+ ... '<tool name="echo" args=\'{"message": "hello"}\'/>'
374
+ ... )
375
+ ...
376
+ ... # Check results
377
+ ... for result in results:
378
+ ... if result.error:
379
+ ... print(f"Error: {result.error}")
380
+ ... else:
381
+ ... print(f"Success: {result.result}")
382
+ ... print(f"Duration: {result.duration}s")
383
+ ... print(f"From cache: {result.cached}")
235
384
  """
236
385
  # Ensure initialization
237
386
  await self.initialize()
@@ -260,7 +409,11 @@ class ToolProcessor:
260
409
  args = {"raw": args_str}
261
410
 
262
411
  if name:
263
- calls.append(ToolCall(tool=name, arguments=args, id=tc.get("id")))
412
+ # Build ToolCall kwargs, only include id if present
413
+ call_kwargs: dict[str, Any] = {"tool": name, "arguments": args}
414
+ if "id" in tc and tc["id"]:
415
+ call_kwargs["id"] = tc["id"]
416
+ calls.append(ToolCall(**call_kwargs))
264
417
  else:
265
418
  # Assume it's a single tool call
266
419
  calls = [ToolCall(**data)]
@@ -268,7 +421,9 @@ class ToolProcessor:
268
421
  # List of tool calls
269
422
  calls = [ToolCall(**tc) for tc in data]
270
423
  else:
271
- self.logger.warning(f"Unsupported input type: {type(data)}")
424
+ # Defensive: handle unexpected types at runtime
425
+ # This shouldn't happen per type signature, but helps with debugging
426
+ self.logger.warning(f"Unsupported input type: {type(data)}") # type: ignore[unreachable]
272
427
  return []
273
428
 
274
429
  if not calls:
@@ -279,6 +434,10 @@ class ToolProcessor:
279
434
 
280
435
  # Execute tool calls
281
436
  async with log_context_span("tool_execution", {"num_calls": len(calls)}):
437
+ # Assert that initialization completed successfully
438
+ assert self.registry is not None, "Registry must be initialized"
439
+ assert self.executor is not None, "Executor must be initialized"
440
+
282
441
  # Check if any tools are unknown - search across all namespaces
283
442
  unknown_tools = []
284
443
  all_tools = await self.registry.list_tools() # Returns list of (namespace, name) tuples
@@ -292,7 +451,7 @@ class ToolProcessor:
292
451
  self.logger.debug(f"Unknown tools: {unknown_tools}")
293
452
 
294
453
  # Execute tools
295
- results = await self.executor.execute(calls, timeout=timeout)
454
+ results: list[ToolResult] = await self.executor.execute(calls, timeout=timeout)
296
455
 
297
456
  # Log metrics for each tool call
298
457
  for call, result in zip(calls, results, strict=False):
@@ -348,22 +507,66 @@ class ToolProcessor:
348
507
  """
349
508
  Execute a list of ToolCall objects directly.
350
509
 
510
+ This is a lower-level method for executing tool calls when you already
511
+ have parsed ToolCall objects. For most use cases, prefer process()
512
+ which handles parsing automatically.
513
+
351
514
  Args:
352
- calls: List of tool calls to execute
353
- timeout: Optional execution timeout
354
- use_cache: Whether to use cached results
515
+ calls: List of ToolCall objects to execute. Each call must have:
516
+ - tool: Name of the tool to execute
517
+ - arguments: Dictionary of arguments for the tool
518
+ timeout: Optional timeout in seconds for tool execution.
519
+ Overrides default_timeout if provided. Default: None
520
+ use_cache: Whether to use cached results. If False, forces
521
+ fresh execution even if cached results exist. Default: True
355
522
 
356
523
  Returns:
357
- List of tool results
524
+ List of ToolResult objects, one per input ToolCall.
525
+ **Always returns a list** (never None), even if empty.
526
+
527
+ Each result contains:
528
+ - tool: Name of the tool that was executed
529
+ - result: The tool's output (None if error)
530
+ - error: Error message if execution failed (None if success)
531
+ - duration: Execution time in seconds
532
+ - cached: Whether result was retrieved from cache
533
+
534
+ Raises:
535
+ RuntimeError: If processor is not initialized
536
+ ToolNotFoundError: If a tool is not registered
537
+ ToolTimeoutError: If tool execution exceeds timeout
538
+ ToolCircuitOpenError: If circuit breaker is open
539
+ ToolRateLimitedError: If rate limit is exceeded
540
+
541
+ Example:
542
+ >>> from chuk_tool_processor import ToolCall
543
+ >>>
544
+ >>> # Create tool calls directly
545
+ >>> calls = [
546
+ ... ToolCall(tool="calculator", arguments={"a": 5, "b": 3}),
547
+ ... ToolCall(tool="weather", arguments={"city": "London"}),
548
+ ... ]
549
+ >>>
550
+ >>> async with ToolProcessor() as processor:
551
+ ... results = await processor.execute(calls)
552
+ ... for result in results:
553
+ ... print(f"{result.tool}: {result.result}")
358
554
  """
359
555
  # Ensure initialization
360
556
  await self.initialize()
361
557
 
558
+ # Safety check: ensure we have an executor
559
+ if self.executor is None:
560
+ raise RuntimeError("Executor not initialized. Call initialize() first.")
561
+
362
562
  # Execute with the configured executor
363
- return await self.executor.execute(
563
+ results = await self.executor.execute(
364
564
  calls=calls, timeout=timeout, use_cache=use_cache if hasattr(self.executor, "use_cache") else True
365
565
  )
366
566
 
567
+ # Ensure we always return a list (never None)
568
+ return results if results is not None else []
569
+
367
570
  async def _extract_tool_calls(self, text: str) -> list[ToolCall]:
368
571
  """
369
572
  Extract tool calls from text using all available parsers.
@@ -391,7 +594,8 @@ class ToolProcessor:
391
594
  for result in parser_results:
392
595
  if isinstance(result, Exception):
393
596
  continue
394
- if result:
597
+ # At this point, result is list[ToolCall], not an exception
598
+ if result and isinstance(result, list):
395
599
  all_calls.extend(result)
396
600
 
397
601
  # ------------------------------------------------------------------ #
@@ -410,7 +614,7 @@ class ToolProcessor:
410
614
 
411
615
  return list(unique_calls.values())
412
616
 
413
- async def _try_parser(self, parser, text: str) -> list[ToolCall]:
617
+ async def _try_parser(self, parser: Any, text: str) -> list[ToolCall]:
414
618
  """Try a single parser with metrics and logging."""
415
619
  parser_name = parser.__class__.__name__
416
620
 
@@ -419,7 +623,7 @@ class ToolProcessor:
419
623
 
420
624
  try:
421
625
  # Try to parse
422
- calls = await parser.try_parse(text)
626
+ calls: list[ToolCall] = await parser.try_parse(text)
423
627
 
424
628
  # Log success
425
629
  duration = time.time() - start_time
@@ -444,6 +648,121 @@ class ToolProcessor:
444
648
  self.logger.debug(f"Parser {parser_name} failed: {str(e)}")
445
649
  return []
446
650
 
651
+ # ------------------------------------------------------------------ #
652
+ # Tool discovery and introspection #
653
+ # ------------------------------------------------------------------ #
654
+ async def list_tools(self) -> list[str]:
655
+ """
656
+ List all registered tool names.
657
+
658
+ This method provides programmatic access to all tools in the registry.
659
+
660
+ Returns:
661
+ List of tool names (strings).
662
+
663
+ Example:
664
+ >>> async with ToolProcessor() as processor:
665
+ ... tools = await processor.list_tools()
666
+ ... for name in tools:
667
+ ... print(f"Available tool: {name}")
668
+
669
+ Raises:
670
+ RuntimeError: If processor is not initialized. Call initialize()
671
+ or use the processor in a context manager.
672
+ """
673
+ await self.initialize()
674
+
675
+ if self.registry is None:
676
+ raise RuntimeError("Registry not initialized")
677
+
678
+ # Get tool tuples and extract names
679
+ tool_tuples = await self.registry.list_tools()
680
+ return [name for _, name in tool_tuples]
681
+
682
+ async def get_tool_count(self) -> int:
683
+ """
684
+ Get the number of registered tools.
685
+
686
+ Returns:
687
+ Number of registered tools.
688
+
689
+ Example:
690
+ >>> async with ToolProcessor() as processor:
691
+ ... count = await processor.get_tool_count()
692
+ ... print(f"Total tools: {count}")
693
+ """
694
+ await self.initialize()
695
+
696
+ if self.registry is None:
697
+ raise RuntimeError("Registry not initialized")
698
+
699
+ tool_tuples = await self.registry.list_tools()
700
+ return len(tool_tuples)
701
+
702
+ # ------------------------------------------------------------------ #
703
+ # Context manager support for automatic cleanup #
704
+ # ------------------------------------------------------------------ #
705
+ async def __aenter__(self):
706
+ """Context manager entry - ensures initialization."""
707
+ await self.initialize()
708
+ return self
709
+
710
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
711
+ """Context manager exit with automatic cleanup."""
712
+ await self.close()
713
+ return False
714
+
715
+ async def close(self) -> None:
716
+ """
717
+ Close the processor and clean up resources.
718
+
719
+ This method ensures proper cleanup of executor resources, caches,
720
+ and any other stateful components.
721
+ """
722
+ self.logger.debug("Closing tool processor")
723
+
724
+ try:
725
+ # Close the executor if it has a close method
726
+ if self.executor and hasattr(self.executor, "close"):
727
+ close_method = self.executor.close
728
+ if asyncio.iscoroutinefunction(close_method):
729
+ await close_method()
730
+ elif callable(close_method):
731
+ close_method()
732
+
733
+ # Close the strategy if it has a close method
734
+ if self.strategy and hasattr(self.strategy, "close"):
735
+ close_method = self.strategy.close
736
+ if asyncio.iscoroutinefunction(close_method):
737
+ await close_method()
738
+ elif callable(close_method):
739
+ result = close_method()
740
+ # Check if the result is a coroutine and await it
741
+ if asyncio.iscoroutine(result):
742
+ await result
743
+
744
+ # Clear cached results if using caching
745
+ if self.enable_caching and self.executor:
746
+ # Walk the executor chain to find the CachingToolExecutor
747
+ current = self.executor
748
+ while current:
749
+ if isinstance(current, CachingToolExecutor):
750
+ if hasattr(current.cache, "clear"):
751
+ clear_method = current.cache.clear
752
+ if asyncio.iscoroutinefunction(clear_method):
753
+ await clear_method()
754
+ else:
755
+ clear_result = clear_method()
756
+ if asyncio.iscoroutine(clear_result):
757
+ await clear_result
758
+ break
759
+ current = getattr(current, "executor", None)
760
+
761
+ self.logger.debug("Tool processor closed successfully")
762
+
763
+ except Exception as e:
764
+ self.logger.error(f"Error during processor cleanup: {e}")
765
+
447
766
 
448
767
  # Create a global processor instance
449
768
  _global_processor: ToolProcessor | None = None
@@ -19,14 +19,11 @@ import sys
19
19
 
20
20
 
21
21
  # Auto-initialize shutdown error suppression when logging package is imported
22
- def _initialize_shutdown_fixes():
22
+ def _initialize_shutdown_fixes() -> None:
23
23
  """Initialize shutdown error suppression when the package is imported."""
24
- try:
25
- from .context import _setup_shutdown_error_suppression
26
-
27
- _setup_shutdown_error_suppression()
28
- except ImportError:
29
- pass
24
+ # Note: _setup_shutdown_error_suppression removed as it's no longer needed
25
+ # Keeping this function as a no-op for backward compatibility
26
+ pass
30
27
 
31
28
 
32
29
  # Initialize when package is imported
@@ -64,7 +61,7 @@ __all__ = [
64
61
  async def setup_logging(
65
62
  level: int = logging.INFO,
66
63
  structured: bool = True,
67
- log_file: str = None,
64
+ log_file: str | None = None,
68
65
  ) -> None:
69
66
  """
70
67
  Set up the logging system.