chuk-tool-processor 0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of chuk-tool-processor might be problematic. Click here for more details.
- chuk_tool_processor-0.4/PKG-INFO +831 -0
- chuk_tool_processor-0.4/README.md +819 -0
- chuk_tool_processor-0.4/pyproject.toml +40 -0
- chuk_tool_processor-0.4/setup.cfg +4 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/__init__.py +0 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/core/__init__.py +1 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/core/exceptions.py +45 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/core/processor.py +488 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/execution/__init__.py +0 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/execution/strategies/__init__.py +0 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/execution/strategies/inprocess_strategy.py +619 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/execution/strategies/subprocess_strategy.py +598 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/execution/tool_executor.py +342 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/execution/wrappers/__init__.py +0 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/execution/wrappers/caching.py +576 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/execution/wrappers/rate_limiting.py +262 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/execution/wrappers/retry.py +286 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/logging/__init__.py +106 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/logging/context.py +243 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/logging/formatter.py +98 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/logging/helpers.py +187 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/logging/metrics.py +128 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/mcp/__init__.py +21 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/mcp/mcp_tool.py +135 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/mcp/register_mcp_tools.py +100 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/mcp/setup_mcp_sse.py +99 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +80 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/mcp/stream_manager.py +409 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/mcp/transport/__init__.py +14 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/mcp/transport/base_transport.py +103 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/mcp/transport/sse_transport.py +508 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/mcp/transport/stdio_transport.py +197 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/models/__init__.py +1 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/models/execution_strategy.py +68 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/models/streaming_tool.py +110 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/models/tool_call.py +59 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/models/tool_export_mixin.py +29 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/models/tool_result.py +155 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/models/validated_tool.py +157 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/plugins/__init__.py +1 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/plugins/discovery.py +183 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/plugins/parsers/__init__.py +1 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/plugins/parsers/base.py +26 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/plugins/parsers/function_call_tool.py +100 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/plugins/parsers/json_tool.py +50 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/plugins/parsers/openai_tool.py +88 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/plugins/parsers/xml_tool.py +99 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/registry/__init__.py +60 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/registry/auto_register.py +189 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/registry/decorators.py +165 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/registry/interface.py +113 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/registry/metadata.py +82 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/registry/provider.py +138 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/registry/providers/__init__.py +80 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/registry/providers/memory.py +141 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/registry/tool_export.py +245 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/utils/__init__.py +0 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor/utils/validation.py +126 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor.egg-info/PKG-INFO +831 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor.egg-info/SOURCES.txt +61 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor.egg-info/dependency_links.txt +1 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor.egg-info/requires.txt +5 -0
- chuk_tool_processor-0.4/src/chuk_tool_processor.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chuk-tool-processor
|
|
3
|
+
Version: 0.4
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: chuk-mcp>=0.1.12
|
|
8
|
+
Requires-Dist: dotenv>=0.9.9
|
|
9
|
+
Requires-Dist: openai>=1.76.0
|
|
10
|
+
Requires-Dist: pydantic>=2.11.3
|
|
11
|
+
Requires-Dist: uuid>=1.30
|
|
12
|
+
|
|
13
|
+
# CHUK Tool Processor
|
|
14
|
+
|
|
15
|
+
An async-native framework for registering, discovering, and executing tools referenced in LLM responses. Built from the ground up for production use with comprehensive error handling, monitoring, and scalability features.
|
|
16
|
+
|
|
17
|
+
[](https://www.python.org/downloads/)
|
|
18
|
+
[](https://docs.python.org/3/library/asyncio.html)
|
|
19
|
+
[](https://opensource.org/licenses/MIT)
|
|
20
|
+
|
|
21
|
+
## 🚀 Quick Start
|
|
22
|
+
|
|
23
|
+
### Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# From source (recommended for development)
|
|
27
|
+
git clone https://github.com/chrishayuk/chuk-tool-processor.git
|
|
28
|
+
cd chuk-tool-processor
|
|
29
|
+
pip install -e .
|
|
30
|
+
|
|
31
|
+
# Or install from PyPI (when available)
|
|
32
|
+
pip install chuk-tool-processor
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Your First Tool in 60 Seconds
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import asyncio
|
|
39
|
+
from chuk_tool_processor import ToolProcessor, register_tool, initialize
|
|
40
|
+
|
|
41
|
+
# 1. Create a tool
|
|
42
|
+
@register_tool(name="calculator", description="Perform basic math operations")
|
|
43
|
+
class Calculator:
|
|
44
|
+
async def execute(self, operation: str, a: float, b: float) -> dict:
|
|
45
|
+
operations = {
|
|
46
|
+
"add": a + b,
|
|
47
|
+
"subtract": a - b,
|
|
48
|
+
"multiply": a * b,
|
|
49
|
+
"divide": a / b if b != 0 else None
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if operation not in operations:
|
|
53
|
+
raise ValueError(f"Unknown operation: {operation}")
|
|
54
|
+
|
|
55
|
+
result = operations[operation]
|
|
56
|
+
if result is None:
|
|
57
|
+
raise ValueError("Cannot divide by zero")
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
"operation": operation,
|
|
61
|
+
"operands": [a, b],
|
|
62
|
+
"result": result
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async def main():
|
|
66
|
+
# 2. Initialize the system
|
|
67
|
+
await initialize()
|
|
68
|
+
|
|
69
|
+
# 3. Create processor
|
|
70
|
+
processor = ToolProcessor()
|
|
71
|
+
|
|
72
|
+
# 4. Process LLM output containing tool calls
|
|
73
|
+
llm_response = '''
|
|
74
|
+
I'll calculate 15 * 23 for you.
|
|
75
|
+
|
|
76
|
+
<tool name="calculator" args='{"operation": "multiply", "a": 15, "b": 23}'/>
|
|
77
|
+
|
|
78
|
+
The result is 345.
|
|
79
|
+
'''
|
|
80
|
+
|
|
81
|
+
# 5. Execute the tools
|
|
82
|
+
results = await processor.process(llm_response)
|
|
83
|
+
|
|
84
|
+
# 6. Handle results
|
|
85
|
+
for result in results:
|
|
86
|
+
if result.error:
|
|
87
|
+
print(f"❌ Tool '{result.tool}' failed: {result.error}")
|
|
88
|
+
else:
|
|
89
|
+
print(f"✅ Tool '{result.tool}' result: {result.result}")
|
|
90
|
+
print(f"⏱️ Executed in {result.duration:.3f}s")
|
|
91
|
+
|
|
92
|
+
if __name__ == "__main__":
|
|
93
|
+
asyncio.run(main())
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Output:**
|
|
97
|
+
```
|
|
98
|
+
✅ Tool 'calculator' result: {'operation': 'multiply', 'operands': [15, 23], 'result': 345}
|
|
99
|
+
⏱️ Executed in 0.001s
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## 🎯 Key Features
|
|
103
|
+
|
|
104
|
+
- **🔄 Async-Native**: Built for `async/await` from the ground up
|
|
105
|
+
- **🛡️ Production Ready**: Comprehensive error handling, timeouts, retries
|
|
106
|
+
- **📦 Multiple Execution**: In-process and subprocess strategies
|
|
107
|
+
- **🚀 High Performance**: Caching, rate limiting, and concurrency control
|
|
108
|
+
- **📊 Monitoring**: Structured logging and metrics collection
|
|
109
|
+
- **🔗 MCP Integration**: Full Model Context Protocol support
|
|
110
|
+
- **📡 Streaming**: Real-time incremental results
|
|
111
|
+
- **🔧 Extensible**: Plugin system for custom parsers and strategies
|
|
112
|
+
|
|
113
|
+
## 📖 Getting Started Guide
|
|
114
|
+
|
|
115
|
+
### Environment Setup
|
|
116
|
+
|
|
117
|
+
Create a `.env` file or set environment variables:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Optional: Registry provider (default: memory)
|
|
121
|
+
export CHUK_TOOL_REGISTRY_PROVIDER=memory
|
|
122
|
+
|
|
123
|
+
# Optional: Default timeout for tool execution (default: 30.0)
|
|
124
|
+
export CHUK_DEFAULT_TIMEOUT=30.0
|
|
125
|
+
|
|
126
|
+
# Optional: Logging level (default: INFO)
|
|
127
|
+
export CHUK_LOG_LEVEL=INFO
|
|
128
|
+
|
|
129
|
+
# Optional: Enable structured JSON logging (default: true)
|
|
130
|
+
export CHUK_STRUCTURED_LOGGING=true
|
|
131
|
+
|
|
132
|
+
# MCP Integration (if using MCP servers)
|
|
133
|
+
export MCP_BEARER_TOKEN=your_bearer_token_here
|
|
134
|
+
export MCP_CONFIG_FILE=/path/to/mcp_config.json
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Basic Tool Development
|
|
138
|
+
|
|
139
|
+
#### 1. Simple Function-Based Tool
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from chuk_tool_processor.registry.auto_register import register_fn_tool
|
|
143
|
+
|
|
144
|
+
async def get_current_time(timezone: str = "UTC") -> str:
|
|
145
|
+
"""Get the current time in the specified timezone."""
|
|
146
|
+
from datetime import datetime
|
|
147
|
+
import pytz
|
|
148
|
+
|
|
149
|
+
tz = pytz.timezone(timezone)
|
|
150
|
+
current_time = datetime.now(tz)
|
|
151
|
+
return current_time.strftime("%Y-%m-%d %H:%M:%S %Z")
|
|
152
|
+
|
|
153
|
+
# Register the function as a tool
|
|
154
|
+
await register_fn_tool(get_current_time, namespace="utilities")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
#### 2. Class-Based Tool with Validation
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
from chuk_tool_processor.models.validated_tool import ValidatedTool
|
|
161
|
+
from pydantic import BaseModel, Field
|
|
162
|
+
|
|
163
|
+
@register_tool(name="weather", namespace="api")
|
|
164
|
+
class WeatherTool(ValidatedTool):
|
|
165
|
+
"""Get weather information for a location."""
|
|
166
|
+
|
|
167
|
+
class Arguments(BaseModel):
|
|
168
|
+
location: str = Field(..., description="City name or coordinates")
|
|
169
|
+
units: str = Field("metric", description="Temperature units: metric, imperial, kelvin")
|
|
170
|
+
include_forecast: bool = Field(False, description="Include 5-day forecast")
|
|
171
|
+
|
|
172
|
+
class Result(BaseModel):
|
|
173
|
+
location: str
|
|
174
|
+
temperature: float
|
|
175
|
+
conditions: str
|
|
176
|
+
humidity: int
|
|
177
|
+
forecast: list[dict] = Field(default_factory=list)
|
|
178
|
+
|
|
179
|
+
async def _execute(self, location: str, units: str, include_forecast: bool) -> Result:
|
|
180
|
+
# Simulate API call
|
|
181
|
+
await asyncio.sleep(0.1)
|
|
182
|
+
|
|
183
|
+
return self.Result(
|
|
184
|
+
location=location,
|
|
185
|
+
temperature=22.5,
|
|
186
|
+
conditions="Partly cloudy",
|
|
187
|
+
humidity=65,
|
|
188
|
+
forecast=[{"day": "Tomorrow", "temp": 24, "conditions": "Sunny"}] if include_forecast else []
|
|
189
|
+
)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
#### 3. Streaming Tool
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
from chuk_tool_processor.models.streaming_tool import StreamingTool
|
|
196
|
+
import asyncio
|
|
197
|
+
|
|
198
|
+
@register_tool(name="file_processor")
|
|
199
|
+
class FileProcessorTool(StreamingTool):
|
|
200
|
+
"""Process a large file line by line."""
|
|
201
|
+
|
|
202
|
+
class Arguments(BaseModel):
|
|
203
|
+
file_path: str
|
|
204
|
+
operation: str = "count_lines"
|
|
205
|
+
|
|
206
|
+
class Result(BaseModel):
|
|
207
|
+
line_number: int
|
|
208
|
+
content: str
|
|
209
|
+
processed_at: str
|
|
210
|
+
|
|
211
|
+
async def _stream_execute(self, file_path: str, operation: str):
|
|
212
|
+
"""Stream results as each line is processed."""
|
|
213
|
+
from datetime import datetime
|
|
214
|
+
|
|
215
|
+
# Simulate processing a large file
|
|
216
|
+
total_lines = 100
|
|
217
|
+
|
|
218
|
+
for i in range(1, total_lines + 1):
|
|
219
|
+
await asyncio.sleep(0.01) # Simulate processing time
|
|
220
|
+
|
|
221
|
+
yield self.Result(
|
|
222
|
+
line_number=i,
|
|
223
|
+
content=f"Processed line {i}",
|
|
224
|
+
processed_at=datetime.now().isoformat()
|
|
225
|
+
)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### Advanced Configuration
|
|
229
|
+
|
|
230
|
+
#### Production-Ready Processor Setup
|
|
231
|
+
|
|
232
|
+
```python
|
|
233
|
+
from chuk_tool_processor import ToolProcessor
|
|
234
|
+
from chuk_tool_processor.execution.strategies.subprocess_strategy import SubprocessStrategy
|
|
235
|
+
|
|
236
|
+
async def create_production_processor():
|
|
237
|
+
"""Create a production-ready processor with all features enabled."""
|
|
238
|
+
|
|
239
|
+
processor = ToolProcessor(
|
|
240
|
+
# Execution settings
|
|
241
|
+
default_timeout=30.0,
|
|
242
|
+
max_concurrency=10,
|
|
243
|
+
|
|
244
|
+
# Use subprocess strategy for isolation
|
|
245
|
+
strategy=SubprocessStrategy(
|
|
246
|
+
registry=await get_default_registry(),
|
|
247
|
+
max_workers=4,
|
|
248
|
+
default_timeout=30.0
|
|
249
|
+
),
|
|
250
|
+
|
|
251
|
+
# Enable caching for performance
|
|
252
|
+
enable_caching=True,
|
|
253
|
+
cache_ttl=300, # 5 minutes
|
|
254
|
+
|
|
255
|
+
# Rate limiting to prevent abuse
|
|
256
|
+
enable_rate_limiting=True,
|
|
257
|
+
global_rate_limit=100, # 100 requests per minute globally
|
|
258
|
+
tool_rate_limits={
|
|
259
|
+
"expensive_api": (10, 60), # 10 per minute
|
|
260
|
+
"file_processor": (5, 60), # 5 per minute
|
|
261
|
+
"weather": (50, 60) # 50 per minute
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
# Automatic retries for reliability
|
|
265
|
+
enable_retries=True,
|
|
266
|
+
max_retries=3,
|
|
267
|
+
|
|
268
|
+
# Specify which parsers to use
|
|
269
|
+
parser_plugins=["xml_tool", "openai_tool", "json_tool", "function_call"]
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
await processor.initialize()
|
|
273
|
+
return processor
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
#### Custom Tool with All Features
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
from chuk_tool_processor.execution.wrappers.caching import cacheable
|
|
280
|
+
from chuk_tool_processor.execution.wrappers.rate_limiting import rate_limited
|
|
281
|
+
from chuk_tool_processor.execution.wrappers.retry import retryable
|
|
282
|
+
|
|
283
|
+
@register_tool(name="advanced_api", namespace="external")
|
|
284
|
+
@cacheable(ttl=600) # Cache for 10 minutes
|
|
285
|
+
@rate_limited(limit=20, period=60.0) # 20 calls per minute
|
|
286
|
+
@retryable(max_retries=3, base_delay=1.0) # Retry on failures
|
|
287
|
+
class AdvancedApiTool(ValidatedTool):
|
|
288
|
+
"""Example tool with all production features."""
|
|
289
|
+
|
|
290
|
+
class Arguments(BaseModel):
|
|
291
|
+
query: str = Field(..., min_length=1, max_length=1000)
|
|
292
|
+
format: str = Field("json", regex="^(json|xml|csv)$")
|
|
293
|
+
timeout: float = Field(10.0, gt=0, le=30)
|
|
294
|
+
|
|
295
|
+
class Result(BaseModel):
|
|
296
|
+
data: dict
|
|
297
|
+
format: str
|
|
298
|
+
processing_time: float
|
|
299
|
+
cached: bool = False
|
|
300
|
+
|
|
301
|
+
async def _execute(self, query: str, format: str, timeout: float) -> Result:
|
|
302
|
+
start_time = time.time()
|
|
303
|
+
|
|
304
|
+
# Simulate expensive API call
|
|
305
|
+
await asyncio.sleep(0.5)
|
|
306
|
+
|
|
307
|
+
# Simulate potential failure (for retry testing)
|
|
308
|
+
if random.random() < 0.1: # 10% failure rate
|
|
309
|
+
raise Exception("Temporary API failure")
|
|
310
|
+
|
|
311
|
+
processing_time = time.time() - start_time
|
|
312
|
+
|
|
313
|
+
return self.Result(
|
|
314
|
+
data={"query": query, "results": ["result1", "result2", "result3"]},
|
|
315
|
+
format=format,
|
|
316
|
+
processing_time=processing_time
|
|
317
|
+
)
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
### Working with LLM Responses
|
|
321
|
+
|
|
322
|
+
#### Supported Input Formats
|
|
323
|
+
|
|
324
|
+
The processor automatically detects and parses multiple formats:
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
# 1. XML Tool Tags (most common)
|
|
328
|
+
xml_response = """
|
|
329
|
+
Let me search for information about Python.
|
|
330
|
+
|
|
331
|
+
<tool name="search" args='{"query": "Python programming", "limit": 5}'/>
|
|
332
|
+
|
|
333
|
+
I'll also get the current time.
|
|
334
|
+
|
|
335
|
+
<tool name="get_current_time" args='{"timezone": "UTC"}'/>
|
|
336
|
+
"""
|
|
337
|
+
|
|
338
|
+
# 2. OpenAI Chat Completions Format
|
|
339
|
+
openai_response = {
|
|
340
|
+
"tool_calls": [
|
|
341
|
+
{
|
|
342
|
+
"id": "call_123",
|
|
343
|
+
"type": "function",
|
|
344
|
+
"function": {
|
|
345
|
+
"name": "search",
|
|
346
|
+
"arguments": '{"query": "Python programming", "limit": 5}'
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
]
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# 3. Direct ToolCall objects
|
|
353
|
+
tool_calls = [
|
|
354
|
+
{
|
|
355
|
+
"tool": "search",
|
|
356
|
+
"arguments": {"query": "Python programming", "limit": 5}
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
"tool": "get_current_time",
|
|
360
|
+
"arguments": {"timezone": "UTC"}
|
|
361
|
+
}
|
|
362
|
+
]
|
|
363
|
+
|
|
364
|
+
# Process any format
|
|
365
|
+
processor = ToolProcessor()
|
|
366
|
+
results1 = await processor.process(xml_response)
|
|
367
|
+
results2 = await processor.process(openai_response)
|
|
368
|
+
results3 = await processor.process(tool_calls)
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
#### Error Handling Best Practices
|
|
372
|
+
|
|
373
|
+
```python
|
|
374
|
+
async def robust_tool_processing(llm_response: str):
|
|
375
|
+
"""Example of robust error handling."""
|
|
376
|
+
processor = ToolProcessor(
|
|
377
|
+
default_timeout=30.0,
|
|
378
|
+
enable_retries=True,
|
|
379
|
+
max_retries=3
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
try:
|
|
383
|
+
results = await processor.process(llm_response, timeout=60.0)
|
|
384
|
+
|
|
385
|
+
successful_results = []
|
|
386
|
+
failed_results = []
|
|
387
|
+
|
|
388
|
+
for result in results:
|
|
389
|
+
if result.error:
|
|
390
|
+
failed_results.append(result)
|
|
391
|
+
logger.error(
|
|
392
|
+
f"Tool {result.tool} failed: {result.error}",
|
|
393
|
+
extra={
|
|
394
|
+
"tool": result.tool,
|
|
395
|
+
"duration": result.duration,
|
|
396
|
+
"attempts": getattr(result, "attempts", 1),
|
|
397
|
+
"machine": result.machine
|
|
398
|
+
}
|
|
399
|
+
)
|
|
400
|
+
else:
|
|
401
|
+
successful_results.append(result)
|
|
402
|
+
logger.info(
|
|
403
|
+
f"Tool {result.tool} succeeded",
|
|
404
|
+
extra={
|
|
405
|
+
"tool": result.tool,
|
|
406
|
+
"duration": result.duration,
|
|
407
|
+
"cached": getattr(result, "cached", False)
|
|
408
|
+
}
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
return {
|
|
412
|
+
"successful": successful_results,
|
|
413
|
+
"failed": failed_results,
|
|
414
|
+
"total": len(results)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
except Exception as e:
|
|
418
|
+
logger.exception("Failed to process LLM response")
|
|
419
|
+
raise
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
### MCP (Model Context Protocol) Integration
|
|
423
|
+
|
|
424
|
+
#### Quick MCP Setup with SSE
|
|
425
|
+
|
|
426
|
+
```python
|
|
427
|
+
from chuk_tool_processor.mcp import setup_mcp_sse
|
|
428
|
+
import os
|
|
429
|
+
|
|
430
|
+
async def setup_mcp_tools():
|
|
431
|
+
"""Set up MCP tools from external servers."""
|
|
432
|
+
|
|
433
|
+
# Configure MCP servers
|
|
434
|
+
servers = [
|
|
435
|
+
{
|
|
436
|
+
"name": "weather-service",
|
|
437
|
+
"url": "https://weather-mcp.example.com",
|
|
438
|
+
"api_key": os.getenv("WEATHER_API_KEY")
|
|
439
|
+
},
|
|
440
|
+
{
|
|
441
|
+
"name": "database-service",
|
|
442
|
+
"url": "https://db-mcp.example.com",
|
|
443
|
+
"api_key": os.getenv("DB_API_KEY")
|
|
444
|
+
}
|
|
445
|
+
]
|
|
446
|
+
|
|
447
|
+
# Initialize MCP with full configuration
|
|
448
|
+
processor, stream_manager = await setup_mcp_sse(
|
|
449
|
+
servers=servers,
|
|
450
|
+
namespace="mcp", # Tools available as mcp.tool_name
|
|
451
|
+
default_timeout=30.0,
|
|
452
|
+
max_concurrency=5,
|
|
453
|
+
enable_caching=True,
|
|
454
|
+
cache_ttl=300,
|
|
455
|
+
enable_rate_limiting=True,
|
|
456
|
+
global_rate_limit=100,
|
|
457
|
+
enable_retries=True,
|
|
458
|
+
max_retries=3
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
return processor, stream_manager
|
|
462
|
+
|
|
463
|
+
# Use MCP tools
|
|
464
|
+
processor, manager = await setup_mcp_tools()
|
|
465
|
+
|
|
466
|
+
# Tools are now available in the processor
|
|
467
|
+
results = await processor.process('''
|
|
468
|
+
<tool name="mcp.weather" args='{"location": "London"}'/>
|
|
469
|
+
<tool name="mcp.database_query" args='{"sql": "SELECT COUNT(*) FROM users"}'/>
|
|
470
|
+
''')
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
#### MCP with Stdio Transport
|
|
474
|
+
|
|
475
|
+
```python
|
|
476
|
+
from chuk_tool_processor.mcp import setup_mcp_stdio
|
|
477
|
+
|
|
478
|
+
# Create MCP config file (mcp_config.json)
|
|
479
|
+
mcp_config = {
|
|
480
|
+
"weather": {
|
|
481
|
+
"command": "python",
|
|
482
|
+
"args": ["-m", "weather_mcp_server"],
|
|
483
|
+
"env": {"API_KEY": "your_weather_key"}
|
|
484
|
+
},
|
|
485
|
+
"calculator": {
|
|
486
|
+
"command": "node",
|
|
487
|
+
"args": ["calculator-server.js"]
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
# Setup MCP with stdio
|
|
492
|
+
processor, stream_manager = await setup_mcp_stdio(
|
|
493
|
+
config_file="mcp_config.json",
|
|
494
|
+
servers=["weather", "calculator"],
|
|
495
|
+
namespace="tools"
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# Use the tools
|
|
499
|
+
results = await processor.process('<tool name="tools.weather" args=\'{"city": "Paris"}\'/>')
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
### Monitoring and Observability
|
|
503
|
+
|
|
504
|
+
#### Structured Logging Setup
|
|
505
|
+
|
|
506
|
+
```python
|
|
507
|
+
from chuk_tool_processor.logging import setup_logging, get_logger, log_context_span
|
|
508
|
+
import logging
|
|
509
|
+
|
|
510
|
+
# Setup logging
|
|
511
|
+
await setup_logging(
|
|
512
|
+
level=logging.INFO,
|
|
513
|
+
structured=True, # JSON output
|
|
514
|
+
log_file="tool_processor.log" # Optional file output
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
# Use structured logging in your application
|
|
518
|
+
logger = get_logger("my_app")
|
|
519
|
+
|
|
520
|
+
async def process_user_request(user_id: str, request: str):
|
|
521
|
+
"""Example of using structured logging with context."""
|
|
522
|
+
|
|
523
|
+
async with log_context_span("user_request", {"user_id": user_id}):
|
|
524
|
+
logger.info("Processing user request", extra={
|
|
525
|
+
"request_length": len(request),
|
|
526
|
+
"user_id": user_id
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
try:
|
|
530
|
+
results = await processor.process(request)
|
|
531
|
+
|
|
532
|
+
logger.info("Request processed successfully", extra={
|
|
533
|
+
"num_tools": len(results),
|
|
534
|
+
"user_id": user_id
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
return results
|
|
538
|
+
|
|
539
|
+
except Exception as e:
|
|
540
|
+
logger.error("Request processing failed", extra={
|
|
541
|
+
"error": str(e),
|
|
542
|
+
"user_id": user_id
|
|
543
|
+
})
|
|
544
|
+
raise
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
#### Metrics Collection
|
|
548
|
+
|
|
549
|
+
```python
|
|
550
|
+
from chuk_tool_processor.logging import metrics
|
|
551
|
+
|
|
552
|
+
# Metrics are automatically collected for:
|
|
553
|
+
# - Tool execution success/failure rates
|
|
554
|
+
# - Execution durations
|
|
555
|
+
# - Cache hit/miss rates
|
|
556
|
+
# - Parser performance
|
|
557
|
+
# - Registry operations
|
|
558
|
+
|
|
559
|
+
# Access metrics programmatically
|
|
560
|
+
async def get_tool_stats():
|
|
561
|
+
"""Get statistics about tool usage."""
|
|
562
|
+
|
|
563
|
+
# Example: Get cache statistics
|
|
564
|
+
if hasattr(processor.executor, 'cache'):
|
|
565
|
+
cache_stats = await processor.executor.cache.get_stats()
|
|
566
|
+
print(f"Cache hit rate: {cache_stats['hit_rate']:.2%}")
|
|
567
|
+
print(f"Total entries: {cache_stats['entry_count']}")
|
|
568
|
+
|
|
569
|
+
# Custom metrics can be logged
|
|
570
|
+
await metrics.log_tool_execution(
|
|
571
|
+
tool="custom_metric",
|
|
572
|
+
success=True,
|
|
573
|
+
duration=1.5,
|
|
574
|
+
cached=False
|
|
575
|
+
)
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
### Testing Your Tools
|
|
579
|
+
|
|
580
|
+
#### Unit Testing
|
|
581
|
+
|
|
582
|
+
```python
|
|
583
|
+
import pytest
|
|
584
|
+
from chuk_tool_processor import ToolProcessor, register_tool, initialize
|
|
585
|
+
|
|
586
|
+
@pytest.mark.asyncio
|
|
587
|
+
async def test_calculator_tool():
|
|
588
|
+
"""Test the calculator tool."""
|
|
589
|
+
|
|
590
|
+
# Setup
|
|
591
|
+
await initialize()
|
|
592
|
+
processor = ToolProcessor()
|
|
593
|
+
|
|
594
|
+
# Test successful operation
|
|
595
|
+
results = await processor.process(
|
|
596
|
+
'<tool name="calculator" args=\'{"operation": "add", "a": 5, "b": 3}\'/>'
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
assert len(results) == 1
|
|
600
|
+
result = results[0]
|
|
601
|
+
assert result.error is None
|
|
602
|
+
assert result.result["result"] == 8
|
|
603
|
+
assert result.result["operation"] == "add"
|
|
604
|
+
|
|
605
|
+
@pytest.mark.asyncio
|
|
606
|
+
async def test_calculator_error_handling():
|
|
607
|
+
"""Test calculator error handling."""
|
|
608
|
+
|
|
609
|
+
await initialize()
|
|
610
|
+
processor = ToolProcessor()
|
|
611
|
+
|
|
612
|
+
# Test division by zero
|
|
613
|
+
results = await processor.process(
|
|
614
|
+
'<tool name="calculator" args=\'{"operation": "divide", "a": 5, "b": 0}\'/>'
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
assert len(results) == 1
|
|
618
|
+
result = results[0]
|
|
619
|
+
assert result.error is not None
|
|
620
|
+
assert "Cannot divide by zero" in result.error
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
#### Integration Testing
|
|
624
|
+
|
|
625
|
+
```python
|
|
626
|
+
@pytest.mark.asyncio
|
|
627
|
+
async def test_full_workflow():
|
|
628
|
+
"""Test a complete workflow with multiple tools."""
|
|
629
|
+
|
|
630
|
+
# Register additional test tools
|
|
631
|
+
@register_tool(name="formatter")
|
|
632
|
+
class FormatterTool:
|
|
633
|
+
async def execute(self, text: str, format: str) -> str:
|
|
634
|
+
if format == "upper":
|
|
635
|
+
return text.upper()
|
|
636
|
+
elif format == "lower":
|
|
637
|
+
return text.lower()
|
|
638
|
+
return text
|
|
639
|
+
|
|
640
|
+
await initialize()
|
|
641
|
+
processor = ToolProcessor(enable_caching=True)
|
|
642
|
+
|
|
643
|
+
# Test multiple tool calls
|
|
644
|
+
llm_response = """
|
|
645
|
+
<tool name="calculator" args='{"operation": "multiply", "a": 6, "b": 7}'/>
|
|
646
|
+
<tool name="formatter" args='{"text": "Hello World", "format": "upper"}'/>
|
|
647
|
+
"""
|
|
648
|
+
|
|
649
|
+
results = await processor.process(llm_response)
|
|
650
|
+
|
|
651
|
+
assert len(results) == 2
|
|
652
|
+
|
|
653
|
+
# Check calculator result
|
|
654
|
+
calc_result = next(r for r in results if r.tool == "calculator")
|
|
655
|
+
assert calc_result.result["result"] == 42
|
|
656
|
+
|
|
657
|
+
# Check formatter result
|
|
658
|
+
format_result = next(r for r in results if r.tool == "formatter")
|
|
659
|
+
assert format_result.result == "HELLO WORLD"
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### Performance Optimization
|
|
663
|
+
|
|
664
|
+
#### Concurrent Execution
|
|
665
|
+
|
|
666
|
+
```python
|
|
667
|
+
# Configure for high-throughput scenarios
|
|
668
|
+
processor = ToolProcessor(
|
|
669
|
+
max_concurrency=20, # Allow 20 concurrent tool executions
|
|
670
|
+
default_timeout=60.0, # Longer timeout for complex operations
|
|
671
|
+
enable_caching=True, # Cache frequently used results
|
|
672
|
+
cache_ttl=900, # 15-minute cache
|
|
673
|
+
enable_rate_limiting=True,
|
|
674
|
+
global_rate_limit=500 # 500 requests per minute
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
# Process multiple requests concurrently
|
|
678
|
+
async def process_batch(requests: list[str]):
|
|
679
|
+
"""Process multiple LLM responses concurrently."""
|
|
680
|
+
|
|
681
|
+
tasks = [processor.process(request) for request in requests]
|
|
682
|
+
all_results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
683
|
+
|
|
684
|
+
successful = []
|
|
685
|
+
failed = []
|
|
686
|
+
|
|
687
|
+
for i, result in enumerate(all_results):
|
|
688
|
+
if isinstance(result, Exception):
|
|
689
|
+
failed.append({"request_index": i, "error": str(result)})
|
|
690
|
+
else:
|
|
691
|
+
successful.append({"request_index": i, "results": result})
|
|
692
|
+
|
|
693
|
+
return {"successful": successful, "failed": failed}
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
#### Memory Management
|
|
697
|
+
|
|
698
|
+
```python
|
|
699
|
+
# For long-running applications, periodically clear caches
|
|
700
|
+
async def maintenance_task():
|
|
701
|
+
"""Periodic maintenance for long-running applications."""
|
|
702
|
+
|
|
703
|
+
while True:
|
|
704
|
+
await asyncio.sleep(3600) # Every hour
|
|
705
|
+
|
|
706
|
+
# Clear old cache entries
|
|
707
|
+
if hasattr(processor.executor, 'cache'):
|
|
708
|
+
# Clear entire cache or implement LRU eviction
|
|
709
|
+
stats_before = await processor.executor.cache.get_stats()
|
|
710
|
+
await processor.executor.cache.clear()
|
|
711
|
+
|
|
712
|
+
logger.info("Cache cleared", extra={
|
|
713
|
+
"entries_cleared": stats_before.get("entry_count", 0)
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
# Run maintenance in background
|
|
717
|
+
asyncio.create_task(maintenance_task())
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
## 🔧 Configuration Reference
|
|
721
|
+
|
|
722
|
+
### Environment Variables
|
|
723
|
+
|
|
724
|
+
| Variable | Default | Description |
|
|
725
|
+
|----------|---------|-------------|
|
|
726
|
+
| `CHUK_TOOL_REGISTRY_PROVIDER` | `memory` | Registry backend (memory, redis, etc.) |
|
|
727
|
+
| `CHUK_DEFAULT_TIMEOUT` | `30.0` | Default tool execution timeout (seconds) |
|
|
728
|
+
| `CHUK_LOG_LEVEL` | `INFO` | Logging level (DEBUG, INFO, WARNING, ERROR) |
|
|
729
|
+
| `CHUK_STRUCTURED_LOGGING` | `true` | Enable JSON structured logging |
|
|
730
|
+
| `CHUK_MAX_CONCURRENCY` | `10` | Default max concurrent executions |
|
|
731
|
+
| `MCP_BEARER_TOKEN` | - | Bearer token for MCP SSE authentication |
|
|
732
|
+
| `MCP_CONFIG_FILE` | - | Path to MCP configuration file |
|
|
733
|
+
|
|
734
|
+
### ToolProcessor Configuration
|
|
735
|
+
|
|
736
|
+
```python
|
|
737
|
+
processor = ToolProcessor(
|
|
738
|
+
# Execution
|
|
739
|
+
default_timeout=30.0, # Default timeout per tool
|
|
740
|
+
max_concurrency=10, # Max concurrent executions
|
|
741
|
+
|
|
742
|
+
# Strategy (choose one)
|
|
743
|
+
strategy=InProcessStrategy(...), # Fast, shared memory
|
|
744
|
+
# strategy=SubprocessStrategy(...), # Isolated, safer
|
|
745
|
+
|
|
746
|
+
# Caching
|
|
747
|
+
enable_caching=True, # Enable result caching
|
|
748
|
+
cache_ttl=300, # Cache TTL in seconds
|
|
749
|
+
|
|
750
|
+
# Rate limiting
|
|
751
|
+
enable_rate_limiting=False, # Enable rate limiting
|
|
752
|
+
global_rate_limit=100, # Global requests per minute
|
|
753
|
+
tool_rate_limits={ # Per-tool limits
|
|
754
|
+
"expensive_tool": (10, 60), # 10 per minute
|
|
755
|
+
},
|
|
756
|
+
|
|
757
|
+
# Retries
|
|
758
|
+
enable_retries=True, # Enable automatic retries
|
|
759
|
+
max_retries=3, # Max retry attempts
|
|
760
|
+
|
|
761
|
+
# Parsing
|
|
762
|
+
parser_plugins=[ # Enabled parsers
|
|
763
|
+
"xml_tool",
|
|
764
|
+
"openai_tool",
|
|
765
|
+
"json_tool",
|
|
766
|
+
"function_call"
|
|
767
|
+
]
|
|
768
|
+
)
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
## 🤝 Contributing
|
|
772
|
+
|
|
773
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.
|
|
774
|
+
|
|
775
|
+
### Development Setup
|
|
776
|
+
|
|
777
|
+
```bash
|
|
778
|
+
# Clone and setup development environment
|
|
779
|
+
git clone https://github.com/chrishayuk/chuk-tool-processor.git
|
|
780
|
+
cd chuk-tool-processor
|
|
781
|
+
|
|
782
|
+
# Create virtual environment
|
|
783
|
+
python -m venv venv
|
|
784
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
785
|
+
|
|
786
|
+
# Install in development mode with all extras
|
|
787
|
+
pip install -e ".[dev,test,mcp,all]"
|
|
788
|
+
|
|
789
|
+
# Run tests
|
|
790
|
+
pytest
|
|
791
|
+
|
|
792
|
+
# Run with coverage
|
|
793
|
+
pytest --cov=chuk_tool_processor
|
|
794
|
+
|
|
795
|
+
# Format code
|
|
796
|
+
black chuk_tool_processor
|
|
797
|
+
isort chuk_tool_processor
|
|
798
|
+
|
|
799
|
+
# Type checking
|
|
800
|
+
mypy chuk_tool_processor
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
### Adding New Features
|
|
804
|
+
|
|
805
|
+
1. **New Tool Types**: Extend `ValidatedTool` or `StreamingTool`
|
|
806
|
+
2. **New Parsers**: Implement `ParserPlugin` interface
|
|
807
|
+
3. **New Strategies**: Implement `ExecutionStrategy` interface
|
|
808
|
+
4. **New Wrappers**: Create execution wrappers for cross-cutting concerns
|
|
809
|
+
|
|
810
|
+
## 📚 Documentation
|
|
811
|
+
|
|
812
|
+
- [API Reference](docs/api.md)
|
|
813
|
+
- [Architecture Guide](docs/architecture.md)
|
|
814
|
+
- [Plugin Development](docs/plugins.md)
|
|
815
|
+
- [MCP Integration Guide](docs/mcp.md)
|
|
816
|
+
- [Performance Tuning](docs/performance.md)
|
|
817
|
+
- [Deployment Guide](docs/deployment.md)
|
|
818
|
+
|
|
819
|
+
## 🆘 Support
|
|
820
|
+
|
|
821
|
+
- [GitHub Issues](https://github.com/chrishayuk/chuk-tool-processor/issues)
|
|
822
|
+
- [Discussions](https://github.com/chrishayuk/chuk-tool-processor/discussions)
|
|
823
|
+
- [Discord Community](https://discord.gg/chuk-tools)
|
|
824
|
+
|
|
825
|
+
## 📄 License
|
|
826
|
+
|
|
827
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
828
|
+
|
|
829
|
+
---
|
|
830
|
+
|
|
831
|
+
**Made with ❤️ by the CHUK team**
|