forked from cleveragents/cleveragents-core
feat(change): add tool router for providers
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
# Tool Call Router
|
||||
|
||||
The **ToolCallRouter** translates between LLM provider-specific tool call formats and the internal `ToolRunner`. It supports OpenAI, Anthropic, and LangChain formats, producing normalized `NormalizedToolCallResult` objects regardless of the input format.
|
||||
|
||||
## Provider Format Mapping
|
||||
|
||||
| Provider | Tool Call Shape | Arguments Key | Arguments Type |
|
||||
|------------|-----------------|---------------|----------------|
|
||||
| **OpenAI** | `{"name": "...", "arguments": "..."}` | `arguments` | JSON string |
|
||||
| **Anthropic** | `{"name": "...", "input": {...}}` | `input` | dict |
|
||||
| **LangChain** | `{"name": "...", "type": "tool_call", "args": {...}}` | `args` | dict |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Provider (OpenAI/Anthropic/LangChain)
|
||||
|
|
||||
v
|
||||
ToolCallRouter.route(payload)
|
||||
|-- detect_provider_format(payload)
|
||||
|-- normalize_tool_call(payload) -> ToolCallRequest
|
||||
|-- generate_tool_call_id(plan_id, sequence)
|
||||
|-- ToolRunner.execute(name, args)
|
||||
|-- classify_tool_error(error) [if failed]
|
||||
|-- _check_is_validation(spec) [validation surfacing]
|
||||
v
|
||||
NormalizedToolCallResult
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Routing
|
||||
|
||||
```python
|
||||
from cleveragents.tool.registry import ToolRegistry
|
||||
from cleveragents.tool.runner import ToolRunner
|
||||
from cleveragents.tool.router import ToolCallRouter
|
||||
|
||||
registry = ToolRegistry()
|
||||
# ... register tools ...
|
||||
runner = ToolRunner(registry)
|
||||
router = ToolCallRouter(registry=registry, runner=runner, plan_id="plan-001")
|
||||
|
||||
# Route an OpenAI-format tool call
|
||||
result = router.route({
|
||||
"name": "my-ns/my-tool",
|
||||
"arguments": '{"key": "value"}'
|
||||
})
|
||||
print(result.result.success)
|
||||
print(result.tool_call_id)
|
||||
print(result.provider_format) # "openai"
|
||||
```
|
||||
|
||||
### Batch Routing
|
||||
|
||||
```python
|
||||
results = router.route_batch([
|
||||
{"name": "ns/tool-a", "arguments": '{"x": 1}'},
|
||||
{"name": "ns/tool-b", "input": {"y": 2}},
|
||||
])
|
||||
for r in results:
|
||||
print(f"{r.tool_name}: success={r.result.success}")
|
||||
```
|
||||
|
||||
### Streaming Execution
|
||||
|
||||
```python
|
||||
for update in router.route_streaming(payload):
|
||||
if isinstance(update, StreamingToolUpdate):
|
||||
print(f"Status: {update.status}")
|
||||
elif isinstance(update, NormalizedToolCallResult):
|
||||
print(f"Final: success={update.result.success}")
|
||||
```
|
||||
|
||||
### Schema Export
|
||||
|
||||
```python
|
||||
from cleveragents.tool.router import ProviderFormat
|
||||
|
||||
schemas = router.export_schemas(ProviderFormat.OPENAI)
|
||||
# Returns provider-normalized schemas for all registered tools
|
||||
```
|
||||
|
||||
## Stable ID Generation
|
||||
|
||||
Tool call IDs are generated deterministically from `plan_id + sequence`:
|
||||
|
||||
```python
|
||||
from cleveragents.tool.router import generate_tool_call_id
|
||||
|
||||
call_id = generate_tool_call_id("plan-001", 0)
|
||||
# Returns: "tc_<24-char-hex>"
|
||||
```
|
||||
|
||||
The same inputs always produce the same ID, enabling reproducible tracking.
|
||||
|
||||
## Error Classification
|
||||
|
||||
Errors are classified into structured categories:
|
||||
|
||||
| Category | Matches |
|
||||
|----------|---------|
|
||||
| `timeout` | "timeout", "timed out" |
|
||||
| `permission` | "permission", "access denied", "forbidden" |
|
||||
| `not_found` | "not found" |
|
||||
| `resource` | "resource", "memory", "disk" |
|
||||
| `schema` | "schema", "validation", "json" |
|
||||
| `parse` | "parse", "decode", "deserializ" |
|
||||
| `execution` | Everything else |
|
||||
| `unknown` | Empty error messages |
|
||||
|
||||
## Validation Surfacing
|
||||
|
||||
When a tool is detected as a validation (read-only tool with "valid" in its name), the router:
|
||||
|
||||
1. Sets `is_validation=True` on the result
|
||||
2. Extracts `passed` from the tool output
|
||||
3. Includes the `validation_mode` if available in the output schema
|
||||
|
||||
## Provider Metadata
|
||||
|
||||
The router captures provider-level metadata passed during routing:
|
||||
|
||||
```python
|
||||
result = router.route(payload, provider_metadata={
|
||||
"model": "gpt-4",
|
||||
"provider_id": "openai",
|
||||
"latency_ms": 150,
|
||||
})
|
||||
print(result.provider_metadata["model"]) # "gpt-4"
|
||||
```
|
||||
|
||||
## Schema Normalization
|
||||
|
||||
Schemas are normalized per provider with field pruning and description truncation:
|
||||
|
||||
| Provider | Schema Key | Default Max Description |
|
||||
|----------|-----------|------------------------|
|
||||
| OpenAI | `parameters` | 1024 chars |
|
||||
| Anthropic | `input_schema` | 1024 chars |
|
||||
| LangChain | `args_schema` | 1024 chars |
|
||||
Reference in New Issue
Block a user