chuk-tool-processor 0.9.2__py3-none-any.whl → 0.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of chuk-tool-processor might be problematic. Click here for more details.
- chuk_tool_processor/__init__.py +114 -0
- chuk_tool_processor/core/processor.py +363 -44
- chuk_tool_processor/logging/__init__.py +5 -8
- chuk_tool_processor/logging/context.py +2 -5
- chuk_tool_processor/mcp/__init__.py +3 -0
- chuk_tool_processor/mcp/mcp_tool.py +8 -3
- chuk_tool_processor/mcp/models.py +87 -0
- chuk_tool_processor/mcp/setup_mcp_stdio.py +92 -12
- chuk_tool_processor/mcp/stream_manager.py +94 -0
- chuk_tool_processor/mcp/transport/http_streamable_transport.py +2 -2
- chuk_tool_processor/models/tool_export_mixin.py +4 -4
- chuk_tool_processor/observability/metrics.py +3 -3
- chuk_tool_processor/observability/tracing.py +13 -12
- chuk_tool_processor/py.typed +0 -0
- chuk_tool_processor/registry/interface.py +7 -7
- chuk_tool_processor/registry/providers/__init__.py +2 -1
- chuk_tool_processor/registry/tool_export.py +1 -6
- {chuk_tool_processor-0.9.2.dist-info → chuk_tool_processor-0.10.dist-info}/METADATA +775 -159
- {chuk_tool_processor-0.9.2.dist-info → chuk_tool_processor-0.10.dist-info}/RECORD +21 -19
- {chuk_tool_processor-0.9.2.dist-info → chuk_tool_processor-0.10.dist-info}/WHEEL +0 -0
- {chuk_tool_processor-0.9.2.dist-info → chuk_tool_processor-0.10.dist-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>
|
|
@@ -20,71 +20,153 @@ Classifier: Framework :: AsyncIO
|
|
|
20
20
|
Classifier: Typing :: Typed
|
|
21
21
|
Requires-Python: >=3.11
|
|
22
22
|
Description-Content-Type: text/markdown
|
|
23
|
-
Requires-Dist: chuk-mcp>=0.
|
|
23
|
+
Requires-Dist: chuk-mcp>=0.8.1
|
|
24
24
|
Requires-Dist: dotenv>=0.9.9
|
|
25
25
|
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
|
|
71
92
|
|
|
72
|
-
|
|
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)
|
|
73
96
|
|
|
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)
|
|
97
|
+
```python
|
|
98
|
+
import asyncio
|
|
99
|
+
from chuk_tool_processor import ToolProcessor, register_tool, initialize
|
|
84
100
|
|
|
85
|
-
|
|
101
|
+
@register_tool(name="weather")
|
|
102
|
+
class WeatherTool:
|
|
103
|
+
async def execute(self, city: str) -> dict:
|
|
104
|
+
return {"temp": 72, "condition": "sunny", "city": city}
|
|
86
105
|
|
|
87
|
-
|
|
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'}
|
|
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,6 +194,70 @@ 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
|
+
|
|
227
|
+
## Compatibility Matrix
|
|
228
|
+
|
|
229
|
+
Runs the same on macOS, Linux, and Windows — locally, serverside, and inside containers.
|
|
230
|
+
|
|
231
|
+
| Component | Supported Versions | Notes |
|
|
232
|
+
|-----------|-------------------|-------|
|
|
233
|
+
| **Python** | 3.11, 3.12, 3.13 | Python 3.11+ required |
|
|
234
|
+
| **Operating Systems** | macOS, Linux, Windows | All platforms fully supported |
|
|
235
|
+
| **LLM Providers** | OpenAI, Anthropic, Local models | Any LLM that outputs tool calls |
|
|
236
|
+
| **MCP Transports** | HTTP Streamable, STDIO, SSE | All MCP 1.0 transports |
|
|
237
|
+
| **MCP Servers** | Notion, SQLite, Atlassian, Echo, Custom | Any MCP-compliant server |
|
|
238
|
+
|
|
239
|
+
**Tested Configurations:**
|
|
240
|
+
- ✅ macOS 14+ (Apple Silicon & Intel)
|
|
241
|
+
- ✅ Ubuntu 20.04+ / Debian 11+
|
|
242
|
+
- ✅ Windows 10+ (native & WSL2)
|
|
243
|
+
- ✅ Python 3.11.0+, 3.12.0+, 3.13.0+
|
|
244
|
+
- ✅ OpenAI GPT-4, GPT-4 Turbo
|
|
245
|
+
- ✅ Anthropic Claude 3 (Opus, Sonnet, Haiku)
|
|
246
|
+
- ✅ Local models (Ollama, LM Studio)
|
|
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
|
+
|
|
115
261
|
## Quick Start
|
|
116
262
|
|
|
117
263
|
### Installation
|
|
@@ -124,21 +270,111 @@ pip install chuk-tool-processor
|
|
|
124
270
|
|
|
125
271
|
# Using uv (recommended)
|
|
126
272
|
uv pip install chuk-tool-processor
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
<details>
|
|
276
|
+
<summary><strong>Install from source or with extras</strong></summary>
|
|
127
277
|
|
|
128
|
-
|
|
278
|
+
```bash
|
|
279
|
+
# From source
|
|
129
280
|
git clone https://github.com/chrishayuk/chuk-tool-processor.git
|
|
130
281
|
cd chuk-tool-processor
|
|
131
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()
|
|
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
|
+
|
|
321
|
+
## 60-Second Quick Start
|
|
322
|
+
|
|
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`:
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
python examples/01_getting_started/hello_tool.py
|
|
132
362
|
```
|
|
133
363
|
|
|
364
|
+
Single file that demonstrates:
|
|
365
|
+
- Registering a tool
|
|
366
|
+
- Parsing OpenAI & Anthropic formats
|
|
367
|
+
- Executing and getting results
|
|
368
|
+
|
|
369
|
+
Takes 60 seconds to understand, 3 minutes to master.
|
|
370
|
+
|
|
134
371
|
### 3-Minute Example
|
|
135
372
|
|
|
136
373
|
Copy-paste this into a file and run it:
|
|
137
374
|
|
|
138
375
|
```python
|
|
139
376
|
import asyncio
|
|
140
|
-
from chuk_tool_processor
|
|
141
|
-
from chuk_tool_processor.registry import initialize, register_tool
|
|
377
|
+
from chuk_tool_processor import ToolProcessor, register_tool, initialize
|
|
142
378
|
|
|
143
379
|
# Step 1: Define a tool
|
|
144
380
|
@register_tool(name="calculator")
|
|
@@ -152,40 +388,166 @@ class Calculator:
|
|
|
152
388
|
# Step 2: Process LLM output
|
|
153
389
|
async def main():
|
|
154
390
|
await initialize()
|
|
155
|
-
processor = ToolProcessor()
|
|
156
391
|
|
|
157
|
-
#
|
|
158
|
-
|
|
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}\'/>'
|
|
159
396
|
|
|
160
|
-
|
|
161
|
-
|
|
397
|
+
# Process it
|
|
398
|
+
results = await processor.process(llm_output)
|
|
162
399
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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!
|
|
170
407
|
|
|
171
408
|
asyncio.run(main())
|
|
172
409
|
```
|
|
173
410
|
|
|
174
|
-
**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
|
|
175
415
|
|
|
176
416
|
> **Why not just use OpenAI tool calls?**
|
|
177
|
-
> 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
|
+
```
|
|
527
|
+
|
|
528
|
+
## Documentation Quick Reference
|
|
529
|
+
|
|
530
|
+
| Document | What It Covers |
|
|
531
|
+
|----------|----------------|
|
|
532
|
+
| 📘 [CONFIGURATION.md](docs/CONFIGURATION.md) | **All config knobs & defaults**: ToolProcessor options, timeouts, retry policy, rate limits, circuit breakers, caching, environment variables |
|
|
533
|
+
| 🚨 [ERRORS.md](docs/ERRORS.md) | **Error taxonomy**: All error codes, exception classes, error details structure, handling patterns, retryability guide |
|
|
534
|
+
| 📊 [OBSERVABILITY.md](docs/OBSERVABILITY.md) | **Metrics & tracing**: OpenTelemetry setup, Prometheus metrics, spans reference, PromQL queries |
|
|
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 |
|
|
536
|
+
| 🎯 [examples/](examples/) | **20+ working examples**: MCP integration, OAuth flows, streaming, production patterns |
|
|
178
537
|
|
|
179
538
|
## Choose Your Path
|
|
180
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
|
+
|
|
181
542
|
| Your Goal | What You Need | Where to Look |
|
|
182
543
|
|-----------|---------------|---------------|
|
|
183
|
-
| ☕ **Just process LLM tool calls** | Basic tool registration + processor | [
|
|
544
|
+
| ☕ **Just process LLM tool calls** | Basic tool registration + processor | [60-Second Quick Start](#60-second-quick-start) |
|
|
184
545
|
| 🔌 **Connect to external tools** | MCP integration (HTTP/STDIO/SSE) | [MCP Integration](#5-mcp-integration-external-tools) |
|
|
185
|
-
| 🛡️ **Production deployment** | Timeouts, retries, rate limits, caching | [
|
|
186
|
-
| 🔒 **Run untrusted code safely** |
|
|
187
|
-
| 📊 **Monitor and observe** | OpenTelemetry + Prometheus | [
|
|
546
|
+
| 🛡️ **Production deployment** | Timeouts, retries, rate limits, caching | [CONFIGURATION.md](docs/CONFIGURATION.md) |
|
|
547
|
+
| 🔒 **Run untrusted code safely** | Isolated strategy (subprocess) | [Isolated Strategy](#using-isolated-strategy) |
|
|
548
|
+
| 📊 **Monitor and observe** | OpenTelemetry + Prometheus | [OBSERVABILITY.md](docs/OBSERVABILITY.md) |
|
|
188
549
|
| 🌊 **Stream incremental results** | StreamingTool pattern | [StreamingTool](#streamingtool-real-time-results) |
|
|
550
|
+
| 🚨 **Handle errors reliably** | Error codes & taxonomy | [ERRORS.md](docs/ERRORS.md) |
|
|
189
551
|
|
|
190
552
|
### Real-World Quick Start
|
|
191
553
|
|
|
@@ -194,8 +556,7 @@ Here are the most common patterns you'll use:
|
|
|
194
556
|
**Pattern 1: Local tools only**
|
|
195
557
|
```python
|
|
196
558
|
import asyncio
|
|
197
|
-
from chuk_tool_processor
|
|
198
|
-
from chuk_tool_processor.registry import initialize, register_tool
|
|
559
|
+
from chuk_tool_processor import ToolProcessor, register_tool, initialize
|
|
199
560
|
|
|
200
561
|
@register_tool(name="my_tool")
|
|
201
562
|
class MyTool:
|
|
@@ -204,20 +565,22 @@ class MyTool:
|
|
|
204
565
|
|
|
205
566
|
async def main():
|
|
206
567
|
await initialize()
|
|
207
|
-
processor = ToolProcessor()
|
|
208
568
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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'}
|
|
212
573
|
|
|
213
574
|
asyncio.run(main())
|
|
214
575
|
```
|
|
215
576
|
|
|
577
|
+
<details>
|
|
578
|
+
<summary><strong>More patterns: MCP integration (local + remote tools)</strong></summary>
|
|
579
|
+
|
|
216
580
|
**Pattern 2: Mix local + remote MCP tools (Notion)**
|
|
217
581
|
```python
|
|
218
582
|
import asyncio
|
|
219
|
-
from chuk_tool_processor
|
|
220
|
-
from chuk_tool_processor.mcp import setup_mcp_http_streamable
|
|
583
|
+
from chuk_tool_processor import register_tool, initialize, setup_mcp_http_streamable
|
|
221
584
|
|
|
222
585
|
@register_tool(name="local_calculator")
|
|
223
586
|
class Calculator:
|
|
@@ -247,10 +610,13 @@ async def main():
|
|
|
247
610
|
print(f"Local result: {results[0].result}")
|
|
248
611
|
print(f"Notion result: {results[1].result}")
|
|
249
612
|
|
|
613
|
+
# Clean up
|
|
614
|
+
await manager.close()
|
|
615
|
+
|
|
250
616
|
asyncio.run(main())
|
|
251
617
|
```
|
|
252
618
|
|
|
253
|
-
See `examples/notion_oauth.py` for complete OAuth flow.
|
|
619
|
+
See `examples/04_mcp_integration/notion_oauth.py` for complete OAuth flow.
|
|
254
620
|
|
|
255
621
|
**Pattern 3: Local SQLite database via STDIO**
|
|
256
622
|
```python
|
|
@@ -289,7 +655,9 @@ async def main():
|
|
|
289
655
|
asyncio.run(main())
|
|
290
656
|
```
|
|
291
657
|
|
|
292
|
-
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>
|
|
293
661
|
|
|
294
662
|
## Core Concepts
|
|
295
663
|
|
|
@@ -302,8 +670,10 @@ The **registry** is where you register tools for execution. Tools can be:
|
|
|
302
670
|
- **StreamingTool** for real-time incremental results
|
|
303
671
|
- **Functions** registered via `register_fn_tool()`
|
|
304
672
|
|
|
673
|
+
> **Note:** The registry is global, processors are scoped.
|
|
674
|
+
|
|
305
675
|
```python
|
|
306
|
-
from chuk_tool_processor
|
|
676
|
+
from chuk_tool_processor import register_tool
|
|
307
677
|
from chuk_tool_processor.models.validated_tool import ValidatedTool
|
|
308
678
|
from pydantic import BaseModel, Field
|
|
309
679
|
|
|
@@ -329,18 +699,16 @@ class WeatherTool(ValidatedTool):
|
|
|
329
699
|
| Strategy | Use Case | Trade-offs |
|
|
330
700
|
|----------|----------|------------|
|
|
331
701
|
| **InProcessStrategy** | Fast, trusted tools | Speed ✅, Isolation ❌ |
|
|
332
|
-
| **
|
|
702
|
+
| **IsolatedStrategy** | Untrusted or risky code | Isolation ✅, Speed ❌ |
|
|
333
703
|
|
|
334
704
|
```python
|
|
335
705
|
import asyncio
|
|
336
|
-
from chuk_tool_processor
|
|
337
|
-
from chuk_tool_processor.execution.strategies.subprocess_strategy import SubprocessStrategy
|
|
338
|
-
from chuk_tool_processor.registry import get_default_registry
|
|
706
|
+
from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
|
|
339
707
|
|
|
340
708
|
async def main():
|
|
341
709
|
registry = await get_default_registry()
|
|
342
710
|
processor = ToolProcessor(
|
|
343
|
-
strategy=
|
|
711
|
+
strategy=IsolatedStrategy(
|
|
344
712
|
registry=registry,
|
|
345
713
|
max_workers=4,
|
|
346
714
|
default_timeout=30.0
|
|
@@ -351,6 +719,8 @@ async def main():
|
|
|
351
719
|
asyncio.run(main())
|
|
352
720
|
```
|
|
353
721
|
|
|
722
|
+
**Note:** `IsolatedStrategy` is an alias of `SubprocessStrategy` for backwards compatibility. Use `IsolatedStrategy` for clarity—it better communicates the security boundary intent.
|
|
723
|
+
|
|
354
724
|
### 3. Execution Wrappers (Middleware)
|
|
355
725
|
|
|
356
726
|
**Wrappers** add production features as composable layers:
|
|
@@ -416,6 +786,8 @@ Connect to **remote tool servers** using the [Model Context Protocol](https://mo
|
|
|
416
786
|
|
|
417
787
|
#### HTTP Streamable (⭐ Recommended for Cloud Services)
|
|
418
788
|
|
|
789
|
+
**Use for:** Cloud SaaS services (OAuth, long-running streams, resilient reconnects)
|
|
790
|
+
|
|
419
791
|
Modern HTTP streaming transport for cloud-based MCP servers like Notion:
|
|
420
792
|
|
|
421
793
|
```python
|
|
@@ -444,8 +816,13 @@ results = await processor.process(
|
|
|
444
816
|
)
|
|
445
817
|
```
|
|
446
818
|
|
|
819
|
+
<details>
|
|
820
|
+
<summary><strong>Other MCP Transports (STDIO for local tools, SSE for legacy)</strong></summary>
|
|
821
|
+
|
|
447
822
|
#### STDIO (Best for Local/On-Device Tools)
|
|
448
823
|
|
|
824
|
+
**Use for:** Local/embedded tools and databases (SQLite, file systems, local services)
|
|
825
|
+
|
|
449
826
|
For running local MCP servers as subprocesses—great for databases, file systems, and local tools:
|
|
450
827
|
|
|
451
828
|
```python
|
|
@@ -484,6 +861,8 @@ results = await processor.process(
|
|
|
484
861
|
|
|
485
862
|
#### SSE (Legacy Support)
|
|
486
863
|
|
|
864
|
+
**Use for:** Legacy compatibility only. Prefer HTTP Streamable for new integrations.
|
|
865
|
+
|
|
487
866
|
For backward compatibility with older MCP servers using Server-Sent Events:
|
|
488
867
|
|
|
489
868
|
```python
|
|
@@ -505,6 +884,8 @@ processor, manager = await setup_mcp_sse(
|
|
|
505
884
|
)
|
|
506
885
|
```
|
|
507
886
|
|
|
887
|
+
</details>
|
|
888
|
+
|
|
508
889
|
**Transport Comparison:**
|
|
509
890
|
|
|
510
891
|
| Transport | Use Case | Real Examples |
|
|
@@ -513,6 +894,18 @@ processor, manager = await setup_mcp_sse(
|
|
|
513
894
|
| **STDIO** | Local tools, databases | SQLite (`mcp-server-sqlite`), Echo (`chuk-mcp-echo`) |
|
|
514
895
|
| **SSE** | Legacy cloud services | Atlassian (`mcp.atlassian.com`) |
|
|
515
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
|
+
|
|
516
909
|
**Relationship with [chuk-mcp](https://github.com/chrishayuk/chuk-mcp):**
|
|
517
910
|
- `chuk-mcp` is a low-level MCP protocol client (handles transports, protocol negotiation)
|
|
518
911
|
- `chuk-tool-processor` wraps `chuk-mcp` to integrate external tools into your execution pipeline
|
|
@@ -526,7 +919,7 @@ CHUK Tool Processor supports multiple patterns for defining tools:
|
|
|
526
919
|
|
|
527
920
|
#### Simple Function-Based Tools
|
|
528
921
|
```python
|
|
529
|
-
from chuk_tool_processor
|
|
922
|
+
from chuk_tool_processor import register_fn_tool
|
|
530
923
|
from datetime import datetime
|
|
531
924
|
from zoneinfo import ZoneInfo
|
|
532
925
|
|
|
@@ -584,17 +977,26 @@ class FileProcessor(StreamingTool):
|
|
|
584
977
|
|
|
585
978
|
```python
|
|
586
979
|
import asyncio
|
|
587
|
-
from chuk_tool_processor
|
|
588
|
-
from chuk_tool_processor.registry import initialize
|
|
980
|
+
from chuk_tool_processor import ToolProcessor, initialize
|
|
589
981
|
|
|
590
982
|
async def main():
|
|
591
983
|
await initialize()
|
|
592
984
|
processor = ToolProcessor()
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
|
598
1000
|
|
|
599
1001
|
asyncio.run(main())
|
|
600
1002
|
```
|
|
@@ -603,23 +1005,32 @@ asyncio.run(main())
|
|
|
603
1005
|
|
|
604
1006
|
#### Basic Usage
|
|
605
1007
|
|
|
606
|
-
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:
|
|
607
1009
|
|
|
608
1010
|
```python
|
|
609
1011
|
import asyncio
|
|
610
|
-
from chuk_tool_processor
|
|
611
|
-
from chuk_tool_processor.registry import initialize
|
|
1012
|
+
from chuk_tool_processor import ToolProcessor, initialize
|
|
612
1013
|
|
|
613
1014
|
async def main():
|
|
614
1015
|
await initialize()
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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!
|
|
623
1034
|
|
|
624
1035
|
asyncio.run(main())
|
|
625
1036
|
```
|
|
@@ -627,21 +1038,32 @@ asyncio.run(main())
|
|
|
627
1038
|
#### Production Configuration
|
|
628
1039
|
|
|
629
1040
|
```python
|
|
630
|
-
from chuk_tool_processor
|
|
1041
|
+
from chuk_tool_processor import ToolProcessor, initialize
|
|
1042
|
+
import asyncio
|
|
631
1043
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
default_timeout=30.0,
|
|
635
|
-
max_concurrency=20,
|
|
1044
|
+
async def main():
|
|
1045
|
+
await initialize()
|
|
636
1046
|
|
|
637
|
-
#
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
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())
|
|
645
1067
|
```
|
|
646
1068
|
|
|
647
1069
|
### Advanced Production Features
|
|
@@ -653,7 +1075,7 @@ Beyond basic configuration, CHUK Tool Processor includes several advanced featur
|
|
|
653
1075
|
Prevent cascading failures by automatically opening circuits for failing tools:
|
|
654
1076
|
|
|
655
1077
|
```python
|
|
656
|
-
from chuk_tool_processor
|
|
1078
|
+
from chuk_tool_processor import ToolProcessor
|
|
657
1079
|
|
|
658
1080
|
processor = ToolProcessor(
|
|
659
1081
|
enable_circuit_breaker=True,
|
|
@@ -695,8 +1117,8 @@ assert call1.idempotency_key == call2.idempotency_key
|
|
|
695
1117
|
|
|
696
1118
|
# Used automatically by caching layer
|
|
697
1119
|
processor = ToolProcessor(enable_caching=True)
|
|
698
|
-
results1 = await processor.
|
|
699
|
-
results2 = await processor.
|
|
1120
|
+
results1 = await processor.process([call1]) # Executes
|
|
1121
|
+
results2 = await processor.process([call2]) # Cache hit!
|
|
700
1122
|
```
|
|
701
1123
|
|
|
702
1124
|
**Benefits:**
|
|
@@ -704,6 +1126,8 @@ results2 = await processor.execute([call2]) # Cache hit!
|
|
|
704
1126
|
- Deterministic cache keys
|
|
705
1127
|
- No manual key management needed
|
|
706
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
|
+
|
|
707
1131
|
#### Tool Schema Export
|
|
708
1132
|
|
|
709
1133
|
Export tool definitions to multiple formats for LLM prompting:
|
|
@@ -750,7 +1174,9 @@ mcp_format = spec.to_mcp() # For MCP servers
|
|
|
750
1174
|
|
|
751
1175
|
#### Machine-Readable Error Codes
|
|
752
1176
|
|
|
753
|
-
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:
|
|
754
1180
|
|
|
755
1181
|
```python
|
|
756
1182
|
from chuk_tool_processor.core.exceptions import (
|
|
@@ -832,22 +1258,20 @@ result = await tool.execute(**llm_output)
|
|
|
832
1258
|
|
|
833
1259
|
## Advanced Topics
|
|
834
1260
|
|
|
835
|
-
### Using
|
|
1261
|
+
### Using Isolated Strategy
|
|
836
1262
|
|
|
837
|
-
Use `
|
|
1263
|
+
Use `IsolatedStrategy` when running untrusted, third-party, or potentially unsafe code that shouldn't share the same process as your main app.
|
|
838
1264
|
|
|
839
1265
|
For isolation and safety when running untrusted code:
|
|
840
1266
|
|
|
841
1267
|
```python
|
|
842
1268
|
import asyncio
|
|
843
|
-
from chuk_tool_processor
|
|
844
|
-
from chuk_tool_processor.execution.strategies.subprocess_strategy import SubprocessStrategy
|
|
845
|
-
from chuk_tool_processor.registry import get_default_registry
|
|
1269
|
+
from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
|
|
846
1270
|
|
|
847
1271
|
async def main():
|
|
848
1272
|
registry = await get_default_registry()
|
|
849
1273
|
processor = ToolProcessor(
|
|
850
|
-
strategy=
|
|
1274
|
+
strategy=IsolatedStrategy(
|
|
851
1275
|
registry=registry,
|
|
852
1276
|
max_workers=4,
|
|
853
1277
|
default_timeout=30.0
|
|
@@ -858,6 +1282,10 @@ async def main():
|
|
|
858
1282
|
asyncio.run(main())
|
|
859
1283
|
```
|
|
860
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
|
+
|
|
861
1289
|
### Real-World MCP Examples
|
|
862
1290
|
|
|
863
1291
|
#### Example 1: Notion Integration with OAuth
|
|
@@ -867,7 +1295,7 @@ Complete OAuth flow connecting to Notion's MCP server:
|
|
|
867
1295
|
```python
|
|
868
1296
|
from chuk_tool_processor.mcp import setup_mcp_http_streamable
|
|
869
1297
|
|
|
870
|
-
# 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)
|
|
871
1299
|
processor, manager = await setup_mcp_http_streamable(
|
|
872
1300
|
servers=[{
|
|
873
1301
|
"name": "notion",
|
|
@@ -888,6 +1316,9 @@ results = await processor.process(
|
|
|
888
1316
|
)
|
|
889
1317
|
```
|
|
890
1318
|
|
|
1319
|
+
<details>
|
|
1320
|
+
<summary><strong>Click to expand more MCP examples (SQLite, Echo Server)</strong></summary>
|
|
1321
|
+
|
|
891
1322
|
#### Example 2: Local SQLite Database Access
|
|
892
1323
|
|
|
893
1324
|
Run SQLite MCP server locally for database operations:
|
|
@@ -959,10 +1390,15 @@ results = await processor.process(
|
|
|
959
1390
|
)
|
|
960
1391
|
```
|
|
961
1392
|
|
|
962
|
-
|
|
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.
|
|
963
1396
|
|
|
964
1397
|
#### OAuth Token Refresh
|
|
965
1398
|
|
|
1399
|
+
<details>
|
|
1400
|
+
<summary><strong>Click to expand OAuth token refresh guide</strong></summary>
|
|
1401
|
+
|
|
966
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.
|
|
967
1403
|
|
|
968
1404
|
**How it works:**
|
|
@@ -1031,7 +1467,9 @@ processor, manager = await setup_mcp_sse(
|
|
|
1031
1467
|
- Token refresh is attempted only once per tool call (no infinite retry loops)
|
|
1032
1468
|
- After successful refresh, the updated headers are used for all subsequent calls
|
|
1033
1469
|
|
|
1034
|
-
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>
|
|
1035
1473
|
|
|
1036
1474
|
### Observability
|
|
1037
1475
|
|
|
@@ -1100,24 +1538,32 @@ asyncio.run(main())
|
|
|
1100
1538
|
|
|
1101
1539
|
#### OpenTelemetry & Prometheus (Drop-in Observability)
|
|
1102
1540
|
|
|
1103
|
-
|
|
1541
|
+
<details>
|
|
1542
|
+
<summary><strong>Click to expand complete observability guide</strong></summary>
|
|
1104
1543
|
|
|
1105
|
-
**
|
|
1544
|
+
**3-Line Setup:**
|
|
1106
1545
|
|
|
1107
1546
|
```python
|
|
1108
1547
|
from chuk_tool_processor.observability import setup_observability
|
|
1109
1548
|
|
|
1110
|
-
# Enable everything
|
|
1111
1549
|
setup_observability(
|
|
1112
1550
|
service_name="my-tool-service",
|
|
1113
|
-
enable_tracing=True,
|
|
1114
|
-
enable_metrics=True,
|
|
1115
|
-
metrics_port=9090
|
|
1551
|
+
enable_tracing=True, # → OpenTelemetry traces
|
|
1552
|
+
enable_metrics=True, # → Prometheus metrics at :9090/metrics
|
|
1553
|
+
metrics_port=9090
|
|
1116
1554
|
)
|
|
1117
|
-
|
|
1118
|
-
# Every tool execution is now automatically traced and metered!
|
|
1555
|
+
# That's it! Every tool execution is now automatically traced and metered.
|
|
1119
1556
|
```
|
|
1120
1557
|
|
|
1558
|
+
**What you get automatically:**
|
|
1559
|
+
- ✅ Distributed traces (Jaeger, Zipkin, any OTLP collector)
|
|
1560
|
+
- ✅ Prometheus metrics (error rate, latency P50/P95/P99, cache hit rate)
|
|
1561
|
+
- ✅ Circuit breaker state monitoring
|
|
1562
|
+
- ✅ Retry attempt tracking
|
|
1563
|
+
- ✅ Zero code changes to your tools
|
|
1564
|
+
|
|
1565
|
+
**Why Telemetry Matters**: In production, you need to know *what* your tools are doing, *how long* they take, *when* they fail, and *why*. CHUK Tool Processor provides **enterprise-grade telemetry** that operations teams expect—with zero manual instrumentation.
|
|
1566
|
+
|
|
1121
1567
|
**What You Get (Automatically)**
|
|
1122
1568
|
|
|
1123
1569
|
✅ **Distributed Traces** - Understand exactly what happened in each tool call
|
|
@@ -1151,13 +1597,14 @@ pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp prom
|
|
|
1151
1597
|
uv pip install chuk-tool-processor --group observability
|
|
1152
1598
|
```
|
|
1153
1599
|
|
|
1600
|
+
> **⚠️ SRE Note**: Observability packages are **optional**. If not installed, all observability calls are no-ops—your tools run normally without tracing/metrics. Zero crashes, zero warnings. Safe to deploy without observability dependencies.
|
|
1601
|
+
|
|
1154
1602
|
**Quick Start: See Your Tools in Action**
|
|
1155
1603
|
|
|
1156
1604
|
```python
|
|
1157
1605
|
import asyncio
|
|
1158
1606
|
from chuk_tool_processor.observability import setup_observability
|
|
1159
|
-
from chuk_tool_processor
|
|
1160
|
-
from chuk_tool_processor.registry import initialize, register_tool
|
|
1607
|
+
from chuk_tool_processor import ToolProcessor, initialize, register_tool
|
|
1161
1608
|
|
|
1162
1609
|
@register_tool(name="weather_api")
|
|
1163
1610
|
class WeatherTool:
|
|
@@ -1375,7 +1822,7 @@ export OTEL_EXPORTER_OTLP_ENDPOINT=http://datadog-agent:4317
|
|
|
1375
1822
|
- Testing observability features
|
|
1376
1823
|
- Environment variable configuration
|
|
1377
1824
|
|
|
1378
|
-
🎯 **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
|
|
1379
1826
|
|
|
1380
1827
|
**Benefits**
|
|
1381
1828
|
|
|
@@ -1386,6 +1833,8 @@ export OTEL_EXPORTER_OTLP_ENDPOINT=http://datadog-agent:4317
|
|
|
1386
1833
|
✅ **Optional** - Gracefully degrades if packages not installed
|
|
1387
1834
|
✅ **Zero-overhead** - No performance impact when disabled
|
|
1388
1835
|
|
|
1836
|
+
</details>
|
|
1837
|
+
|
|
1389
1838
|
### Error Handling
|
|
1390
1839
|
|
|
1391
1840
|
```python
|
|
@@ -1403,8 +1852,7 @@ for result in results:
|
|
|
1403
1852
|
|
|
1404
1853
|
```python
|
|
1405
1854
|
import pytest
|
|
1406
|
-
from chuk_tool_processor
|
|
1407
|
-
from chuk_tool_processor.registry import initialize
|
|
1855
|
+
from chuk_tool_processor import ToolProcessor, initialize
|
|
1408
1856
|
|
|
1409
1857
|
@pytest.mark.asyncio
|
|
1410
1858
|
async def test_calculator():
|
|
@@ -1418,6 +1866,40 @@ async def test_calculator():
|
|
|
1418
1866
|
assert results[0].result["result"] == 8
|
|
1419
1867
|
```
|
|
1420
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
|
+
|
|
1421
1903
|
## Configuration
|
|
1422
1904
|
|
|
1423
1905
|
### Timeout Configuration
|
|
@@ -1428,6 +1910,7 @@ CHUK Tool Processor uses a unified timeout configuration system that applies to
|
|
|
1428
1910
|
from chuk_tool_processor.mcp.transport import TimeoutConfig
|
|
1429
1911
|
|
|
1430
1912
|
# Create custom timeout configuration
|
|
1913
|
+
# (Defaults are: connect=30, operation=30, quick=5, shutdown=2)
|
|
1431
1914
|
timeout_config = TimeoutConfig(
|
|
1432
1915
|
connect=30.0, # Connection establishment, initialization, session discovery
|
|
1433
1916
|
operation=30.0, # Normal operations (tool calls, listing tools/resources/prompts)
|
|
@@ -1555,7 +2038,7 @@ CHUK Tool Processor provides multiple layers of safety:
|
|
|
1555
2038
|
| Concern | Protection | Configuration |
|
|
1556
2039
|
|---------|------------|---------------|
|
|
1557
2040
|
| **Timeouts** | Every tool has a timeout | `default_timeout=30.0` |
|
|
1558
|
-
| **Process Isolation** | Run tools in separate processes | `strategy=
|
|
2041
|
+
| **Process Isolation** | Run tools in separate processes | `strategy=IsolatedStrategy()` |
|
|
1559
2042
|
| **Rate Limiting** | Prevent abuse and API overuse | `enable_rate_limiting=True` |
|
|
1560
2043
|
| **Input Validation** | Pydantic validation on arguments | Use `ValidatedTool` |
|
|
1561
2044
|
| **Error Containment** | Failures don't crash the processor | Built-in exception handling |
|
|
@@ -1567,13 +2050,58 @@ CHUK Tool Processor provides multiple layers of safety:
|
|
|
1567
2050
|
- **Resource Limits**: For hard CPU/memory caps, use OS-level controls (cgroups on Linux, Job Objects on Windows, or Docker resource limits).
|
|
1568
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.
|
|
1569
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
|
+
|
|
1570
2100
|
Example security-focused setup for untrusted code:
|
|
1571
2101
|
|
|
1572
2102
|
```python
|
|
1573
2103
|
import asyncio
|
|
1574
|
-
from chuk_tool_processor
|
|
1575
|
-
from chuk_tool_processor.execution.strategies.subprocess_strategy import SubprocessStrategy
|
|
1576
|
-
from chuk_tool_processor.registry import get_default_registry
|
|
2104
|
+
from chuk_tool_processor import ToolProcessor, IsolatedStrategy, get_default_registry
|
|
1577
2105
|
|
|
1578
2106
|
async def create_secure_processor():
|
|
1579
2107
|
# Maximum isolation for untrusted code
|
|
@@ -1581,7 +2109,7 @@ async def create_secure_processor():
|
|
|
1581
2109
|
registry = await get_default_registry()
|
|
1582
2110
|
|
|
1583
2111
|
processor = ToolProcessor(
|
|
1584
|
-
strategy=
|
|
2112
|
+
strategy=IsolatedStrategy(
|
|
1585
2113
|
registry=registry,
|
|
1586
2114
|
max_workers=4,
|
|
1587
2115
|
default_timeout=10.0
|
|
@@ -1599,6 +2127,25 @@ async def create_secure_processor():
|
|
|
1599
2127
|
# - Use read-only filesystems where possible
|
|
1600
2128
|
```
|
|
1601
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
|
+
|
|
1602
2149
|
## Architecture Principles
|
|
1603
2150
|
|
|
1604
2151
|
1. **Composability**: Stack strategies and wrappers like middleware
|
|
@@ -1612,26 +2159,26 @@ async def create_secure_processor():
|
|
|
1612
2159
|
Check out the [`examples/`](examples/) directory for complete working examples:
|
|
1613
2160
|
|
|
1614
2161
|
### Getting Started
|
|
1615
|
-
- **
|
|
1616
|
-
- **
|
|
1617
|
-
- **
|
|
1618
|
-
- **
|
|
1619
|
-
- **
|
|
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
|
|
1620
2170
|
|
|
1621
2171
|
### MCP Integration (Real-World)
|
|
1622
|
-
- **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
|
|
1623
2173
|
- Shows: Authorization Server discovery, client registration, PKCE flow, token exchange
|
|
1624
|
-
- **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
|
|
1625
2175
|
- Shows: Command/args passing, environment variables, file paths, initialization timeouts
|
|
1626
|
-
- **Echo Server**: `examples/stdio_echo.py` - Minimal STDIO transport example
|
|
2176
|
+
- **Echo Server**: `examples/04_mcp_integration/stdio_echo.py` - Minimal STDIO transport example
|
|
1627
2177
|
- Shows: Simplest possible MCP integration for testing
|
|
1628
|
-
- **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)
|
|
1629
2179
|
|
|
1630
2180
|
### Advanced MCP
|
|
1631
|
-
- **
|
|
1632
|
-
- **STDIO**: `examples/mcp_stdio_example.py`
|
|
1633
|
-
- **SSE**: `examples/mcp_sse_example.py`
|
|
1634
|
-
- **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`
|
|
1635
2182
|
|
|
1636
2183
|
## FAQ
|
|
1637
2184
|
|
|
@@ -1656,18 +2203,20 @@ A: Use pytest with `@pytest.mark.asyncio`. See [Testing Tools](#testing-tools) f
|
|
|
1656
2203
|
**Q: Does this work with streaming LLM responses?**
|
|
1657
2204
|
A: Yes—as tool calls appear in the stream, extract and process them. The processor handles partial/incremental tool call lists.
|
|
1658
2205
|
|
|
1659
|
-
**Q: What's the difference between InProcess and
|
|
1660
|
-
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.
|
|
1661
2208
|
|
|
1662
2209
|
## Comparison with Other Tools
|
|
1663
2210
|
|
|
1664
2211
|
| Feature | chuk-tool-processor | LangChain Tools | OpenAI Tools | MCP SDK |
|
|
1665
2212
|
|---------|-------------------|-----------------|--------------|---------|
|
|
1666
2213
|
| **Async-native** | ✅ | ⚠️ Partial | ✅ | ✅ |
|
|
1667
|
-
| **Process isolation** | ✅
|
|
2214
|
+
| **Process isolation** | ✅ IsolatedStrategy | ❌ | ❌ | ⚠️ |
|
|
1668
2215
|
| **Built-in retries** | ✅ | ❌ † | ❌ | ❌ |
|
|
1669
2216
|
| **Rate limiting** | ✅ | ❌ † | ⚠️ ‡ | ❌ |
|
|
1670
2217
|
| **Caching** | ✅ | ⚠️ † | ❌ ‡ | ❌ |
|
|
2218
|
+
| **Idempotency & de-dup** | ✅ SHA256 keys | ❌ | ❌ | ❌ |
|
|
2219
|
+
| **Per-tool policies** | ✅ (timeouts/retries/limits) | ⚠️ | ❌ | ❌ |
|
|
1671
2220
|
| **Multiple parsers** | ✅ (XML, OpenAI, JSON) | ⚠️ | ✅ | ✅ |
|
|
1672
2221
|
| **Streaming tools** | ✅ | ⚠️ | ⚠️ | ✅ |
|
|
1673
2222
|
| **MCP integration** | ✅ All transports | ❌ | ❌ | ✅ (protocol only) |
|
|
@@ -1696,6 +2245,73 @@ A: InProcess is faster (same process), Subprocess is safer (isolated process). U
|
|
|
1696
2245
|
- Use directly if you need protocol-level control
|
|
1697
2246
|
- Use chuk-tool-processor if you want high-level tool execution
|
|
1698
2247
|
|
|
2248
|
+
## Development & Publishing
|
|
2249
|
+
|
|
2250
|
+
### For Contributors
|
|
2251
|
+
|
|
2252
|
+
Development setup:
|
|
2253
|
+
|
|
2254
|
+
```bash
|
|
2255
|
+
# Clone repository
|
|
2256
|
+
git clone https://github.com/chrishayuk/chuk-tool-processor.git
|
|
2257
|
+
cd chuk-tool-processor
|
|
2258
|
+
|
|
2259
|
+
# Install development dependencies
|
|
2260
|
+
uv sync --dev
|
|
2261
|
+
|
|
2262
|
+
# Run tests
|
|
2263
|
+
make test
|
|
2264
|
+
|
|
2265
|
+
# Run all quality checks
|
|
2266
|
+
make check
|
|
2267
|
+
```
|
|
2268
|
+
|
|
2269
|
+
### For Maintainers: Publishing Releases
|
|
2270
|
+
|
|
2271
|
+
The project uses **fully automated CI/CD** for releases. Publishing is as simple as:
|
|
2272
|
+
|
|
2273
|
+
```bash
|
|
2274
|
+
# 1. Bump version
|
|
2275
|
+
make bump-patch # or bump-minor, bump-major
|
|
2276
|
+
|
|
2277
|
+
# 2. Commit version change
|
|
2278
|
+
git add pyproject.toml
|
|
2279
|
+
git commit -m "version X.Y.Z"
|
|
2280
|
+
git push
|
|
2281
|
+
|
|
2282
|
+
# 3. Create release (automated)
|
|
2283
|
+
make publish
|
|
2284
|
+
```
|
|
2285
|
+
|
|
2286
|
+
This will:
|
|
2287
|
+
- Create and push a git tag
|
|
2288
|
+
- Trigger GitHub Actions to create a release with auto-generated changelog
|
|
2289
|
+
- Run tests across all platforms and Python versions
|
|
2290
|
+
- Build and publish to PyPI automatically
|
|
2291
|
+
|
|
2292
|
+
For detailed release documentation, see:
|
|
2293
|
+
- **[RELEASING.md](RELEASING.md)** - Complete release process guide
|
|
2294
|
+
- **[docs/CI-CD.md](docs/CI-CD.md)** - Full CI/CD pipeline documentation
|
|
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
|
+
|
|
1699
2315
|
## Contributing & Support
|
|
1700
2316
|
|
|
1701
2317
|
- **GitHub**: [chrishayuk/chuk-tool-processor](https://github.com/chrishayuk/chuk-tool-processor)
|