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.
- chuk_tool_processor/execution/strategies/inprocess_strategy.py +107 -8
- chuk_tool_processor/execution/strategies/subprocess_strategy.py +110 -13
- chuk_tool_processor/mcp/mcp_tool.py +351 -149
- chuk_tool_processor/mcp/register_mcp_tools.py +80 -33
- chuk_tool_processor/mcp/stream_manager.py +319 -65
- chuk_tool_processor-0.6.2.dist-info/METADATA +697 -0
- {chuk_tool_processor-0.6.dist-info → chuk_tool_processor-0.6.2.dist-info}/RECORD +9 -9
- chuk_tool_processor-0.6.dist-info/METADATA +0 -830
- {chuk_tool_processor-0.6.dist-info → chuk_tool_processor-0.6.2.dist-info}/WHEEL +0 -0
- {chuk_tool_processor-0.6.dist-info → chuk_tool_processor-0.6.2.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
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
|
-
#
|
|
220
|
-
tool_impl = await self.
|
|
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
|
-
#
|
|
427
|
-
impl = await self.
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
412
|
-
tool_impl = await self.
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
#
|
|
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:
|