chuk-tool-processor 0.9.7__tar.gz → 0.11__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.11}/PKG-INFO +806 -160
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/README.md +805 -159
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/pyproject.toml +37 -13
- chuk_tool_processor-0.11/src/chuk_tool_processor/__init__.py +117 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/core/exceptions.py +55 -4
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/core/processor.py +365 -46
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/__init__.py +5 -8
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/context.py +2 -5
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/__init__.py +4 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/mcp_tool.py +8 -3
- chuk_tool_processor-0.11/src/chuk_tool_processor/mcp/models.py +151 -0
- chuk_tool_processor-0.11/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +201 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/stream_manager.py +94 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/tool_export_mixin.py +4 -4
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/observability/metrics.py +3 -3
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/observability/tracing.py +13 -12
- chuk_tool_processor-0.11/src/chuk_tool_processor/registry/__init__.py +110 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/decorators.py +42 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/interface.py +15 -10
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/metadata.py +26 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/providers/__init__.py +2 -1
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/providers/memory.py +23 -7
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/tool_export.py +20 -23
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor.egg-info/PKG-INFO +806 -160
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/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/src/chuk_tool_processor/registry/__init__.py +0 -60
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/setup.cfg +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/core/__init__.py +0 -0
- {chuk_tool_processor-0.9.7/src/chuk_tool_processor → chuk_tool_processor-0.11/src/chuk_tool_processor/execution}/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/strategies/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/strategies/inprocess_strategy.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/strategies/subprocess_strategy.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/tool_executor.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/caching.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/circuit_breaker.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/rate_limiting.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/retry.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/formatter.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/helpers.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/metrics.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/register_mcp_tools.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/setup_mcp_http_streamable.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/setup_mcp_sse.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/base_transport.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/http_streamable_transport.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/models.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/sse_transport.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/stdio_transport.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/execution_strategy.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/streaming_tool.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/tool_call.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/tool_result.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/tool_spec.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/validated_tool.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/observability/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/observability/setup.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/discovery.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/base.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/json_tool.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/openai_tool.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/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.11/src/chuk_tool_processor/py.typed +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/auto_register.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/provider.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/utils/__init__.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor/utils/validation.py +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor.egg-info/dependency_links.txt +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/src/chuk_tool_processor.egg-info/requires.txt +0 -0
- {chuk_tool_processor-0.9.7 → chuk_tool_processor-0.11}/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.11
|
|
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).
|
|
71
90
|
|
|
72
|
-
|
|
91
|
+
## Executive TL;DR
|
|
73
92
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
- **Caching**: Intelligent result caching with TTL and idempotency key support
|
|
78
|
-
- **Circuit Breakers**: Prevent cascading failures with automatic fault detection
|
|
79
|
-
- **Error Handling**: Machine-readable error codes with structured details
|
|
80
|
-
- **Observability**: Structured logging, metrics, request tracing
|
|
81
|
-
- **Safety**: Subprocess isolation for untrusted code
|
|
82
|
-
- **Type Safety**: Pydantic validation with LLM-friendly argument coercion
|
|
83
|
-
- **Tool Discovery**: Formal schema export (OpenAI, Anthropic, MCP formats)
|
|
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)
|
|
84
96
|
|
|
85
|
-
|
|
97
|
+
```python
|
|
98
|
+
import asyncio
|
|
99
|
+
from chuk_tool_processor import ToolProcessor, tool
|
|
86
100
|
|
|
87
|
-
|
|
101
|
+
@tool(name="weather") # Clean decorator syntax
|
|
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
|
+
# No need for initialize() - auto-initializes on first use!
|
|
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'}
|
|
112
|
+
|
|
113
|
+
asyncio.run(main())
|
|
114
|
+
```
|
|
115
|
+
|
|
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
|
|
162
|
+
|
|
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?
|
|
168
|
+
|
|
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
|
+
```
|
|
146
274
|
|
|
147
|
-
|
|
275
|
+
<details>
|
|
276
|
+
<summary><strong>Install from source or with extras</strong></summary>
|
|
277
|
+
|
|
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]
|
|
151
292
|
```
|
|
152
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()
|
|
309
|
+
```
|
|
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,11 +374,10 @@ 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, tool
|
|
176
378
|
|
|
177
|
-
# Step 1: Define a tool
|
|
178
|
-
@
|
|
379
|
+
# Step 1: Define a tool with the clean @tool decorator
|
|
380
|
+
@tool(name="calculator")
|
|
179
381
|
class Calculator:
|
|
180
382
|
async def execute(self, operation: str, a: float, b: float) -> dict:
|
|
181
383
|
ops = {"add": a + b, "multiply": a * b, "subtract": a - b}
|
|
@@ -185,30 +387,223 @@ class Calculator:
|
|
|
185
387
|
|
|
186
388
|
# Step 2: Process LLM output
|
|
187
389
|
async def main():
|
|
188
|
-
|
|
189
|
-
processor = ToolProcessor()
|
|
390
|
+
# No initialize() needed - it auto-initializes!
|
|
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
|
|
415
|
+
- ✅ Auto-initialization (no boilerplate!)
|
|
209
416
|
|
|
210
417
|
> **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,
|
|
418
|
+
> 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.
|
|
419
|
+
|
|
420
|
+
### Enhanced Developer Experience
|
|
421
|
+
|
|
422
|
+
CHUK Tool Processor provides intuitive APIs and helpful error messages:
|
|
423
|
+
|
|
424
|
+
**1. Clean Decorator Syntax**
|
|
425
|
+
```python
|
|
426
|
+
from chuk_tool_processor import tool
|
|
427
|
+
|
|
428
|
+
@tool(name="calculator") # Short and clean!
|
|
429
|
+
class Calculator:
|
|
430
|
+
async def execute(self, a: int, b: int) -> int:
|
|
431
|
+
return a + b
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**2. Auto-Initialization (No Boilerplate)**
|
|
435
|
+
```python
|
|
436
|
+
from chuk_tool_processor import ToolProcessor
|
|
437
|
+
|
|
438
|
+
# No initialize() needed - it auto-initializes!
|
|
439
|
+
async with ToolProcessor() as p:
|
|
440
|
+
results = await p.process(llm_output)
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**3. Type-Safe Tool Discovery**
|
|
444
|
+
```python
|
|
445
|
+
from chuk_tool_processor import get_default_registry, ToolInfo
|
|
446
|
+
|
|
447
|
+
registry = await get_default_registry()
|
|
448
|
+
|
|
449
|
+
# List all registered tools with clear, typed results
|
|
450
|
+
tools = await registry.list_tools()
|
|
451
|
+
for tool in tools: # Each tool is a ToolInfo object
|
|
452
|
+
print(f"{tool.namespace}:{tool.name}") # Clear attribute access!
|
|
453
|
+
# No more confusing tuple unpacking: (namespace, name) vs (name, namespace)?
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
**4. Helpful Error Messages**
|
|
457
|
+
```python
|
|
458
|
+
# Typo in tool name? Get helpful suggestions!
|
|
459
|
+
try:
|
|
460
|
+
await registry.get_tool_strict("calcuator", namespace="default")
|
|
461
|
+
except Exception as e:
|
|
462
|
+
print(e)
|
|
463
|
+
# Output:
|
|
464
|
+
# Tool 'calcuator' not found in namespace 'default'
|
|
465
|
+
#
|
|
466
|
+
# Did you mean: calculator?
|
|
467
|
+
#
|
|
468
|
+
# Available namespaces: default, math, mcp
|
|
469
|
+
#
|
|
470
|
+
# Tip: Use `await registry.list_tools()` to see all registered tools
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
**5. Clean MCP Configuration**
|
|
474
|
+
```python
|
|
475
|
+
from chuk_tool_processor.mcp import setup_mcp_stdio, MCPConfig, MCPServerConfig
|
|
476
|
+
|
|
477
|
+
# Clean Pydantic config object instead of 14+ parameters!
|
|
478
|
+
processor, manager = await setup_mcp_stdio(
|
|
479
|
+
config=MCPConfig(
|
|
480
|
+
servers=[MCPServerConfig(name="echo", command="uvx", args=["mcp-echo"])],
|
|
481
|
+
namespace="tools",
|
|
482
|
+
enable_caching=True,
|
|
483
|
+
cache_ttl=600,
|
|
484
|
+
)
|
|
485
|
+
)
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
**Key improvements:**
|
|
489
|
+
- ✅ **`@tool` decorator**: Shorter, cleaner than `@register_tool`
|
|
490
|
+
- ✅ **Auto-initialization**: No need for explicit `initialize()` calls
|
|
491
|
+
- ✅ **Type-safe tool listing**: `ToolInfo` objects instead of confusing tuples
|
|
492
|
+
- ✅ **Helpful errors**: Fuzzy matching suggestions when tools aren't found
|
|
493
|
+
- ✅ **MCPConfig**: Clean Pydantic model instead of 14+ parameters
|
|
494
|
+
- ✅ **Better discoverability**: Clear guidance on how to explore available tools
|
|
495
|
+
|
|
496
|
+
## Quick Decision Tree (Commit This to Memory)
|
|
497
|
+
|
|
498
|
+
```
|
|
499
|
+
╭──────────────────────────────────────────╮
|
|
500
|
+
│ Do you trust the code you're executing? │
|
|
501
|
+
│ ✅ Yes → InProcessStrategy │
|
|
502
|
+
│ ⚠️ No → IsolatedStrategy (sandboxed) │
|
|
503
|
+
│ │
|
|
504
|
+
│ Where do your tools live? │
|
|
505
|
+
│ 📦 Local → @tool decorator │
|
|
506
|
+
│ 🌐 Remote → setup_mcp_* with MCPConfig │
|
|
507
|
+
╰──────────────────────────────────────────╯
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
**That's all you need to pick the right pattern.**
|
|
511
|
+
|
|
512
|
+
## Registry & Processor Lifecycle
|
|
513
|
+
|
|
514
|
+
Understanding the lifecycle helps you use CHUK Tool Processor correctly:
|
|
515
|
+
|
|
516
|
+
1. **Auto-initialization** — Registry auto-initializes on first access (or call `await initialize()` explicitly)
|
|
517
|
+
2. Create a **`ToolProcessor(...)`** (or use the one returned by `setup_mcp_*`)
|
|
518
|
+
3. Use **`async with ToolProcessor() as p:`** to ensure cleanup
|
|
519
|
+
4. **`setup_mcp_*`** returns `(processor, manager)` — reuse that `processor`
|
|
520
|
+
5. If you need a custom registry, pass it explicitly to the strategy
|
|
521
|
+
6. You rarely need `get_default_registry()` unless you're composing advanced setups
|
|
522
|
+
|
|
523
|
+
**New in this version:** The registry auto-initializes when you create a `ToolProcessor` or access `get_default_registry()`, so you can skip the explicit `initialize()` call in most cases!
|
|
524
|
+
|
|
525
|
+
```python
|
|
526
|
+
# New simplified pattern (auto-initialization)
|
|
527
|
+
async with ToolProcessor() as p: # Auto-initializes on first use!
|
|
528
|
+
results = await p.process(llm_output)
|
|
529
|
+
# Processor automatically cleaned up on exit
|
|
530
|
+
|
|
531
|
+
# Traditional explicit pattern (still works)
|
|
532
|
+
await initialize() # Explicit initialization
|
|
533
|
+
async with ToolProcessor() as p:
|
|
534
|
+
results = await p.process(llm_output)
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
## Production Features by Example
|
|
538
|
+
|
|
539
|
+
### Idempotency & Deduplication
|
|
540
|
+
|
|
541
|
+
Automatically deduplicate LLM retry quirks using SHA256-based idempotency keys:
|
|
542
|
+
|
|
543
|
+
```python
|
|
544
|
+
from chuk_tool_processor import ToolProcessor, initialize
|
|
545
|
+
|
|
546
|
+
await initialize()
|
|
547
|
+
async with ToolProcessor(enable_caching=True, cache_ttl=300) as p:
|
|
548
|
+
# LLM retries the same call (common with streaming or errors)
|
|
549
|
+
call1 = '<tool name="search" args=\'{"query": "Python"}\'/>'
|
|
550
|
+
call2 = '<tool name="search" args=\'{"query": "Python"}\'/>' # Identical
|
|
551
|
+
|
|
552
|
+
results1 = await p.process(call1) # Executes
|
|
553
|
+
results2 = await p.process(call2) # Cache hit! (idempotency key match)
|
|
554
|
+
|
|
555
|
+
assert results1[0].cached == False
|
|
556
|
+
assert results2[0].cached == True
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### Cancellation & Deadlines
|
|
560
|
+
|
|
561
|
+
Cooperative cancellation with request-scoped deadlines:
|
|
562
|
+
|
|
563
|
+
```python
|
|
564
|
+
import asyncio
|
|
565
|
+
from chuk_tool_processor import ToolProcessor, initialize
|
|
566
|
+
|
|
567
|
+
async def main():
|
|
568
|
+
await initialize()
|
|
569
|
+
async with ToolProcessor(default_timeout=60.0) as p:
|
|
570
|
+
try:
|
|
571
|
+
# Hard deadline for the whole batch (e.g., user request budget)
|
|
572
|
+
async with asyncio.timeout(5.0):
|
|
573
|
+
async for event in p.astream('<tool name="slow_report" args=\'{"n": 1000000}\'/>'):
|
|
574
|
+
print("chunk:", event)
|
|
575
|
+
except TimeoutError:
|
|
576
|
+
print("Request cancelled: deadline exceeded")
|
|
577
|
+
# Processor automatically cancels the tool and cleans up
|
|
578
|
+
|
|
579
|
+
asyncio.run(main())
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Per-Tool Policy Overrides
|
|
583
|
+
|
|
584
|
+
Override timeouts, retries, and rate limits per tool:
|
|
585
|
+
|
|
586
|
+
```python
|
|
587
|
+
from chuk_tool_processor import ToolProcessor, initialize
|
|
588
|
+
|
|
589
|
+
await initialize()
|
|
590
|
+
async with ToolProcessor(
|
|
591
|
+
default_timeout=30.0,
|
|
592
|
+
enable_retries=True,
|
|
593
|
+
max_retries=2,
|
|
594
|
+
enable_rate_limiting=True,
|
|
595
|
+
global_rate_limit=120, # 120 requests/min across all tools
|
|
596
|
+
tool_rate_limits={
|
|
597
|
+
"expensive_api": (5, 60), # 5 requests per 60 seconds
|
|
598
|
+
"fast_local": (1000, 60), # 1000 requests per 60 seconds
|
|
599
|
+
}
|
|
600
|
+
) as p:
|
|
601
|
+
# Tools run with their specific policies
|
|
602
|
+
results = await p.process('''
|
|
603
|
+
<tool name="expensive_api" args='{"q":"abc"}'/>
|
|
604
|
+
<tool name="fast_local" args='{"data":"xyz"}'/>
|
|
605
|
+
''')
|
|
606
|
+
```
|
|
212
607
|
|
|
213
608
|
## Documentation Quick Reference
|
|
214
609
|
|
|
@@ -217,17 +612,19 @@ asyncio.run(main())
|
|
|
217
612
|
| 📘 [CONFIGURATION.md](docs/CONFIGURATION.md) | **All config knobs & defaults**: ToolProcessor options, timeouts, retry policy, rate limits, circuit breakers, caching, environment variables |
|
|
218
613
|
| 🚨 [ERRORS.md](docs/ERRORS.md) | **Error taxonomy**: All error codes, exception classes, error details structure, handling patterns, retryability guide |
|
|
219
614
|
| 📊 [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 |
|
|
615
|
+
| 🔌 [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
616
|
| 🎯 [examples/](examples/) | **20+ working examples**: MCP integration, OAuth flows, streaming, production patterns |
|
|
222
617
|
|
|
223
618
|
## Choose Your Path
|
|
224
619
|
|
|
620
|
+
**Use this when OpenAI/Claude tool calling is not enough** — because you need retries, caching, rate limits, subprocess isolation, or MCP integration.
|
|
621
|
+
|
|
225
622
|
| Your Goal | What You Need | Where to Look |
|
|
226
623
|
|-----------|---------------|---------------|
|
|
227
624
|
| ☕ **Just process LLM tool calls** | Basic tool registration + processor | [60-Second Quick Start](#60-second-quick-start) |
|
|
228
625
|
| 🔌 **Connect to external tools** | MCP integration (HTTP/STDIO/SSE) | [MCP Integration](#5-mcp-integration-external-tools) |
|
|
229
626
|
| 🛡️ **Production deployment** | Timeouts, retries, rate limits, caching | [CONFIGURATION.md](docs/CONFIGURATION.md) |
|
|
230
|
-
| 🔒 **Run untrusted code safely** |
|
|
627
|
+
| 🔒 **Run untrusted code safely** | Isolated strategy (subprocess) | [Isolated Strategy](#using-isolated-strategy) |
|
|
231
628
|
| 📊 **Monitor and observe** | OpenTelemetry + Prometheus | [OBSERVABILITY.md](docs/OBSERVABILITY.md) |
|
|
232
629
|
| 🌊 **Stream incremental results** | StreamingTool pattern | [StreamingTool](#streamingtool-real-time-results) |
|
|
233
630
|
| 🚨 **Handle errors reliably** | Error codes & taxonomy | [ERRORS.md](docs/ERRORS.md) |
|
|
@@ -239,8 +636,7 @@ Here are the most common patterns you'll use:
|
|
|
239
636
|
**Pattern 1: Local tools only**
|
|
240
637
|
```python
|
|
241
638
|
import asyncio
|
|
242
|
-
from chuk_tool_processor
|
|
243
|
-
from chuk_tool_processor.registry import initialize, register_tool
|
|
639
|
+
from chuk_tool_processor import ToolProcessor, register_tool, initialize
|
|
244
640
|
|
|
245
641
|
@register_tool(name="my_tool")
|
|
246
642
|
class MyTool:
|
|
@@ -249,20 +645,22 @@ class MyTool:
|
|
|
249
645
|
|
|
250
646
|
async def main():
|
|
251
647
|
await initialize()
|
|
252
|
-
processor = ToolProcessor()
|
|
253
648
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
649
|
+
async with ToolProcessor() as processor:
|
|
650
|
+
llm_output = '<tool name="my_tool" args=\'{"arg": "hello"}\'/>'
|
|
651
|
+
results = await processor.process(llm_output)
|
|
652
|
+
print(results[0].result) # {'result': 'Processed: hello'}
|
|
257
653
|
|
|
258
654
|
asyncio.run(main())
|
|
259
655
|
```
|
|
260
656
|
|
|
657
|
+
<details>
|
|
658
|
+
<summary><strong>More patterns: MCP integration (local + remote tools)</strong></summary>
|
|
659
|
+
|
|
261
660
|
**Pattern 2: Mix local + remote MCP tools (Notion)**
|
|
262
661
|
```python
|
|
263
662
|
import asyncio
|
|
264
|
-
from chuk_tool_processor
|
|
265
|
-
from chuk_tool_processor.mcp import setup_mcp_http_streamable
|
|
663
|
+
from chuk_tool_processor import register_tool, initialize, setup_mcp_http_streamable
|
|
266
664
|
|
|
267
665
|
@register_tool(name="local_calculator")
|
|
268
666
|
class Calculator:
|
|
@@ -292,12 +690,49 @@ async def main():
|
|
|
292
690
|
print(f"Local result: {results[0].result}")
|
|
293
691
|
print(f"Notion result: {results[1].result}")
|
|
294
692
|
|
|
693
|
+
# Clean up
|
|
694
|
+
await manager.close()
|
|
695
|
+
|
|
295
696
|
asyncio.run(main())
|
|
296
697
|
```
|
|
297
698
|
|
|
298
|
-
See `examples/notion_oauth.py` for complete OAuth flow.
|
|
699
|
+
See `examples/04_mcp_integration/notion_oauth.py` for complete OAuth flow.
|
|
700
|
+
|
|
701
|
+
**Pattern 3: Local SQLite database via STDIO (New Clean API)**
|
|
702
|
+
```python
|
|
703
|
+
import asyncio
|
|
704
|
+
from chuk_tool_processor.mcp import setup_mcp_stdio, MCPConfig, MCPServerConfig
|
|
705
|
+
|
|
706
|
+
async def main():
|
|
707
|
+
# NEW: Clean Pydantic config approach (recommended!)
|
|
708
|
+
processor, manager = await setup_mcp_stdio(
|
|
709
|
+
config=MCPConfig(
|
|
710
|
+
servers=[
|
|
711
|
+
MCPServerConfig(
|
|
712
|
+
name="sqlite",
|
|
713
|
+
command="uvx",
|
|
714
|
+
args=["mcp-server-sqlite", "--db-path", "./app.db"],
|
|
715
|
+
)
|
|
716
|
+
],
|
|
717
|
+
namespace="db",
|
|
718
|
+
initialization_timeout=120.0, # First run downloads the package
|
|
719
|
+
enable_caching=True,
|
|
720
|
+
cache_ttl=600,
|
|
721
|
+
)
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
# Query your local database via MCP
|
|
725
|
+
results = await processor.process(
|
|
726
|
+
'<tool name="db.query" args=\'{"sql": "SELECT * FROM users LIMIT 10"}\'/>'
|
|
727
|
+
)
|
|
728
|
+
print(results[0].result)
|
|
729
|
+
|
|
730
|
+
asyncio.run(main())
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
<details>
|
|
734
|
+
<summary><strong>Legacy approach (still works)</strong></summary>
|
|
299
735
|
|
|
300
|
-
**Pattern 3: Local SQLite database via STDIO**
|
|
301
736
|
```python
|
|
302
737
|
import asyncio
|
|
303
738
|
import json
|
|
@@ -322,7 +757,7 @@ async def main():
|
|
|
322
757
|
config_file="mcp_config.json",
|
|
323
758
|
servers=["sqlite"],
|
|
324
759
|
namespace="db",
|
|
325
|
-
initialization_timeout=120.0
|
|
760
|
+
initialization_timeout=120.0
|
|
326
761
|
)
|
|
327
762
|
|
|
328
763
|
# Query your local database via MCP
|
|
@@ -333,8 +768,11 @@ async def main():
|
|
|
333
768
|
|
|
334
769
|
asyncio.run(main())
|
|
335
770
|
```
|
|
771
|
+
</details>
|
|
772
|
+
|
|
773
|
+
See `examples/04_mcp_integration/stdio_sqlite.py` for complete working example.
|
|
336
774
|
|
|
337
|
-
|
|
775
|
+
</details>
|
|
338
776
|
|
|
339
777
|
## Core Concepts
|
|
340
778
|
|
|
@@ -347,8 +785,10 @@ The **registry** is where you register tools for execution. Tools can be:
|
|
|
347
785
|
- **StreamingTool** for real-time incremental results
|
|
348
786
|
- **Functions** registered via `register_fn_tool()`
|
|
349
787
|
|
|
788
|
+
> **Note:** The registry is global, processors are scoped.
|
|
789
|
+
|
|
350
790
|
```python
|
|
351
|
-
from chuk_tool_processor
|
|
791
|
+
from chuk_tool_processor import register_tool
|
|
352
792
|
from chuk_tool_processor.models.validated_tool import ValidatedTool
|
|
353
793
|
from pydantic import BaseModel, Field
|
|
354
794
|
|
|
@@ -374,18 +814,16 @@ class WeatherTool(ValidatedTool):
|
|
|
374
814
|
| Strategy | Use Case | Trade-offs |
|
|
375
815
|
|----------|----------|------------|
|
|
376
816
|
| **InProcessStrategy** | Fast, trusted tools | Speed ✅, Isolation ❌ |
|
|
377
|
-
| **
|
|
817
|
+
| **IsolatedStrategy** | Untrusted or risky code | Isolation ✅, Speed ❌ |
|
|
378
818
|
|
|
379
819
|
```python
|
|
380
820
|
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
|
|
821
|
+
from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
|
|
384
822
|
|
|
385
823
|
async def main():
|
|
386
824
|
registry = await get_default_registry()
|
|
387
825
|
processor = ToolProcessor(
|
|
388
|
-
strategy=
|
|
826
|
+
strategy=IsolatedStrategy(
|
|
389
827
|
registry=registry,
|
|
390
828
|
max_workers=4,
|
|
391
829
|
default_timeout=30.0
|
|
@@ -396,6 +834,8 @@ async def main():
|
|
|
396
834
|
asyncio.run(main())
|
|
397
835
|
```
|
|
398
836
|
|
|
837
|
+
**Note:** `IsolatedStrategy` is an alias of `SubprocessStrategy` for backwards compatibility. Use `IsolatedStrategy` for clarity—it better communicates the security boundary intent.
|
|
838
|
+
|
|
399
839
|
### 3. Execution Wrappers (Middleware)
|
|
400
840
|
|
|
401
841
|
**Wrappers** add production features as composable layers:
|
|
@@ -461,6 +901,8 @@ Connect to **remote tool servers** using the [Model Context Protocol](https://mo
|
|
|
461
901
|
|
|
462
902
|
#### HTTP Streamable (⭐ Recommended for Cloud Services)
|
|
463
903
|
|
|
904
|
+
**Use for:** Cloud SaaS services (OAuth, long-running streams, resilient reconnects)
|
|
905
|
+
|
|
464
906
|
Modern HTTP streaming transport for cloud-based MCP servers like Notion:
|
|
465
907
|
|
|
466
908
|
```python
|
|
@@ -489,8 +931,13 @@ results = await processor.process(
|
|
|
489
931
|
)
|
|
490
932
|
```
|
|
491
933
|
|
|
934
|
+
<details>
|
|
935
|
+
<summary><strong>Other MCP Transports (STDIO for local tools, SSE for legacy)</strong></summary>
|
|
936
|
+
|
|
492
937
|
#### STDIO (Best for Local/On-Device Tools)
|
|
493
938
|
|
|
939
|
+
**Use for:** Local/embedded tools and databases (SQLite, file systems, local services)
|
|
940
|
+
|
|
494
941
|
For running local MCP servers as subprocesses—great for databases, file systems, and local tools:
|
|
495
942
|
|
|
496
943
|
```python
|
|
@@ -529,6 +976,8 @@ results = await processor.process(
|
|
|
529
976
|
|
|
530
977
|
#### SSE (Legacy Support)
|
|
531
978
|
|
|
979
|
+
**Use for:** Legacy compatibility only. Prefer HTTP Streamable for new integrations.
|
|
980
|
+
|
|
532
981
|
For backward compatibility with older MCP servers using Server-Sent Events:
|
|
533
982
|
|
|
534
983
|
```python
|
|
@@ -550,6 +999,8 @@ processor, manager = await setup_mcp_sse(
|
|
|
550
999
|
)
|
|
551
1000
|
```
|
|
552
1001
|
|
|
1002
|
+
</details>
|
|
1003
|
+
|
|
553
1004
|
**Transport Comparison:**
|
|
554
1005
|
|
|
555
1006
|
| Transport | Use Case | Real Examples |
|
|
@@ -558,6 +1009,18 @@ processor, manager = await setup_mcp_sse(
|
|
|
558
1009
|
| **STDIO** | Local tools, databases | SQLite (`mcp-server-sqlite`), Echo (`chuk-mcp-echo`) |
|
|
559
1010
|
| **SSE** | Legacy cloud services | Atlassian (`mcp.atlassian.com`) |
|
|
560
1011
|
|
|
1012
|
+
**How MCP fits into the architecture:**
|
|
1013
|
+
|
|
1014
|
+
```
|
|
1015
|
+
LLM Output
|
|
1016
|
+
↓
|
|
1017
|
+
Tool Processor
|
|
1018
|
+
↓
|
|
1019
|
+
┌──────────────┬────────────────────┐
|
|
1020
|
+
│ Local Tools │ Remote Tools (MCP) │
|
|
1021
|
+
└──────────────┴────────────────────┘
|
|
1022
|
+
```
|
|
1023
|
+
|
|
561
1024
|
**Relationship with [chuk-mcp](https://github.com/chrishayuk/chuk-mcp):**
|
|
562
1025
|
- `chuk-mcp` is a low-level MCP protocol client (handles transports, protocol negotiation)
|
|
563
1026
|
- `chuk-tool-processor` wraps `chuk-mcp` to integrate external tools into your execution pipeline
|
|
@@ -571,7 +1034,7 @@ CHUK Tool Processor supports multiple patterns for defining tools:
|
|
|
571
1034
|
|
|
572
1035
|
#### Simple Function-Based Tools
|
|
573
1036
|
```python
|
|
574
|
-
from chuk_tool_processor
|
|
1037
|
+
from chuk_tool_processor import register_fn_tool
|
|
575
1038
|
from datetime import datetime
|
|
576
1039
|
from zoneinfo import ZoneInfo
|
|
577
1040
|
|
|
@@ -589,7 +1052,11 @@ register_fn_tool(get_current_time, namespace="utilities")
|
|
|
589
1052
|
For production tools, use Pydantic validation:
|
|
590
1053
|
|
|
591
1054
|
```python
|
|
592
|
-
|
|
1055
|
+
from chuk_tool_processor import tool
|
|
1056
|
+
from chuk_tool_processor.models import ValidatedTool
|
|
1057
|
+
from pydantic import BaseModel, Field
|
|
1058
|
+
|
|
1059
|
+
@tool(name="weather") # Clean @tool decorator
|
|
593
1060
|
class WeatherTool(ValidatedTool):
|
|
594
1061
|
class Arguments(BaseModel):
|
|
595
1062
|
location: str = Field(..., description="City name")
|
|
@@ -603,14 +1070,28 @@ class WeatherTool(ValidatedTool):
|
|
|
603
1070
|
return self.Result(temperature=22.5, conditions="Sunny")
|
|
604
1071
|
```
|
|
605
1072
|
|
|
1073
|
+
<details>
|
|
1074
|
+
<summary><strong>Alternative: Using @register_tool (still works)</strong></summary>
|
|
1075
|
+
|
|
1076
|
+
```python
|
|
1077
|
+
from chuk_tool_processor import register_tool
|
|
1078
|
+
|
|
1079
|
+
@register_tool(name="weather") # Longer form, but identical functionality
|
|
1080
|
+
class WeatherTool(ValidatedTool):
|
|
1081
|
+
# ... same as above
|
|
1082
|
+
```
|
|
1083
|
+
</details>
|
|
1084
|
+
|
|
606
1085
|
#### StreamingTool (Real-time Results)
|
|
607
1086
|
|
|
608
1087
|
For long-running operations that produce incremental results:
|
|
609
1088
|
|
|
610
1089
|
```python
|
|
1090
|
+
from chuk_tool_processor import tool
|
|
611
1091
|
from chuk_tool_processor.models import StreamingTool
|
|
1092
|
+
from pydantic import BaseModel
|
|
612
1093
|
|
|
613
|
-
@
|
|
1094
|
+
@tool(name="file_processor") # Clean @tool decorator
|
|
614
1095
|
class FileProcessor(StreamingTool):
|
|
615
1096
|
class Arguments(BaseModel):
|
|
616
1097
|
file_path: str
|
|
@@ -629,17 +1110,26 @@ class FileProcessor(StreamingTool):
|
|
|
629
1110
|
|
|
630
1111
|
```python
|
|
631
1112
|
import asyncio
|
|
632
|
-
from chuk_tool_processor
|
|
633
|
-
from chuk_tool_processor.registry import initialize
|
|
1113
|
+
from chuk_tool_processor import ToolProcessor, initialize
|
|
634
1114
|
|
|
635
1115
|
async def main():
|
|
636
1116
|
await initialize()
|
|
637
1117
|
processor = ToolProcessor()
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
1118
|
+
|
|
1119
|
+
# Stream can be cancelled by breaking or raising an exception
|
|
1120
|
+
try:
|
|
1121
|
+
async for event in processor.astream('<tool name="file_processor" args=\'{"file_path":"README.md"}\'/>'):
|
|
1122
|
+
# 'event' is a streamed chunk (either your Result model instance or a dict)
|
|
1123
|
+
line = event["line"] if isinstance(event, dict) else getattr(event, "line", None)
|
|
1124
|
+
content = event["content"] if isinstance(event, dict) else getattr(event, "content", None)
|
|
1125
|
+
print(f"Line {line}: {content}")
|
|
1126
|
+
|
|
1127
|
+
# Example: cancel after 100 lines
|
|
1128
|
+
if line and line > 100:
|
|
1129
|
+
break # Cleanup happens automatically
|
|
1130
|
+
except asyncio.CancelledError:
|
|
1131
|
+
# Stream cleanup is automatic even on cancellation
|
|
1132
|
+
pass
|
|
643
1133
|
|
|
644
1134
|
asyncio.run(main())
|
|
645
1135
|
```
|
|
@@ -648,23 +1138,32 @@ asyncio.run(main())
|
|
|
648
1138
|
|
|
649
1139
|
#### Basic Usage
|
|
650
1140
|
|
|
651
|
-
Call `await initialize()` once at startup to load your registry.
|
|
1141
|
+
Call `await initialize()` once at startup to load your registry. Use context managers for automatic cleanup:
|
|
652
1142
|
|
|
653
1143
|
```python
|
|
654
1144
|
import asyncio
|
|
655
|
-
from chuk_tool_processor
|
|
656
|
-
from chuk_tool_processor.registry import initialize
|
|
1145
|
+
from chuk_tool_processor import ToolProcessor, initialize
|
|
657
1146
|
|
|
658
1147
|
async def main():
|
|
659
1148
|
await initialize()
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
1149
|
+
|
|
1150
|
+
# Context manager automatically handles cleanup
|
|
1151
|
+
async with ToolProcessor() as processor:
|
|
1152
|
+
# Discover available tools
|
|
1153
|
+
tools = await processor.list_tools()
|
|
1154
|
+
print(f"Available tools: {tools}")
|
|
1155
|
+
|
|
1156
|
+
# Process LLM output
|
|
1157
|
+
llm_output = '<tool name="calculator" args=\'{"operation":"add","a":2,"b":3}\'/>'
|
|
1158
|
+
results = await processor.process(llm_output)
|
|
1159
|
+
|
|
1160
|
+
for result in results:
|
|
1161
|
+
if result.error:
|
|
1162
|
+
print(f"Error: {result.error}")
|
|
1163
|
+
else:
|
|
1164
|
+
print(f"Success: {result.result}")
|
|
1165
|
+
|
|
1166
|
+
# Processor automatically cleaned up here!
|
|
668
1167
|
|
|
669
1168
|
asyncio.run(main())
|
|
670
1169
|
```
|
|
@@ -672,21 +1171,32 @@ asyncio.run(main())
|
|
|
672
1171
|
#### Production Configuration
|
|
673
1172
|
|
|
674
1173
|
```python
|
|
675
|
-
from chuk_tool_processor
|
|
1174
|
+
from chuk_tool_processor import ToolProcessor, initialize
|
|
1175
|
+
import asyncio
|
|
676
1176
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
default_timeout=30.0,
|
|
680
|
-
max_concurrency=20,
|
|
1177
|
+
async def main():
|
|
1178
|
+
await initialize()
|
|
681
1179
|
|
|
682
|
-
#
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
1180
|
+
# Use context manager with production config
|
|
1181
|
+
async with ToolProcessor(
|
|
1182
|
+
# Execution settings
|
|
1183
|
+
default_timeout=30.0,
|
|
1184
|
+
max_concurrency=20,
|
|
1185
|
+
|
|
1186
|
+
# Production features
|
|
1187
|
+
enable_caching=True,
|
|
1188
|
+
cache_ttl=600,
|
|
1189
|
+
enable_rate_limiting=True,
|
|
1190
|
+
global_rate_limit=100,
|
|
1191
|
+
enable_retries=True,
|
|
1192
|
+
max_retries=3
|
|
1193
|
+
) as processor:
|
|
1194
|
+
# Use processor...
|
|
1195
|
+
results = await processor.process(llm_output)
|
|
1196
|
+
|
|
1197
|
+
# Automatic cleanup on exit
|
|
1198
|
+
|
|
1199
|
+
asyncio.run(main())
|
|
690
1200
|
```
|
|
691
1201
|
|
|
692
1202
|
### Advanced Production Features
|
|
@@ -698,7 +1208,7 @@ Beyond basic configuration, CHUK Tool Processor includes several advanced featur
|
|
|
698
1208
|
Prevent cascading failures by automatically opening circuits for failing tools:
|
|
699
1209
|
|
|
700
1210
|
```python
|
|
701
|
-
from chuk_tool_processor
|
|
1211
|
+
from chuk_tool_processor import ToolProcessor
|
|
702
1212
|
|
|
703
1213
|
processor = ToolProcessor(
|
|
704
1214
|
enable_circuit_breaker=True,
|
|
@@ -740,8 +1250,8 @@ assert call1.idempotency_key == call2.idempotency_key
|
|
|
740
1250
|
|
|
741
1251
|
# Used automatically by caching layer
|
|
742
1252
|
processor = ToolProcessor(enable_caching=True)
|
|
743
|
-
results1 = await processor.
|
|
744
|
-
results2 = await processor.
|
|
1253
|
+
results1 = await processor.process([call1]) # Executes
|
|
1254
|
+
results2 = await processor.process([call2]) # Cache hit!
|
|
745
1255
|
```
|
|
746
1256
|
|
|
747
1257
|
**Benefits:**
|
|
@@ -749,6 +1259,8 @@ results2 = await processor.execute([call2]) # Cache hit!
|
|
|
749
1259
|
- Deterministic cache keys
|
|
750
1260
|
- No manual key management needed
|
|
751
1261
|
|
|
1262
|
+
**Cache scope:** In-memory per-process by default. Cache backend is pluggable—see [CONFIGURATION.md](docs/CONFIGURATION.md) for custom cache backends.
|
|
1263
|
+
|
|
752
1264
|
#### Tool Schema Export
|
|
753
1265
|
|
|
754
1266
|
Export tool definitions to multiple formats for LLM prompting:
|
|
@@ -795,7 +1307,9 @@ mcp_format = spec.to_mcp() # For MCP servers
|
|
|
795
1307
|
|
|
796
1308
|
#### Machine-Readable Error Codes
|
|
797
1309
|
|
|
798
|
-
Structured error handling with error codes for programmatic responses
|
|
1310
|
+
Structured error handling with error codes for programmatic responses.
|
|
1311
|
+
|
|
1312
|
+
**Error Contract:** Every error includes a machine-readable code, human-readable message, and structured details:
|
|
799
1313
|
|
|
800
1314
|
```python
|
|
801
1315
|
from chuk_tool_processor.core.exceptions import (
|
|
@@ -877,22 +1391,20 @@ result = await tool.execute(**llm_output)
|
|
|
877
1391
|
|
|
878
1392
|
## Advanced Topics
|
|
879
1393
|
|
|
880
|
-
### Using
|
|
1394
|
+
### Using Isolated Strategy
|
|
881
1395
|
|
|
882
|
-
Use `
|
|
1396
|
+
Use `IsolatedStrategy` when running untrusted, third-party, or potentially unsafe code that shouldn't share the same process as your main app.
|
|
883
1397
|
|
|
884
1398
|
For isolation and safety when running untrusted code:
|
|
885
1399
|
|
|
886
1400
|
```python
|
|
887
1401
|
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
|
|
1402
|
+
from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
|
|
891
1403
|
|
|
892
1404
|
async def main():
|
|
893
1405
|
registry = await get_default_registry()
|
|
894
1406
|
processor = ToolProcessor(
|
|
895
|
-
strategy=
|
|
1407
|
+
strategy=IsolatedStrategy(
|
|
896
1408
|
registry=registry,
|
|
897
1409
|
max_workers=4,
|
|
898
1410
|
default_timeout=30.0
|
|
@@ -903,6 +1415,10 @@ async def main():
|
|
|
903
1415
|
asyncio.run(main())
|
|
904
1416
|
```
|
|
905
1417
|
|
|
1418
|
+
> **Security & Isolation — Threat Model**
|
|
1419
|
+
>
|
|
1420
|
+
> 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.
|
|
1421
|
+
|
|
906
1422
|
### Real-World MCP Examples
|
|
907
1423
|
|
|
908
1424
|
#### Example 1: Notion Integration with OAuth
|
|
@@ -912,7 +1428,7 @@ Complete OAuth flow connecting to Notion's MCP server:
|
|
|
912
1428
|
```python
|
|
913
1429
|
from chuk_tool_processor.mcp import setup_mcp_http_streamable
|
|
914
1430
|
|
|
915
|
-
# After completing OAuth flow (see examples/notion_oauth.py for full flow)
|
|
1431
|
+
# After completing OAuth flow (see examples/04_mcp_integration/notion_oauth.py for full flow)
|
|
916
1432
|
processor, manager = await setup_mcp_http_streamable(
|
|
917
1433
|
servers=[{
|
|
918
1434
|
"name": "notion",
|
|
@@ -933,6 +1449,9 @@ results = await processor.process(
|
|
|
933
1449
|
)
|
|
934
1450
|
```
|
|
935
1451
|
|
|
1452
|
+
<details>
|
|
1453
|
+
<summary><strong>Click to expand more MCP examples (SQLite, Echo Server)</strong></summary>
|
|
1454
|
+
|
|
936
1455
|
#### Example 2: Local SQLite Database Access
|
|
937
1456
|
|
|
938
1457
|
Run SQLite MCP server locally for database operations:
|
|
@@ -1004,10 +1523,15 @@ results = await processor.process(
|
|
|
1004
1523
|
)
|
|
1005
1524
|
```
|
|
1006
1525
|
|
|
1007
|
-
|
|
1526
|
+
</details>
|
|
1527
|
+
|
|
1528
|
+
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
1529
|
|
|
1009
1530
|
#### OAuth Token Refresh
|
|
1010
1531
|
|
|
1532
|
+
<details>
|
|
1533
|
+
<summary><strong>Click to expand OAuth token refresh guide</strong></summary>
|
|
1534
|
+
|
|
1011
1535
|
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
1536
|
|
|
1013
1537
|
**How it works:**
|
|
@@ -1076,7 +1600,9 @@ processor, manager = await setup_mcp_sse(
|
|
|
1076
1600
|
- Token refresh is attempted only once per tool call (no infinite retry loops)
|
|
1077
1601
|
- After successful refresh, the updated headers are used for all subsequent calls
|
|
1078
1602
|
|
|
1079
|
-
See `examples/notion_oauth.py` for a complete OAuth 2.1 implementation with PKCE and automatic token refresh.
|
|
1603
|
+
See `examples/04_mcp_integration/notion_oauth.py` for a complete OAuth 2.1 implementation with PKCE and automatic token refresh.
|
|
1604
|
+
|
|
1605
|
+
</details>
|
|
1080
1606
|
|
|
1081
1607
|
### Observability
|
|
1082
1608
|
|
|
@@ -1145,6 +1671,9 @@ asyncio.run(main())
|
|
|
1145
1671
|
|
|
1146
1672
|
#### OpenTelemetry & Prometheus (Drop-in Observability)
|
|
1147
1673
|
|
|
1674
|
+
<details>
|
|
1675
|
+
<summary><strong>Click to expand complete observability guide</strong></summary>
|
|
1676
|
+
|
|
1148
1677
|
**3-Line Setup:**
|
|
1149
1678
|
|
|
1150
1679
|
```python
|
|
@@ -1208,8 +1737,7 @@ uv pip install chuk-tool-processor --group observability
|
|
|
1208
1737
|
```python
|
|
1209
1738
|
import asyncio
|
|
1210
1739
|
from chuk_tool_processor.observability import setup_observability
|
|
1211
|
-
from chuk_tool_processor
|
|
1212
|
-
from chuk_tool_processor.registry import initialize, register_tool
|
|
1740
|
+
from chuk_tool_processor import ToolProcessor, initialize, register_tool
|
|
1213
1741
|
|
|
1214
1742
|
@register_tool(name="weather_api")
|
|
1215
1743
|
class WeatherTool:
|
|
@@ -1427,7 +1955,7 @@ export OTEL_EXPORTER_OTLP_ENDPOINT=http://datadog-agent:4317
|
|
|
1427
1955
|
- Testing observability features
|
|
1428
1956
|
- Environment variable configuration
|
|
1429
1957
|
|
|
1430
|
-
🎯 **Working Example**: See `examples/observability_demo.py` for a complete demonstration with retries, caching, and circuit breakers
|
|
1958
|
+
🎯 **Working Example**: See `examples/02_production_features/observability_demo.py` for a complete demonstration with retries, caching, and circuit breakers
|
|
1431
1959
|
|
|
1432
1960
|
**Benefits**
|
|
1433
1961
|
|
|
@@ -1438,6 +1966,8 @@ export OTEL_EXPORTER_OTLP_ENDPOINT=http://datadog-agent:4317
|
|
|
1438
1966
|
✅ **Optional** - Gracefully degrades if packages not installed
|
|
1439
1967
|
✅ **Zero-overhead** - No performance impact when disabled
|
|
1440
1968
|
|
|
1969
|
+
</details>
|
|
1970
|
+
|
|
1441
1971
|
### Error Handling
|
|
1442
1972
|
|
|
1443
1973
|
```python
|
|
@@ -1455,8 +1985,7 @@ for result in results:
|
|
|
1455
1985
|
|
|
1456
1986
|
```python
|
|
1457
1987
|
import pytest
|
|
1458
|
-
from chuk_tool_processor
|
|
1459
|
-
from chuk_tool_processor.registry import initialize
|
|
1988
|
+
from chuk_tool_processor import ToolProcessor, initialize
|
|
1460
1989
|
|
|
1461
1990
|
@pytest.mark.asyncio
|
|
1462
1991
|
async def test_calculator():
|
|
@@ -1470,6 +1999,40 @@ async def test_calculator():
|
|
|
1470
1999
|
assert results[0].result["result"] == 8
|
|
1471
2000
|
```
|
|
1472
2001
|
|
|
2002
|
+
**Fake tool pattern for testing:**
|
|
2003
|
+
|
|
2004
|
+
```python
|
|
2005
|
+
import pytest
|
|
2006
|
+
from chuk_tool_processor import ToolProcessor, register_tool, initialize
|
|
2007
|
+
|
|
2008
|
+
@register_tool(name="fake_tool")
|
|
2009
|
+
class FakeTool:
|
|
2010
|
+
"""No-op tool for testing processor behavior."""
|
|
2011
|
+
call_count = 0
|
|
2012
|
+
|
|
2013
|
+
async def execute(self, **kwargs) -> dict:
|
|
2014
|
+
FakeTool.call_count += 1
|
|
2015
|
+
return {"called": True, "args": kwargs}
|
|
2016
|
+
|
|
2017
|
+
@pytest.mark.asyncio
|
|
2018
|
+
async def test_processor_with_fake_tool():
|
|
2019
|
+
await initialize()
|
|
2020
|
+
processor = ToolProcessor()
|
|
2021
|
+
|
|
2022
|
+
# Reset counter
|
|
2023
|
+
FakeTool.call_count = 0
|
|
2024
|
+
|
|
2025
|
+
# Execute fake tool
|
|
2026
|
+
results = await processor.process(
|
|
2027
|
+
'<tool name="fake_tool" args=\'{"test_arg": "value"}\'/>'
|
|
2028
|
+
)
|
|
2029
|
+
|
|
2030
|
+
# Assert behavior
|
|
2031
|
+
assert FakeTool.call_count == 1
|
|
2032
|
+
assert results[0].result["called"] is True
|
|
2033
|
+
assert results[0].result["args"]["test_arg"] == "value"
|
|
2034
|
+
```
|
|
2035
|
+
|
|
1473
2036
|
## Configuration
|
|
1474
2037
|
|
|
1475
2038
|
### Timeout Configuration
|
|
@@ -1480,6 +2043,7 @@ CHUK Tool Processor uses a unified timeout configuration system that applies to
|
|
|
1480
2043
|
from chuk_tool_processor.mcp.transport import TimeoutConfig
|
|
1481
2044
|
|
|
1482
2045
|
# Create custom timeout configuration
|
|
2046
|
+
# (Defaults are: connect=30, operation=30, quick=5, shutdown=2)
|
|
1483
2047
|
timeout_config = TimeoutConfig(
|
|
1484
2048
|
connect=30.0, # Connection establishment, initialization, session discovery
|
|
1485
2049
|
operation=30.0, # Normal operations (tool calls, listing tools/resources/prompts)
|
|
@@ -1607,7 +2171,7 @@ CHUK Tool Processor provides multiple layers of safety:
|
|
|
1607
2171
|
| Concern | Protection | Configuration |
|
|
1608
2172
|
|---------|------------|---------------|
|
|
1609
2173
|
| **Timeouts** | Every tool has a timeout | `default_timeout=30.0` |
|
|
1610
|
-
| **Process Isolation** | Run tools in separate processes | `strategy=
|
|
2174
|
+
| **Process Isolation** | Run tools in separate processes | `strategy=IsolatedStrategy()` |
|
|
1611
2175
|
| **Rate Limiting** | Prevent abuse and API overuse | `enable_rate_limiting=True` |
|
|
1612
2176
|
| **Input Validation** | Pydantic validation on arguments | Use `ValidatedTool` |
|
|
1613
2177
|
| **Error Containment** | Failures don't crash the processor | Built-in exception handling |
|
|
@@ -1619,13 +2183,58 @@ CHUK Tool Processor provides multiple layers of safety:
|
|
|
1619
2183
|
- **Resource Limits**: For hard CPU/memory caps, use OS-level controls (cgroups on Linux, Job Objects on Windows, or Docker resource limits).
|
|
1620
2184
|
- **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
2185
|
|
|
2186
|
+
#### OS-Level Hardening
|
|
2187
|
+
|
|
2188
|
+
For production deployments, add these hardening measures:
|
|
2189
|
+
|
|
2190
|
+
| Concern | Docker/Container Solution | Direct Example |
|
|
2191
|
+
|---------|--------------------------|----------------|
|
|
2192
|
+
| **CPU/RAM caps** | `--cpus`, `--memory` flags | `docker run --cpus="1.5" --memory="512m" myapp` |
|
|
2193
|
+
| **Network egress** | Deny-by-default with firewall rules | `--network=none` or custom network with egress filtering |
|
|
2194
|
+
| **Filesystem** | Read-only root + writable scratch | `--read-only --tmpfs /tmp:rw,size=100m` |
|
|
2195
|
+
|
|
2196
|
+
**Example: Run processor in locked-down container**
|
|
2197
|
+
|
|
2198
|
+
```bash
|
|
2199
|
+
# Dockerfile
|
|
2200
|
+
FROM python:3.11-slim
|
|
2201
|
+
WORKDIR /app
|
|
2202
|
+
COPY requirements.txt .
|
|
2203
|
+
RUN pip install -r requirements.txt --no-cache-dir
|
|
2204
|
+
COPY . .
|
|
2205
|
+
USER nobody # Run as non-root
|
|
2206
|
+
CMD ["python", "app.py"]
|
|
2207
|
+
|
|
2208
|
+
# Run with resource limits and network restrictions
|
|
2209
|
+
docker run \
|
|
2210
|
+
--cpus="2" \
|
|
2211
|
+
--memory="1g" \
|
|
2212
|
+
--memory-swap="1g" \
|
|
2213
|
+
--read-only \
|
|
2214
|
+
--tmpfs /tmp:rw,size=200m,mode=1777 \
|
|
2215
|
+
--network=custom-net \
|
|
2216
|
+
--cap-drop=ALL \
|
|
2217
|
+
myapp:latest
|
|
2218
|
+
```
|
|
2219
|
+
|
|
2220
|
+
**Network egress controls (deny-by-default)**
|
|
2221
|
+
|
|
2222
|
+
```bash
|
|
2223
|
+
# Create restricted network with no internet access (for local-only tools)
|
|
2224
|
+
docker network create --internal restricted-net
|
|
2225
|
+
|
|
2226
|
+
# Or use iptables for per-tool CIDR allowlists
|
|
2227
|
+
iptables -A OUTPUT -d 10.0.0.0/8 -j ACCEPT # Allow private ranges
|
|
2228
|
+
iptables -A OUTPUT -d 172.16.0.0/12 -j ACCEPT
|
|
2229
|
+
iptables -A OUTPUT -d 192.168.0.0/16 -j ACCEPT
|
|
2230
|
+
iptables -A OUTPUT -j DROP # Deny everything else
|
|
2231
|
+
```
|
|
2232
|
+
|
|
1622
2233
|
Example security-focused setup for untrusted code:
|
|
1623
2234
|
|
|
1624
2235
|
```python
|
|
1625
2236
|
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
|
|
2237
|
+
from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
|
|
1629
2238
|
|
|
1630
2239
|
async def create_secure_processor():
|
|
1631
2240
|
# Maximum isolation for untrusted code
|
|
@@ -1633,7 +2242,7 @@ async def create_secure_processor():
|
|
|
1633
2242
|
registry = await get_default_registry()
|
|
1634
2243
|
|
|
1635
2244
|
processor = ToolProcessor(
|
|
1636
|
-
strategy=
|
|
2245
|
+
strategy=IsolatedStrategy(
|
|
1637
2246
|
registry=registry,
|
|
1638
2247
|
max_workers=4,
|
|
1639
2248
|
default_timeout=10.0
|
|
@@ -1651,6 +2260,25 @@ async def create_secure_processor():
|
|
|
1651
2260
|
# - Use read-only filesystems where possible
|
|
1652
2261
|
```
|
|
1653
2262
|
|
|
2263
|
+
## Design Goals & Non-Goals
|
|
2264
|
+
|
|
2265
|
+
**What CHUK Tool Processor does:**
|
|
2266
|
+
- ✅ Parse tool calls from any LLM format (XML, OpenAI, JSON)
|
|
2267
|
+
- ✅ Execute tools with production policies (timeouts, retries, rate limits, caching)
|
|
2268
|
+
- ✅ Isolate untrusted code in subprocesses
|
|
2269
|
+
- ✅ Connect to remote tool servers via MCP (HTTP/STDIO/SSE)
|
|
2270
|
+
- ✅ Provide composable execution layers (strategies + wrappers)
|
|
2271
|
+
- ✅ Export tool schemas for LLM prompting
|
|
2272
|
+
|
|
2273
|
+
**What CHUK Tool Processor explicitly does NOT do:**
|
|
2274
|
+
- ❌ Manage conversations or chat history
|
|
2275
|
+
- ❌ Provide prompt engineering or prompt templates
|
|
2276
|
+
- ❌ Bundle an LLM client (bring your own OpenAI/Anthropic/local)
|
|
2277
|
+
- ❌ Implement agent frameworks or chains
|
|
2278
|
+
- ❌ Make decisions about which tools to call
|
|
2279
|
+
|
|
2280
|
+
**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.
|
|
2281
|
+
|
|
1654
2282
|
## Architecture Principles
|
|
1655
2283
|
|
|
1656
2284
|
1. **Composability**: Stack strategies and wrappers like middleware
|
|
@@ -1664,29 +2292,26 @@ async def create_secure_processor():
|
|
|
1664
2292
|
Check out the [`examples/`](examples/) directory for complete working examples:
|
|
1665
2293
|
|
|
1666
2294
|
### 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
|
|
2295
|
+
- **60-second hello**: `examples/01_getting_started/hello_tool.py` - Absolute minimal example (copy-paste-run)
|
|
2296
|
+
- **Quick start**: `examples/01_getting_started/quickstart_demo.py` - Basic tool registration and execution
|
|
2297
|
+
- **Execution strategies**: `examples/01_getting_started/execution_strategies_demo.py` - InProcess vs Subprocess
|
|
2298
|
+
- **Production wrappers**: `examples/02_production_features/wrappers_demo.py` - Caching, retries, rate limiting
|
|
2299
|
+
- **Streaming tools**: `examples/03_streaming/streaming_demo.py` - Real-time incremental results
|
|
2300
|
+
- **Streaming tool calls**: `examples/03_streaming/streaming_tool_calls_demo.py` - Handle partial tool calls from streaming LLMs
|
|
2301
|
+
- **Schema helper**: `examples/05_schema_and_types/schema_helper_demo.py` - Auto-generate schemas from typed tools (Pydantic → OpenAI/Anthropic/MCP)
|
|
2302
|
+
- **Observability**: `examples/02_production_features/observability_demo.py` - OpenTelemetry + Prometheus integration
|
|
1675
2303
|
|
|
1676
2304
|
### MCP Integration (Real-World)
|
|
1677
|
-
- **Notion + OAuth**: `examples/notion_oauth.py` - Complete OAuth 2.1 flow with HTTP Streamable
|
|
2305
|
+
- **Notion + OAuth**: `examples/04_mcp_integration/notion_oauth.py` - Complete OAuth 2.1 flow with HTTP Streamable
|
|
1678
2306
|
- Shows: Authorization Server discovery, client registration, PKCE flow, token exchange
|
|
1679
|
-
- **SQLite Local**: `examples/stdio_sqlite.py` - Local database access via STDIO
|
|
2307
|
+
- **SQLite Local**: `examples/04_mcp_integration/stdio_sqlite.py` - Local database access via STDIO
|
|
1680
2308
|
- Shows: Command/args passing, environment variables, file paths, initialization timeouts
|
|
1681
|
-
- **Echo Server**: `examples/stdio_echo.py` - Minimal STDIO transport example
|
|
2309
|
+
- **Echo Server**: `examples/04_mcp_integration/stdio_echo.py` - Minimal STDIO transport example
|
|
1682
2310
|
- Shows: Simplest possible MCP integration for testing
|
|
1683
|
-
- **Atlassian + OAuth**: `examples/atlassian_sse.py` - OAuth with SSE transport (legacy)
|
|
2311
|
+
- **Atlassian + OAuth**: `examples/04_mcp_integration/atlassian_sse.py` - OAuth with SSE transport (legacy)
|
|
1684
2312
|
|
|
1685
2313
|
### 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`
|
|
2314
|
+
- **Plugin system**: `examples/06_plugins/plugins_builtins_demo.py`, `examples/06_plugins/plugins_custom_parser_demo.py`
|
|
1690
2315
|
|
|
1691
2316
|
## FAQ
|
|
1692
2317
|
|
|
@@ -1711,18 +2336,20 @@ A: Use pytest with `@pytest.mark.asyncio`. See [Testing Tools](#testing-tools) f
|
|
|
1711
2336
|
**Q: Does this work with streaming LLM responses?**
|
|
1712
2337
|
A: Yes—as tool calls appear in the stream, extract and process them. The processor handles partial/incremental tool call lists.
|
|
1713
2338
|
|
|
1714
|
-
**Q: What's the difference between InProcess and
|
|
1715
|
-
A: InProcess is faster (same process),
|
|
2339
|
+
**Q: What's the difference between InProcess and Isolated strategies?**
|
|
2340
|
+
A: InProcess is faster (same process), Isolated is safer (separate subprocess). Use InProcess for trusted code, Isolated for untrusted.
|
|
1716
2341
|
|
|
1717
2342
|
## Comparison with Other Tools
|
|
1718
2343
|
|
|
1719
2344
|
| Feature | chuk-tool-processor | LangChain Tools | OpenAI Tools | MCP SDK |
|
|
1720
2345
|
|---------|-------------------|-----------------|--------------|---------|
|
|
1721
2346
|
| **Async-native** | ✅ | ⚠️ Partial | ✅ | ✅ |
|
|
1722
|
-
| **Process isolation** | ✅
|
|
2347
|
+
| **Process isolation** | ✅ IsolatedStrategy | ❌ | ❌ | ⚠️ |
|
|
1723
2348
|
| **Built-in retries** | ✅ | ❌ † | ❌ | ❌ |
|
|
1724
2349
|
| **Rate limiting** | ✅ | ❌ † | ⚠️ ‡ | ❌ |
|
|
1725
2350
|
| **Caching** | ✅ | ⚠️ † | ❌ ‡ | ❌ |
|
|
2351
|
+
| **Idempotency & de-dup** | ✅ SHA256 keys | ❌ | ❌ | ❌ |
|
|
2352
|
+
| **Per-tool policies** | ✅ (timeouts/retries/limits) | ⚠️ | ❌ | ❌ |
|
|
1726
2353
|
| **Multiple parsers** | ✅ (XML, OpenAI, JSON) | ⚠️ | ✅ | ✅ |
|
|
1727
2354
|
| **Streaming tools** | ✅ | ⚠️ | ⚠️ | ✅ |
|
|
1728
2355
|
| **MCP integration** | ✅ All transports | ❌ | ❌ | ✅ (protocol only) |
|
|
@@ -1799,6 +2426,25 @@ For detailed release documentation, see:
|
|
|
1799
2426
|
- **[RELEASING.md](RELEASING.md)** - Complete release process guide
|
|
1800
2427
|
- **[docs/CI-CD.md](docs/CI-CD.md)** - Full CI/CD pipeline documentation
|
|
1801
2428
|
|
|
2429
|
+
## Stability & Versioning
|
|
2430
|
+
|
|
2431
|
+
CHUK Tool Processor follows **[Semantic Versioning 2.0.0](https://semver.org/)** for predictable upgrades:
|
|
2432
|
+
|
|
2433
|
+
* **Breaking changes** = **major** version bump (e.g., 1.x → 2.0)
|
|
2434
|
+
* **New features** (backward-compatible) = **minor** version bump (e.g., 1.2 → 1.3)
|
|
2435
|
+
* **Bug fixes** (backward-compatible) = **patch** version bump (e.g., 1.2.3 → 1.2.4)
|
|
2436
|
+
|
|
2437
|
+
**Public API surface**: Everything exported via the package root (`from chuk_tool_processor import ...`) is considered public API and follows semver guarantees.
|
|
2438
|
+
|
|
2439
|
+
**Deprecation policy**: Deprecated APIs will:
|
|
2440
|
+
1. Log a warning for **one minor release**
|
|
2441
|
+
2. Be removed in the **next major release**
|
|
2442
|
+
|
|
2443
|
+
**Upgrading safely**:
|
|
2444
|
+
* Patch and minor updates are **safe to deploy** without code changes
|
|
2445
|
+
* Major updates may require migration—see release notes
|
|
2446
|
+
* Pin to `chuk-tool-processor~=1.2` for minor updates only, or `chuk-tool-processor==1.2.3` for exact versions
|
|
2447
|
+
|
|
1802
2448
|
## Contributing & Support
|
|
1803
2449
|
|
|
1804
2450
|
- **GitHub**: [chrishayuk/chuk-tool-processor](https://github.com/chrishayuk/chuk-tool-processor)
|