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

Potentially problematic release.


This version of chuk-tool-processor might be problematic. Click here for more details.

@@ -7,7 +7,12 @@ This strategy executes tools concurrently in the same process using asyncio.
7
7
  It has special support for streaming tools, accessing their stream_execute method
8
8
  directly to enable true item-by-item streaming.
9
9
 
10
- FIXED: Ensures consistent timeout handling across all execution paths.
10
+ Enhanced tool name resolution that properly handles:
11
+ - Simple names: "get_current_time"
12
+ - Namespaced names: "diagnostic_test.get_current_time"
13
+ - Cross-namespace fallback searching
14
+
15
+ Ensures consistent timeout handling across all execution paths.
11
16
  ENHANCED: Clean shutdown handling to prevent anyio cancel scope errors.
12
17
  """
13
18
  from __future__ import annotations
@@ -17,7 +22,7 @@ import inspect
17
22
  import os
18
23
  from contextlib import asynccontextmanager
19
24
  from datetime import datetime, timezone
20
- from typing import Any, List, Optional, AsyncIterator, Set
25
+ from typing import Any, List, Optional, AsyncIterator, Set, Tuple
21
26
 
22
27
  from chuk_tool_processor.core.exceptions import ToolExecutionError
23
28
  from chuk_tool_processor.models.execution_strategy import ExecutionStrategy
@@ -216,15 +221,15 @@ class InProcessStrategy(ExecutionStrategy):
216
221
  return
217
222
 
218
223
  try:
219
- # Get the tool implementation
220
- tool_impl = await self.registry.get_tool(call.tool, call.namespace)
224
+ # Use enhanced tool resolution instead of direct lookup
225
+ tool_impl, resolved_namespace = await self._resolve_tool_info(call.tool, call.namespace)
221
226
  if tool_impl is None:
222
227
  # Tool not found
223
228
  now = datetime.now(timezone.utc)
224
229
  result = ToolResult(
225
230
  tool=call.tool,
226
231
  result=None,
227
- error=f"Tool '{call.tool}' not found",
232
+ error=f"Tool '{call.tool}' not found in any namespace",
228
233
  start_time=now,
229
234
  end_time=now,
230
235
  machine=os.uname().nodename,
@@ -233,6 +238,8 @@ class InProcessStrategy(ExecutionStrategy):
233
238
  await queue.put(result)
234
239
  return
235
240
 
241
+ logger.debug(f"Resolved streaming tool '{call.tool}' to namespace '{resolved_namespace}'")
242
+
236
243
  # Instantiate if class
237
244
  tool = tool_impl() if inspect.isclass(tool_impl) else tool_impl
238
245
 
@@ -423,19 +430,21 @@ class InProcessStrategy(ExecutionStrategy):
423
430
  )
424
431
 
425
432
  try:
426
- # Get the tool implementation
427
- impl = await self.registry.get_tool(call.tool, call.namespace)
433
+ # Use enhanced tool resolution instead of direct lookup
434
+ impl, resolved_namespace = await self._resolve_tool_info(call.tool, call.namespace)
428
435
  if impl is None:
429
436
  return ToolResult(
430
437
  tool=call.tool,
431
438
  result=None,
432
- error=f"Tool '{call.tool}' not found",
439
+ error=f"Tool '{call.tool}' not found in any namespace",
433
440
  start_time=start,
434
441
  end_time=datetime.now(timezone.utc),
435
442
  machine=machine,
436
443
  pid=pid,
437
444
  )
438
445
 
446
+ logger.debug(f"Resolved tool '{call.tool}' to namespace '{resolved_namespace}'")
447
+
439
448
  # Instantiate if class
440
449
  tool = impl() if inspect.isclass(impl) else impl
441
450
 
@@ -591,6 +600,96 @@ class InProcessStrategy(ExecutionStrategy):
591
600
  machine=machine,
592
601
  pid=pid,
593
602
  )
603
+
604
+ async def _resolve_tool_info(self, tool_name: str, preferred_namespace: str = "default") -> Tuple[Optional[Any], Optional[str]]:
605
+ """
606
+ Enhanced tool name resolution with comprehensive fallback logic.
607
+
608
+ This method handles:
609
+ 1. Simple names: "get_current_time" -> search in specified namespace first, then all namespaces
610
+ 2. Namespaced names: "diagnostic_test.get_current_time" -> extract namespace and tool name
611
+ 3. Fallback searching across all namespaces when not found in default
612
+
613
+ Args:
614
+ tool_name: Name of the tool to resolve
615
+ preferred_namespace: Preferred namespace to search first
616
+
617
+ Returns:
618
+ Tuple of (tool_object, resolved_namespace) or (None, None) if not found
619
+ """
620
+ logger.debug(f"Resolving tool: '{tool_name}' (preferred namespace: '{preferred_namespace}')")
621
+
622
+ # Strategy 1: Handle namespaced tool names (namespace.tool_name format)
623
+ if '.' in tool_name:
624
+ parts = tool_name.split('.', 1) # Split on first dot only
625
+ namespace = parts[0]
626
+ actual_tool_name = parts[1]
627
+
628
+ logger.debug(f"Namespaced lookup: namespace='{namespace}', tool='{actual_tool_name}'")
629
+
630
+ tool = await self.registry.get_tool(actual_tool_name, namespace)
631
+ if tool is not None:
632
+ logger.debug(f"Found tool '{actual_tool_name}' in namespace '{namespace}'")
633
+ return tool, namespace
634
+ else:
635
+ logger.debug(f"Tool '{actual_tool_name}' not found in namespace '{namespace}'")
636
+ return None, None
637
+
638
+ # Strategy 2: Simple tool name - try preferred namespace first
639
+ if preferred_namespace:
640
+ logger.debug(f"Simple tool lookup: trying preferred namespace '{preferred_namespace}' for '{tool_name}'")
641
+ tool = await self.registry.get_tool(tool_name, preferred_namespace)
642
+ if tool is not None:
643
+ logger.debug(f"Found tool '{tool_name}' in preferred namespace '{preferred_namespace}'")
644
+ return tool, preferred_namespace
645
+
646
+ # Strategy 3: Try default namespace if different from preferred
647
+ if preferred_namespace != "default":
648
+ logger.debug(f"Simple tool lookup: trying default namespace for '{tool_name}'")
649
+ tool = await self.registry.get_tool(tool_name, "default")
650
+ if tool is not None:
651
+ logger.debug(f"Found tool '{tool_name}' in default namespace")
652
+ return tool, "default"
653
+
654
+ # Strategy 4: Search all namespaces as fallback
655
+ logger.debug(f"Tool '{tool_name}' not in preferred/default namespace, searching all namespaces...")
656
+
657
+ try:
658
+ # Get all available namespaces
659
+ namespaces = await self.registry.list_namespaces()
660
+ logger.debug(f"Available namespaces: {namespaces}")
661
+
662
+ # Search each namespace
663
+ for namespace in namespaces:
664
+ if namespace in [preferred_namespace, "default"]:
665
+ continue # Already tried these
666
+
667
+ logger.debug(f"Searching namespace '{namespace}' for tool '{tool_name}'")
668
+ tool = await self.registry.get_tool(tool_name, namespace)
669
+ if tool is not None:
670
+ logger.debug(f"Found tool '{tool_name}' in namespace '{namespace}'")
671
+ return tool, namespace
672
+
673
+ # Strategy 5: Final fallback - list all tools and do fuzzy matching
674
+ logger.debug(f"Tool '{tool_name}' not found in any namespace, trying fuzzy matching...")
675
+ all_tools = await self.registry.list_tools()
676
+
677
+ # Look for exact matches in tool name (ignoring namespace)
678
+ for namespace, registered_name in all_tools:
679
+ if registered_name == tool_name:
680
+ logger.debug(f"Fuzzy match: found '{registered_name}' in namespace '{namespace}'")
681
+ tool = await self.registry.get_tool(registered_name, namespace)
682
+ if tool is not None:
683
+ return tool, namespace
684
+
685
+ # Log all available tools for debugging
686
+ logger.debug(f"Available tools: {all_tools}")
687
+
688
+ except Exception as e:
689
+ logger.error(f"Error during namespace search: {e}")
690
+
691
+ logger.warning(f"Tool '{tool_name}' not found in any namespace")
692
+ return None, None
594
693
 
595
694
  @property
596
695
  def supports_streaming(self) -> bool:
@@ -6,6 +6,11 @@ Subprocess execution strategy - truly runs tools in separate OS processes.
6
6
  This strategy executes tools in separate Python processes using a process pool,
7
7
  providing isolation and potentially better parallelism on multi-core systems.
8
8
 
9
+ Enhanced tool name resolution that properly handles:
10
+ - Simple names: "get_current_time"
11
+ - Namespaced names: "diagnostic_test.get_current_time"
12
+ - Cross-namespace fallback searching
13
+
9
14
  Properly handles tool serialization and ensures tool_name is preserved.
10
15
  """
