348 lines
9.7 KiB
Markdown
348 lines
9.7 KiB
Markdown
# Migration to Model-Level Tool Routing
|
||
|
||
**Date**: 2025-11-23
|
||
**Status**: Complete
|
||
**Impact**: Simplified architecture, LLM decides tool usage
|
||
|
||
## Summary
|
||
|
||
Removed application-level routing (`use_agent` parameter) in favor of model-level routing where mistral:7b autonomously decides whether to use tools or answer directly.
|
||
|
||
## Architectural Change
|
||
|
||
### Before (Application-Level Routing):
|
||
```python
|
||
# AI Controller decides routing
|
||
if request.use_agent:
|
||
→ Route to agent (mistral:7b with tools)
|
||
else:
|
||
→ Direct Ollama call (any model)
|
||
```
|
||
|
||
**Problem**: Application layer must decide which queries need tools
|
||
|
||
### After (Model-Level Routing):
|
||
```python
|
||
# Always route through agent, LLM decides tool usage
|
||
→ Unified Agent (mistral:7b with tools)
|
||
→ LLM analyzes query autonomously
|
||
→ LLM decides: use tools OR answer directly
|
||
```
|
||
|
||
**Solution**: LLM understands context and decides intelligently
|
||
|
||
## Why This is Better
|
||
|
||
### ✅ LLM Already Has This Capability
|
||
|
||
LangGraph's `create_react_agent` means:
|
||
- mistral:7b sees available tools during generation
|
||
- mistral:7b outputs tool calls when needed
|
||
- mistral:7b answers directly when tools aren't needed
|
||
- **No application-level classification required**
|
||
|
||
### ✅ Simpler Code
|
||
|
||
**Removed**:
|
||
- `use_agent: bool` parameter from request schema
|
||
- Conditional routing logic in ai_controller.py
|
||
- Need to document when to use `use_agent=true`
|
||
|
||
**Result**: Single code path for all requests
|
||
|
||
### ✅ More Intelligent
|
||
|
||
The LLM understands nuance better than boolean flags:
|
||
|
||
| Query | LLM Decision | Application Would Have |
|
||
|-------|--------------|------------------------|
|
||
| "What is Docker?" | Answer directly (no tools) | ❌ Might route wrong |
|
||
| "Is core-api running?" | Use tool (needs real data) | ✅ Correct |
|
||
| "List services and explain what Docker is" | Use tool + knowledge | ✅ Handles complexity |
|
||
|
||
### ✅ Consistent UX
|
||
|
||
- Always get thinking indicators `[💭 Analyzing...]`
|
||
- Always see tool usage `[🔧 Checking services...]`
|
||
- More transparent reasoning process
|
||
|
||
### ✅ Perfect for Homelab Context
|
||
|
||
- **Token usage doesn't matter** - Running locally on Ollama (free)
|
||
- **Latency increase minimal** - ~1-2s extra for simple queries
|
||
- **Flexibility matters more** - Edge cases handled automatically
|
||
|
||
## Implementation Changes
|
||
|
||
### 1. Removed `use_agent` Parameter
|
||
|
||
**File**: [src/api/v1/schemas.py](../../services/core-api/src/api/v1/schemas.py:44)
|
||
|
||
```python
|
||
# REMOVED
|
||
use_agent: bool = Field(
|
||
default=True,
|
||
description="Use intelligent agent with tool calling and reasoning (recommended)"
|
||
)
|
||
```
|
||
|
||
Now all requests go through agent by default.
|
||
|
||
### 2. Simplified AI Controller
|
||
|
||
**File**: [src/controllers/ai_controller.py](../../services/core-api/src/controllers/ai_controller.py:307-373)
|
||
|
||
```python
|
||
# Before
|
||
if request.use_agent and AGENT_AVAILABLE:
|
||
# Route to agent
|
||
else:
|
||
# Direct Ollama
|
||
|
||
# After
|
||
if AGENT_AVAILABLE:
|
||
try:
|
||
# Always route through agent
|
||
# mistral:7b decides tool usage
|
||
except Exception as e:
|
||
# Fallback to direct Ollama if agent fails
|
||
```
|
||
|
||
Added try-except for graceful fallback if agent initialization fails.
|
||
|
||
### 3. Maintained Fallback
|
||
|
||
If agent is unavailable or fails:
|
||
- Falls back to direct Ollama call
|
||
- Uses requested model (gemma:2b, gemma:7b, etc.)
|
||
- No intelligent tool routing, just basic chat
|
||
|
||
## How It Works
|
||
|
||
### LangGraph ReAct Loop
|
||
|
||
```
|
||
User Query
|
||
↓
|
||
mistral:7b (with bound tools)
|
||
↓
|
||
[Thought] Analyze query + available tools
|
||
↓
|
||
[Decision] Does this need a tool?
|
||
├─→ NO → Generate answer directly
|
||
└─→ YES → Call tool(s) → Get results → Synthesize answer
|
||
```
|
||
|
||
The model sees tool descriptions and autonomously decides:
|
||
|
||
```python
|
||
# Tools are bound to the LLM
|
||
llm_with_tools = ChatOllama(model="mistral:7b").bind_tools(tools)
|
||
|
||
# LLM output contains tool_calls if it wants to use tools
|
||
response = llm_with_tools.invoke(messages)
|
||
|
||
if response.tool_calls:
|
||
# Execute tools
|
||
else:
|
||
# Return answer directly
|
||
```
|
||
|
||
**Key Point**: The application doesn't decide tool usage - it just checks if the LLM outputted tool calls.
|
||
|
||
## Test Results
|
||
|
||
All query types work correctly with mistral:7b deciding autonomously:
|
||
|
||
### Test 1: Simple Math (No Tools)
|
||
```json
|
||
Query: "What is 2+2?"
|
||
Response: "The sum of 2+2 is 4."
|
||
Tool Calls: None ✓
|
||
Time: ~2s
|
||
```
|
||
|
||
### Test 2: Infrastructure Query (Needs Tools)
|
||
```json
|
||
Query: "List all running services"
|
||
Response: [Detailed service list with ports]
|
||
Tool Calls: list_services ✓
|
||
Time: ~5s
|
||
```
|
||
|
||
### Test 3: Knowledge Question (No Tools)
|
||
```json
|
||
Query: "What is Docker?"
|
||
Response: [Detailed Docker explanation]
|
||
Tool Calls: None ✓
|
||
Time: ~2s
|
||
```
|
||
|
||
### Test 4: Streaming with Tools
|
||
```
|
||
Query: "Check service health for core-api"
|
||
Stream: [💭 Analyzing...] → "To check the health status..."
|
||
Tool Calls: check_service_health ✓
|
||
Time: ~4s
|
||
```
|
||
|
||
## Performance Impact
|
||
|
||
### Latency Comparison
|
||
|
||
| Query Type | Before (use_agent=false) | After (always agent) | Delta |
|
||
|------------|-------------------------|---------------------|-------|
|
||
| Simple math | ~1s (gemma:2b direct) | ~2s (mistral:7b) | +1s |
|
||
| Knowledge | ~1-2s (gemma:7b direct) | ~2s (mistral:7b) | ~0s |
|
||
| Tool needed | ~5s (mistral:7b agent) | ~5s (mistral:7b) | 0s |
|
||
| Multi-tool | ~10s (mistral:7b agent) | ~10s (mistral:7b) | 0s |
|
||
|
||
**Verdict**: Minimal impact (<2s for simple queries), acceptable for homelab use
|
||
|
||
### Token Usage
|
||
|
||
- Agent adds reasoning tokens (~100-200 extra per request)
|
||
- **Impact**: Zero (local Ollama, tokens are free)
|
||
|
||
### Memory Usage
|
||
|
||
- Consistent: Always uses mistral:7b (~4GB when loaded)
|
||
- Before: Mixed (gemma:2b ~1GB, gemma:7b ~3GB, mistral:7b ~4GB)
|
||
- **Result**: More predictable resource usage
|
||
|
||
## Benefits Summary
|
||
|
||
| Aspect | Benefit |
|
||
|--------|---------|
|
||
| **Code Complexity** | Reduced - single code path |
|
||
| **Maintainability** | Improved - less conditional logic |
|
||
| **Flexibility** | Increased - LLM handles edge cases |
|
||
| **User Experience** | Consistent - always see reasoning |
|
||
| **Performance** | Acceptable - ~1-2s increase for simple queries |
|
||
| **Context Awareness** | Better - LLM understands nuance |
|
||
|
||
## OpenAI Compatibility
|
||
|
||
Still fully compatible with OpenAI clients:
|
||
|
||
```bash
|
||
# Works with any OpenAI-compatible client
|
||
curl -X POST http://api.schweitz.net/v1/chat/completions \
|
||
-H "Content-Type: application/json" \
|
||
-d '{
|
||
"model": "gpt-3.5-turbo",
|
||
"messages": [{"role": "user", "content": "List services"}],
|
||
"stream": true
|
||
}'
|
||
```
|
||
|
||
**No `use_agent` parameter needed** - agent is transparent to client
|
||
|
||
## Migration for Clients
|
||
|
||
### Before
|
||
```python
|
||
# Client had to know when to use agent
|
||
response = client.chat.completions.create(
|
||
model="gpt-3.5-turbo",
|
||
messages=[{"role": "user", "content": "List services"}],
|
||
extra_body={"use_agent": True} # Had to specify
|
||
)
|
||
```
|
||
|
||
### After
|
||
```python
|
||
# Client doesn't need to know about agent
|
||
response = client.chat.completions.create(
|
||
model="gpt-3.5-turbo",
|
||
messages=[{"role": "user", "content": "List services"}]
|
||
# Agent automatically handles everything
|
||
)
|
||
```
|
||
|
||
**Migration**: Remove `use_agent` parameter from client code - it's ignored now
|
||
|
||
## Fallback Behavior
|
||
|
||
If agent fails to initialize or encounters an error:
|
||
|
||
```python
|
||
try:
|
||
# Route through agent
|
||
response = await agent.chat(...)
|
||
except Exception as e:
|
||
logger.error(f"Agent failed, falling back to direct Ollama: {e}")
|
||
# Fall through to direct Ollama call
|
||
# Uses requested model without tool capabilities
|
||
```
|
||
|
||
Ensures service remains available even if agent has issues.
|
||
|
||
## Research Findings
|
||
|
||
From LangChain/LangGraph best practices:
|
||
|
||
1. **Tool calling is model-level** - LLMs natively support tool calling, application should just expose tools
|
||
2. **ReAct pattern** - LangGraph's `create_react_agent` implements Reason+Act loop where LLM decides actions
|
||
3. **Simpler is better** - Industry consensus is to let LLM decide tool usage rather than hardcode routing
|
||
4. **`bind_tools()` vs routing** - Use `bind_tools()` for flexibility, use routing only when needed (cost, latency critical)
|
||
|
||
For homelab context where tokens are free and flexibility matters, model-level routing is the clear winner.
|
||
|
||
## Future Enhancements
|
||
|
||
### 1. Model Routing (Optional)
|
||
|
||
Could add intelligent model selection:
|
||
|
||
```python
|
||
# Agent detects task type
|
||
if task_type == "code":
|
||
use codestral:latest
|
||
elif task_type == "analysis":
|
||
use mixtral:8x7b
|
||
else:
|
||
use mistral:7b (default)
|
||
```
|
||
|
||
### 2. Tool Result Caching
|
||
|
||
Cache infrastructure queries:
|
||
- Service list (60s TTL)
|
||
- Domain list (5min TTL)
|
||
- Reduces repeated tool calls
|
||
|
||
### 3. Parallel Tool Execution
|
||
|
||
When agent needs multiple independent tools:
|
||
```python
|
||
# Sequential: 3 tools × 2s = 6s
|
||
# Parallel: max(tool times) = ~2s
|
||
```
|
||
|
||
## Documentation Updates Needed
|
||
|
||
- [ ] Update API documentation to remove `use_agent`
|
||
- [ ] Update Open WebUI integration guide
|
||
- [ ] Add architecture diagrams showing model-level routing
|
||
- [x] Document test results and performance characteristics
|
||
|
||
## Conclusion
|
||
|
||
**Migration successful!** The system now:
|
||
- ✅ Uses model-level routing (LLM decides tool usage)
|
||
- ✅ Simpler codebase (removed `use_agent` parameter)
|
||
- ✅ More intelligent (LLM understands context)
|
||
- ✅ Consistent UX (always see reasoning)
|
||
- ✅ Maintains fallback (direct Ollama if agent fails)
|
||
- ✅ OpenAI-compatible (clients don't need to change)
|
||
|
||
The agent is now transparent to users - they just chat naturally and mistral:7b intelligently decides when to use tools.
|
||
|
||
## Related Files
|
||
|
||
- [AI Controller](../../services/core-api/src/controllers/ai_controller.py) - Simplified routing
|
||
- [Request Schema](../../services/core-api/src/api/v1/schemas.py) - Removed `use_agent`
|
||
- [Agent Orchestrator](../../services/core-api/src/agent/orchestrator.py) - Unchanged (already did model-level)
|
||
- [Agent Flow Diagrams](../architecture/agent-flow-diagrams.md) - Visual architecture
|