chuk-tool-processor 0.12.2__tar.gz → 0.13__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.
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/PKG-INFO +54 -1
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/README.md +53 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/pyproject.toml +1 -1
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/strategies/inprocess_strategy.py +49 -8
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/strategies/subprocess_strategy.py +25 -6
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/tool_executor.py +2 -1
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/models/execution_strategy.py +19 -4
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor.egg-info/PKG-INFO +54 -1
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/setup.cfg +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/__init__.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/core/__init__.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/core/exceptions.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/core/processor.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/__init__.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/code_sandbox.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/strategies/__init__.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/wrappers/__init__.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/wrappers/caching.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/wrappers/circuit_breaker.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/wrappers/rate_limiting.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/execution/wrappers/retry.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/logging/__init__.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/logging/context.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/logging/formatter.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/logging/helpers.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/logging/metrics.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/__init__.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/mcp_tool.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/models.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/register_mcp_tools.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/setup_mcp_http_streamable.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/setup_mcp_sse.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/stream_manager.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/transport/__init__.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/transport/base_transport.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/transport/http_streamable_transport.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/transport/models.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/transport/sse_transport.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/transport/stdio_transport.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/models/__init__.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/models/streaming_tool.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/models/tool_call.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/models/tool_export_mixin.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/models/tool_result.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/models/tool_spec.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/models/validated_tool.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/observability/__init__.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/observability/metrics.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/observability/setup.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/observability/tracing.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/plugins/__init__.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/plugins/discovery.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/plugins/parsers/__init__.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/plugins/parsers/base.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/plugins/parsers/json_tool.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/plugins/parsers/openai_tool.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/plugins/parsers/xml_tool.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/py.typed +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/__init__.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/auto_register.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/decorators.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/interface.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/metadata.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/provider.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/providers/__init__.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/providers/memory.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/tool_export.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/utils/__init__.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/utils/fast_json.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/utils/validation.py +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor.egg-info/SOURCES.txt +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor.egg-info/dependency_links.txt +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor.egg-info/requires.txt +0 -0
- {chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chuk-tool-processor
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13
|
|
4
4
|
Summary: Async-native framework for registering, discovering, and executing tools referenced in LLM responses
|
|
5
5
|
Author-email: CHUK Team <chrishayuk@somejunkmailbox.com>
|
|
6
6
|
Maintainer-email: CHUK Team <chrishayuk@somejunkmailbox.com>
|
|
@@ -652,6 +652,57 @@ async with ToolProcessor(
|
|
|
652
652
|
''')
|
|
653
653
|
```
|
|
654
654
|
|
|
655
|
+
### Parallel Execution & Streaming Results
|
|
656
|
+
|
|
657
|
+
Tools execute concurrently by default. Results return in **completion order** — faster tools return immediately without waiting for slower ones:
|
|
658
|
+
|
|
659
|
+
```python
|
|
660
|
+
import asyncio
|
|
661
|
+
from chuk_tool_processor.execution.strategies.inprocess_strategy import InProcessStrategy
|
|
662
|
+
from chuk_tool_processor.models.tool_call import ToolCall
|
|
663
|
+
|
|
664
|
+
# Tools with different execution times
|
|
665
|
+
calls = [
|
|
666
|
+
ToolCall(tool="slow_api", arguments={"query": "complex"}), # 500ms
|
|
667
|
+
ToolCall(tool="medium_api", arguments={"query": "medium"}), # 200ms
|
|
668
|
+
ToolCall(tool="fast_api", arguments={"query": "simple"}), # 50ms
|
|
669
|
+
]
|
|
670
|
+
|
|
671
|
+
# Results return as: fast_api, medium_api, slow_api (completion order)
|
|
672
|
+
results = await strategy.run(calls)
|
|
673
|
+
|
|
674
|
+
# Match results back to original calls by tool name
|
|
675
|
+
for result in results:
|
|
676
|
+
print(f"{result.tool}: {result.result}")
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
**Stream results as they arrive** with `stream_run()`:
|
|
680
|
+
|
|
681
|
+
```python
|
|
682
|
+
async for result in strategy.stream_run(calls):
|
|
683
|
+
# Process each result immediately as it completes
|
|
684
|
+
print(f"Completed: {result.tool}")
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
**Track when tools start** with `on_tool_start` callback:
|
|
688
|
+
|
|
689
|
+
```python
|
|
690
|
+
async def on_start(call: ToolCall):
|
|
691
|
+
print(f"Starting: {call.tool}")
|
|
692
|
+
|
|
693
|
+
async for result in strategy.stream_run(calls, on_tool_start=on_start):
|
|
694
|
+
print(f"Completed: {result.tool}")
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
**Control concurrency** with `max_concurrency`:
|
|
698
|
+
|
|
699
|
+
```python
|
|
700
|
+
# Limit to 2 concurrent tools (others queue)
|
|
701
|
+
strategy = InProcessStrategy(registry, max_concurrency=2)
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
> **See:** `examples/parallel_execution_demo.py` for a complete demonstration.
|
|
705
|
+
|
|
655
706
|
## Documentation Quick Reference
|
|
656
707
|
|
|
657
708
|
| Document | What It Covers |
|
|
@@ -863,6 +914,8 @@ class WeatherTool(ValidatedTool):
|
|
|
863
914
|
| **InProcessStrategy** | Fast, trusted tools | Speed ✅, Isolation ❌ |
|
|
864
915
|
| **IsolatedStrategy** | Untrusted or risky code | Isolation ✅, Speed ❌ |
|
|
865
916
|
|
|
917
|
+
**Parallel Execution:** Both strategies execute tools concurrently by default. Results return in **completion order** (faster tools return first), not submission order. Use `ToolResult.tool` to match results to original calls.
|
|
918
|
+
|
|
866
919
|
```python
|
|
867
920
|
import asyncio
|
|
868
921
|
from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
|
|
@@ -620,6 +620,57 @@ async with ToolProcessor(
|
|
|
620
620
|
''')
|
|
621
621
|
```
|
|
622
622
|
|
|
623
|
+
### Parallel Execution & Streaming Results
|
|
624
|
+
|
|
625
|
+
Tools execute concurrently by default. Results return in **completion order** — faster tools return immediately without waiting for slower ones:
|
|
626
|
+
|
|
627
|
+
```python
|
|
628
|
+
import asyncio
|
|
629
|
+
from chuk_tool_processor.execution.strategies.inprocess_strategy import InProcessStrategy
|
|
630
|
+
from chuk_tool_processor.models.tool_call import ToolCall
|
|
631
|
+
|
|
632
|
+
# Tools with different execution times
|
|
633
|
+
calls = [
|
|
634
|
+
ToolCall(tool="slow_api", arguments={"query": "complex"}), # 500ms
|
|
635
|
+
ToolCall(tool="medium_api", arguments={"query": "medium"}), # 200ms
|
|
636
|
+
ToolCall(tool="fast_api", arguments={"query": "simple"}), # 50ms
|
|
637
|
+
]
|
|
638
|
+
|
|
639
|
+
# Results return as: fast_api, medium_api, slow_api (completion order)
|
|
640
|
+
results = await strategy.run(calls)
|
|
641
|
+
|
|
642
|
+
# Match results back to original calls by tool name
|
|
643
|
+
for result in results:
|
|
644
|
+
print(f"{result.tool}: {result.result}")
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
**Stream results as they arrive** with `stream_run()`:
|
|
648
|
+
|
|
649
|
+
```python
|
|
650
|
+
async for result in strategy.stream_run(calls):
|
|
651
|
+
# Process each result immediately as it completes
|
|
652
|
+
print(f"Completed: {result.tool}")
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
**Track when tools start** with `on_tool_start` callback:
|
|
656
|
+
|
|
657
|
+
```python
|
|
658
|
+
async def on_start(call: ToolCall):
|
|
659
|
+
print(f"Starting: {call.tool}")
|
|
660
|
+
|
|
661
|
+
async for result in strategy.stream_run(calls, on_tool_start=on_start):
|
|
662
|
+
print(f"Completed: {result.tool}")
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
**Control concurrency** with `max_concurrency`:
|
|
666
|
+
|
|
667
|
+
```python
|
|
668
|
+
# Limit to 2 concurrent tools (others queue)
|
|
669
|
+
strategy = InProcessStrategy(registry, max_concurrency=2)
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
> **See:** `examples/parallel_execution_demo.py` for a complete demonstration.
|
|
673
|
+
|
|
623
674
|
## Documentation Quick Reference
|
|
624
675
|
|
|
625
676
|
| Document | What It Covers |
|
|
@@ -831,6 +882,8 @@ class WeatherTool(ValidatedTool):
|
|
|
831
882
|
| **InProcessStrategy** | Fast, trusted tools | Speed ✅, Isolation ❌ |
|
|
832
883
|
| **IsolatedStrategy** | Untrusted or risky code | Isolation ✅, Speed ❌ |
|
|
833
884
|
|
|
885
|
+
**Parallel Execution:** Both strategies execute tools concurrently by default. Results return in **completion order** (faster tools return first), not submission order. Use `ToolResult.tool` to match results to original calls.
|
|
886
|
+
|
|
834
887
|
```python
|
|
835
888
|
import asyncio
|
|
836
889
|
from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "chuk-tool-processor"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.13"
|
|
8
8
|
description = "Async-native framework for registering, discovering, and executing tools referenced in LLM responses"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -7,6 +7,18 @@ 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
|
+
PARALLEL EXECUTION:
|
|
11
|
+
- All tool calls execute concurrently using asyncio tasks
|
|
12
|
+
- Results are returned/yielded as each tool completes (completion order, not submission order)
|
|
13
|
+
- Faster tools return immediately without waiting for slower ones
|
|
14
|
+
- Use run() for batch results in completion order
|
|
15
|
+
- Use stream_run() to yield results as they arrive
|
|
16
|
+
|
|
17
|
+
STREAMING SUPPORT:
|
|
18
|
+
- stream_run() yields ToolResult objects as each tool completes
|
|
19
|
+
- Optional on_tool_start callback for emitting start events
|
|
20
|
+
- True streaming for tools that implement stream_execute
|
|
21
|
+
|
|
10
22
|
Enhanced tool name resolution that properly handles:
|
|
11
23
|
- Simple names: "get_current_time"
|
|
12
24
|
- Namespaced names: "diagnostic_test.get_current_time"
|
|
@@ -23,7 +35,7 @@ import builtins
|
|
|
23
35
|
import inspect
|
|
24
36
|
import os
|
|
25
37
|
import platform
|
|
26
|
-
from collections.abc import AsyncIterator
|
|
38
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
27
39
|
from contextlib import asynccontextmanager, suppress
|
|
28
40
|
from datetime import UTC, datetime
|
|
29
41
|
from typing import Any
|
|
@@ -122,14 +134,17 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
122
134
|
timeout: float | None = None,
|
|
123
135
|
) -> list[ToolResult]:
|
|
124
136
|
"""
|
|
125
|
-
Execute tool calls concurrently and
|
|
137
|
+
Execute tool calls concurrently and return results as they complete.
|
|
138
|
+
|
|
139
|
+
NOTE: Results are returned in COMPLETION ORDER, not submission order.
|
|
140
|
+
This allows faster tools to return immediately without waiting for slower ones.
|
|
126
141
|
|
|
127
142
|
Args:
|
|
128
143
|
calls: List of tool calls to execute
|
|
129
144
|
timeout: Optional timeout for execution
|
|
130
145
|
|
|
131
146
|
Returns:
|
|
132
|
-
List of tool results in
|
|
147
|
+
List of tool results in completion order (not submission order)
|
|
133
148
|
"""
|
|
134
149
|
if not calls:
|
|
135
150
|
return []
|
|
@@ -138,6 +153,7 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
138
153
|
effective_timeout = timeout if timeout is not None else self.default_timeout
|
|
139
154
|
logger.debug("Executing %d calls with %ss timeout each", len(calls), effective_timeout)
|
|
140
155
|
|
|
156
|
+
# Create all tasks immediately so they start running in parallel
|
|
141
157
|
tasks = []
|
|
142
158
|
for call in calls:
|
|
143
159
|
task = asyncio.create_task(
|
|
@@ -148,17 +164,35 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
148
164
|
tasks.append(task)
|
|
149
165
|
|
|
150
166
|
async with log_context_span("inprocess_execution", {"num_calls": len(calls)}):
|
|
151
|
-
return
|
|
167
|
+
# Use as_completed to return results as they finish, not all at once
|
|
168
|
+
results = []
|
|
169
|
+
for completed_task in asyncio.as_completed(tasks):
|
|
170
|
+
result = await completed_task
|
|
171
|
+
results.append(result)
|
|
172
|
+
return results
|
|
152
173
|
|
|
153
174
|
# ------------------------------------------------------------------ #
|
|
154
175
|
async def stream_run(
|
|
155
176
|
self,
|
|
156
177
|
calls: list[ToolCall],
|
|
157
178
|
timeout: float | None = None,
|
|
179
|
+
on_tool_start: Callable[[ToolCall], Awaitable[None]] | None = None,
|
|
158
180
|
) -> AsyncIterator[ToolResult]:
|
|
159
181
|
"""
|
|
160
182
|
Execute tool calls concurrently and *yield* results as soon as they are
|
|
161
|
-
produced
|
|
183
|
+
produced in completion order (not submission order).
|
|
184
|
+
|
|
185
|
+
This method allows results to stream back as each tool completes, without
|
|
186
|
+
waiting for all tools to finish. Faster tools return immediately.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
calls: List of tool calls to execute concurrently
|
|
190
|
+
timeout: Optional timeout for each tool execution
|
|
191
|
+
on_tool_start: Optional callback invoked when each tool starts execution.
|
|
192
|
+
Useful for emitting start events before results arrive.
|
|
193
|
+
|
|
194
|
+
Yields:
|
|
195
|
+
ToolResult objects as each tool completes (in completion order)
|
|
162
196
|
"""
|
|
163
197
|
if not calls:
|
|
164
198
|
return
|
|
@@ -168,9 +202,7 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
168
202
|
|
|
169
203
|
queue: asyncio.Queue[ToolResult] = asyncio.Queue()
|
|
170
204
|
tasks = {
|
|
171
|
-
asyncio.create_task(
|
|
172
|
-
self._stream_tool_call(call, queue, effective_timeout) # Always pass timeout
|
|
173
|
-
)
|
|
205
|
+
asyncio.create_task(self._stream_tool_call(call, queue, effective_timeout, on_tool_start))
|
|
174
206
|
for call in calls
|
|
175
207
|
if call.id not in self._direct_streaming_calls
|
|
176
208
|
}
|
|
@@ -194,6 +226,7 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
194
226
|
call: ToolCall,
|
|
195
227
|
queue: asyncio.Queue,
|
|
196
228
|
timeout: float, # Make timeout required
|
|
229
|
+
on_tool_start: Callable[[ToolCall], Awaitable[None]] | None = None,
|
|
197
230
|
) -> None:
|
|
198
231
|
"""
|
|
199
232
|
Execute a tool call with streaming support.
|
|
@@ -205,6 +238,7 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
205
238
|
call: The tool call to execute
|
|
206
239
|
queue: Queue to put results into
|
|
207
240
|
timeout: Timeout in seconds (required)
|
|
241
|
+
on_tool_start: Optional callback to invoke when tool execution starts
|
|
208
242
|
"""
|
|
209
243
|
# Skip if call is being handled directly by the executor
|
|
210
244
|
if call.id in self._direct_streaming_calls:
|
|
@@ -225,6 +259,13 @@ class InProcessStrategy(ExecutionStrategy):
|
|
|
225
259
|
await queue.put(result)
|
|
226
260
|
return
|
|
227
261
|
|
|
262
|
+
# Invoke start callback if provided
|
|
263
|
+
if on_tool_start:
|
|
264
|
+
try:
|
|
265
|
+
await on_tool_start(call)
|
|
266
|
+
except Exception as e:
|
|
267
|
+
logger.warning(f"on_tool_start callback failed for {call.tool}: {e}")
|
|
268
|
+
|
|
228
269
|
try:
|
|
229
270
|
# Use enhanced tool resolution instead of direct lookup
|
|
230
271
|
tool_impl, resolved_namespace = await self._resolve_tool_info(call.tool, call.namespace)
|
|
@@ -25,7 +25,7 @@ import os
|
|
|
25
25
|
import pickle
|
|
26
26
|
import platform
|
|
27
27
|
import signal
|
|
28
|
-
from collections.abc import AsyncIterator
|
|
28
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
29
29
|
from datetime import UTC, datetime
|
|
30
30
|
from typing import Any
|
|
31
31
|
|
|
@@ -264,14 +264,17 @@ class SubprocessStrategy(ExecutionStrategy):
|
|
|
264
264
|
timeout: float | None = None,
|
|
265
265
|
) -> list[ToolResult]:
|
|
266
266
|
"""
|
|
267
|
-
Execute tool calls in separate processes.
|
|
267
|
+
Execute tool calls in separate processes and return results as they complete.
|
|
268
|
+
|
|
269
|
+
NOTE: Results are returned in COMPLETION ORDER, not submission order.
|
|
270
|
+
This allows faster tools to return immediately without waiting for slower ones.
|
|
268
271
|
|
|
269
272
|
Args:
|
|
270
273
|
calls: List of tool calls to execute
|
|
271
274
|
timeout: Optional timeout for each execution (overrides default)
|
|
272
275
|
|
|
273
276
|
Returns:
|
|
274
|
-
List of tool results in
|
|
277
|
+
List of tool results in completion order (not submission order)
|
|
275
278
|
"""
|
|
276
279
|
if not calls:
|
|
277
280
|
return []
|
|
@@ -308,14 +311,19 @@ class SubprocessStrategy(ExecutionStrategy):
|
|
|
308
311
|
task.add_done_callback(self._active_tasks.discard)
|
|
309
312
|
tasks.append(task)
|
|
310
313
|
|
|
311
|
-
# Execute all tasks concurrently
|
|
314
|
+
# Execute all tasks concurrently and return results in completion order
|
|
312
315
|
async with log_context_span("subprocess_execution", {"num_calls": len(calls)}):
|
|
313
|
-
|
|
316
|
+
results = []
|
|
317
|
+
for completed_task in asyncio.as_completed(tasks):
|
|
318
|
+
result = await completed_task
|
|
319
|
+
results.append(result)
|
|
320
|
+
return results
|
|
314
321
|
|
|
315
322
|
async def stream_run(
|
|
316
323
|
self,
|
|
317
324
|
calls: list[ToolCall],
|
|
318
325
|
timeout: float | None = None,
|
|
326
|
+
on_tool_start: Callable[[ToolCall], Awaitable[None]] | None = None,
|
|
319
327
|
) -> AsyncIterator[ToolResult]:
|
|
320
328
|
"""
|
|
321
329
|
Execute tool calls and yield results as they become available.
|
|
@@ -323,9 +331,11 @@ class SubprocessStrategy(ExecutionStrategy):
|
|
|
323
331
|
Args:
|
|
324
332
|
calls: List of tool calls to execute
|
|
325
333
|
timeout: Optional timeout for each execution
|
|
334
|
+
on_tool_start: Optional callback invoked when each tool starts execution.
|
|
335
|
+
Useful for emitting start events before results arrive.
|
|
326
336
|
|
|
327
337
|
Yields:
|
|
328
|
-
Tool results as they complete (
|
|
338
|
+
Tool results as they complete (in completion order)
|
|
329
339
|
"""
|
|
330
340
|
if not calls:
|
|
331
341
|
return
|
|
@@ -358,6 +368,7 @@ class SubprocessStrategy(ExecutionStrategy):
|
|
|
358
368
|
call,
|
|
359
369
|
queue,
|
|
360
370
|
effective_timeout, # Always pass concrete timeout
|
|
371
|
+
on_tool_start,
|
|
361
372
|
)
|
|
362
373
|
)
|
|
363
374
|
self._active_tasks.add(task)
|
|
@@ -385,8 +396,16 @@ class SubprocessStrategy(ExecutionStrategy):
|
|
|
385
396
|
call: ToolCall,
|
|
386
397
|
queue: asyncio.Queue,
|
|
387
398
|
timeout: float, # Make timeout required
|
|
399
|
+
on_tool_start: Callable[[ToolCall], Awaitable[None]] | None = None,
|
|
388
400
|
) -> None:
|
|
389
401
|
"""Execute a single call and put the result in the queue."""
|
|
402
|
+
# Invoke start callback if provided
|
|
403
|
+
if on_tool_start:
|
|
404
|
+
try:
|
|
405
|
+
await on_tool_start(call)
|
|
406
|
+
except Exception as e:
|
|
407
|
+
logger.warning(f"on_tool_start callback failed for {call.tool}: {e}")
|
|
408
|
+
|
|
390
409
|
result = await self._execute_single_call(call, timeout)
|
|
391
410
|
await queue.put(result)
|
|
392
411
|
|
|
@@ -107,7 +107,8 @@ class ToolExecutor:
|
|
|
107
107
|
use_cache: Whether to use cached results (for caching wrappers)
|
|
108
108
|
|
|
109
109
|
Returns:
|
|
110
|
-
List of tool results in
|
|
110
|
+
List of tool results in completion order (not submission order).
|
|
111
|
+
Use ToolResult.tool to match results back to their original calls.
|
|
111
112
|
"""
|
|
112
113
|
if not calls:
|
|
113
114
|
return []
|
|
@@ -6,7 +6,7 @@ Abstract base class for tool execution strategies.
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
8
|
from abc import ABC, abstractmethod
|
|
9
|
-
from collections.abc import AsyncIterator
|
|
9
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
10
10
|
|
|
11
11
|
from chuk_tool_processor.models.tool_call import ToolCall
|
|
12
12
|
from chuk_tool_processor.models.tool_result import ToolResult
|
|
@@ -18,6 +18,11 @@ class ExecutionStrategy(ABC):
|
|
|
18
18
|
|
|
19
19
|
All execution strategies must implement at least the run method,
|
|
20
20
|
and optionally stream_run for streaming support.
|
|
21
|
+
|
|
22
|
+
PARALLEL EXECUTION NOTE:
|
|
23
|
+
Results are returned in COMPLETION ORDER, not submission order.
|
|
24
|
+
This allows faster tools to return immediately without waiting for slower ones.
|
|
25
|
+
Use the ToolResult.tool attribute to match results back to their original calls.
|
|
21
26
|
"""
|
|
22
27
|
|
|
23
28
|
@abstractmethod
|
|
@@ -30,11 +35,17 @@ class ExecutionStrategy(ABC):
|
|
|
30
35
|
timeout: Optional timeout in seconds for each call
|
|
31
36
|
|
|
32
37
|
Returns:
|
|
33
|
-
List of ToolResult objects in
|
|
38
|
+
List of ToolResult objects in completion order (not submission order).
|
|
39
|
+
Use ToolResult.tool to match results back to their original calls.
|
|
34
40
|
"""
|
|
35
41
|
pass
|
|
36
42
|
|
|
37
|
-
async def stream_run(
|
|
43
|
+
async def stream_run(
|
|
44
|
+
self,
|
|
45
|
+
calls: list[ToolCall],
|
|
46
|
+
timeout: float | None = None,
|
|
47
|
+
on_tool_start: Callable[[ToolCall], Awaitable[None]] | None = None, # noqa: ARG002
|
|
48
|
+
) -> AsyncIterator[ToolResult]:
|
|
38
49
|
"""
|
|
39
50
|
Execute tool calls and yield results as they become available.
|
|
40
51
|
|
|
@@ -44,10 +55,14 @@ class ExecutionStrategy(ABC):
|
|
|
44
55
|
Args:
|
|
45
56
|
calls: List of ToolCall objects to execute
|
|
46
57
|
timeout: Optional timeout in seconds for each call
|
|
58
|
+
on_tool_start: Optional callback invoked when each tool starts execution.
|
|
59
|
+
Useful for emitting start events before results arrive.
|
|
47
60
|
|
|
48
61
|
Yields:
|
|
49
|
-
ToolResult objects as they become available
|
|
62
|
+
ToolResult objects as they become available (in completion order)
|
|
50
63
|
"""
|
|
64
|
+
# Default implementation ignores on_tool_start since we batch execute
|
|
65
|
+
# Subclasses with true streaming can use the callback
|
|
51
66
|
results = await self.run(calls, timeout=timeout)
|
|
52
67
|
for result in results:
|
|
53
68
|
yield result
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chuk-tool-processor
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13
|
|
4
4
|
Summary: Async-native framework for registering, discovering, and executing tools referenced in LLM responses
|
|
5
5
|
Author-email: CHUK Team <chrishayuk@somejunkmailbox.com>
|
|
6
6
|
Maintainer-email: CHUK Team <chrishayuk@somejunkmailbox.com>
|
|
@@ -652,6 +652,57 @@ async with ToolProcessor(
|
|
|
652
652
|
''')
|
|
653
653
|
```
|
|
654
654
|
|
|
655
|
+
### Parallel Execution & Streaming Results
|
|
656
|
+
|
|
657
|
+
Tools execute concurrently by default. Results return in **completion order** — faster tools return immediately without waiting for slower ones:
|
|
658
|
+
|
|
659
|
+
```python
|
|
660
|
+
import asyncio
|
|
661
|
+
from chuk_tool_processor.execution.strategies.inprocess_strategy import InProcessStrategy
|
|
662
|
+
from chuk_tool_processor.models.tool_call import ToolCall
|
|
663
|
+
|
|
664
|
+
# Tools with different execution times
|
|
665
|
+
calls = [
|
|
666
|
+
ToolCall(tool="slow_api", arguments={"query": "complex"}), # 500ms
|
|
667
|
+
ToolCall(tool="medium_api", arguments={"query": "medium"}), # 200ms
|
|
668
|
+
ToolCall(tool="fast_api", arguments={"query": "simple"}), # 50ms
|
|
669
|
+
]
|
|
670
|
+
|
|
671
|
+
# Results return as: fast_api, medium_api, slow_api (completion order)
|
|
672
|
+
results = await strategy.run(calls)
|
|
673
|
+
|
|
674
|
+
# Match results back to original calls by tool name
|
|
675
|
+
for result in results:
|
|
676
|
+
print(f"{result.tool}: {result.result}")
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
**Stream results as they arrive** with `stream_run()`:
|
|
680
|
+
|
|
681
|
+
```python
|
|
682
|
+
async for result in strategy.stream_run(calls):
|
|
683
|
+
# Process each result immediately as it completes
|
|
684
|
+
print(f"Completed: {result.tool}")
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
**Track when tools start** with `on_tool_start` callback:
|
|
688
|
+
|
|
689
|
+
```python
|
|
690
|
+
async def on_start(call: ToolCall):
|
|
691
|
+
print(f"Starting: {call.tool}")
|
|
692
|
+
|
|
693
|
+
async for result in strategy.stream_run(calls, on_tool_start=on_start):
|
|
694
|
+
print(f"Completed: {result.tool}")
|
|
695
|
+
```
|
|
696
|
+
|
|
697
|
+
**Control concurrency** with `max_concurrency`:
|
|
698
|
+
|
|
699
|
+
```python
|
|
700
|
+
# Limit to 2 concurrent tools (others queue)
|
|
701
|
+
strategy = InProcessStrategy(registry, max_concurrency=2)
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
> **See:** `examples/parallel_execution_demo.py` for a complete demonstration.
|
|
705
|
+
|
|
655
706
|
## Documentation Quick Reference
|
|
656
707
|
|
|
657
708
|
| Document | What It Covers |
|
|
@@ -863,6 +914,8 @@ class WeatherTool(ValidatedTool):
|
|
|
863
914
|
| **InProcessStrategy** | Fast, trusted tools | Speed ✅, Isolation ❌ |
|
|
864
915
|
| **IsolatedStrategy** | Untrusted or risky code | Isolation ✅, Speed ❌ |
|
|
865
916
|
|
|
917
|
+
**Parallel Execution:** Both strategies execute tools concurrently by default. Results return in **completion order** (faster tools return first), not submission order. Use `ToolResult.tool` to match results to original calls.
|
|
918
|
+
|
|
866
919
|
```python
|
|
867
920
|
import asyncio
|
|
868
921
|
from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
|
|
File without changes
|
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/core/__init__.py
RENAMED
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/core/exceptions.py
RENAMED
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/core/processor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/logging/__init__.py
RENAMED
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/logging/context.py
RENAMED
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/logging/formatter.py
RENAMED
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/logging/helpers.py
RENAMED
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/logging/metrics.py
RENAMED
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/__init__.py
RENAMED
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/mcp_tool.py
RENAMED
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/mcp/setup_mcp_sse.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/models/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/models/tool_call.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/models/tool_spec.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/plugins/__init__.py
RENAMED
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/plugins/discovery.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/metadata.py
RENAMED
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/registry/provider.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/utils/__init__.py
RENAMED
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/utils/fast_json.py
RENAMED
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor/utils/validation.py
RENAMED
|
File without changes
|
{chuk_tool_processor-0.12.2 → chuk_tool_processor-0.13}/src/chuk_tool_processor.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|