11
16
  from __future__ import annotations
@@ -54,7 +59,7 @@ def _serialized_tool_worker(
54
59
  serialized_tool_data: bytes
55
60
  ) -> Dict[str, Any]:
56
61
  """
57
- FIXED: Worker function that uses serialized tools and ensures tool_name is available.
62
+ Worker function that uses serialized tools and ensures tool_name is available.
58
63
 
59
64
  This worker deserializes the complete tool and executes it, with multiple
60
65
  fallbacks to ensure tool_name is properly set.
@@ -94,7 +99,7 @@ def _serialized_tool_worker(
94
99
  # Deserialize the complete tool
95
100
  tool = pickle.loads(serialized_tool_data)
96
101
 
97
- # FIXED: Multiple fallbacks to ensure tool_name is available
102
+ # Multiple fallbacks to ensure tool_name is available
98
103
 
99
104
  # Fallback 1: If tool doesn't have tool_name, set it directly
100
105
  if not hasattr(tool, 'tool_name') or not tool.tool_name:
@@ -161,7 +166,7 @@ class SubprocessStrategy(ExecutionStrategy):
161
166
  among them. Each tool executes in its own process, providing isolation and
162
167
  parallelism.
163
168
 
164
- FIXED: Now properly handles tool serialization and tool_name preservation.
169
+ Enhanced tool name resolution and proper tool serialization.
165
170
  """
