chuk-tool-processor 0.9.7__tar.gz → 0.10__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.9.7 → chuk_tool_processor-0.10}/PKG-INFO +666 -153
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/README.md +665 -152
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/pyproject.toml +35 -13
- chuk_tool_processor-0.10/src/chuk_tool_processor/__init__.py +114 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/core/processor.py +363 -44
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/logging/__init__.py +5 -8
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/logging/context.py +2 -5
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/__init__.py +3 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/mcp_tool.py +8 -3
- chuk_tool_processor-0.10/src/chuk_tool_processor/mcp/models.py +87 -0
- chuk_tool_processor-0.10/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +162 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/stream_manager.py +94 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/models/tool_export_mixin.py +4 -4
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/observability/metrics.py +3 -3
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/observability/tracing.py +13 -12
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/registry/interface.py +7 -7
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/registry/providers/__init__.py +2 -1
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/registry/tool_export.py +1 -6
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor.egg-info/PKG-INFO +666 -153
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor.egg-info/SOURCES.txt +2 -0
- chuk_tool_processor-0.9.7/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +0 -82
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/setup.cfg +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/core/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/core/exceptions.py +0 -0
- {chuk_tool_processor-0.9.7/src/chuk_tool_processor → chuk_tool_processor-0.10/src/chuk_tool_processor/execution}/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/execution/strategies/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/execution/strategies/inprocess_strategy.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/execution/strategies/subprocess_strategy.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/execution/tool_executor.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/execution/wrappers/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/execution/wrappers/caching.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/execution/wrappers/circuit_breaker.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/execution/wrappers/rate_limiting.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/execution/wrappers/retry.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/logging/formatter.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/logging/helpers.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/logging/metrics.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/register_mcp_tools.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/setup_mcp_http_streamable.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/setup_mcp_sse.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/transport/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/transport/base_transport.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/transport/http_streamable_transport.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/transport/models.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/transport/sse_transport.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/mcp/transport/stdio_transport.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/models/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/models/execution_strategy.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/models/streaming_tool.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/models/tool_call.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/models/tool_result.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/models/tool_spec.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/models/validated_tool.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/observability/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/observability/setup.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/plugins/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/plugins/discovery.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/plugins/parsers/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/plugins/parsers/base.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/plugins/parsers/json_tool.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/plugins/parsers/openai_tool.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/plugins/parsers/xml_tool.py +0 -0
- /chuk_tool_processor-0.9.7/src/chuk_tool_processor/execution/__init__.py → /chuk_tool_processor-0.10/src/chuk_tool_processor/py.typed +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/registry/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/registry/auto_register.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/registry/decorators.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/registry/metadata.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/registry/provider.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/registry/providers/memory.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/utils/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor/utils/validation.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor.egg-info/dependency_links.txt +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/src/chuk_tool_processor.egg-info/requires.txt +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.10}/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.10
|
|
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>
|
|
@@ -26,65 +26,147 @@ Requires-Dist: psutil>=7.0.0
|
|
|
26
26
|
Requires-Dist: pydantic>=2.11.3
|
|
27
27
|
Requires-Dist: uuid>=1.30
|
|
28
28
|
|
|
29
|
-
# CHUK Tool Processor
|
|
29
|
+
# CHUK Tool Processor — Production-grade execution for LLM tool calls
|
|
30
30
|
|
|
31
31
|
[](https://pypi.org/project/chuk-tool-processor/)
|
|
32
32
|
[](https://pypi.org/project/chuk-tool-processor/)
|
|
33
33
|
[](LICENSE)
|
|
34
|
+
[](https://www.python.org/dev/peps/pep-0561/)
|
|
35
|
+
[](https://pypi.org/project/chuk-tool-processor/)
|
|
36
|
+
[](docs/OBSERVABILITY.md)
|
|
34
37
|
|
|
35
|
-
**
|
|
38
|
+
**Reliable tool execution for LLMs — timeouts, retries, caching, rate limits, circuit breakers, and MCP integration — in one composable layer.**
|
|
36
39
|
|
|
37
|
-
|
|
40
|
+
---
|
|
38
41
|
|
|
39
|
-
## The
|
|
42
|
+
## The Missing Layer for Reliable Tool Execution
|
|
40
43
|
|
|
41
|
-
|
|
44
|
+
LLMs are good at *calling* tools. The hard part is **executing** those tools reliably.
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- Cache expensive results
|
|
48
|
-
- Rate limit API calls
|
|
49
|
-
- Run untrusted code safely
|
|
50
|
-
- Connect to external tool servers
|
|
51
|
-
- Log everything for debugging
|
|
52
|
-
3. **Get results back** to continue the LLM conversation
|
|
46
|
+
**CHUK Tool Processor:**
|
|
47
|
+
- Parses tool calls from any model (Anthropic XML, OpenAI `tool_calls`, JSON)
|
|
48
|
+
- Executes them with **timeouts, retries, caching, rate limits, circuit breaker, observability**
|
|
49
|
+
- Runs tools locally, in **isolated subprocesses**, or **remote via MCP**
|
|
53
50
|
|
|
54
|
-
|
|
51
|
+
CHUK Tool Processor is the execution layer between LLM responses and real tools.
|
|
55
52
|
|
|
56
|
-
|
|
53
|
+
It sits **below** agent frameworks and prompt orchestration, and **above** raw tool implementations.
|
|
57
54
|
|
|
58
|
-
|
|
55
|
+
```
|
|
56
|
+
LLM Output
|
|
57
|
+
↓
|
|
58
|
+
CHUK Tool Processor
|
|
59
|
+
↓
|
|
60
|
+
┌──────────────┬────────────────────┐
|
|
61
|
+
│ Local Tools │ Remote Tools (MCP) │
|
|
62
|
+
└──────────────┴────────────────────┘
|
|
63
|
+
```
|
|
59
64
|
|
|
60
|
-
|
|
65
|
+
**How it works internally:**
|
|
61
66
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
```
|
|
68
|
+
LLM Output
|
|
69
|
+
↓
|
|
70
|
+
Parsers (XML / OpenAI / JSON)
|
|
71
|
+
↓
|
|
72
|
+
┌─────────────────────────────┐
|
|
73
|
+
│ Execution Middleware │
|
|
74
|
+
│ (Applied in this order) │
|
|
75
|
+
│ • Cache │
|
|
76
|
+
│ • Rate Limit │
|
|
77
|
+
│ • Retry (with backoff) │
|
|
78
|
+
│ • Circuit Breaker │
|
|
79
|
+
└─────────────────────────────┘
|
|
80
|
+
↓
|
|
81
|
+
Execution Strategy
|
|
82
|
+
┌──────────────────────┐
|
|
83
|
+
│ • InProcess │ ← Fast, trusted
|
|
84
|
+
│ • Isolated/Subprocess│ ← Safe, untrusted
|
|
85
|
+
│ • Remote via MCP │ ← Distributed
|
|
86
|
+
└──────────────────────┘
|
|
87
|
+
```
|
|
69
88
|
|
|
70
|
-
|
|
89
|
+
Works with OpenAI, Anthropic, local models (Ollama/MLX/vLLM), and any framework (LangChain, LlamaIndex, custom).
|
|
90
|
+
|
|
91
|
+
## Executive TL;DR
|
|
92
|
+
|
|
93
|
+
* **Parse any format:** `XML` (Anthropic), `OpenAI tool_calls`, or raw `JSON`
|
|
94
|
+
* **Execute with production policies:** timeouts/retries/cache/rate-limits/circuit-breaker/idempotency
|
|
95
|
+
* **Run anywhere:** locally (fast), isolated (subprocess sandbox), or remote via MCP (HTTP/STDIO/SSE)
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
import asyncio
|
|
99
|
+
from chuk_tool_processor import ToolProcessor, register_tool, initialize
|
|
100
|
+
|
|
101
|
+
@register_tool(name="weather")
|
|
102
|
+
class WeatherTool:
|
|
103
|
+
async def execute(self, city: str) -> dict:
|
|
104
|
+
return {"temp": 72, "condition": "sunny", "city": city}
|
|
105
|
+
|
|
106
|
+
async def main():
|
|
107
|
+
await initialize()
|
|
108
|
+
async with ToolProcessor(enable_caching=True, enable_retries=True) as p:
|
|
109
|
+
# Works with OpenAI, Anthropic, or JSON formats
|
|
110
|
+
result = await p.process('<tool name="weather" args=\'{"city": "SF"}\'/>')
|
|
111
|
+
print(result[0].result) # {'temp': 72, 'condition': 'sunny', 'city': 'SF'}
|
|
71
112
|
|
|
72
|
-
|
|
113
|
+
asyncio.run(main())
|
|
114
|
+
```
|
|
73
115
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
-
|
|
116
|
+
> **If you only remember three things:**
|
|
117
|
+
>
|
|
118
|
+
> 1. **Parse** `XML`, `OpenAI tool_calls`, or raw `JSON` automatically
|
|
119
|
+
> 2. **Execute** with timeouts/retries/cache/rate-limits/circuit-breaker
|
|
120
|
+
> 3. **Run** tools locally, isolated (subprocess), or remote via MCP
|
|
121
|
+
|
|
122
|
+
## When to Use This
|
|
123
|
+
|
|
124
|
+
Use **CHUK Tool Processor** when:
|
|
125
|
+
- Your LLM calls tools or APIs
|
|
126
|
+
- You need **retries, timeouts, caching, or rate limits**
|
|
127
|
+
- You need to **run untrusted tools safely**
|
|
128
|
+
- Your tools are **local or remote (MCP)**
|
|
129
|
+
|
|
130
|
+
Do **not** use this if:
|
|
131
|
+
- You want an agent framework
|
|
132
|
+
- You want conversation flow/memory orchestration
|
|
133
|
+
|
|
134
|
+
**This is the execution layer, not the agent.**
|
|
135
|
+
|
|
136
|
+
> **Not a framework.**
|
|
137
|
+
> If LangChain/LlamaIndex help decide *which* tool to call,
|
|
138
|
+
> CHUK Tool Processor makes sure the tool call **actually succeeds**.
|
|
139
|
+
|
|
140
|
+
## Table of Contents
|
|
141
|
+
|
|
142
|
+
- [The Problem](#the-problem)
|
|
143
|
+
- [Why chuk-tool-processor?](#why-chuk-tool-processor)
|
|
144
|
+
- [Compatibility Matrix](#compatibility-matrix)
|
|
145
|
+
- [Developer Experience Highlights](#developer-experience-highlights)
|
|
146
|
+
- [Quick Start](#quick-start)
|
|
147
|
+
- [Documentation Quick Reference](#documentation-quick-reference)
|
|
148
|
+
- [Choose Your Path](#choose-your-path)
|
|
149
|
+
- [Core Concepts](#core-concepts)
|
|
150
|
+
- [Getting Started](#getting-started)
|
|
151
|
+
- [Advanced Topics](#advanced-topics)
|
|
152
|
+
- [Configuration](#configuration)
|
|
153
|
+
- [Architecture Principles](#architecture-principles)
|
|
154
|
+
- [Examples](#examples)
|
|
155
|
+
- [FAQ](#faq)
|
|
156
|
+
- [Comparison with Other Tools](#comparison-with-other-tools)
|
|
157
|
+
- [Development & Publishing](#development--publishing)
|
|
158
|
+
- [Stability & Versioning](#stability--versioning)
|
|
159
|
+
- [Contributing & Support](#contributing--support)
|
|
160
|
+
|
|
161
|
+
## The Problem
|
|
84
162
|
|
|
85
|
-
|
|
163
|
+
LLMs generate tool calls. **The hard part is executing them reliably.**
|
|
164
|
+
|
|
165
|
+
CHUK Tool Processor **is that execution layer.**
|
|
166
|
+
|
|
167
|
+
## Why chuk-tool-processor?
|
|
86
168
|
|
|
87
|
-
|
|
169
|
+
**Composable execution layers:**
|
|
88
170
|
|
|
89
171
|
```
|
|
90
172
|
┌─────────────────────────────────┐
|
|
@@ -104,7 +186,7 @@ CHUK Tool Processor uses a **composable stack architecture**:
|
|
|
104
186
|
├─────────────────────────────────┤
|
|
105
187
|
│ Execution Strategy │ ← How to run tools
|
|
106
188
|
│ • InProcess (fast) │
|
|
107
|
-
│ •
|
|
189
|
+
│ • Isolated (subprocess) │
|
|
108
190
|
├─────────────────────────────────┤
|
|
109
191
|
│ Tool Registry │ ← Your registered tools
|
|
110
192
|
└─────────────────────────────────┘
|
|
@@ -112,8 +194,40 @@ CHUK Tool Processor uses a **composable stack architecture**:
|
|
|
112
194
|
|
|
113
195
|
Each layer is **optional** and **configurable**. Mix and match what you need.
|
|
114
196
|
|
|
197
|
+
### It's a Building Block, Not a Framework
|
|
198
|
+
|
|
199
|
+
Unlike full-fledged LLM frameworks (LangChain, LlamaIndex, etc.), CHUK Tool Processor:
|
|
200
|
+
|
|
201
|
+
- ✅ **Does one thing well**: Process tool calls reliably
|
|
202
|
+
- ✅ **Plugs into any LLM app**: Works with any framework or no framework
|
|
203
|
+
- ✅ **Composable by design**: Stack strategies and wrappers like middleware
|
|
204
|
+
- ✅ **No opinions about your LLM**: Bring your own OpenAI, Anthropic, local model
|
|
205
|
+
- ❌ **Doesn't manage conversations**: That's your job
|
|
206
|
+
- ❌ **Doesn't do prompt engineering**: Use whatever prompting you want
|
|
207
|
+
- ❌ **Doesn't bundle an LLM client**: Use any client library you prefer
|
|
208
|
+
|
|
209
|
+
### It's Built for Production
|
|
210
|
+
|
|
211
|
+
Research code vs production code is about handling the edges. CHUK Tool Processor includes:
|
|
212
|
+
|
|
213
|
+
- ✅ **Timeouts** — Every tool execution has proper timeout handling
|
|
214
|
+
- ✅ **Retries** — Automatic retry with exponential backoff and deadline awareness
|
|
215
|
+
- ✅ **Rate Limiting** — Global and per-tool rate limits with sliding windows → [CONFIGURATION.md](docs/CONFIGURATION.md)
|
|
216
|
+
- ✅ **Caching** — Intelligent result caching with TTL and idempotency key support
|
|
217
|
+
- ✅ **Circuit Breakers** — Prevent cascading failures with automatic fault detection
|
|
218
|
+
- ✅ **Idempotency** — SHA256-based deduplication of LLM retry quirks
|
|
219
|
+
- ✅ **Error Handling** — Machine-readable error codes with structured details → [ERRORS.md](docs/ERRORS.md)
|
|
220
|
+
- ✅ **Observability** — Structured logging, metrics, OpenTelemetry tracing → [OBSERVABILITY.md](docs/OBSERVABILITY.md)
|
|
221
|
+
- ✅ **Safety** — Subprocess isolation for untrusted code (zero crash blast radius)
|
|
222
|
+
- ✅ **Type Safety** — PEP 561 compliant with full mypy support
|
|
223
|
+
- ✅ **Resource Management** — Context managers for automatic cleanup
|
|
224
|
+
- ✅ **Tool Discovery** — Formal schema export (OpenAI, Anthropic, MCP formats)
|
|
225
|
+
- ✅ **Cancellation** — Cooperative cancellation with request-scoped deadlines
|
|
226
|
+
|
|
115
227
|
## Compatibility Matrix
|
|
116
228
|
|
|
229
|
+
Runs the same on macOS, Linux, and Windows — locally, serverside, and inside containers.
|
|
230
|
+
|
|
117
231
|
| Component | Supported Versions | Notes |
|
|
118
232
|
|-----------|-------------------|-------|
|
|
119
233
|
| **Python** | 3.11, 3.12, 3.13 | Python 3.11+ required |
|
|
@@ -131,6 +245,19 @@ Each layer is **optional** and **configurable**. Mix and match what you need.
|
|
|
131
245
|
- ✅ Anthropic Claude 3 (Opus, Sonnet, Haiku)
|
|
132
246
|
- ✅ Local models (Ollama, LM Studio)
|
|
133
247
|
|
|
248
|
+
## Developer Experience Highlights
|
|
249
|
+
|
|
250
|
+
**What makes CHUK Tool Processor easy to use:**
|
|
251
|
+
|
|
252
|
+
* **Auto-parsing**: XML (Claude), OpenAI `tool_calls`, direct JSON—all work automatically
|
|
253
|
+
* **One call**: `process()` handles multiple calls & formats in a single invocation
|
|
254
|
+
* **Auto-coercion**: Pydantic-powered argument cleanup (whitespace, type conversion, extra fields ignored)
|
|
255
|
+
* **Safe defaults**: timeouts, retries, caching toggles built-in
|
|
256
|
+
* **Observability in one line**: `setup_observability(...)` for traces + metrics
|
|
257
|
+
* **MCP in one call**: `setup_mcp_http_streamable|stdio|sse(...)` connects to remote tools instantly
|
|
258
|
+
* **Context managers**: `async with ToolProcessor() as p:` ensures automatic cleanup
|
|
259
|
+
* **Full type safety**: PEP 561 compliant—mypy, pyright, and IDEs get complete type information
|
|
260
|
+
|
|
134
261
|
## Quick Start
|
|
135
262
|
|
|
136
263
|
### Installation
|
|
@@ -143,19 +270,95 @@ pip install chuk-tool-processor
|
|
|
143
270
|
|
|
144
271
|
# Using uv (recommended)
|
|
145
272
|
uv pip install chuk-tool-processor
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
<details>
|
|
276
|
+
<summary><strong>Install from source or with extras</strong></summary>
|
|
146
277
|
|
|
147
|
-
|
|
278
|
+
```bash
|
|
279
|
+
# From source
|
|
148
280
|
git clone https://github.com/chrishayuk/chuk-tool-processor.git
|
|
149
281
|
cd chuk-tool-processor
|
|
150
282
|
uv pip install -e .
|
|
283
|
+
|
|
284
|
+
# With observability extras (OpenTelemetry + Prometheus)
|
|
285
|
+
pip install chuk-tool-processor[observability]
|
|
286
|
+
|
|
287
|
+
# With MCP extras
|
|
288
|
+
pip install chuk-tool-processor[mcp]
|
|
289
|
+
|
|
290
|
+
# All extras
|
|
291
|
+
pip install chuk-tool-processor[all]
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
</details>
|
|
295
|
+
|
|
296
|
+
<details>
|
|
297
|
+
<summary><strong>Type Checking Support (PEP 561 compliant)</strong></summary>
|
|
298
|
+
|
|
299
|
+
CHUK Tool Processor includes **full type checking support**:
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
# mypy, pyright, and IDEs get full type information!
|
|
303
|
+
from chuk_tool_processor import ToolProcessor, ToolCall, ToolResult
|
|
304
|
+
|
|
305
|
+
async with ToolProcessor() as processor:
|
|
306
|
+
# Full autocomplete and type checking
|
|
307
|
+
results: list[ToolResult] = await processor.process(llm_output)
|
|
308
|
+
tools: list[str] = await processor.list_tools()
|
|
151
309
|
```
|
|
152
310
|
|
|
311
|
+
**Features:**
|
|
312
|
+
- ✅ `py.typed` marker for PEP 561 compliance
|
|
313
|
+
- ✅ Comprehensive type hints on all public APIs
|
|
314
|
+
- ✅ Works with mypy, pyright, pylance
|
|
315
|
+
- ✅ Full IDE autocomplete support
|
|
316
|
+
|
|
317
|
+
**No special mypy configuration needed** - just import and use!
|
|
318
|
+
|
|
319
|
+
</details>
|
|
320
|
+
|
|
153
321
|
## 60-Second Quick Start
|
|
154
322
|
|
|
155
|
-
|
|
323
|
+
### From raw LLM output to safe execution in 3 lines
|
|
324
|
+
|
|
325
|
+
```python
|
|
326
|
+
from chuk_tool_processor import ToolProcessor, initialize
|
|
327
|
+
|
|
328
|
+
await initialize()
|
|
329
|
+
async with ToolProcessor() as p:
|
|
330
|
+
results = await p.process('<tool name="calculator" args=\'{"operation":"multiply","a":15,"b":23}\'/>')
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Note:** This assumes you've registered a "calculator" tool. See complete example below.
|
|
334
|
+
|
|
335
|
+
### Works with Both OpenAI and Anthropic (No Adapters Needed)
|
|
336
|
+
|
|
337
|
+
```python
|
|
338
|
+
from chuk_tool_processor import ToolProcessor, register_tool, initialize
|
|
339
|
+
|
|
340
|
+
@register_tool(name="search")
|
|
341
|
+
class SearchTool:
|
|
342
|
+
async def execute(self, query: str) -> dict:
|
|
343
|
+
return {"results": [f"Found: {query}"]}
|
|
344
|
+
|
|
345
|
+
await initialize()
|
|
346
|
+
async with ToolProcessor() as p:
|
|
347
|
+
# OpenAI format
|
|
348
|
+
openai_response = {"tool_calls": [{"type": "function", "function": {"name": "search", "arguments": '{"query": "Python"}'}}]}
|
|
349
|
+
|
|
350
|
+
# Anthropic format
|
|
351
|
+
anthropic_response = '<tool name="search" args=\'{"query": "Python"}\'/>'
|
|
352
|
+
|
|
353
|
+
# Both work identically
|
|
354
|
+
results_openai = await p.process(openai_response)
|
|
355
|
+
results_anthropic = await p.process(anthropic_response)
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
**Absolutely minimal example** → See `examples/01_getting_started/hello_tool.py`:
|
|
156
359
|
|
|
157
360
|
```bash
|
|
158
|
-
python examples/hello_tool.py
|
|
361
|
+
python examples/01_getting_started/hello_tool.py
|
|
159
362
|
```
|
|
160
363
|
|
|
161
364
|
Single file that demonstrates:
|
|
@@ -171,8 +374,7 @@ Copy-paste this into a file and run it:
|
|
|
171
374
|
|
|
172
375
|
```python
|
|
173
376
|
import asyncio
|
|
174
|
-
from chuk_tool_processor
|
|
175
|
-
from chuk_tool_processor.registry import initialize, register_tool
|
|
377
|
+
from chuk_tool_processor import ToolProcessor, register_tool, initialize
|
|
176
378
|
|
|
177
379
|
# Step 1: Define a tool
|
|
178
380
|
@register_tool(name="calculator")
|
|
@@ -186,29 +388,142 @@ class Calculator:
|
|
|
186
388
|
# Step 2: Process LLM output
|
|
187
389
|
async def main():
|
|
188
390
|
await initialize()
|
|
189
|
-
processor = ToolProcessor()
|
|
190
391
|
|
|
191
|
-
#
|
|
192
|
-
|
|
392
|
+
# Use context manager for automatic cleanup
|
|
393
|
+
async with ToolProcessor() as processor:
|
|
394
|
+
# Your LLM returned this tool call
|
|
395
|
+
llm_output = '<tool name="calculator" args=\'{"operation": "multiply", "a": 15, "b": 23}\'/>'
|
|
193
396
|
|
|
194
|
-
|
|
195
|
-
|
|
397
|
+
# Process it
|
|
398
|
+
results = await processor.process(llm_output)
|
|
196
399
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
400
|
+
# Each result is a ToolResult with: tool, result, error, duration, cached
|
|
401
|
+
if results[0].error:
|
|
402
|
+
print(f"Error: {results[0].error}")
|
|
403
|
+
else:
|
|
404
|
+
print(results[0].result) # {'result': 345}
|
|
405
|
+
|
|
406
|
+
# Processor automatically cleaned up!
|
|
204
407
|
|
|
205
408
|
asyncio.run(main())
|
|
206
409
|
```
|
|
207
410
|
|
|
208
|
-
**That's it.** You now have production-ready tool execution with
|
|
411
|
+
**That's it.** You now have production-ready tool execution with:
|
|
412
|
+
- ✅ Automatic timeouts, retries, and caching
|
|
413
|
+
- ✅ Clean resource management (context manager)
|
|
414
|
+
- ✅ Full type checking support
|
|
209
415
|
|
|
210
416
|
> **Why not just use OpenAI tool calls?**
|
|
211
|
-
> OpenAI's function calling is great for parsing, but you still need: parsing multiple formats (Anthropic XML, etc.), timeouts, retries, rate limits, caching, subprocess isolation,
|
|
417
|
+
> OpenAI's function calling is great for parsing, but you still need: parsing multiple formats (Anthropic XML, etc.), timeouts, retries, rate limits, caching, subprocess isolation, connecting to external MCP servers, and **per-tool** policy control with cross-provider parsing and MCP fan-out. CHUK Tool Processor **is** that missing middle layer.
|
|
418
|
+
|
|
419
|
+
## Quick Decision Tree (Commit This to Memory)
|
|
420
|
+
|
|
421
|
+
```
|
|
422
|
+
╭──────────────────────────────────────────╮
|
|
423
|
+
│ Do you trust the code you're executing? │
|
|
424
|
+
│ ✅ Yes → InProcessStrategy │
|
|
425
|
+
│ ⚠️ No → IsolatedStrategy (sandboxed) │
|
|
426
|
+
│ │
|
|
427
|
+
│ Where do your tools live? │
|
|
428
|
+
│ 📦 Local → @register_tool │
|
|
429
|
+
│ 🌐 Remote → setup_mcp_http_streamable │
|
|
430
|
+
╰──────────────────────────────────────────╯
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
**That's all you need to pick the right pattern.**
|
|
434
|
+
|
|
435
|
+
## Registry & Processor Lifecycle
|
|
436
|
+
|
|
437
|
+
Understanding the lifecycle helps you use CHUK Tool Processor correctly:
|
|
438
|
+
|
|
439
|
+
1. **`await initialize()`** — loads the global registry; call **once per process** at application startup
|
|
440
|
+
2. Create a **`ToolProcessor(...)`** (or use the one returned by `setup_mcp_*`)
|
|
441
|
+
3. Use **`async with ToolProcessor() as p:`** to ensure cleanup
|
|
442
|
+
4. **`setup_mcp_*`** returns `(processor, manager)` — reuse that `processor`
|
|
443
|
+
5. If you need a custom registry, pass it explicitly to the strategy
|
|
444
|
+
6. You rarely need `get_default_registry()` unless you're composing advanced setups
|
|
445
|
+
|
|
446
|
+
**⚠️ Important:** `initialize()` must run **once per process**, not once per request or processor instance. Running it multiple times will duplicate tools in the registry.
|
|
447
|
+
|
|
448
|
+
```python
|
|
449
|
+
# Standard pattern
|
|
450
|
+
await initialize() # Step 1: Register tools
|
|
451
|
+
|
|
452
|
+
async with ToolProcessor() as p: # Step 2-3: Create + auto cleanup
|
|
453
|
+
results = await p.process(llm_output)
|
|
454
|
+
# Step 4: Processor automatically cleaned up on exit
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
## Production Features by Example
|
|
458
|
+
|
|
459
|
+
### Idempotency & Deduplication
|
|
460
|
+
|
|
461
|
+
Automatically deduplicate LLM retry quirks using SHA256-based idempotency keys:
|
|
462
|
+
|
|
463
|
+
```python
|
|
464
|
+
from chuk_tool_processor import ToolProcessor, initialize
|
|
465
|
+
|
|
466
|
+
await initialize()
|
|
467
|
+
async with ToolProcessor(enable_caching=True, cache_ttl=300) as p:
|
|
468
|
+
# LLM retries the same call (common with streaming or errors)
|
|
469
|
+
call1 = '<tool name="search" args=\'{"query": "Python"}\'/>'
|
|
470
|
+
call2 = '<tool name="search" args=\'{"query": "Python"}\'/>' # Identical
|
|
471
|
+
|
|
472
|
+
results1 = await p.process(call1) # Executes
|
|
473
|
+
results2 = await p.process(call2) # Cache hit! (idempotency key match)
|
|
474
|
+
|
|
475
|
+
assert results1[0].cached == False
|
|
476
|
+
assert results2[0].cached == True
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Cancellation & Deadlines
|
|
480
|
+
|
|
481
|
+
Cooperative cancellation with request-scoped deadlines:
|
|
482
|
+
|
|
483
|
+
```python
|
|
484
|
+
import asyncio
|
|
485
|
+
from chuk_tool_processor import ToolProcessor, initialize
|
|
486
|
+
|
|
487
|
+
async def main():
|
|
488
|
+
await initialize()
|
|
489
|
+
async with ToolProcessor(default_timeout=60.0) as p:
|
|
490
|
+
try:
|
|
491
|
+
# Hard deadline for the whole batch (e.g., user request budget)
|
|
492
|
+
async with asyncio.timeout(5.0):
|
|
493
|
+
async for event in p.astream('<tool name="slow_report" args=\'{"n": 1000000}\'/>'):
|
|
494
|
+
print("chunk:", event)
|
|
495
|
+
except TimeoutError:
|
|
496
|
+
print("Request cancelled: deadline exceeded")
|
|
497
|
+
# Processor automatically cancels the tool and cleans up
|
|
498
|
+
|
|
499
|
+
asyncio.run(main())
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Per-Tool Policy Overrides
|
|
503
|
+
|
|
504
|
+
Override timeouts, retries, and rate limits per tool:
|
|
505
|
+
|
|
506
|
+
```python
|
|
507
|
+
from chuk_tool_processor import ToolProcessor, initialize
|
|
508
|
+
|
|
509
|
+
await initialize()
|
|
510
|
+
async with ToolProcessor(
|
|
511
|
+
default_timeout=30.0,
|
|
512
|
+
enable_retries=True,
|
|
513
|
+
max_retries=2,
|
|
514
|
+
enable_rate_limiting=True,
|
|
515
|
+
global_rate_limit=120, # 120 requests/min across all tools
|
|
516
|
+
tool_rate_limits={
|
|
517
|
+
"expensive_api": (5, 60), # 5 requests per 60 seconds
|
|
518
|
+
"fast_local": (1000, 60), # 1000 requests per 60 seconds
|
|
519
|
+
}
|
|
520
|
+
) as p:
|
|
521
|
+
# Tools run with their specific policies
|
|
522
|
+
results = await p.process('''
|
|
523
|
+
<tool name="expensive_api" args='{"q":"abc"}'/>
|
|
524
|
+
<tool name="fast_local" args='{"data":"xyz"}'/>
|
|
525
|
+
''')
|
|
526
|
+
```
|
|
212
527
|
|
|
213
528
|
## Documentation Quick Reference
|
|
214
529
|
|
|
@@ -217,17 +532,19 @@ asyncio.run(main())
|
|
|
217
532
|
| 📘 [CONFIGURATION.md](docs/CONFIGURATION.md) | **All config knobs & defaults**: ToolProcessor options, timeouts, retry policy, rate limits, circuit breakers, caching, environment variables |
|
|
218
533
|
| 🚨 [ERRORS.md](docs/ERRORS.md) | **Error taxonomy**: All error codes, exception classes, error details structure, handling patterns, retryability guide |
|
|
219
534
|
| 📊 [OBSERVABILITY.md](docs/OBSERVABILITY.md) | **Metrics & tracing**: OpenTelemetry setup, Prometheus metrics, spans reference, PromQL queries |
|
|
220
|
-
| 🔌 [examples/hello_tool.py](examples/hello_tool.py) | **60-second starter**: Single-file, copy-paste-and-run example |
|
|
535
|
+
| 🔌 [examples/01_getting_started/hello_tool.py](examples/01_getting_started/hello_tool.py) | **60-second starter**: Single-file, copy-paste-and-run example |
|
|
221
536
|
| 🎯 [examples/](examples/) | **20+ working examples**: MCP integration, OAuth flows, streaming, production patterns |
|
|
222
537
|
|
|
223
538
|
## Choose Your Path
|
|
224
539
|
|
|
540
|
+
**Use this when OpenAI/Claude tool calling is not enough** — because you need retries, caching, rate limits, subprocess isolation, or MCP integration.
|
|
541
|
+
|
|
225
542
|
| Your Goal | What You Need | Where to Look |
|
|
226
543
|
|-----------|---------------|---------------|
|
|
227
544
|
| ☕ **Just process LLM tool calls** | Basic tool registration + processor | [60-Second Quick Start](#60-second-quick-start) |
|
|
228
545
|
| 🔌 **Connect to external tools** | MCP integration (HTTP/STDIO/SSE) | [MCP Integration](#5-mcp-integration-external-tools) |
|
|
229
546
|
| 🛡️ **Production deployment** | Timeouts, retries, rate limits, caching | [CONFIGURATION.md](docs/CONFIGURATION.md) |
|
|
230
|
-
| 🔒 **Run untrusted code safely** |
|
|
547
|
+
| 🔒 **Run untrusted code safely** | Isolated strategy (subprocess) | [Isolated Strategy](#using-isolated-strategy) |
|
|
231
548
|
| 📊 **Monitor and observe** | OpenTelemetry + Prometheus | [OBSERVABILITY.md](docs/OBSERVABILITY.md) |
|
|
232
549
|
| 🌊 **Stream incremental results** | StreamingTool pattern | [StreamingTool](#streamingtool-real-time-results) |
|
|
233
550
|
| 🚨 **Handle errors reliably** | Error codes & taxonomy | [ERRORS.md](docs/ERRORS.md) |
|
|
@@ -239,8 +556,7 @@ Here are the most common patterns you'll use:
|
|
|
239
556
|
**Pattern 1: Local tools only**
|
|
240
557
|
```python
|
|
241
558
|
import asyncio
|
|
242
|
-
from chuk_tool_processor
|
|
243
|
-
from chuk_tool_processor.registry import initialize, register_tool
|
|
559
|
+
from chuk_tool_processor import ToolProcessor, register_tool, initialize
|
|
244
560
|
|
|
245
561
|
@register_tool(name="my_tool")
|
|
246
562
|
class MyTool:
|
|
@@ -249,20 +565,22 @@ class MyTool:
|
|
|
249
565
|
|
|
250
566
|
async def main():
|
|
251
567
|
await initialize()
|
|
252
|
-
processor = ToolProcessor()
|
|
253
568
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
569
|
+
async with ToolProcessor() as processor:
|
|
570
|
+
llm_output = '<tool name="my_tool" args=\'{"arg": "hello"}\'/>'
|
|
571
|
+
results = await processor.process(llm_output)
|
|
572
|
+
print(results[0].result) # {'result': 'Processed: hello'}
|
|
257
573
|
|
|
258
574
|
asyncio.run(main())
|
|
259
575
|
```
|
|
260
576
|
|
|
577
|
+
<details>
|
|
578
|
+
<summary><strong>More patterns: MCP integration (local + remote tools)</strong></summary>
|
|
579
|
+
|
|
261
580
|
**Pattern 2: Mix local + remote MCP tools (Notion)**
|
|
262
581
|
```python
|
|
263
582
|
import asyncio
|
|
264
|
-
from chuk_tool_processor
|
|
265
|
-
from chuk_tool_processor.mcp import setup_mcp_http_streamable
|
|
583
|
+
from chuk_tool_processor import register_tool, initialize, setup_mcp_http_streamable
|
|
266
584
|
|
|
267
585
|
@register_tool(name="local_calculator")
|
|
268
586
|
class Calculator:
|
|
@@ -292,10 +610,13 @@ async def main():
|
|
|
292
610
|
print(f"Local result: {results[0].result}")
|
|
293
611
|
print(f"Notion result: {results[1].result}")
|
|
294
612
|
|
|
613
|
+
# Clean up
|
|
614
|
+
await manager.close()
|
|
615
|
+
|
|
295
616
|
asyncio.run(main())
|
|
296
617
|
```
|
|
297
618
|
|
|
298
|
-
See `examples/notion_oauth.py` for complete OAuth flow.
|
|
619
|
+
See `examples/04_mcp_integration/notion_oauth.py` for complete OAuth flow.
|
|
299
620
|
|
|
300
621
|
**Pattern 3: Local SQLite database via STDIO**
|
|
301
622
|
```python
|
|
@@ -334,7 +655,9 @@ async def main():
|
|
|
334
655
|
asyncio.run(main())
|
|
335
656
|
```
|
|
336
657
|
|
|
337
|
-
See `examples/stdio_sqlite.py` for complete working example.
|
|
658
|
+
See `examples/04_mcp_integration/stdio_sqlite.py` for complete working example.
|
|
659
|
+
|
|
660
|
+
</details>
|
|
338
661
|
|
|
339
662
|
## Core Concepts
|
|
340
663
|
|
|
@@ -347,8 +670,10 @@ The **registry** is where you register tools for execution. Tools can be:
|
|
|
347
670
|
- **StreamingTool** for real-time incremental results
|
|
348
671
|
- **Functions** registered via `register_fn_tool()`
|
|
349
672
|
|
|
673
|
+
> **Note:** The registry is global, processors are scoped.
|
|
674
|
+
|
|
350
675
|
```python
|
|
351
|
-
from chuk_tool_processor
|
|
676
|
+
from chuk_tool_processor import register_tool
|
|
352
677
|
from chuk_tool_processor.models.validated_tool import ValidatedTool
|
|
353
678
|
from pydantic import BaseModel, Field
|
|
354
679
|
|
|
@@ -374,18 +699,16 @@ class WeatherTool(ValidatedTool):
|
|
|
374
699
|
| Strategy | Use Case | Trade-offs |
|
|
375
700
|
|----------|----------|------------|
|
|
376
701
|
| **InProcessStrategy** | Fast, trusted tools | Speed ✅, Isolation ❌ |
|
|
377
|
-
| **
|
|
702
|
+
| **IsolatedStrategy** | Untrusted or risky code | Isolation ✅, Speed ❌ |
|
|
378
703
|
|
|
379
704
|
```python
|
|
380
705
|
import asyncio
|
|
381
|
-
from chuk_tool_processor
|
|
382
|
-
from chuk_tool_processor.execution.strategies.subprocess_strategy import SubprocessStrategy
|
|
383
|
-
from chuk_tool_processor.registry import get_default_registry
|
|
706
|
+
from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
|
|
384
707
|
|
|
385
708
|
async def main():
|
|
386
709
|
registry = await get_default_registry()
|
|
387
710
|
processor = ToolProcessor(
|
|
388
|
-
strategy=
|
|
711
|
+
strategy=IsolatedStrategy(
|
|
389
712
|
registry=registry,
|
|
390
713
|
max_workers=4,
|
|
391
714
|
default_timeout=30.0
|
|
@@ -396,6 +719,8 @@ async def main():
|
|
|
396
719
|
asyncio.run(main())
|
|
397
720
|
```
|
|
398
721
|
|
|
722
|
+
**Note:** `IsolatedStrategy` is an alias of `SubprocessStrategy` for backwards compatibility. Use `IsolatedStrategy` for clarity—it better communicates the security boundary intent.
|
|
723
|
+
|
|
399
724
|
### 3. Execution Wrappers (Middleware)
|
|
400
725
|
|
|
401
726
|
**Wrappers** add production features as composable layers:
|
|
@@ -461,6 +786,8 @@ Connect to **remote tool servers** using the [Model Context Protocol](https://mo
|
|
|
461
786
|
|
|
462
787
|
#### HTTP Streamable (⭐ Recommended for Cloud Services)
|
|
463
788
|
|
|
789
|
+
**Use for:** Cloud SaaS services (OAuth, long-running streams, resilient reconnects)
|
|
790
|
+
|
|
464
791
|
Modern HTTP streaming transport for cloud-based MCP servers like Notion:
|
|
465
792
|
|
|
466
793
|
```python
|
|
@@ -489,8 +816,13 @@ results = await processor.process(
|
|
|
489
816
|
)
|
|
490
817
|
```
|
|
491
818
|
|
|
819
|
+
<details>
|
|
820
|
+
<summary><strong>Other MCP Transports (STDIO for local tools, SSE for legacy)</strong></summary>
|
|
821
|
+
|
|
492
822
|
#### STDIO (Best for Local/On-Device Tools)
|
|
493
823
|
|
|
824
|
+
**Use for:** Local/embedded tools and databases (SQLite, file systems, local services)
|
|
825
|
+
|
|
494
826
|
For running local MCP servers as subprocesses—great for databases, file systems, and local tools:
|
|
495
827
|
|
|
496
828
|
```python
|
|
@@ -529,6 +861,8 @@ results = await processor.process(
|
|
|
529
861
|
|
|
530
862
|
#### SSE (Legacy Support)
|
|
531
863
|
|
|
864
|
+
**Use for:** Legacy compatibility only. Prefer HTTP Streamable for new integrations.
|
|
865
|
+
|
|
532
866
|
For backward compatibility with older MCP servers using Server-Sent Events:
|
|
533
867
|
|
|
534
868
|
```python
|
|
@@ -550,6 +884,8 @@ processor, manager = await setup_mcp_sse(
|
|
|
550
884
|
)
|
|
551
885
|
```
|
|
552
886
|
|
|
887
|
+
</details>
|
|
888
|
+
|
|
553
889
|
**Transport Comparison:**
|
|
554
890
|
|
|
555
891
|
| Transport | Use Case | Real Examples |
|
|
@@ -558,6 +894,18 @@ processor, manager = await setup_mcp_sse(
|
|
|
558
894
|
| **STDIO** | Local tools, databases | SQLite (`mcp-server-sqlite`), Echo (`chuk-mcp-echo`) |
|
|
559
895
|
| **SSE** | Legacy cloud services | Atlassian (`mcp.atlassian.com`) |
|
|
560
896
|
|
|
897
|
+
**How MCP fits into the architecture:**
|
|
898
|
+
|
|
899
|
+
```
|
|
900
|
+
LLM Output
|
|
901
|
+
↓
|
|
902
|
+
Tool Processor
|
|
903
|
+
↓
|
|
904
|
+
┌──────────────┬────────────────────┐
|
|
905
|
+
│ Local Tools │ Remote Tools (MCP) │
|
|
906
|
+
└──────────────┴────────────────────┘
|
|
907
|
+
```
|
|
908
|
+
|
|
561
909
|
**Relationship with [chuk-mcp](https://github.com/chrishayuk/chuk-mcp):**
|
|
562
910
|
- `chuk-mcp` is a low-level MCP protocol client (handles transports, protocol negotiation)
|
|
563
911
|
- `chuk-tool-processor` wraps `chuk-mcp` to integrate external tools into your execution pipeline
|
|
@@ -571,7 +919,7 @@ CHUK Tool Processor supports multiple patterns for defining tools:
|
|
|
571
919
|
|
|
572
920
|
#### Simple Function-Based Tools
|
|
573
921
|
```python
|
|
574
|
-
from chuk_tool_processor
|
|
922
|
+
from chuk_tool_processor import register_fn_tool
|
|
575
923
|
from datetime import datetime
|
|
576
924
|
from zoneinfo import ZoneInfo
|
|
577
925
|
|
|
@@ -629,17 +977,26 @@ class FileProcessor(StreamingTool):
|
|
|
629
977
|
|
|
630
978
|
```python
|
|
631
979
|
import asyncio
|
|
632
|
-
from chuk_tool_processor
|
|
633
|
-
from chuk_tool_processor.registry import initialize
|
|
980
|
+
from chuk_tool_processor import ToolProcessor, initialize
|
|
634
981
|
|
|
635
982
|
async def main():
|
|
636
983
|
await initialize()
|
|
637
984
|
processor = ToolProcessor()
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
985
|
+
|
|
986
|
+
# Stream can be cancelled by breaking or raising an exception
|
|
987
|
+
try:
|
|
988
|
+
async for event in processor.astream('<tool name="file_processor" args=\'{"file_path":"README.md"}\'/>'):
|
|
989
|
+
# 'event' is a streamed chunk (either your Result model instance or a dict)
|
|
990
|
+
line = event["line"] if isinstance(event, dict) else getattr(event, "line", None)
|
|
991
|
+
content = event["content"] if isinstance(event, dict) else getattr(event, "content", None)
|
|
992
|
+
print(f"Line {line}: {content}")
|
|
993
|
+
|
|
994
|
+
# Example: cancel after 100 lines
|
|
995
|
+
if line and line > 100:
|
|
996
|
+
break # Cleanup happens automatically
|
|
997
|
+
except asyncio.CancelledError:
|
|
998
|
+
# Stream cleanup is automatic even on cancellation
|
|
999
|
+
pass
|
|
643
1000
|
|
|
644
1001
|
asyncio.run(main())
|
|
645
1002
|
```
|
|
@@ -648,23 +1005,32 @@ asyncio.run(main())
|
|
|
648
1005
|
|
|
649
1006
|
#### Basic Usage
|
|
650
1007
|
|
|
651
|
-
Call `await initialize()` once at startup to load your registry.
|
|
1008
|
+
Call `await initialize()` once at startup to load your registry. Use context managers for automatic cleanup:
|
|
652
1009
|
|
|
653
1010
|
```python
|
|
654
1011
|
import asyncio
|
|
655
|
-
from chuk_tool_processor
|
|
656
|
-
from chuk_tool_processor.registry import initialize
|
|
1012
|
+
from chuk_tool_processor import ToolProcessor, initialize
|
|
657
1013
|
|
|
658
1014
|
async def main():
|
|
659
1015
|
await initialize()
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
1016
|
+
|
|
1017
|
+
# Context manager automatically handles cleanup
|
|
1018
|
+
async with ToolProcessor() as processor:
|
|
1019
|
+
# Discover available tools
|
|
1020
|
+
tools = await processor.list_tools()
|
|
1021
|
+
print(f"Available tools: {tools}")
|
|
1022
|
+
|
|
1023
|
+
# Process LLM output
|
|
1024
|
+
llm_output = '<tool name="calculator" args=\'{"operation":"add","a":2,"b":3}\'/>'
|
|
1025
|
+
results = await processor.process(llm_output)
|
|
1026
|
+
|
|
1027
|
+
for result in results:
|
|
1028
|
+
if result.error:
|
|
1029
|
+
print(f"Error: {result.error}")
|
|
1030
|
+
else:
|
|
1031
|
+
print(f"Success: {result.result}")
|
|
1032
|
+
|
|
1033
|
+
# Processor automatically cleaned up here!
|
|
668
1034
|
|
|
669
1035
|
asyncio.run(main())
|
|
670
1036
|
```
|
|
@@ -672,21 +1038,32 @@ asyncio.run(main())
|
|
|
672
1038
|
#### Production Configuration
|
|
673
1039
|
|
|
674
1040
|
```python
|
|
675
|
-
from chuk_tool_processor
|
|
1041
|
+
from chuk_tool_processor import ToolProcessor, initialize
|
|
1042
|
+
import asyncio
|
|
676
1043
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
default_timeout=30.0,
|
|
680
|
-
max_concurrency=20,
|
|
1044
|
+
async def main():
|
|
1045
|
+
await initialize()
|
|
681
1046
|
|
|
682
|
-
#
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1047
|
+
# Use context manager with production config
|
|
1048
|
+
async with ToolProcessor(
|
|
1049
|
+
# Execution settings
|
|
1050
|
+
default_timeout=30.0,
|
|
1051
|
+
max_concurrency=20,
|
|
1052
|
+
|
|
1053
|
+
# Production features
|
|
1054
|
+
enable_caching=True,
|
|
1055
|
+
cache_ttl=600,
|
|
1056
|
+
enable_rate_limiting=True,
|
|
1057
|
+
global_rate_limit=100,
|
|
1058
|
+
enable_retries=True,
|
|
1059
|
+
max_retries=3
|
|
1060
|
+
) as processor:
|
|
1061
|
+
# Use processor...
|
|
1062
|
+
results = await processor.process(llm_output)
|
|
1063
|
+
|
|
1064
|
+
# Automatic cleanup on exit
|
|
1065
|
+
|
|
1066
|
+
asyncio.run(main())
|
|
690
1067
|
```
|
|
691
1068
|
|
|
692
1069
|
### Advanced Production Features
|
|
@@ -698,7 +1075,7 @@ Beyond basic configuration, CHUK Tool Processor includes several advanced featur
|
|
|
698
1075
|
Prevent cascading failures by automatically opening circuits for failing tools:
|
|
699
1076
|
|
|
700
1077
|
```python
|
|
701
|
-
from chuk_tool_processor
|
|
1078
|
+
from chuk_tool_processor import ToolProcessor
|
|
702
1079
|
|
|
703
1080
|
processor = ToolProcessor(
|
|
704
1081
|
enable_circuit_breaker=True,
|
|
@@ -740,8 +1117,8 @@ assert call1.idempotency_key == call2.idempotency_key
|
|
|
740
1117
|
|
|
741
1118
|
# Used automatically by caching layer
|
|
742
1119
|
processor = ToolProcessor(enable_caching=True)
|
|
743
|
-
results1 = await processor.
|
|
744
|
-
results2 = await processor.
|
|
1120
|
+
results1 = await processor.process([call1]) # Executes
|
|
1121
|
+
results2 = await processor.process([call2]) # Cache hit!
|
|
745
1122
|
```
|
|
746
1123
|
|
|
747
1124
|
**Benefits:**
|
|
@@ -749,6 +1126,8 @@ results2 = await processor.execute([call2]) # Cache hit!
|
|
|
749
1126
|
- Deterministic cache keys
|
|
750
1127
|
- No manual key management needed
|
|
751
1128
|
|
|
1129
|
+
**Cache scope:** In-memory per-process by default. Cache backend is pluggable—see [CONFIGURATION.md](docs/CONFIGURATION.md) for custom cache backends.
|
|
1130
|
+
|
|
752
1131
|
#### Tool Schema Export
|
|
753
1132
|
|
|
754
1133
|
Export tool definitions to multiple formats for LLM prompting:
|
|
@@ -795,7 +1174,9 @@ mcp_format = spec.to_mcp() # For MCP servers
|
|
|
795
1174
|
|
|
796
1175
|
#### Machine-Readable Error Codes
|
|
797
1176
|
|
|
798
|
-
Structured error handling with error codes for programmatic responses
|
|
1177
|
+
Structured error handling with error codes for programmatic responses.
|
|
1178
|
+
|
|
1179
|
+
**Error Contract:** Every error includes a machine-readable code, human-readable message, and structured details:
|
|
799
1180
|
|
|
800
1181
|
```python
|
|
801
1182
|
from chuk_tool_processor.core.exceptions import (
|
|
@@ -877,22 +1258,20 @@ result = await tool.execute(**llm_output)
|
|
|
877
1258
|
|
|
878
1259
|
## Advanced Topics
|
|
879
1260
|
|
|
880
|
-
### Using
|
|
1261
|
+
### Using Isolated Strategy
|
|
881
1262
|
|
|
882
|
-
Use `
|
|
1263
|
+
Use `IsolatedStrategy` when running untrusted, third-party, or potentially unsafe code that shouldn't share the same process as your main app.
|
|
883
1264
|
|
|
884
1265
|
For isolation and safety when running untrusted code:
|
|
885
1266
|
|
|
886
1267
|
```python
|
|
887
1268
|
import asyncio
|
|
888
|
-
from chuk_tool_processor
|
|
889
|
-
from chuk_tool_processor.execution.strategies.subprocess_strategy import SubprocessStrategy
|
|
890
|
-
from chuk_tool_processor.registry import get_default_registry
|
|
1269
|
+
from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
|
|
891
1270
|
|
|
892
1271
|
async def main():
|
|
893
1272
|
registry = await get_default_registry()
|
|
894
1273
|
processor = ToolProcessor(
|
|
895
|
-
strategy=
|
|
1274
|
+
strategy=IsolatedStrategy(
|
|
896
1275
|
registry=registry,
|
|
897
1276
|
max_workers=4,
|
|
898
1277
|
default_timeout=30.0
|
|
@@ -903,6 +1282,10 @@ async def main():
|
|
|
903
1282
|
asyncio.run(main())
|
|
904
1283
|
```
|
|
905
1284
|
|
|
1285
|
+
> **Security & Isolation — Threat Model**
|
|
1286
|
+
>
|
|
1287
|
+
> Untrusted tool code runs in subprocesses; faults and crashes don't bring down your app. **Zero crash blast radius.** For hard CPU/RAM/network limits, run the processor inside a container with `--cpus`, `--memory`, and egress filtering. Secrets are never injected by default—pass them explicitly via tool arguments or scoped environment variables.
|
|
1288
|
+
|
|
906
1289
|
### Real-World MCP Examples
|
|
907
1290
|
|
|
908
1291
|
#### Example 1: Notion Integration with OAuth
|
|
@@ -912,7 +1295,7 @@ Complete OAuth flow connecting to Notion's MCP server:
|
|
|
912
1295
|
```python
|
|
913
1296
|
from chuk_tool_processor.mcp import setup_mcp_http_streamable
|
|
914
1297
|
|
|
915
|
-
# After completing OAuth flow (see examples/notion_oauth.py for full flow)
|
|
1298
|
+
# After completing OAuth flow (see examples/04_mcp_integration/notion_oauth.py for full flow)
|
|
916
1299
|
processor, manager = await setup_mcp_http_streamable(
|
|
917
1300
|
servers=[{
|
|
918
1301
|
"name": "notion",
|
|
@@ -933,6 +1316,9 @@ results = await processor.process(
|
|
|
933
1316
|
)
|
|
934
1317
|
```
|
|
935
1318
|
|
|
1319
|
+
<details>
|
|
1320
|
+
<summary><strong>Click to expand more MCP examples (SQLite, Echo Server)</strong></summary>
|
|
1321
|
+
|
|
936
1322
|
#### Example 2: Local SQLite Database Access
|
|
937
1323
|
|
|
938
1324
|
Run SQLite MCP server locally for database operations:
|
|
@@ -1004,10 +1390,15 @@ results = await processor.process(
|
|
|
1004
1390
|
)
|
|
1005
1391
|
```
|
|
1006
1392
|
|
|
1007
|
-
|
|
1393
|
+
</details>
|
|
1394
|
+
|
|
1395
|
+
See `examples/04_mcp_integration/notion_oauth.py`, `examples/04_mcp_integration/stdio_sqlite.py`, and `examples/04_mcp_integration/stdio_echo.py` for complete working implementations.
|
|
1008
1396
|
|
|
1009
1397
|
#### OAuth Token Refresh
|
|
1010
1398
|
|
|
1399
|
+
<details>
|
|
1400
|
+
<summary><strong>Click to expand OAuth token refresh guide</strong></summary>
|
|
1401
|
+
|
|
1011
1402
|
For MCP servers that use OAuth authentication, CHUK Tool Processor supports automatic token refresh when access tokens expire. This prevents your tools from failing due to expired tokens during long-running sessions.
|
|
1012
1403
|
|
|
1013
1404
|
**How it works:**
|
|
@@ -1076,7 +1467,9 @@ processor, manager = await setup_mcp_sse(
|
|
|
1076
1467
|
- Token refresh is attempted only once per tool call (no infinite retry loops)
|
|
1077
1468
|
- After successful refresh, the updated headers are used for all subsequent calls
|
|
1078
1469
|
|
|
1079
|
-
See `examples/notion_oauth.py` for a complete OAuth 2.1 implementation with PKCE and automatic token refresh.
|
|
1470
|
+
See `examples/04_mcp_integration/notion_oauth.py` for a complete OAuth 2.1 implementation with PKCE and automatic token refresh.
|
|
1471
|
+
|
|
1472
|
+
</details>
|
|
1080
1473
|
|
|
1081
1474
|
### Observability
|
|
1082
1475
|
|
|
@@ -1145,6 +1538,9 @@ asyncio.run(main())
|
|
|
1145
1538
|
|
|
1146
1539
|
#### OpenTelemetry & Prometheus (Drop-in Observability)
|
|
1147
1540
|
|
|
1541
|
+
<details>
|
|
1542
|
+
<summary><strong>Click to expand complete observability guide</strong></summary>
|
|
1543
|
+
|
|
1148
1544
|
**3-Line Setup:**
|
|
1149
1545
|
|
|
1150
1546
|
```python
|
|
@@ -1208,8 +1604,7 @@ uv pip install chuk-tool-processor --group observability
|
|
|
1208
1604
|
```python
|
|
1209
1605
|
import asyncio
|
|
1210
1606
|
from chuk_tool_processor.observability import setup_observability
|
|
1211
|
-
from chuk_tool_processor
|
|
1212
|
-
from chuk_tool_processor.registry import initialize, register_tool
|
|
1607
|
+
from chuk_tool_processor import ToolProcessor, initialize, register_tool
|
|
1213
1608
|
|
|
1214
1609
|
@register_tool(name="weather_api")
|
|
1215
1610
|
class WeatherTool:
|
|
@@ -1427,7 +1822,7 @@ export OTEL_EXPORTER_OTLP_ENDPOINT=http://datadog-agent:4317
|
|
|
1427
1822
|
- Testing observability features
|
|
1428
1823
|
- Environment variable configuration
|
|
1429
1824
|
|
|
1430
|
-
🎯 **Working Example**: See `examples/observability_demo.py` for a complete demonstration with retries, caching, and circuit breakers
|
|
1825
|
+
🎯 **Working Example**: See `examples/02_production_features/observability_demo.py` for a complete demonstration with retries, caching, and circuit breakers
|
|
1431
1826
|
|
|
1432
1827
|
**Benefits**
|
|
1433
1828
|
|
|
@@ -1438,6 +1833,8 @@ export OTEL_EXPORTER_OTLP_ENDPOINT=http://datadog-agent:4317
|
|
|
1438
1833
|
✅ **Optional** - Gracefully degrades if packages not installed
|
|
1439
1834
|
✅ **Zero-overhead** - No performance impact when disabled
|
|
1440
1835
|
|
|
1836
|
+
</details>
|
|
1837
|
+
|
|
1441
1838
|
### Error Handling
|
|
1442
1839
|
|
|
1443
1840
|
```python
|
|
@@ -1455,8 +1852,7 @@ for result in results:
|
|
|
1455
1852
|
|
|
1456
1853
|
```python
|
|
1457
1854
|
import pytest
|
|
1458
|
-
from chuk_tool_processor
|
|
1459
|
-
from chuk_tool_processor.registry import initialize
|
|
1855
|
+
from chuk_tool_processor import ToolProcessor, initialize
|
|
1460
1856
|
|
|
1461
1857
|
@pytest.mark.asyncio
|
|
1462
1858
|
async def test_calculator():
|
|
@@ -1470,6 +1866,40 @@ async def test_calculator():
|
|
|
1470
1866
|
assert results[0].result["result"] == 8
|
|
1471
1867
|
```
|
|
1472
1868
|
|
|
1869
|
+
**Fake tool pattern for testing:**
|
|
1870
|
+
|
|
1871
|
+
```python
|
|
1872
|
+
import pytest
|
|
1873
|
+
from chuk_tool_processor import ToolProcessor, register_tool, initialize
|
|
1874
|
+
|
|
1875
|
+
@register_tool(name="fake_tool")
|
|
1876
|
+
class FakeTool:
|
|
1877
|
+
"""No-op tool for testing processor behavior."""
|
|
1878
|
+
call_count = 0
|
|
1879
|
+
|
|
1880
|
+
async def execute(self, **kwargs) -> dict:
|
|
1881
|
+
FakeTool.call_count += 1
|
|
1882
|
+
return {"called": True, "args": kwargs}
|
|
1883
|
+
|
|
1884
|
+
@pytest.mark.asyncio
|
|
1885
|
+
async def test_processor_with_fake_tool():
|
|
1886
|
+
await initialize()
|
|
1887
|
+
processor = ToolProcessor()
|
|
1888
|
+
|
|
1889
|
+
# Reset counter
|
|
1890
|
+
FakeTool.call_count = 0
|
|
1891
|
+
|
|
1892
|
+
# Execute fake tool
|
|
1893
|
+
results = await processor.process(
|
|
1894
|
+
'<tool name="fake_tool" args=\'{"test_arg": "value"}\'/>'
|
|
1895
|
+
)
|
|
1896
|
+
|
|
1897
|
+
# Assert behavior
|
|
1898
|
+
assert FakeTool.call_count == 1
|
|
1899
|
+
assert results[0].result["called"] is True
|
|
1900
|
+
assert results[0].result["args"]["test_arg"] == "value"
|
|
1901
|
+
```
|
|
1902
|
+
|
|
1473
1903
|
## Configuration
|
|
1474
1904
|
|
|
1475
1905
|
### Timeout Configuration
|
|
@@ -1480,6 +1910,7 @@ CHUK Tool Processor uses a unified timeout configuration system that applies to
|
|
|
1480
1910
|
from chuk_tool_processor.mcp.transport import TimeoutConfig
|
|
1481
1911
|
|
|
1482
1912
|
# Create custom timeout configuration
|
|
1913
|
+
# (Defaults are: connect=30, operation=30, quick=5, shutdown=2)
|
|
1483
1914
|
timeout_config = TimeoutConfig(
|
|
1484
1915
|
connect=30.0, # Connection establishment, initialization, session discovery
|
|
1485
1916
|
operation=30.0, # Normal operations (tool calls, listing tools/resources/prompts)
|
|
@@ -1607,7 +2038,7 @@ CHUK Tool Processor provides multiple layers of safety:
|
|
|
1607
2038
|
| Concern | Protection | Configuration |
|
|
1608
2039
|
|---------|------------|---------------|
|
|
1609
2040
|
| **Timeouts** | Every tool has a timeout | `default_timeout=30.0` |
|
|
1610
|
-
| **Process Isolation** | Run tools in separate processes | `strategy=
|
|
2041
|
+
| **Process Isolation** | Run tools in separate processes | `strategy=IsolatedStrategy()` |
|
|
1611
2042
|
| **Rate Limiting** | Prevent abuse and API overuse | `enable_rate_limiting=True` |
|
|
1612
2043
|
| **Input Validation** | Pydantic validation on arguments | Use `ValidatedTool` |
|
|
1613
2044
|
| **Error Containment** | Failures don't crash the processor | Built-in exception handling |
|
|
@@ -1619,13 +2050,58 @@ CHUK Tool Processor provides multiple layers of safety:
|
|
|
1619
2050
|
- **Resource Limits**: For hard CPU/memory caps, use OS-level controls (cgroups on Linux, Job Objects on Windows, or Docker resource limits).
|
|
1620
2051
|
- **Secrets**: Never injected automatically. Pass secrets explicitly via tool arguments or environment variables, and prefer scoped env vars for subprocess tools to minimize exposure.
|
|
1621
2052
|
|
|
2053
|
+
#### OS-Level Hardening
|
|
2054
|
+
|
|
2055
|
+
For production deployments, add these hardening measures:
|
|
2056
|
+
|
|
2057
|
+
| Concern | Docker/Container Solution | Direct Example |
|
|
2058
|
+
|---------|--------------------------|----------------|
|
|
2059
|
+
| **CPU/RAM caps** | `--cpus`, `--memory` flags | `docker run --cpus="1.5" --memory="512m" myapp` |
|
|
2060
|
+
| **Network egress** | Deny-by-default with firewall rules | `--network=none` or custom network with egress filtering |
|
|
2061
|
+
| **Filesystem** | Read-only root + writable scratch | `--read-only --tmpfs /tmp:rw,size=100m` |
|
|
2062
|
+
|
|
2063
|
+
**Example: Run processor in locked-down container**
|
|
2064
|
+
|
|
2065
|
+
```bash
|
|
2066
|
+
# Dockerfile
|
|
2067
|
+
FROM python:3.11-slim
|
|
2068
|
+
WORKDIR /app
|
|
2069
|
+
COPY requirements.txt .
|
|
2070
|
+
RUN pip install -r requirements.txt --no-cache-dir
|
|
2071
|
+
COPY . .
|
|
2072
|
+
USER nobody # Run as non-root
|
|
2073
|
+
CMD ["python", "app.py"]
|
|
2074
|
+
|
|
2075
|
+
# Run with resource limits and network restrictions
|
|
2076
|
+
docker run \
|
|
2077
|
+
--cpus="2" \
|
|
2078
|
+
--memory="1g" \
|
|
2079
|
+
--memory-swap="1g" \
|
|
2080
|
+
--read-only \
|
|
2081
|
+
--tmpfs /tmp:rw,size=200m,mode=1777 \
|
|
2082
|
+
--network=custom-net \
|
|
2083
|
+
--cap-drop=ALL \
|
|
2084
|
+
myapp:latest
|
|
2085
|
+
```
|
|
2086
|
+
|
|
2087
|
+
**Network egress controls (deny-by-default)**
|
|
2088
|
+
|
|
2089
|
+
```bash
|
|
2090
|
+
# Create restricted network with no internet access (for local-only tools)
|
|
2091
|
+
docker network create --internal restricted-net
|
|
2092
|
+
|
|
2093
|
+
# Or use iptables for per-tool CIDR allowlists
|
|
2094
|
+
iptables -A OUTPUT -d 10.0.0.0/8 -j ACCEPT # Allow private ranges
|
|
2095
|
+
iptables -A OUTPUT -d 172.16.0.0/12 -j ACCEPT
|
|
2096
|
+
iptables -A OUTPUT -d 192.168.0.0/16 -j ACCEPT
|
|
2097
|
+
iptables -A OUTPUT -j DROP # Deny everything else
|
|
2098
|
+
```
|
|
2099
|
+
|
|
1622
2100
|
Example security-focused setup for untrusted code:
|
|
1623
2101
|
|
|
1624
2102
|
```python
|
|
1625
2103
|
import asyncio
|
|
1626
|
-
from chuk_tool_processor
|
|
1627
|
-
from chuk_tool_processor.execution.strategies.subprocess_strategy import SubprocessStrategy
|
|
1628
|
-
from chuk_tool_processor.registry import get_default_registry
|
|
2104
|
+
from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
|
|
1629
2105
|
|
|
1630
2106
|
async def create_secure_processor():
|
|
1631
2107
|
# Maximum isolation for untrusted code
|
|
@@ -1633,7 +2109,7 @@ async def create_secure_processor():
|
|
|
1633
2109
|
registry = await get_default_registry()
|
|
1634
2110
|
|
|
1635
2111
|
processor = ToolProcessor(
|
|
1636
|
-
strategy=
|
|
2112
|
+
strategy=IsolatedStrategy(
|
|
1637
2113
|
registry=registry,
|
|
1638
2114
|
max_workers=4,
|
|
1639
2115
|
default_timeout=10.0
|
|
@@ -1651,6 +2127,25 @@ async def create_secure_processor():
|
|
|
1651
2127
|
# - Use read-only filesystems where possible
|
|
1652
2128
|
```
|
|
1653
2129
|
|
|
2130
|
+
## Design Goals & Non-Goals
|
|
2131
|
+
|
|
2132
|
+
**What CHUK Tool Processor does:**
|
|
2133
|
+
- ✅ Parse tool calls from any LLM format (XML, OpenAI, JSON)
|
|
2134
|
+
- ✅ Execute tools with production policies (timeouts, retries, rate limits, caching)
|
|
2135
|
+
- ✅ Isolate untrusted code in subprocesses
|
|
2136
|
+
- ✅ Connect to remote tool servers via MCP (HTTP/STDIO/SSE)
|
|
2137
|
+
- ✅ Provide composable execution layers (strategies + wrappers)
|
|
2138
|
+
- ✅ Export tool schemas for LLM prompting
|
|
2139
|
+
|
|
2140
|
+
**What CHUK Tool Processor explicitly does NOT do:**
|
|
2141
|
+
- ❌ Manage conversations or chat history
|
|
2142
|
+
- ❌ Provide prompt engineering or prompt templates
|
|
2143
|
+
- ❌ Bundle an LLM client (bring your own OpenAI/Anthropic/local)
|
|
2144
|
+
- ❌ Implement agent frameworks or chains
|
|
2145
|
+
- ❌ Make decisions about which tools to call
|
|
2146
|
+
|
|
2147
|
+
**Why this matters:** CHUK Tool Processor stays focused on reliable tool execution. It's a building block, not a framework. This makes it composable with any LLM application architecture.
|
|
2148
|
+
|
|
1654
2149
|
## Architecture Principles
|
|
1655
2150
|
|
|
1656
2151
|
1. **Composability**: Stack strategies and wrappers like middleware
|
|
@@ -1664,29 +2159,26 @@ async def create_secure_processor():
|
|
|
1664
2159
|
Check out the [`examples/`](examples/) directory for complete working examples:
|
|
1665
2160
|
|
|
1666
2161
|
### Getting Started
|
|
1667
|
-
- **60-second hello**: `examples/hello_tool.py` - Absolute minimal example (copy-paste-run)
|
|
1668
|
-
- **Quick start**: `examples/quickstart_demo.py` - Basic tool registration and execution
|
|
1669
|
-
- **Execution strategies**: `examples/execution_strategies_demo.py` - InProcess vs Subprocess
|
|
1670
|
-
- **Production wrappers**: `examples/wrappers_demo.py` - Caching, retries, rate limiting
|
|
1671
|
-
- **Streaming tools**: `examples/streaming_demo.py` - Real-time incremental results
|
|
1672
|
-
- **Streaming tool calls**: `examples/streaming_tool_calls_demo.py` - Handle partial tool calls from streaming LLMs
|
|
1673
|
-
- **Schema helper**: `examples/schema_helper_demo.py` - Auto-generate schemas from typed tools (Pydantic → OpenAI/Anthropic/MCP)
|
|
1674
|
-
- **Observability**: `examples/observability_demo.py` - OpenTelemetry + Prometheus integration
|
|
2162
|
+
- **60-second hello**: `examples/01_getting_started/hello_tool.py` - Absolute minimal example (copy-paste-run)
|
|
2163
|
+
- **Quick start**: `examples/01_getting_started/quickstart_demo.py` - Basic tool registration and execution
|
|
2164
|
+
- **Execution strategies**: `examples/01_getting_started/execution_strategies_demo.py` - InProcess vs Subprocess
|
|
2165
|
+
- **Production wrappers**: `examples/02_production_features/wrappers_demo.py` - Caching, retries, rate limiting
|
|
2166
|
+
- **Streaming tools**: `examples/03_streaming/streaming_demo.py` - Real-time incremental results
|
|
2167
|
+
- **Streaming tool calls**: `examples/03_streaming/streaming_tool_calls_demo.py` - Handle partial tool calls from streaming LLMs
|
|
2168
|
+
- **Schema helper**: `examples/05_schema_and_types/schema_helper_demo.py` - Auto-generate schemas from typed tools (Pydantic → OpenAI/Anthropic/MCP)
|
|
2169
|
+
- **Observability**: `examples/02_production_features/observability_demo.py` - OpenTelemetry + Prometheus integration
|
|
1675
2170
|
|
|
1676
2171
|
### MCP Integration (Real-World)
|
|
1677
|
-
- **Notion + OAuth**: `examples/notion_oauth.py` - Complete OAuth 2.1 flow with HTTP Streamable
|
|
2172
|
+
- **Notion + OAuth**: `examples/04_mcp_integration/notion_oauth.py` - Complete OAuth 2.1 flow with HTTP Streamable
|
|
1678
2173
|
- Shows: Authorization Server discovery, client registration, PKCE flow, token exchange
|
|
1679
|
-
- **SQLite Local**: `examples/stdio_sqlite.py` - Local database access via STDIO
|
|
2174
|
+
- **SQLite Local**: `examples/04_mcp_integration/stdio_sqlite.py` - Local database access via STDIO
|
|
1680
2175
|
- Shows: Command/args passing, environment variables, file paths, initialization timeouts
|
|
1681
|
-
- **Echo Server**: `examples/stdio_echo.py` - Minimal STDIO transport example
|
|
2176
|
+
- **Echo Server**: `examples/04_mcp_integration/stdio_echo.py` - Minimal STDIO transport example
|
|
1682
2177
|
- Shows: Simplest possible MCP integration for testing
|
|
1683
|
-
- **Atlassian + OAuth**: `examples/atlassian_sse.py` - OAuth with SSE transport (legacy)
|
|
2178
|
+
- **Atlassian + OAuth**: `examples/04_mcp_integration/atlassian_sse.py` - OAuth with SSE transport (legacy)
|
|
1684
2179
|
|
|
1685
2180
|
### Advanced MCP
|
|
1686
|
-
- **
|
|
1687
|
-
- **STDIO**: `examples/mcp_stdio_example.py`
|
|
1688
|
-
- **SSE**: `examples/mcp_sse_example.py`
|
|
1689
|
-
- **Plugin system**: `examples/plugins_builtins_demo.py`, `examples/plugins_custom_parser_demo.py`
|
|
2181
|
+
- **Plugin system**: `examples/06_plugins/plugins_builtins_demo.py`, `examples/06_plugins/plugins_custom_parser_demo.py`
|
|
1690
2182
|
|
|
1691
2183
|
## FAQ
|
|
1692
2184
|
|
|
@@ -1711,18 +2203,20 @@ A: Use pytest with `@pytest.mark.asyncio`. See [Testing Tools](#testing-tools) f
|
|
|
1711
2203
|
**Q: Does this work with streaming LLM responses?**
|
|
1712
2204
|
A: Yes—as tool calls appear in the stream, extract and process them. The processor handles partial/incremental tool call lists.
|
|
1713
2205
|
|
|
1714
|
-
**Q: What's the difference between InProcess and
|
|
1715
|
-
A: InProcess is faster (same process),
|
|
2206
|
+
**Q: What's the difference between InProcess and Isolated strategies?**
|
|
2207
|
+
A: InProcess is faster (same process), Isolated is safer (separate subprocess). Use InProcess for trusted code, Isolated for untrusted.
|
|
1716
2208
|
|
|
1717
2209
|
## Comparison with Other Tools
|
|
1718
2210
|
|
|
1719
2211
|
| Feature | chuk-tool-processor | LangChain Tools | OpenAI Tools | MCP SDK |
|
|
1720
2212
|
|---------|-------------------|-----------------|--------------|---------|
|
|
1721
2213
|
| **Async-native** | ✅ | ⚠️ Partial | ✅ | ✅ |
|
|
1722
|
-
| **Process isolation** | ✅
|
|
2214
|
+
| **Process isolation** | ✅ IsolatedStrategy | ❌ | ❌ | ⚠️ |
|
|
1723
2215
|
| **Built-in retries** | ✅ | ❌ † | ❌ | ❌ |
|
|
1724
2216
|
| **Rate limiting** | ✅ | ❌ † | ⚠️ ‡ | ❌ |
|
|
1725
2217
|
| **Caching** | ✅ | ⚠️ † | ❌ ‡ | ❌ |
|
|
2218
|
+
| **Idempotency & de-dup** | ✅ SHA256 keys | ❌ | ❌ | ❌ |
|
|
2219
|
+
| **Per-tool policies** | ✅ (timeouts/retries/limits) | ⚠️ | ❌ | ❌ |
|
|
1726
2220
|
| **Multiple parsers** | ✅ (XML, OpenAI, JSON) | ⚠️ | ✅ | ✅ |
|
|
1727
2221
|
| **Streaming tools** | ✅ | ⚠️ | ⚠️ | ✅ |
|
|
1728
2222
|
| **MCP integration** | ✅ All transports | ❌ | ❌ | ✅ (protocol only) |
|
|
@@ -1799,6 +2293,25 @@ For detailed release documentation, see:
|
|
|
1799
2293
|
- **[RELEASING.md](RELEASING.md)** - Complete release process guide
|
|
1800
2294
|
- **[docs/CI-CD.md](docs/CI-CD.md)** - Full CI/CD pipeline documentation
|
|
1801
2295
|
|
|
2296
|
+
## Stability & Versioning
|
|
2297
|
+
|
|
2298
|
+
CHUK Tool Processor follows **[Semantic Versioning 2.0.0](https://semver.org/)** for predictable upgrades:
|
|
2299
|
+
|
|
2300
|
+
* **Breaking changes** = **major** version bump (e.g., 1.x → 2.0)
|
|
2301
|
+
* **New features** (backward-compatible) = **minor** version bump (e.g., 1.2 → 1.3)
|
|
2302
|
+
* **Bug fixes** (backward-compatible) = **patch** version bump (e.g., 1.2.3 → 1.2.4)
|
|
2303
|
+
|
|
2304
|
+
**Public API surface**: Everything exported via the package root (`from chuk_tool_processor import ...`) is considered public API and follows semver guarantees.
|
|
2305
|
+
|
|
2306
|
+
**Deprecation policy**: Deprecated APIs will:
|
|
2307
|
+
1. Log a warning for **one minor release**
|
|
2308
|
+
2. Be removed in the **next major release**
|
|
2309
|
+
|
|
2310
|
+
**Upgrading safely**:
|
|
2311
|
+
* Patch and minor updates are **safe to deploy** without code changes
|
|
2312
|
+
* Major updates may require migration—see release notes
|
|
2313
|
+
* Pin to `chuk-tool-processor~=1.2` for minor updates only, or `chuk-tool-processor==1.2.3` for exact versions
|
|
2314
|
+
|
|
1802
2315
|
## Contributing & Support
|
|
1803
2316
|
|
|
1804
2317
|
- **GitHub**: [chrishayuk/chuk-tool-processor](https://github.com/chrishayuk/chuk-tool-processor)
|