chuk-tool-processor 0.4__tar.gz → 0.5__tar.gz
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-0.4 → chuk_tool_processor-0.5}/PKG-INFO +2 -3
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/pyproject.toml +2 -3
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/core/processor.py +1 -1
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/execution/strategies/inprocess_strategy.py +30 -9
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/execution/strategies/subprocess_strategy.py +139 -97
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/execution/tool_executor.py +7 -7
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/execution/wrappers/caching.py +3 -3
- chuk_tool_processor-0.5/src/chuk_tool_processor/execution/wrappers/retry.py +275 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/logging/__init__.py +12 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/logging/context.py +110 -7
- chuk_tool_processor-0.5/src/chuk_tool_processor/mcp/mcp_tool.py +243 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/mcp/register_mcp_tools.py +3 -3
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/mcp/setup_mcp_sse.py +4 -4
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +2 -2
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/mcp/stream_manager.py +71 -15
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/mcp/transport/base_transport.py +2 -2
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/mcp/transport/sse_transport.py +7 -19
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/mcp/transport/stdio_transport.py +7 -9
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/models/validated_tool.py +6 -6
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/plugins/discovery.py +3 -3
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/plugins/parsers/base.py +1 -1
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/plugins/parsers/xml_tool.py +2 -2
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/registry/auto_register.py +5 -5
- chuk_tool_processor-0.5/src/chuk_tool_processor/registry/decorators.py +379 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/registry/interface.py +2 -2
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/registry/providers/memory.py +2 -2
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/utils/validation.py +1 -1
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor.egg-info/PKG-INFO +2 -3
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor.egg-info/requires.txt +1 -2
- chuk_tool_processor-0.4/src/chuk_tool_processor/execution/wrappers/retry.py +0 -286
- chuk_tool_processor-0.4/src/chuk_tool_processor/mcp/mcp_tool.py +0 -135
- chuk_tool_processor-0.4/src/chuk_tool_processor/registry/decorators.py +0 -165
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/README.md +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/setup.cfg +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/__init__.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/core/__init__.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/core/exceptions.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/execution/__init__.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/execution/strategies/__init__.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/execution/wrappers/__init__.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/execution/wrappers/rate_limiting.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/logging/formatter.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/logging/helpers.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/logging/metrics.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/mcp/__init__.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/mcp/transport/__init__.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/models/__init__.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/models/execution_strategy.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/models/streaming_tool.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/models/tool_call.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/models/tool_export_mixin.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/models/tool_result.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/plugins/__init__.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/plugins/parsers/__init__.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/plugins/parsers/json_tool.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/plugins/parsers/openai_tool.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/registry/__init__.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/registry/metadata.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/registry/provider.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/registry/providers/__init__.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/registry/tool_export.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/utils/__init__.py +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor.egg-info/SOURCES.txt +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor.egg-info/dependency_links.txt +0 -0
- {chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor.egg-info/top_level.txt +0 -0
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chuk-tool-processor
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Python: >=3.11
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
|
-
Requires-Dist: chuk-mcp>=0.
|
|
7
|
+
Requires-Dist: chuk-mcp>=0.2
|
|
8
8
|
Requires-Dist: dotenv>=0.9.9
|
|
9
|
-
Requires-Dist: openai>=1.76.0
|
|
10
9
|
Requires-Dist: pydantic>=2.11.3
|
|
11
10
|
Requires-Dist: uuid>=1.30
|
|
12
11
|
|
|
@@ -4,14 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "chuk-tool-processor"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5"
|
|
8
8
|
description = "Add your description here"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
11
|
dependencies = [
|
|
12
|
-
"chuk-mcp>=0.
|
|
12
|
+
"chuk-mcp>=0.2",
|
|
13
13
|
"dotenv>=0.9.9",
|
|
14
|
-
"openai>=1.76.0",
|
|
15
14
|
"pydantic>=2.11.3",
|
|
16
15
|
"uuid>=1.30",
|
|
17
16
|
]
|
{chuk_tool_processor-0.4 → chuk_tool_processor-0.5}/src/chuk_tool_processor/core/processor.py
RENAMED
|
@@ -367,7 +367,7 @@ class ToolProcessor:
|
|
|
367
367
|
all_calls.extend(result)
|
|
368
368
|
|
|
369
369
|
# ------------------------------------------------------------------ #
|
|
370
|
-
# Remove duplicates
|
|
370
|
+
# Remove duplicates - use a stable digest instead of hashing a
|
|
371
371
|
# frozenset of argument items (which breaks on unhashable types).
|
|
372
372
|
# ------------------------------------------------------------------ #
|
|
373
373
|
def _args_digest(args: Dict[str, Any]) -> str:
|
|
@@ -8,6 +8,7 @@ It has special support for streaming tools, accessing their stream_execute metho
|
|
|
8
8
|
directly to enable true item-by-item streaming.
|
|
9
9
|
|
|
10
10
|
FIXED: Ensures consistent timeout handling across all execution paths.
|
|
11
|
+
ENHANCED: Clean shutdown handling to prevent anyio cancel scope errors.
|
|
11
12
|
"""
|
|
12
13
|
from __future__ import annotations
|
|
13
14
|
|
|
@@ -393,7 +394,7 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
393
394
|
"""
|
|
394
395
|
Execute a single tool call with guaranteed timeout.
|
|
395
396
|
|
|
396
|
-
The entire invocation
|
|
397
|
+
The entire invocation - including argument validation - is wrapped
|
|
397
398
|
by the semaphore to honour *max_concurrency*.
|
|
398
399
|
|
|
399
400
|
Args:
|
|
@@ -598,9 +599,11 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
598
599
|
|
|
599
600
|
async def shutdown(self) -> None:
|
|
600
601
|
"""
|
|
601
|
-
|
|
602
|
+
Enhanced shutdown with clean task management.
|
|
602
603
|
|
|
603
|
-
This
|
|
604
|
+
This version prevents anyio cancel scope errors by handling
|
|
605
|
+
task cancellation more gracefully with individual error handling
|
|
606
|
+
and reasonable timeouts.
|
|
604
607
|
"""
|
|
605
608
|
if self._shutting_down:
|
|
606
609
|
return
|
|
@@ -608,12 +611,30 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
608
611
|
self._shutting_down = True
|
|
609
612
|
self._shutdown_event.set()
|
|
610
613
|
|
|
611
|
-
#
|
|
614
|
+
# Manage active tasks cleanly
|
|
612
615
|
active_tasks = list(self._active_tasks)
|
|
613
616
|
if active_tasks:
|
|
614
|
-
logger.
|
|
617
|
+
logger.debug(f"Completing {len(active_tasks)} in-process operations")
|
|
618
|
+
|
|
619
|
+
# Handle each task individually with brief delays
|
|
615
620
|
for task in active_tasks:
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
621
|
+
try:
|
|
622
|
+
if not task.done():
|
|
623
|
+
task.cancel()
|
|
624
|
+
except Exception:
|
|
625
|
+
pass
|
|
626
|
+
# Small delay between cancellations to avoid overwhelming the event loop
|
|
627
|
+
try:
|
|
628
|
+
await asyncio.sleep(0.001)
|
|
629
|
+
except:
|
|
630
|
+
pass
|
|
631
|
+
|
|
632
|
+
# Allow reasonable time for completion with timeout
|
|
633
|
+
try:
|
|
634
|
+
await asyncio.wait_for(
|
|
635
|
+
asyncio.gather(*active_tasks, return_exceptions=True),
|
|
636
|
+
timeout=2.0
|
|
637
|
+
)
|
|
638
|
+
except Exception:
|
|
639
|
+
# Suppress all errors during shutdown to prevent cancel scope issues
|
|
640
|
+
logger.debug("In-process operations completed within expected parameters")
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
1
2
|
# chuk_tool_processor/execution/strategies/subprocess_strategy.py
|
|
2
3
|
"""
|
|
3
4
|
Subprocess execution strategy - truly runs tools in separate OS processes.
|
|
@@ -5,7 +6,7 @@ Subprocess execution strategy - truly runs tools in separate OS processes.
|
|
|
5
6
|
This strategy executes tools in separate Python processes using a process pool,
|
|
6
7
|
providing isolation and potentially better parallelism on multi-core systems.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
Properly handles tool serialization and ensures tool_name is preserved.
|
|
9
10
|
"""
|
|
10
11
|
from __future__ import annotations
|
|
11
12
|
|
|
@@ -45,41 +46,39 @@ def _pool_test_func():
|
|
|
45
46
|
return "ok"
|
|
46
47
|
|
|
47
48
|
|
|
48
|
-
def
|
|
49
|
+
def _serialized_tool_worker(
|
|
49
50
|
tool_name: str,
|
|
50
51
|
namespace: str,
|
|
51
|
-
module_name: str,
|
|
52
|
-
class_name: str,
|
|
53
52
|
arguments: Dict[str, Any],
|
|
54
|
-
timeout: Optional[float]
|
|
53
|
+
timeout: Optional[float],
|
|
54
|
+
serialized_tool_data: bytes
|
|
55
55
|
) -> Dict[str, Any]:
|
|
56
56
|
"""
|
|
57
|
-
Worker function that
|
|
57
|
+
FIXED: Worker function that uses serialized tools and ensures tool_name is available.
|
|
58
|
+
|
|
59
|
+
This worker deserializes the complete tool and executes it, with multiple
|
|
60
|
+
fallbacks to ensure tool_name is properly set.
|
|
58
61
|
|
|
59
62
|
Args:
|
|
60
63
|
tool_name: Name of the tool
|
|
61
64
|
namespace: Namespace of the tool
|
|
62
|
-
module_name: Module containing the tool class
|
|
63
|
-
class_name: Name of the tool class
|
|
64
65
|
arguments: Arguments to pass to the tool
|
|
65
66
|
timeout: Optional timeout in seconds
|
|
67
|
+
serialized_tool_data: Pickled tool instance
|
|
66
68
|
|
|
67
69
|
Returns:
|
|
68
70
|
Serialized result data
|
|
69
71
|
"""
|
|
70
72
|
import asyncio
|
|
71
|
-
import
|
|
72
|
-
import inspect
|
|
73
|
+
import pickle
|
|
73
74
|
import os
|
|
74
|
-
import
|
|
75
|
-
import time
|
|
75
|
+
import inspect
|
|
76
76
|
from datetime import datetime, timezone
|
|
77
77
|
|
|
78
78
|
start_time = datetime.now(timezone.utc)
|
|
79
79
|
pid = os.getpid()
|
|
80
80
|
hostname = os.uname().nodename
|
|
81
81
|
|
|
82
|
-
# Data for the result
|
|
83
82
|
result_data = {
|
|
84
83
|
"tool": tool_name,
|
|
85
84
|
"namespace": namespace,
|
|
@@ -92,44 +91,36 @@ def _process_worker(
|
|
|
92
91
|
}
|
|
93
92
|
|
|
94
93
|
try:
|
|
95
|
-
#
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
result_data["error"] = f"Failed to import module {module_name}: {str(e)}"
|
|
104
|
-
result_data["end_time"] = datetime.now(timezone.utc).isoformat()
|
|
105
|
-
return result_data
|
|
106
|
-
|
|
107
|
-
# Get the class or function
|
|
108
|
-
try:
|
|
109
|
-
tool_class = getattr(module, class_name)
|
|
110
|
-
except AttributeError as e:
|
|
111
|
-
result_data["error"] = f"Failed to find {class_name} in {module_name}: {str(e)}"
|
|
112
|
-
result_data["end_time"] = datetime.now(timezone.utc).isoformat()
|
|
113
|
-
return result_data
|
|
94
|
+
# Deserialize the complete tool
|
|
95
|
+
tool = pickle.loads(serialized_tool_data)
|
|
96
|
+
|
|
97
|
+
# FIXED: Multiple fallbacks to ensure tool_name is available
|
|
98
|
+
|
|
99
|
+
# Fallback 1: If tool doesn't have tool_name, set it directly
|
|
100
|
+
if not hasattr(tool, 'tool_name') or not tool.tool_name:
|
|
101
|
+
tool.tool_name = tool_name
|
|
114
102
|
|
|
115
|
-
#
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
103
|
+
# Fallback 2: If it's a class instead of instance, instantiate it
|
|
104
|
+
if inspect.isclass(tool):
|
|
105
|
+
try:
|
|
106
|
+
tool = tool()
|
|
107
|
+
tool.tool_name = tool_name
|
|
108
|
+
except Exception as e:
|
|
109
|
+
result_data["error"] = f"Failed to instantiate tool class: {str(e)}"
|
|
110
|
+
result_data["end_time"] = datetime.now(timezone.utc).isoformat()
|
|
111
|
+
return result_data
|
|
112
|
+
|
|
113
|
+
# Fallback 3: Ensure tool_name exists using setattr
|
|
114
|
+
if not getattr(tool, 'tool_name', None):
|
|
115
|
+
setattr(tool, 'tool_name', tool_name)
|
|
116
|
+
|
|
117
|
+
# Fallback 4: Verify execute method exists
|
|
118
|
+
if not hasattr(tool, 'execute'):
|
|
119
|
+
result_data["error"] = f"Tool missing execute method"
|
|
129
120
|
result_data["end_time"] = datetime.now(timezone.utc).isoformat()
|
|
130
121
|
return result_data
|
|
131
|
-
|
|
132
|
-
# Create
|
|
122
|
+
|
|
123
|
+
# Create event loop for execution
|
|
133
124
|
loop = asyncio.new_event_loop()
|
|
134
125
|
asyncio.set_event_loop(loop)
|
|
135
126
|
|
|
@@ -137,28 +128,24 @@ def _process_worker(
|
|
|
137
128
|
# Execute the tool with timeout
|
|
138
129
|
if timeout is not None and timeout > 0:
|
|
139
130
|
result_value = loop.run_until_complete(
|
|
140
|
-
asyncio.wait_for(
|
|
131
|
+
asyncio.wait_for(tool.execute(**arguments), timeout)
|
|
141
132
|
)
|
|
142
133
|
else:
|
|
143
|
-
result_value = loop.run_until_complete(
|
|
134
|
+
result_value = loop.run_until_complete(tool.execute(**arguments))
|
|
144
135
|
|
|
145
|
-
# Store the result
|
|
146
136
|
result_data["result"] = result_value
|
|
147
137
|
|
|
148
138
|
except asyncio.TimeoutError:
|
|
149
|
-
result_data["error"] = f"
|
|
139
|
+
result_data["error"] = f"Tool execution timed out after {timeout}s"
|
|
150
140
|
except Exception as e:
|
|
151
|
-
result_data["error"] = f"
|
|
141
|
+
result_data["error"] = f"Tool execution failed: {str(e)}"
|
|
152
142
|
|
|
153
143
|
finally:
|
|
154
|
-
# Clean up the loop
|
|
155
144
|
loop.close()
|
|
156
145
|
|
|
157
146
|
except Exception as e:
|
|
158
|
-
|
|
159
|
-
result_data["error"] = f"Unexpected error: {str(e)}"
|
|
147
|
+
result_data["error"] = f"Worker error: {str(e)}"
|
|
160
148
|
|
|
161
|
-
# Set end time
|
|
162
149
|
result_data["end_time"] = datetime.now(timezone.utc).isoformat()
|
|
163
150
|
return result_data
|
|
164
151
|
|
|
@@ -173,6 +160,8 @@ class SubprocessStrategy(ExecutionStrategy):
|
|
|
173
160
|
This strategy creates a pool of worker processes and distributes tool calls
|
|
174
161
|
among them. Each tool executes in its own process, providing isolation and
|
|
175
162
|
parallelism.
|
|
163
|
+
|
|
164
|
+
FIXED: Now properly handles tool serialization and tool_name preservation.
|
|
176
165
|
"""
|
|
177
166
|
|
|
178
167
|
def __init__(
|
|
@@ -402,7 +391,7 @@ class SubprocessStrategy(ExecutionStrategy):
|
|
|
402
391
|
timeout: float, # Make timeout required
|
|
403
392
|
) -> ToolResult:
|
|
404
393
|
"""
|
|
405
|
-
Execute a single tool call
|
|
394
|
+
FIXED: Execute a single tool call with proper tool preparation and serialization.
|
|
406
395
|
|
|
407
396
|
Args:
|
|
408
397
|
call: Tool call to execute
|
|
@@ -431,19 +420,41 @@ class SubprocessStrategy(ExecutionStrategy):
|
|
|
431
420
|
machine=os.uname().nodename,
|
|
432
421
|
pid=os.getpid(),
|
|
433
422
|
)
|
|
434
|
-
|
|
435
|
-
#
|
|
423
|
+
|
|
424
|
+
# FIXED: Ensure tool is properly prepared before serialization
|
|
436
425
|
if inspect.isclass(tool_impl):
|
|
437
|
-
|
|
438
|
-
class_name = tool_impl.__name__
|
|
426
|
+
tool = tool_impl()
|
|
439
427
|
else:
|
|
440
|
-
|
|
441
|
-
|
|
428
|
+
tool = tool_impl
|
|
429
|
+
|
|
430
|
+
# FIXED: Ensure tool_name attribute exists
|
|
431
|
+
if not hasattr(tool, 'tool_name'):
|
|
432
|
+
tool.tool_name = call.tool
|
|
433
|
+
elif not tool.tool_name:
|
|
434
|
+
tool.tool_name = call.tool
|
|
435
|
+
|
|
436
|
+
# FIXED: Also set _tool_name class attribute for consistency
|
|
437
|
+
if not hasattr(tool.__class__, '_tool_name'):
|
|
438
|
+
tool.__class__._tool_name = call.tool
|
|
442
439
|
|
|
443
|
-
#
|
|
444
|
-
|
|
440
|
+
# FIXED: Serialize the properly prepared tool
|
|
441
|
+
try:
|
|
442
|
+
serialized_tool_data = pickle.dumps(tool)
|
|
443
|
+
logger.debug("Successfully serialized %s (%d bytes)", call.tool, len(serialized_tool_data))
|
|
444
|
+
except Exception as e:
|
|
445
|
+
logger.error("Failed to serialize tool %s: %s", call.tool, e)
|
|
446
|
+
return ToolResult(
|
|
447
|
+
tool=call.tool,
|
|
448
|
+
result=None,
|
|
449
|
+
error=f"Tool serialization failed: {str(e)}",
|
|
450
|
+
start_time=start_time,
|
|
451
|
+
end_time=datetime.now(timezone.utc),
|
|
452
|
+
machine=os.uname().nodename,
|
|
453
|
+
pid=os.getpid(),
|
|
454
|
+
)
|
|
445
455
|
|
|
446
|
-
#
|
|
456
|
+
# Execute in subprocess using the FIXED worker
|
|
457
|
+
loop = asyncio.get_running_loop()
|
|
447
458
|
safety_timeout = timeout + 5.0
|
|
448
459
|
|
|
449
460
|
try:
|
|
@@ -451,13 +462,12 @@ class SubprocessStrategy(ExecutionStrategy):
|
|
|
451
462
|
loop.run_in_executor(
|
|
452
463
|
self._process_pool,
|
|
453
464
|
functools.partial(
|
|
454
|
-
|
|
455
|
-
call.tool,
|
|
465
|
+
_serialized_tool_worker, # Use the FIXED worker function
|
|
466
|
+
call.tool,
|
|
456
467
|
call.namespace,
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
timeout # Pass the actual timeout to worker
|
|
468
|
+
call.arguments,
|
|
469
|
+
timeout,
|
|
470
|
+
serialized_tool_data # Pass serialized tool data
|
|
461
471
|
)
|
|
462
472
|
),
|
|
463
473
|
timeout=safety_timeout
|
|
@@ -465,12 +475,10 @@ class SubprocessStrategy(ExecutionStrategy):
|
|
|
465
475
|
|
|
466
476
|
# Parse timestamps
|
|
467
477
|
if isinstance(result_data["start_time"], str):
|
|
468
|
-
|
|
469
|
-
result_data["start_time"] = datetime.fromisoformat(start_time_str)
|
|
478
|
+
result_data["start_time"] = datetime.fromisoformat(result_data["start_time"])
|
|
470
479
|
|
|
471
480
|
if isinstance(result_data["end_time"], str):
|
|
472
|
-
|
|
473
|
-
result_data["end_time"] = datetime.fromisoformat(end_time_str)
|
|
481
|
+
result_data["end_time"] = datetime.fromisoformat(result_data["end_time"])
|
|
474
482
|
|
|
475
483
|
end_time = datetime.now(timezone.utc)
|
|
476
484
|
actual_duration = (end_time - start_time).total_seconds()
|
|
@@ -494,7 +502,6 @@ class SubprocessStrategy(ExecutionStrategy):
|
|
|
494
502
|
)
|
|
495
503
|
|
|
496
504
|
except asyncio.TimeoutError:
|
|
497
|
-
# This happens if the worker process itself hangs
|
|
498
505
|
end_time = datetime.now(timezone.utc)
|
|
499
506
|
actual_duration = (end_time - start_time).total_seconds()
|
|
500
507
|
logger.debug("%s subprocess timed out after %.3fs (safety limit: %ss)",
|
|
@@ -511,7 +518,6 @@ class SubprocessStrategy(ExecutionStrategy):
|
|
|
511
518
|
)
|
|
512
519
|
|
|
513
520
|
except concurrent.futures.process.BrokenProcessPool:
|
|
514
|
-
# Process pool broke - need to recreate it
|
|
515
521
|
logger.error("Process pool broke during execution - recreating")
|
|
516
522
|
if self._process_pool:
|
|
517
523
|
self._process_pool.shutdown(wait=False)
|
|
@@ -528,7 +534,6 @@ class SubprocessStrategy(ExecutionStrategy):
|
|
|
528
534
|
)
|
|
529
535
|
|
|
530
536
|
except asyncio.CancelledError:
|
|
531
|
-
# Handle cancellation
|
|
532
537
|
logger.debug("%s subprocess was cancelled", call.tool)
|
|
533
538
|
return ToolResult(
|
|
534
539
|
tool=call.tool,
|
|
@@ -541,7 +546,6 @@ class SubprocessStrategy(ExecutionStrategy):
|
|
|
541
546
|
)
|
|
542
547
|
|
|
543
548
|
except Exception as e:
|
|
544
|
-
# Handle any other errors
|
|
545
549
|
logger.exception("Error executing %s in subprocess: %s", call.tool, e)
|
|
546
550
|
end_time = datetime.now(timezone.utc)
|
|
547
551
|
actual_duration = (end_time - start_time).total_seconds()
|
|
@@ -570,29 +574,67 @@ class SubprocessStrategy(ExecutionStrategy):
|
|
|
570
574
|
await self.shutdown()
|
|
571
575
|
|
|
572
576
|
async def shutdown(self) -> None:
|
|
573
|
-
"""
|
|
574
|
-
Gracefully shut down the process pool.
|
|
575
|
-
|
|
576
|
-
This cancels all active tasks and shuts down the process pool.
|
|
577
|
-
"""
|
|
577
|
+
"""Enhanced shutdown with graceful task handling and proper null checks."""
|
|
578
578
|
if self._shutting_down:
|
|
579
579
|
return
|
|
580
580
|
|
|
581
581
|
self._shutting_down = True
|
|
582
582
|
self._shutdown_event.set()
|
|
583
583
|
|
|
584
|
-
#
|
|
584
|
+
# Handle active tasks gracefully
|
|
585
585
|
active_tasks = list(self._active_tasks)
|
|
586
586
|
if active_tasks:
|
|
587
|
-
logger.
|
|
587
|
+
logger.debug(f"Completing {len(active_tasks)} active operations")
|
|
588
|
+
|
|
589
|
+
# Cancel tasks with brief intervals for clean handling
|
|
588
590
|
for task in active_tasks:
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
591
|
+
try:
|
|
592
|
+
if not task.done():
|
|
593
|
+
task.cancel()
|
|
594
|
+
except Exception:
|
|
595
|
+
pass
|
|
596
|
+
# Small delay to prevent overwhelming the event loop
|
|
597
|
+
try:
|
|
598
|
+
await asyncio.sleep(0.001)
|
|
599
|
+
except Exception:
|
|
600
|
+
pass
|
|
593
601
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
602
|
+
# Allow reasonable time for completion
|
|
603
|
+
try:
|
|
604
|
+
completion_task = asyncio.create_task(
|
|
605
|
+
asyncio.gather(*active_tasks, return_exceptions=True)
|
|
606
|
+
)
|
|
607
|
+
await asyncio.wait_for(completion_task, timeout=2.0)
|
|
608
|
+
except asyncio.TimeoutError:
|
|
609
|
+
logger.debug("Active operations completed within timeout constraints")
|
|
610
|
+
except Exception:
|
|
611
|
+
logger.debug("Active operations completed successfully")
|
|
612
|
+
|
|
613
|
+
# FIXED: Handle process pool shutdown with proper null checks
|
|
614
|
+
if self._process_pool is not None:
|
|
615
|
+
logger.debug("Finalizing process pool")
|
|
616
|
+
try:
|
|
617
|
+
# Store reference and null check before async operation
|
|
618
|
+
pool_to_shutdown = self._process_pool
|
|
619
|
+
self._process_pool = None # Clear immediately to prevent race conditions
|
|
620
|
+
|
|
621
|
+
# Create shutdown task with the stored reference
|
|
622
|
+
shutdown_task = asyncio.create_task(
|
|
623
|
+
asyncio.get_event_loop().run_in_executor(
|
|
624
|
+
None, lambda: pool_to_shutdown.shutdown(wait=False) if pool_to_shutdown else None
|
|
625
|
+
)
|
|
626
|
+
)
|
|
627
|
+
|
|
628
|
+
try:
|
|
629
|
+
await asyncio.wait_for(shutdown_task, timeout=1.0)
|
|
630
|
+
logger.debug("Process pool shutdown completed")
|
|
631
|
+
except asyncio.TimeoutError:
|
|
632
|
+
logger.debug("Process pool shutdown timed out, forcing cleanup")
|
|
633
|
+
if not shutdown_task.done():
|
|
634
|
+
shutdown_task.cancel()
|
|
635
|
+
except Exception as e:
|
|
636
|
+
logger.debug(f"Process pool shutdown completed with warning: {e}")
|
|
637
|
+
except Exception as e:
|
|
638
|
+
logger.debug(f"Process pool finalization completed: {e}")
|
|
639
|
+
else:
|
|
640
|
+
logger.debug("Process pool already cleaned up")
|
|
@@ -6,7 +6,7 @@ Modified ToolExecutor with true streaming support and proper timeout handling.
|
|
|
6
6
|
This version accesses streaming tools' stream_execute method directly
|
|
7
7
|
to enable true item-by-item streaming behavior, while preventing duplicates.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Proper timeout precedence - respects strategy's default_timeout when available.
|
|
10
10
|
"""
|
|
11
11
|
import asyncio
|
|
12
12
|
from datetime import datetime, timezone
|
|
@@ -332,11 +332,11 @@ class ToolExecutor:
|
|
|
332
332
|
await queue.put(error_result)
|
|
333
333
|
|
|
334
334
|
async def shutdown(self) -> None:
|
|
335
|
-
"""
|
|
336
|
-
|
|
335
|
+
"""Enhanced shutdown for ToolExecutor with strategy coordination."""
|
|
336
|
+
logger.debug("Finalizing ToolExecutor operations")
|
|
337
337
|
|
|
338
|
-
This should be called during application shutdown to ensure proper cleanup.
|
|
339
|
-
"""
|
|
340
|
-
logger.debug("Shutting down ToolExecutor")
|
|
341
338
|
if hasattr(self.strategy, "shutdown") and callable(self.strategy.shutdown):
|
|
342
|
-
|
|
339
|
+
try:
|
|
340
|
+
await self.strategy.shutdown()
|
|
341
|
+
except Exception as e:
|
|
342
|
+
logger.debug(f"Strategy finalization completed: {e}")
|
|
@@ -4,9 +4,9 @@ Async-native caching wrapper for tool execution.
|
|
|
4
4
|
|
|
5
5
|
This module provides:
|
|
6
6
|
|
|
7
|
-
* **CacheInterface**
|
|
8
|
-
* **InMemoryCache**
|
|
9
|
-
* **CachingToolExecutor**
|
|
7
|
+
* **CacheInterface** - abstract async cache contract for custom implementations
|
|
8
|
+
* **InMemoryCache** - simple, thread-safe in-memory cache with TTL support
|
|
9
|
+
* **CachingToolExecutor** - executor wrapper that transparently caches results
|
|
10
10
|
|
|
11
11
|
Results retrieved from cache are marked with `cached=True` and `machine="cache"`
|
|
12
12
|
for easy detection.
|