166
171
 
167
172
  def __init__(
@@ -391,7 +396,7 @@ class SubprocessStrategy(ExecutionStrategy):
391
396
  timeout: float, # Make timeout required
392
397
  ) -> ToolResult:
393
398
  """
394
- FIXED: Execute a single tool call with proper tool preparation and serialization.
399
+ Execute a single tool call with enhanced tool resolution and serialization.
395
400
 
396
401
  Args:
397
402
  call: Tool call to execute
@@ -408,36 +413,38 @@ class SubprocessStrategy(ExecutionStrategy):
408
413
  # Ensure pool is initialized
409
414
  await self._ensure_pool()
410
415
 
411
- # Get tool from registry
412
- tool_impl = await self.registry.get_tool(call.tool, call.namespace)
416
+ # Use enhanced tool resolution instead of direct lookup
417
+ tool_impl, resolved_namespace = await self._resolve_tool_info(call.tool, call.namespace)
413
418
  if tool_impl is None:
414
419
  return ToolResult(
415
420
  tool=call.tool,
416
421
  result=None,
417
- error=f"Tool '{call.tool}' not found",
422
+ error=f"Tool '{call.tool}' not found in any namespace",
418
423
  start_time=start_time,
419
424
  end_time=datetime.now(timezone.utc),
420
425
  machine=os.uname().nodename,
421
426
  pid=os.getpid(),
422
427
  )
423
428
 
424
- # FIXED: Ensure tool is properly prepared before serialization
429
+ logger.debug(f"Resolved subprocess tool '{call.tool}' to namespace '{resolved_namespace}'")
430
+
431
+ # Ensure tool is properly prepared before serialization
425
432
  if inspect.isclass(tool_impl):
426
433
  tool = tool_impl()
427
434
  else:
428
435
  tool = tool_impl
429
436
 
430
- # FIXED: Ensure tool_name attribute exists
437
+ # Ensure tool_name attribute exists
431
438
  if not hasattr(tool, 'tool_name'):
432
439
  tool.tool_name = call.tool
433
440
  elif not tool.tool_name:
434
441
  tool.tool_name = call.tool
435
442
 
436
- # FIXED: Also set _tool_name class attribute for consistency
443
+ # Also set _tool_name class attribute for consistency
437
444
  if not hasattr(tool.__class__, '_tool_name'):
438
445
  tool.__class__._tool_name = call.tool
439
446
 
440
- # FIXED: Serialize the properly prepared tool
447
+ # Serialize the properly prepared tool
441
448
  try:
442
449
  serialized_tool_data = pickle.dumps(tool)
443
450
  logger.debug("Successfully serialized %s (%d bytes)", call.tool, len(serialized_tool_data))
@@ -464,7 +471,7 @@ class SubprocessStrategy(ExecutionStrategy):
464
471
  functools.partial(
465
472
  _serialized_tool_worker, # Use the FIXED worker function
466
473
  call.tool,
467
- call.namespace,
474
+ resolved_namespace, # Use resolved namespace
468
475
  call.arguments,
469
476
  timeout,
470
477
  serialized_tool_data # Pass serialized tool data
@@ -562,6 +569,96 @@ class SubprocessStrategy(ExecutionStrategy):
562
569
  pid=os.getpid(),
563
570
  )
564
571
 
572
+ async def _resolve_tool_info(self, tool_name: str, preferred_namespace: str = "default") -> Tuple[Optional[Any], Optional[str]]:
573
+ """
574
+ Enhanced tool name resolution with comprehensive fallback logic.
575
+
576
+ This method handles:
577
+ 1. Simple names: "get_current_time" -> search in specified namespace first, then all namespaces
578
+ 2. Namespaced names: "diagnostic_test.get_current_time" -> extract namespace and tool name
579
+ 3. Fallback searching across all namespaces when not found in default
580
+
581
+ Args:
582
+ tool_name: Name of the tool to resolve
583
+ preferred_namespace: Preferred namespace to search first
584
+
585
+ Returns:
586
+ Tuple of (tool_object, resolved_namespace) or (None, None) if not found
587
+ """
588
+ logger.debug(f"Resolving tool: '{tool_name}' (preferred namespace: '{preferred_namespace}')")
589
+
590
+ # Strategy 1: Handle namespaced tool names (namespace.tool_name format)
591
+ if '.' in tool_name:
592
+ parts = tool_name.split('.', 1) # Split on first dot only
593
+ namespace = parts[0]
594
+ actual_tool_name = parts[1]
595
+
596
+ logger.debug(f"Namespaced lookup: namespace='{namespace}', tool='{actual_tool_name}'")
597
+
598
+ tool = await self.registry.get_tool(actual_tool_name, namespace)
599
+ if tool is not None:
600
+ logger.debug(f"Found tool '{actual_tool_name}' in namespace '{namespace}'")
601
+ return tool, namespace
602
+ else:
603
+ logger.debug(f"Tool '{actual_tool_name}' not found in namespace '{namespace}'")
604
+ return None, None
605
+
606
+ # Strategy 2: Simple tool name - try preferred namespace first
607
+ if preferred_namespace:
608
+ logger.debug(f"Simple tool lookup: trying preferred namespace '{preferred_namespace}' for '{tool_name}'")
609
+ tool = await self.registry.get_tool(tool_name, preferred_namespace)
610
+ if tool is not None:
611
+ logger.debug(f"Found tool '{tool_name}' in preferred namespace '{preferred_namespace}'")
612
+ return tool, preferred_namespace
613
+
614
+ # Strategy 3: Try default namespace if different from preferred
615
+ if preferred_namespace != "default":
616
+ logger.debug(f"Simple tool lookup: trying default namespace for '{tool_name}'")
617
+ tool = await self.registry.get_tool(tool_name, "default")
618
+ if tool is not None:
619
+ logger.debug(f"Found tool '{tool_name}' in default namespace")
620
+ return tool, "default"
621
+
622
+ # Strategy 4: Search all namespaces as fallback
623
+ logger.debug(f"Tool '{tool_name}' not in preferred/default namespace, searching all namespaces...")
624
+
625
+ try:
626
+ # Get all available namespaces
627
+ namespaces = await self.registry.list_namespaces()
628
+ logger.debug(f"Available namespaces: {namespaces}")
629
+
630
+ # Search each namespace
631
+ for namespace in namespaces:
632
+ if namespace in [preferred_namespace, "default"]:
633
+ continue # Already tried these
634
+
635
+ logger.debug(f"Searching namespace '{namespace}' for tool '{tool_name}'")
636
+ tool = await self.registry.get_tool(tool_name, namespace)
637
+ if tool is not None:
638
+ logger.debug(f"Found tool '{tool_name}' in namespace '{namespace}'")
639
+ return tool, namespace
640
+
641
+ # Strategy 5: Final fallback - list all tools and do fuzzy matching
642
+ logger.debug(f"Tool '{tool_name}' not found in any namespace, trying fuzzy matching...")
643
+ all_tools = await self.registry.list_tools()
644
+
645
+ # Look for exact matches in tool name (ignoring namespace)
646
+ for namespace, registered_name in all_tools:
647
+ if registered_name == tool_name:
648
+ logger.debug(f"Fuzzy match: found '{registered_name}' in namespace '{namespace}'")
649
+ tool = await self.registry.get_tool(registered_name, namespace)
650
+ if tool is not None:
651
+ return tool, namespace
652
+
653
+ # Log all available tools for debugging
654
+ logger.debug(f"Available tools: {all_tools}")
655
+
656
+ except Exception as e:
657
+ logger.error(f"Error during namespace search: {e}")
658
+
659
+ logger.warning(f"Tool '{tool_name}' not found in any namespace")
660
+ return None, None
661
+
565
662
  @property
566
663
  def supports_streaming(self) -> bool:
567
664
  """Check if this strategy supports streaming execution."""
@@ -610,7 +707,7 @@ class SubprocessStrategy(ExecutionStrategy):
610
707
  except Exception:
611
708
  logger.debug("Active operations completed successfully")
612
709
 
613
- # FIXED: Handle process pool shutdown with proper null checks
710
+ # Handle process pool shutdown with proper null checks
614
711
  if self._process_pool is not None:
615
712
  logger.debug("Finalizing process pool")
616
713
  try: