Skip to content

Commit 4cd8913

Browse files
committed
feat: Multi-backend support with fallback chain
Backend priority: 1. OpenRouter (OPENROUTER_API_KEY) - cloud LLMs, best quality 2. Agent Zero API (AGENTZERO_API_URL) - self-hosted instance 3. Local Deterministic - pattern matching, works offline New backends: - llm_providers/agentzero.py - connects to self-hosted Agent Zero - llm_providers/local.py - rule-based, no LLM required Security note: All backends go through the same security flow: User Input -> Backend -> tool_request -> ToolApprovalScreen -> execute The security interceptor is NOT bypassed regardless of backend choice. Tests: 36 passed
1 parent 4a8d2c3 commit 4cd8913

5 files changed

Lines changed: 532 additions & 101 deletions

File tree

.env.example

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,39 @@
1-
# OpenRouter API Key (required)
2-
# Get yours at: https://openrouter.ai/keys
3-
OPENROUTER_API_KEY=sk-or-v1-xxx
1+
# Agent Zero CLI Configuration
2+
# Copy to .env and fill in your values
43

5-
# Default model (optional)
6-
# Options: anthropic/claude-3-sonnet, openai/gpt-4-turbo, meta-llama/llama-3-70b
7-
OPENROUTER_MODEL=anthropic/claude-3-sonnet
4+
# ============================================
5+
# BACKEND PRIORITY (uses first available):
6+
# 1. OpenRouter (cloud LLMs)
7+
# 2. Agent Zero API (self-hosted)
8+
# 3. Local mode (no LLM, pattern matching)
9+
# ============================================
810

9-
# Security mode (optional)
10-
# Options: paranoid, balanced, god_mode
11+
# --- Option 1: OpenRouter (recommended for best quality) ---
12+
# Get API key at: https://openrouter.ai/keys
13+
# OPENROUTER_API_KEY=sk-or-v1-xxx
14+
15+
# Optional: specify models for load balancing (comma-separated)
16+
# OPENROUTER_MODELS=mistralai/devstral-2512:free,qwen/qwen3-coder:free
17+
18+
# --- Option 2: Self-hosted Agent Zero ---
19+
# URL to your Agent Zero instance
20+
# AGENTZERO_API_URL=http://localhost:50001/api_message
21+
# AGENTZERO_API_KEY=your-api-key-if-required
22+
23+
# --- Option 3: Local mode ---
24+
# No configuration needed - works offline with pattern matching
25+
# Limited functionality but no API required
26+
27+
# ============================================
28+
# SECURITY SETTINGS
29+
# ============================================
30+
31+
# Security mode: paranoid | balanced | god_mode
1132
AGENT_SECURITY_MODE=balanced
1233

13-
# Debug mode (optional)
34+
# ============================================
35+
# DEBUG
36+
# ============================================
37+
38+
# Enable debug logging
1439
AGENT_DEBUG=false

backend.py

