chuk-tool-processor 0.10__tar.gz → 0.11__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/PKG-INFO +153 -20
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/README.md +152 -19
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/pyproject.toml +3 -1
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/__init__.py +5 -2
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/core/exceptions.py +55 -4
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/core/processor.py +5 -5
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/__init__.py +2 -1
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/models.py +65 -1
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/setup_mcp_stdio.py +50 -11
- chuk_tool_processor-0.11/src/chuk_tool_processor/registry/__init__.py +110 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/decorators.py +42 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/interface.py +8 -3
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/metadata.py +26 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/providers/memory.py +23 -7
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/tool_export.py +19 -17
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor.egg-info/PKG-INFO +153 -20
- chuk_tool_processor-0.10/src/chuk_tool_processor/registry/__init__.py +0 -60
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/setup.cfg +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/core/__init__.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/__init__.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/strategies/__init__.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/strategies/inprocess_strategy.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/strategies/subprocess_strategy.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/tool_executor.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/__init__.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/caching.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/circuit_breaker.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/rate_limiting.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/execution/wrappers/retry.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/__init__.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/context.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/formatter.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/helpers.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/logging/metrics.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/mcp_tool.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/register_mcp_tools.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/setup_mcp_http_streamable.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/setup_mcp_sse.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/stream_manager.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/__init__.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/base_transport.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/http_streamable_transport.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/models.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/sse_transport.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/mcp/transport/stdio_transport.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/__init__.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/execution_strategy.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/streaming_tool.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/tool_call.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/tool_export_mixin.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/tool_result.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/tool_spec.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/models/validated_tool.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/observability/__init__.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/observability/metrics.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/observability/setup.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/observability/tracing.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/__init__.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/discovery.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/__init__.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/base.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/function_call_tool.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/json_tool.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/openai_tool.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/plugins/parsers/xml_tool.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/py.typed +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/auto_register.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/provider.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/registry/providers/__init__.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/utils/__init__.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/utils/validation.py +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor.egg-info/SOURCES.txt +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor.egg-info/dependency_links.txt +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor.egg-info/requires.txt +0 -0
- {chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chuk-tool-processor
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11
|
|
4
4
|
Summary: Async-native framework for registering, discovering, and executing tools referenced in LLM responses
|
|
5
5
|
Author-email: CHUK Team <chrishayuk@somejunkmailbox.com>
|
|
6
6
|
Maintainer-email: CHUK Team <chrishayuk@somejunkmailbox.com>
|
|
@@ -96,15 +96,15 @@ Works with OpenAI, Anthropic, local models (Ollama/MLX/vLLM), and any framework
|
|
|
96
96
|
|
|
97
97
|
```python
|
|
98
98
|
import asyncio
|
|
99
|
-
from chuk_tool_processor import ToolProcessor,
|
|
99
|
+
from chuk_tool_processor import ToolProcessor, tool
|
|
100
100
|
|
|
101
|
-
@
|
|
101
|
+
@tool(name="weather") # Clean decorator syntax
|
|
102
102
|
class WeatherTool:
|
|
103
103
|
async def execute(self, city: str) -> dict:
|
|
104
104
|
return {"temp": 72, "condition": "sunny", "city": city}
|
|
105
105
|
|
|
106
106
|
async def main():
|
|
107
|
-
|
|
107
|
+
# No need for initialize() - auto-initializes on first use!
|
|
108
108
|
async with ToolProcessor(enable_caching=True, enable_retries=True) as p:
|
|
109
109
|
# Works with OpenAI, Anthropic, or JSON formats
|
|
110
110
|
result = await p.process('<tool name="weather" args=\'{"city": "SF"}\'/>')
|
|
@@ -374,10 +374,10 @@ Copy-paste this into a file and run it:
|
|
|
374
374
|
|
|
375
375
|
```python
|
|
376
376
|
import asyncio
|
|
377
|
-
from chuk_tool_processor import ToolProcessor,
|
|
377
|
+
from chuk_tool_processor import ToolProcessor, tool
|
|
378
378
|
|
|
379
|
-
# Step 1: Define a tool
|
|
380
|
-
@
|
|
379
|
+
# Step 1: Define a tool with the clean @tool decorator
|
|
380
|
+
@tool(name="calculator")
|
|
381
381
|
class Calculator:
|
|
382
382
|
async def execute(self, operation: str, a: float, b: float) -> dict:
|
|
383
383
|
ops = {"add": a + b, "multiply": a * b, "subtract": a - b}
|
|
@@ -387,7 +387,7 @@ class Calculator:
|
|
|
387
387
|
|
|
388
388
|
# Step 2: Process LLM output
|
|
389
389
|
async def main():
|
|
390
|
-
|
|
390
|
+
# No initialize() needed - it auto-initializes!
|
|
391
391
|
|
|
392
392
|
# Use context manager for automatic cleanup
|
|
393
393
|
async with ToolProcessor() as processor:
|
|
@@ -412,10 +412,87 @@ asyncio.run(main())
|
|
|
412
412
|
- ✅ Automatic timeouts, retries, and caching
|
|
413
413
|
- ✅ Clean resource management (context manager)
|
|
414
414
|
- ✅ Full type checking support
|
|
415
|
+
- ✅ Auto-initialization (no boilerplate!)
|
|
415
416
|
|
|
416
417
|
> **Why not just use OpenAI tool calls?**
|
|
417
418
|
> OpenAI's function calling is great for parsing, but you still need: parsing multiple formats (Anthropic XML, etc.), timeouts, retries, rate limits, caching, subprocess isolation, connecting to external MCP servers, and **per-tool** policy control with cross-provider parsing and MCP fan-out. CHUK Tool Processor **is** that missing middle layer.
|
|
418
419
|
|
|
420
|
+
### Enhanced Developer Experience
|
|
421
|
+
|
|
422
|
+
CHUK Tool Processor provides intuitive APIs and helpful error messages:
|
|
423
|
+
|
|
424
|
+
**1. Clean Decorator Syntax**
|
|
425
|
+
```python
|
|
426
|
+
from chuk_tool_processor import tool
|
|
427
|
+
|
|
428
|
+
@tool(name="calculator") # Short and clean!
|
|
429
|
+
class Calculator:
|
|
430
|
+
async def execute(self, a: int, b: int) -> int:
|
|
431
|
+
return a + b
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**2. Auto-Initialization (No Boilerplate)**
|
|
435
|
+
```python
|
|
436
|
+
from chuk_tool_processor import ToolProcessor
|
|
437
|
+
|
|
438
|
+
# No initialize() needed - it auto-initializes!
|
|
439
|
+
async with ToolProcessor() as p:
|
|
440
|
+
results = await p.process(llm_output)
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
**3. Type-Safe Tool Discovery**
|
|
444
|
+
```python
|
|
445
|
+
from chuk_tool_processor import get_default_registry, ToolInfo
|
|
446
|
+
|
|
447
|
+
registry = await get_default_registry()
|
|
448
|
+
|
|
449
|
+
# List all registered tools with clear, typed results
|
|
450
|
+
tools = await registry.list_tools()
|
|
451
|
+
for tool in tools: # Each tool is a ToolInfo object
|
|
452
|
+
print(f"{tool.namespace}:{tool.name}") # Clear attribute access!
|
|
453
|
+
# No more confusing tuple unpacking: (namespace, name) vs (name, namespace)?
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
**4. Helpful Error Messages**
|
|
457
|
+
```python
|
|
458
|
+
# Typo in tool name? Get helpful suggestions!
|
|
459
|
+
try:
|
|
460
|
+
await registry.get_tool_strict("calcuator", namespace="default")
|
|
461
|
+
except Exception as e:
|
|
462
|
+
print(e)
|
|
463
|
+
# Output:
|
|
464
|
+
# Tool 'calcuator' not found in namespace 'default'
|
|
465
|
+
#
|
|
466
|
+
# Did you mean: calculator?
|
|
467
|
+
#
|
|
468
|
+
# Available namespaces: default, math, mcp
|
|
469
|
+
#
|
|
470
|
+
# Tip: Use `await registry.list_tools()` to see all registered tools
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
**5. Clean MCP Configuration**
|
|
474
|
+
```python
|
|
475
|
+
from chuk_tool_processor.mcp import setup_mcp_stdio, MCPConfig, MCPServerConfig
|
|
476
|
+
|
|
477
|
+
# Clean Pydantic config object instead of 14+ parameters!
|
|
478
|
+
processor, manager = await setup_mcp_stdio(
|
|
479
|
+
config=MCPConfig(
|
|
480
|
+
servers=[MCPServerConfig(name="echo", command="uvx", args=["mcp-echo"])],
|
|
481
|
+
namespace="tools",
|
|
482
|
+
enable_caching=True,
|
|
483
|
+
cache_ttl=600,
|
|
484
|
+
)
|
|
485
|
+
)
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
**Key improvements:**
|
|
489
|
+
- ✅ **`@tool` decorator**: Shorter, cleaner than `@register_tool`
|
|
490
|
+
- ✅ **Auto-initialization**: No need for explicit `initialize()` calls
|
|
491
|
+
- ✅ **Type-safe tool listing**: `ToolInfo` objects instead of confusing tuples
|
|
492
|
+
- ✅ **Helpful errors**: Fuzzy matching suggestions when tools aren't found
|
|
493
|
+
- ✅ **MCPConfig**: Clean Pydantic model instead of 14+ parameters
|
|
494
|
+
- ✅ **Better discoverability**: Clear guidance on how to explore available tools
|
|
495
|
+
|
|
419
496
|
## Quick Decision Tree (Commit This to Memory)
|
|
420
497
|
|
|
421
498
|
```
|
|
@@ -425,8 +502,8 @@ asyncio.run(main())
|
|
|
425
502
|
│ ⚠️ No → IsolatedStrategy (sandboxed) │
|
|
426
503
|
│ │
|
|
427
504
|
│ Where do your tools live? │
|
|
428
|
-
│ 📦 Local → @
|
|
429
|
-
│ 🌐 Remote →
|
|
505
|
+
│ 📦 Local → @tool decorator │
|
|
506
|
+
│ 🌐 Remote → setup_mcp_* with MCPConfig │
|
|
430
507
|
╰──────────────────────────────────────────╯
|
|
431
508
|
```
|
|
432
509
|
|
|
@@ -436,22 +513,25 @@ asyncio.run(main())
|
|
|
436
513
|
|
|
437
514
|
Understanding the lifecycle helps you use CHUK Tool Processor correctly:
|
|
438
515
|
|
|
439
|
-
1.
|
|
516
|
+
1. **Auto-initialization** — Registry auto-initializes on first access (or call `await initialize()` explicitly)
|
|
440
517
|
2. Create a **`ToolProcessor(...)`** (or use the one returned by `setup_mcp_*`)
|
|
441
518
|
3. Use **`async with ToolProcessor() as p:`** to ensure cleanup
|
|
442
519
|
4. **`setup_mcp_*`** returns `(processor, manager)` — reuse that `processor`
|
|
443
520
|
5. If you need a custom registry, pass it explicitly to the strategy
|
|
444
521
|
6. You rarely need `get_default_registry()` unless you're composing advanced setups
|
|
445
522
|
|
|
446
|
-
|
|
523
|
+
**New in this version:** The registry auto-initializes when you create a `ToolProcessor` or access `get_default_registry()`, so you can skip the explicit `initialize()` call in most cases!
|
|
447
524
|
|
|
448
525
|
```python
|
|
449
|
-
#
|
|
450
|
-
|
|
526
|
+
# New simplified pattern (auto-initialization)
|
|
527
|
+
async with ToolProcessor() as p: # Auto-initializes on first use!
|
|
528
|
+
results = await p.process(llm_output)
|
|
529
|
+
# Processor automatically cleaned up on exit
|
|
451
530
|
|
|
452
|
-
|
|
531
|
+
# Traditional explicit pattern (still works)
|
|
532
|
+
await initialize() # Explicit initialization
|
|
533
|
+
async with ToolProcessor() as p:
|
|
453
534
|
results = await p.process(llm_output)
|
|
454
|
-
# Step 4: Processor automatically cleaned up on exit
|
|
455
535
|
```
|
|
456
536
|
|
|
457
537
|
## Production Features by Example
|
|
@@ -618,7 +698,41 @@ asyncio.run(main())
|
|
|
618
698
|
|
|
619
699
|
See `examples/04_mcp_integration/notion_oauth.py` for complete OAuth flow.
|
|
620
700
|
|
|
621
|
-
**Pattern 3: Local SQLite database via STDIO**
|
|
701
|
+
**Pattern 3: Local SQLite database via STDIO (New Clean API)**
|
|
702
|
+
```python
|
|
703
|
+
import asyncio
|
|
704
|
+
from chuk_tool_processor.mcp import setup_mcp_stdio, MCPConfig, MCPServerConfig
|
|
705
|
+
|
|
706
|
+
async def main():
|
|
707
|
+
# NEW: Clean Pydantic config approach (recommended!)
|
|
708
|
+
processor, manager = await setup_mcp_stdio(
|
|
709
|
+
config=MCPConfig(
|
|
710
|
+
servers=[
|
|
711
|
+
MCPServerConfig(
|
|
712
|
+
name="sqlite",
|
|
713
|
+
command="uvx",
|
|
714
|
+
args=["mcp-server-sqlite", "--db-path", "./app.db"],
|
|
715
|
+
)
|
|
716
|
+
],
|
|
717
|
+
namespace="db",
|
|
718
|
+
initialization_timeout=120.0, # First run downloads the package
|
|
719
|
+
enable_caching=True,
|
|
720
|
+
cache_ttl=600,
|
|
721
|
+
)
|
|
722
|
+
)
|
|
723
|
+
|
|
724
|
+
# Query your local database via MCP
|
|
725
|
+
results = await processor.process(
|
|
726
|
+
'<tool name="db.query" args=\'{"sql": "SELECT * FROM users LIMIT 10"}\'/>'
|
|
727
|
+
)
|
|
728
|
+
print(results[0].result)
|
|
729
|
+
|
|
730
|
+
asyncio.run(main())
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
<details>
|
|
734
|
+
<summary><strong>Legacy approach (still works)</strong></summary>
|
|
735
|
+
|
|
622
736
|
```python
|
|
623
737
|
import asyncio
|
|
624
738
|
import json
|
|
@@ -643,7 +757,7 @@ async def main():
|
|
|
643
757
|
config_file="mcp_config.json",
|
|
644
758
|
servers=["sqlite"],
|
|
645
759
|
namespace="db",
|
|
646
|
-
initialization_timeout=120.0
|
|
760
|
+
initialization_timeout=120.0
|
|
647
761
|
)
|
|
648
762
|
|
|
649
763
|
# Query your local database via MCP
|
|
@@ -654,6 +768,7 @@ async def main():
|
|
|
654
768
|
|
|
655
769
|
asyncio.run(main())
|
|
656
770
|
```
|
|
771
|
+
</details>
|
|
657
772
|
|
|
658
773
|
See `examples/04_mcp_integration/stdio_sqlite.py` for complete working example.
|
|
659
774
|
|
|
@@ -937,7 +1052,11 @@ register_fn_tool(get_current_time, namespace="utilities")
|
|
|
937
1052
|
For production tools, use Pydantic validation:
|
|
938
1053
|
|
|
939
1054
|
```python
|
|
940
|
-
|
|
1055
|
+
from chuk_tool_processor import tool
|
|
1056
|
+
from chuk_tool_processor.models import ValidatedTool
|
|
1057
|
+
from pydantic import BaseModel, Field
|
|
1058
|
+
|
|
1059
|
+
@tool(name="weather") # Clean @tool decorator
|
|
941
1060
|
class WeatherTool(ValidatedTool):
|
|
942
1061
|
class Arguments(BaseModel):
|
|
943
1062
|
location: str = Field(..., description="City name")
|
|
@@ -951,14 +1070,28 @@ class WeatherTool(ValidatedTool):
|
|
|
951
1070
|
return self.Result(temperature=22.5, conditions="Sunny")
|
|
952
1071
|
```
|
|
953
1072
|
|
|
1073
|
+
<details>
|
|
1074
|
+
<summary><strong>Alternative: Using @register_tool (still works)</strong></summary>
|
|
1075
|
+
|
|
1076
|
+
```python
|
|
1077
|
+
from chuk_tool_processor import register_tool
|
|
1078
|
+
|
|
1079
|
+
@register_tool(name="weather") # Longer form, but identical functionality
|
|
1080
|
+
class WeatherTool(ValidatedTool):
|
|
1081
|
+
# ... same as above
|
|
1082
|
+
```
|
|
1083
|
+
</details>
|
|
1084
|
+
|
|
954
1085
|
#### StreamingTool (Real-time Results)
|
|
955
1086
|
|
|
956
1087
|
For long-running operations that produce incremental results:
|
|
957
1088
|
|
|
958
1089
|
```python
|
|
1090
|
+
from chuk_tool_processor import tool
|
|
959
1091
|
from chuk_tool_processor.models import StreamingTool
|
|
1092
|
+
from pydantic import BaseModel
|
|
960
1093
|
|
|
961
|
-
@
|
|
1094
|
+
@tool(name="file_processor") # Clean @tool decorator
|
|
962
1095
|
class FileProcessor(StreamingTool):
|
|
963
1096
|
class Arguments(BaseModel):
|
|
964
1097
|
file_path: str
|
|
@@ -68,15 +68,15 @@ Works with OpenAI, Anthropic, local models (Ollama/MLX/vLLM), and any framework
|
|
|
68
68
|
|
|
69
69
|
```python
|
|
70
70
|
import asyncio
|
|
71
|
-
from chuk_tool_processor import ToolProcessor,
|
|
71
|
+
from chuk_tool_processor import ToolProcessor, tool
|
|
72
72
|
|
|
73
|
-
@
|
|
73
|
+
@tool(name="weather") # Clean decorator syntax
|
|
74
74
|
class WeatherTool:
|
|
75
75
|
async def execute(self, city: str) -> dict:
|
|
76
76
|
return {"temp": 72, "condition": "sunny", "city": city}
|
|
77
77
|
|
|
78
78
|
async def main():
|
|
79
|
-
|
|
79
|
+
# No need for initialize() - auto-initializes on first use!
|
|
80
80
|
async with ToolProcessor(enable_caching=True, enable_retries=True) as p:
|
|
81
81
|
# Works with OpenAI, Anthropic, or JSON formats
|
|
82
82
|
result = await p.process('<tool name="weather" args=\'{"city": "SF"}\'/>')
|
|
@@ -346,10 +346,10 @@ Copy-paste this into a file and run it:
|
|
|
346
346
|
|
|
347
347
|
```python
|
|
348
348
|
import asyncio
|
|
349
|
-
from chuk_tool_processor import ToolProcessor,
|
|
349
|
+
from chuk_tool_processor import ToolProcessor, tool
|
|
350
350
|
|
|
351
|
-
# Step 1: Define a tool
|
|
352
|
-
@
|
|
351
|
+
# Step 1: Define a tool with the clean @tool decorator
|
|
352
|
+
@tool(name="calculator")
|
|
353
353
|
class Calculator:
|
|
354
354
|
async def execute(self, operation: str, a: float, b: float) -> dict:
|
|
355
355
|
ops = {"add": a + b, "multiply": a * b, "subtract": a - b}
|
|
@@ -359,7 +359,7 @@ class Calculator:
|
|
|
359
359
|
|
|
360
360
|
# Step 2: Process LLM output
|
|
361
361
|
async def main():
|
|
362
|
-
|
|
362
|
+
# No initialize() needed - it auto-initializes!
|
|
363
363
|
|
|
364
364
|
# Use context manager for automatic cleanup
|
|
365
365
|
async with ToolProcessor() as processor:
|
|
@@ -384,10 +384,87 @@ asyncio.run(main())
|
|
|
384
384
|
- ✅ Automatic timeouts, retries, and caching
|
|
385
385
|
- ✅ Clean resource management (context manager)
|
|
386
386
|
- ✅ Full type checking support
|
|
387
|
+
- ✅ Auto-initialization (no boilerplate!)
|
|
387
388
|
|
|
388
389
|
> **Why not just use OpenAI tool calls?**
|
|
389
390
|
> 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.
|
|
390
391
|
|
|
392
|
+
### Enhanced Developer Experience
|
|
393
|
+
|
|
394
|
+
CHUK Tool Processor provides intuitive APIs and helpful error messages:
|
|
395
|
+
|
|
396
|
+
**1. Clean Decorator Syntax**
|
|
397
|
+
```python
|
|
398
|
+
from chuk_tool_processor import tool
|
|
399
|
+
|
|
400
|
+
@tool(name="calculator") # Short and clean!
|
|
401
|
+
class Calculator:
|
|
402
|
+
async def execute(self, a: int, b: int) -> int:
|
|
403
|
+
return a + b
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**2. Auto-Initialization (No Boilerplate)**
|
|
407
|
+
```python
|
|
408
|
+
from chuk_tool_processor import ToolProcessor
|
|
409
|
+
|
|
410
|
+
# No initialize() needed - it auto-initializes!
|
|
411
|
+
async with ToolProcessor() as p:
|
|
412
|
+
results = await p.process(llm_output)
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**3. Type-Safe Tool Discovery**
|
|
416
|
+
```python
|
|
417
|
+
from chuk_tool_processor import get_default_registry, ToolInfo
|
|
418
|
+
|
|
419
|
+
registry = await get_default_registry()
|
|
420
|
+
|
|
421
|
+
# List all registered tools with clear, typed results
|
|
422
|
+
tools = await registry.list_tools()
|
|
423
|
+
for tool in tools: # Each tool is a ToolInfo object
|
|
424
|
+
print(f"{tool.namespace}:{tool.name}") # Clear attribute access!
|
|
425
|
+
# No more confusing tuple unpacking: (namespace, name) vs (name, namespace)?
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**4. Helpful Error Messages**
|
|
429
|
+
```python
|
|
430
|
+
# Typo in tool name? Get helpful suggestions!
|
|
431
|
+
try:
|
|
432
|
+
await registry.get_tool_strict("calcuator", namespace="default")
|
|
433
|
+
except Exception as e:
|
|
434
|
+
print(e)
|
|
435
|
+
# Output:
|
|
436
|
+
# Tool 'calcuator' not found in namespace 'default'
|
|
437
|
+
#
|
|
438
|
+
# Did you mean: calculator?
|
|
439
|
+
#
|
|
440
|
+
# Available namespaces: default, math, mcp
|
|
441
|
+
#
|
|
442
|
+
# Tip: Use `await registry.list_tools()` to see all registered tools
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**5. Clean MCP Configuration**
|
|
446
|
+
```python
|
|
447
|
+
from chuk_tool_processor.mcp import setup_mcp_stdio, MCPConfig, MCPServerConfig
|
|
448
|
+
|
|
449
|
+
# Clean Pydantic config object instead of 14+ parameters!
|
|
450
|
+
processor, manager = await setup_mcp_stdio(
|
|
451
|
+
config=MCPConfig(
|
|
452
|
+
servers=[MCPServerConfig(name="echo", command="uvx", args=["mcp-echo"])],
|
|
453
|
+
namespace="tools",
|
|
454
|
+
enable_caching=True,
|
|
455
|
+
cache_ttl=600,
|
|
456
|
+
)
|
|
457
|
+
)
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
**Key improvements:**
|
|
461
|
+
- ✅ **`@tool` decorator**: Shorter, cleaner than `@register_tool`
|
|
462
|
+
- ✅ **Auto-initialization**: No need for explicit `initialize()` calls
|
|
463
|
+
- ✅ **Type-safe tool listing**: `ToolInfo` objects instead of confusing tuples
|
|
464
|
+
- ✅ **Helpful errors**: Fuzzy matching suggestions when tools aren't found
|
|
465
|
+
- ✅ **MCPConfig**: Clean Pydantic model instead of 14+ parameters
|
|
466
|
+
- ✅ **Better discoverability**: Clear guidance on how to explore available tools
|
|
467
|
+
|
|
391
468
|
## Quick Decision Tree (Commit This to Memory)
|
|
392
469
|
|
|
393
470
|
```
|
|
@@ -397,8 +474,8 @@ asyncio.run(main())
|
|
|
397
474
|
│ ⚠️ No → IsolatedStrategy (sandboxed) │
|
|
398
475
|
│ │
|
|
399
476
|
│ Where do your tools live? │
|
|
400
|
-
│ 📦 Local → @
|
|
401
|
-
│ 🌐 Remote →
|
|
477
|
+
│ 📦 Local → @tool decorator │
|
|
478
|
+
│ 🌐 Remote → setup_mcp_* with MCPConfig │
|
|
402
479
|
╰──────────────────────────────────────────╯
|
|
403
480
|
```
|
|
404
481
|
|
|
@@ -408,22 +485,25 @@ asyncio.run(main())
|
|
|
408
485
|
|
|
409
486
|
Understanding the lifecycle helps you use CHUK Tool Processor correctly:
|
|
410
487
|
|
|
411
|
-
1.
|
|
488
|
+
1. **Auto-initialization** — Registry auto-initializes on first access (or call `await initialize()` explicitly)
|
|
412
489
|
2. Create a **`ToolProcessor(...)`** (or use the one returned by `setup_mcp_*`)
|
|
413
490
|
3. Use **`async with ToolProcessor() as p:`** to ensure cleanup
|
|
414
491
|
4. **`setup_mcp_*`** returns `(processor, manager)` — reuse that `processor`
|
|
415
492
|
5. If you need a custom registry, pass it explicitly to the strategy
|
|
416
493
|
6. You rarely need `get_default_registry()` unless you're composing advanced setups
|
|
417
494
|
|
|
418
|
-
|
|
495
|
+
**New in this version:** The registry auto-initializes when you create a `ToolProcessor` or access `get_default_registry()`, so you can skip the explicit `initialize()` call in most cases!
|
|
419
496
|
|
|
420
497
|
```python
|
|
421
|
-
#
|
|
422
|
-
|
|
498
|
+
# New simplified pattern (auto-initialization)
|
|
499
|
+
async with ToolProcessor() as p: # Auto-initializes on first use!
|
|
500
|
+
results = await p.process(llm_output)
|
|
501
|
+
# Processor automatically cleaned up on exit
|
|
423
502
|
|
|
424
|
-
|
|
503
|
+
# Traditional explicit pattern (still works)
|
|
504
|
+
await initialize() # Explicit initialization
|
|
505
|
+
async with ToolProcessor() as p:
|
|
425
506
|
results = await p.process(llm_output)
|
|
426
|
-
# Step 4: Processor automatically cleaned up on exit
|
|
427
507
|
```
|
|
428
508
|
|
|
429
509
|
## Production Features by Example
|
|
@@ -590,7 +670,41 @@ asyncio.run(main())
|
|
|
590
670
|
|
|
591
671
|
See `examples/04_mcp_integration/notion_oauth.py` for complete OAuth flow.
|
|
592
672
|
|
|
593
|
-
**Pattern 3: Local SQLite database via STDIO**
|
|
673
|
+
**Pattern 3: Local SQLite database via STDIO (New Clean API)**
|
|
674
|
+
```python
|
|
675
|
+
import asyncio
|
|
676
|
+
from chuk_tool_processor.mcp import setup_mcp_stdio, MCPConfig, MCPServerConfig
|
|
677
|
+
|
|
678
|
+
async def main():
|
|
679
|
+
# NEW: Clean Pydantic config approach (recommended!)
|
|
680
|
+
processor, manager = await setup_mcp_stdio(
|
|
681
|
+
config=MCPConfig(
|
|
682
|
+
servers=[
|
|
683
|
+
MCPServerConfig(
|
|
684
|
+
name="sqlite",
|
|
685
|
+
command="uvx",
|
|
686
|
+
args=["mcp-server-sqlite", "--db-path", "./app.db"],
|
|
687
|
+
)
|
|
688
|
+
],
|
|
689
|
+
namespace="db",
|
|
690
|
+
initialization_timeout=120.0, # First run downloads the package
|
|
691
|
+
enable_caching=True,
|
|
692
|
+
cache_ttl=600,
|
|
693
|
+
)
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
# Query your local database via MCP
|
|
697
|
+
results = await processor.process(
|
|
698
|
+
'<tool name="db.query" args=\'{"sql": "SELECT * FROM users LIMIT 10"}\'/>'
|
|
699
|
+
)
|
|
700
|
+
print(results[0].result)
|
|
701
|
+
|
|
702
|
+
asyncio.run(main())
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
<details>
|
|
706
|
+
<summary><strong>Legacy approach (still works)</strong></summary>
|
|
707
|
+
|
|
594
708
|
```python
|
|
595
709
|
import asyncio
|
|
596
710
|
import json
|
|
@@ -615,7 +729,7 @@ async def main():
|
|
|
615
729
|
config_file="mcp_config.json",
|
|
616
730
|
servers=["sqlite"],
|
|
617
731
|
namespace="db",
|
|
618
|
-
initialization_timeout=120.0
|
|
732
|
+
initialization_timeout=120.0
|
|
619
733
|
)
|
|
620
734
|
|
|
621
735
|
# Query your local database via MCP
|
|
@@ -626,6 +740,7 @@ async def main():
|
|
|
626
740
|
|
|
627
741
|
asyncio.run(main())
|
|
628
742
|
```
|
|
743
|
+
</details>
|
|
629
744
|
|
|
630
745
|
See `examples/04_mcp_integration/stdio_sqlite.py` for complete working example.
|
|
631
746
|
|
|
@@ -909,7 +1024,11 @@ register_fn_tool(get_current_time, namespace="utilities")
|
|
|
909
1024
|
For production tools, use Pydantic validation:
|
|
910
1025
|
|
|
911
1026
|
```python
|
|
912
|
-
|
|
1027
|
+
from chuk_tool_processor import tool
|
|
1028
|
+
from chuk_tool_processor.models import ValidatedTool
|
|
1029
|
+
from pydantic import BaseModel, Field
|
|
1030
|
+
|
|
1031
|
+
@tool(name="weather") # Clean @tool decorator
|
|
913
1032
|
class WeatherTool(ValidatedTool):
|
|
914
1033
|
class Arguments(BaseModel):
|
|
915
1034
|
location: str = Field(..., description="City name")
|
|
@@ -923,14 +1042,28 @@ class WeatherTool(ValidatedTool):
|
|
|
923
1042
|
return self.Result(temperature=22.5, conditions="Sunny")
|
|
924
1043
|
```
|
|
925
1044
|
|
|
1045
|
+
<details>
|
|
1046
|
+
<summary><strong>Alternative: Using @register_tool (still works)</strong></summary>
|
|
1047
|
+
|
|
1048
|
+
```python
|
|
1049
|
+
from chuk_tool_processor import register_tool
|
|
1050
|
+
|
|
1051
|
+
@register_tool(name="weather") # Longer form, but identical functionality
|
|
1052
|
+
class WeatherTool(ValidatedTool):
|
|
1053
|
+
# ... same as above
|
|
1054
|
+
```
|
|
1055
|
+
</details>
|
|
1056
|
+
|
|
926
1057
|
#### StreamingTool (Real-time Results)
|
|
927
1058
|
|
|
928
1059
|
For long-running operations that produce incremental results:
|
|
929
1060
|
|
|
930
1061
|
```python
|
|
1062
|
+
from chuk_tool_processor import tool
|
|
931
1063
|
from chuk_tool_processor.models import StreamingTool
|
|
1064
|
+
from pydantic import BaseModel
|
|
932
1065
|
|
|
933
|
-
@
|
|
1066
|
+
@tool(name="file_processor") # Clean @tool decorator
|
|
934
1067
|
class FileProcessor(StreamingTool):
|
|
935
1068
|
class Arguments(BaseModel):
|
|
936
1069
|
file_path: str
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "chuk-tool-processor"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.11"
|
|
8
8
|
description = "Async-native framework for registering, discovering, and executing tools referenced in LLM responses"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -146,6 +146,8 @@ disallow_incomplete_defs = true # NEW: Require either all or no params typed
|
|
|
146
146
|
module = [
|
|
147
147
|
"chuk_mcp.*",
|
|
148
148
|
"httpx_sse.*",
|
|
149
|
+
"opentelemetry.*",
|
|
150
|
+
"prometheus_client.*",
|
|
149
151
|
]
|
|
150
152
|
ignore_missing_imports = true
|
|
151
153
|
|
|
@@ -47,8 +47,9 @@ from chuk_tool_processor.mcp.stream_manager import StreamManager
|
|
|
47
47
|
from chuk_tool_processor.models.tool_call import ToolCall
|
|
48
48
|
from chuk_tool_processor.models.tool_result import ToolResult
|
|
49
49
|
|
|
50
|
-
# Registry functions
|
|
50
|
+
# Registry functions and types
|
|
51
51
|
from chuk_tool_processor.registry import (
|
|
52
|
+
ToolInfo,
|
|
52
53
|
ToolRegistryProvider,
|
|
53
54
|
get_default_registry,
|
|
54
55
|
initialize,
|
|
@@ -56,7 +57,7 @@ from chuk_tool_processor.registry import (
|
|
|
56
57
|
from chuk_tool_processor.registry.auto_register import register_fn_tool
|
|
57
58
|
|
|
58
59
|
# Decorators for registering tools
|
|
59
|
-
from chuk_tool_processor.registry.decorators import register_tool
|
|
60
|
+
from chuk_tool_processor.registry.decorators import register_tool, tool
|
|
60
61
|
|
|
61
62
|
# Type checking imports (not available at runtime)
|
|
62
63
|
if TYPE_CHECKING:
|
|
@@ -85,11 +86,13 @@ __all__ = [
|
|
|
85
86
|
"ToolCall",
|
|
86
87
|
"ToolResult",
|
|
87
88
|
# Registry
|
|
89
|
+
"ToolInfo",
|
|
88
90
|
"initialize",
|
|
89
91
|
"get_default_registry",
|
|
90
92
|
"ToolRegistryProvider",
|
|
91
93
|
# Decorators
|
|
92
94
|
"register_tool",
|
|
95
|
+
"tool",
|
|
93
96
|
"register_fn_tool",
|
|
94
97
|
# Execution strategies
|
|
95
98
|
"InProcessStrategy",
|
{chuk_tool_processor-0.10 → chuk_tool_processor-0.11}/src/chuk_tool_processor/core/exceptions.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# chuk_tool_processor/exceptions.py
|
|
2
|
+
from difflib import get_close_matches
|
|
2
3
|
from enum import Enum
|
|
3
|
-
from typing import Any
|
|
4
|
+
from typing import Any, cast
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
class ErrorCode(str, Enum):
|
|
@@ -73,13 +74,63 @@ class ToolProcessorError(Exception):
|
|
|
73
74
|
class ToolNotFoundError(ToolProcessorError):
|
|
74
75
|
"""Raised when a requested tool is not found in the registry."""
|
|
75
76
|
|
|
76
|
-
def __init__(
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
tool_name: str,
|
|
80
|
+
namespace: str = "default",
|
|
81
|
+
available_tools: list[tuple[str, str]] | list[str] | None = None,
|
|
82
|
+
available_namespaces: list[str] | None = None,
|
|
83
|
+
):
|
|
77
84
|
self.tool_name = tool_name
|
|
78
|
-
|
|
85
|
+
self.namespace = namespace
|
|
86
|
+
|
|
87
|
+
# Build helpful error message
|
|
88
|
+
message_parts = [f"Tool '{tool_name}' not found in namespace '{namespace}'"]
|
|
89
|
+
|
|
90
|
+
# Find similar tool names using fuzzy matching
|
|
91
|
+
similar_tools: list[str] = []
|
|
92
|
+
if available_tools:
|
|
93
|
+
# Handle both tuple format (namespace, name) and string format
|
|
94
|
+
if isinstance(available_tools[0], tuple):
|
|
95
|
+
# Type narrowing: cast to the expected type
|
|
96
|
+
tuple_tools = cast(list[tuple[str, str]], available_tools)
|
|
97
|
+
all_tool_names = [name for _, name in tuple_tools]
|
|
98
|
+
# Also check for namespace:name format
|
|
99
|
+
full_names = [f"{ns}:{name}" for ns, name in tuple_tools]
|
|
100
|
+
similar_in_namespace = get_close_matches(tool_name, all_tool_names, n=3, cutoff=0.6)
|
|
101
|
+
similar_full = get_close_matches(f"{namespace}:{tool_name}", full_names, n=3, cutoff=0.6)
|
|
102
|
+
similar_tools = list(similar_in_namespace) + list(similar_full)
|
|
103
|
+
else:
|
|
104
|
+
# Type narrowing: cast to the expected type
|
|
105
|
+
str_tools = cast(list[str], available_tools)
|
|
106
|
+
similar_tools = list(get_close_matches(tool_name, str_tools, n=3, cutoff=0.6))
|
|
107
|
+
|
|
108
|
+
if similar_tools:
|
|
109
|
+
message_parts.append(f"\n\nDid you mean: {', '.join(similar_tools)}?")
|
|
110
|
+
|
|
111
|
+
# Add available namespaces
|
|
112
|
+
if available_namespaces:
|
|
113
|
+
message_parts.append(f"\n\nAvailable namespaces: {', '.join(available_namespaces)}")
|
|
114
|
+
|
|
115
|
+
# Add helpful tip
|
|
116
|
+
message_parts.append(
|
|
117
|
+
"\n\nTip: Use `await registry.list_tools()` to see all registered tools, "
|
|
118
|
+
"or `await registry.list_namespaces()` to see available namespaces."
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
message = "".join(message_parts)
|
|
122
|
+
|
|
123
|
+
# Store details
|
|
124
|
+
details: dict[str, Any] = {"tool_name": tool_name, "namespace": namespace}
|
|
79
125
|
if available_tools:
|
|
80
126
|
details["available_tools"] = available_tools
|
|
127
|
+
if available_namespaces:
|
|
128
|
+
details["available_namespaces"] = available_namespaces
|
|
129
|
+
if similar_tools:
|
|
130
|
+
details["suggestions"] = similar_tools
|
|
131
|
+
|
|
81
132
|
super().__init__(
|
|
82
|
-
|
|
133
|
+
message,
|
|
83
134
|
code=ErrorCode.TOOL_NOT_FOUND,
|
|
84
135
|
details=details,
|
|
85
136
|
)
|