|
1 | 1 | """ |
2 | 2 | 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) |
23 | 3 |
|
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 |
29 | 8 |
|
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 | +""" |
33 | 12 |
|
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 |
42 | 15 |
|
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 | | - ) |
51 | 16 |
|
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: ... |
58 | 24 |
|
59 | 25 |
|
60 | | -def get_backend(): |
| 26 | +def get_backend() -> BackendProtocol: |
61 | 27 | """ |
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. |
64 | 36 | """ |
65 | 37 | from dotenv import load_dotenv |
66 | 38 | load_dotenv() |
67 | 39 |
|
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: |
71 | 43 | try: |
72 | 44 | 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)") |
75 | 47 | return backend |
76 | 48 | 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}") |
80 | 50 | 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 |
87 | 100 |
|
88 | 101 |
|
89 | | -# For backwards compatibility |
90 | | -__all__ = ["MockAgentBackend", "get_backend"] |
| 102 | +# Backwards compatibility |
| 103 | +__all__ = ["get_backend", "BackendProtocol"] |
0 commit comments