Lines changed: 82 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,103 @@
11
"""
22
Backend factory for AgentZeroCLI
3-
Auto-detects available backends (OpenRouter or Mock)
4-
"""
5-
import os
6-
import asyncio
7-
8-
9-
class MockAgentBackend:
10-
"""
11-
Mock backend for offline/demo mode.
12-
Used when OPENROUTER_API_KEY is not set.
13-
"""
14-
15-
async def send_prompt(self, user_text: str):
16-
"""Simulate sending prompt and receiving stream of thoughts."""
17-
18-
yield {"type": "status", "content": "[MOCK MODE] Simulating response..."}
19-
await asyncio.sleep(0.3)
20-
21-
yield {"type": "thought", "content": f"Processing: '{user_text}'"}
22-
await asyncio.sleep(0.5)
233
24-
thoughts = [
25-
"Analyzing project structure...",
26-
"Found relevant files.",
27-
"Preparing response..."
28-
]
4+
Priority order:
5+
1. OpenRouter (if OPENROUTER_API_KEY set) - best quality, cloud LLMs
6+
2. Agent Zero API (if AGENTZERO_API_URL set) - self-hosted Agent Zero
7+
3. Local Deterministic (always available) - pattern matching, no LLM
298
30-
for thought in thoughts:
31-
yield {"type": "thought", "content": thought}
32-
await asyncio.sleep(0.4)
9+
All backends go through the same security flow:
10+
User Input → Backend → tool_request → ToolApprovalScreen → execute
11+
"""
3312

34-
# Simulate tool request
35-
yield {
36-
"type": "tool_request",
37-
"tool_name": "shell",
38-
"command": "ls -la",
39-
"reason": "List files to understand project structure.",
40-
"risk_level": "low"
41-
}
13+
import os
14+
from typing import Protocol, AsyncGenerator, Any
4215

43-
async def explain_risk(self, command: str):
44-
"""Simulate risk explanation."""
45-
await asyncio.sleep(0.5)
46-
return (
47-
f"[MOCK] Command: {command}\n"
48-
f"Risk Level: LOW\n"
49-
f"This is a simulated analysis."
50-
)
5116

52-
async def execute_tool(self, tool_name: str = "shell", command: str = "", cwd: str | None = None):
53-
"""Execute tool (real execution even in mock mode)."""
54-
from tools.executor import execute_tool as real_execute
55-
56-
async for event in real_execute(tool_name, command, cwd):
57-
yield event
17+
class BackendProtocol(Protocol):
18+
"""Protocol for all backends."""
19+
20+
async def send_prompt(self, user_text: str) -> AsyncGenerator[Any, None]: ...
21+
async def explain_risk(self, command: str) -> str: ...
22+
async def execute_tool(self, tool_name: str, command: str, cwd: str | None = None) -> AsyncGenerator[Any, None]: ...
23+
async def close(self) -> None: ...
5824

5925

60-
def get_backend():
26+
def get_backend() -> BackendProtocol:
6127
"""
62-
Factory function to get the appropriate backend.
63-
Returns OpenRouterBackend if API key is set, otherwise MockAgentBackend.
28+
Factory function to get the best available backend.
29+
30+
Priority:
31+
1. OpenRouter (cloud LLMs) - if OPENROUTER_API_KEY is set
32+
2. Agent Zero API (self-hosted) - if AGENTZERO_API_URL is set
33+
3. Local Deterministic - always available, pattern matching
34+
35+
All tool_requests go through ToolApprovalScreen regardless of backend.
6436
"""
6537
from dotenv import load_dotenv
6638
load_dotenv()
6739

68-
api_key = os.getenv("OPENROUTER_API_KEY")
69-
70-
if api_key:
40+
# Priority 1: OpenRouter (best quality, multiple models)
41+
openrouter_key = os.getenv("OPENROUTER_API_KEY")
42+
if openrouter_key:
7143
try:
7244
from llm_providers.openrouter import OpenRouterBackend
73-
backend = OpenRouterBackend(api_key=api_key)
74-
print(f"[green]✓[/green] OpenRouter connected ({len(backend.models)} models)")
45+
backend = OpenRouterBackend(api_key=openrouter_key)
46+
print(f"[OK] OpenRouter connected ({len(backend.models)} models)")
7547
return backend
7648
except ImportError as e:
77-
print(f"[yellow]⚠[/yellow] OpenRouter import failed: {e}")
78-
print("[yellow]⚠[/yellow] Falling back to mock backend")
79-
return MockAgentBackend()
49+
print(f"[WARN] OpenRouter import failed: {e}")
8050
except Exception as e:
81-
print(f"[yellow]⚠[/yellow] OpenRouter init failed: {e}")
82-
return MockAgentBackend()
83-
else:
84-
print("[yellow]⚠[/yellow] No API key - using mock backend")
85-
print("[dim]Set OPENROUTER_API_KEY in .env for real AI[/dim]")
86-
return MockAgentBackend()
51+
print(f"[WARN] OpenRouter init failed: {e}")
52+
53+
# Priority 2: Agent Zero API (self-hosted)
54+
agentzero_url = os.getenv("AGENTZERO_API_URL")
55+
if agentzero_url:
56+
try:
57+
from llm_providers.agentzero import AgentZeroBackend
58+
backend = AgentZeroBackend(api_url=agentzero_url)
59+
print(f"[OK] Agent Zero connected ({agentzero_url})")
60+
return backend
61+
except ImportError as e:
62+
print(f"[WARN] AgentZero import failed: {e}")
63+
except Exception as e:
64+
print(f"[WARN] AgentZero init failed: {e}")
65+
66+
# Priority 3: Local deterministic (always works)
67+
try:
68+
from llm_providers.local import LocalBackend
69+
backend = LocalBackend()
70+
print("[OK] Local mode (pattern matching, no LLM)")
71+
print("[INFO] For AI responses, set OPENROUTER_API_KEY or AGENTZERO_API_URL")
72+
return backend
73+
except ImportError:
74+
pass
75+
76+
# Fallback: Minimal mock (should never reach here)
77+
print("[WARN] No backend available, using minimal mock")
78+
return MinimalMockBackend()
79+
80+
81+
class MinimalMockBackend:
82+
"""Absolute minimal fallback - should never be used in practice."""
83+
84+
async def send_prompt(self, user_text: str):
85+
yield {
86+
"type": "final_response",
87+
"content": "No backend configured. Set OPENROUTER_API_KEY or AGENTZERO_API_URL in .env"
88+
}
89+
90+
async def explain_risk(self, command: str) -> str:
91+
return f"Cannot analyze '{command}' - no backend configured."
92+
93+
async def execute_tool(self, tool_name: str = "shell", command: str = "", cwd: str | None = None):
94+
from tools.executor import execute_tool as real_execute
95+
async for event in real_execute(tool_name, command, cwd):
96+
yield event
97+
98+
async def close(self):
99+
pass
87100

88101

89-
# For backwards compatibility
90-
__all__ = ["MockAgentBackend", "get_backend"]
102+
# Backwards compatibility
103+
__all__ = ["get_backend", "BackendProtocol"]

0 commit comments

Comments
 (0)