hanzo 0.3.22__py3-none-any.whl → 0.3.23__py3-none-any.whl
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.
Potentially problematic release.
This version of hanzo might be problematic. Click here for more details.
- hanzo/base_agent.py +517 -0
- hanzo/batch_orchestrator.py +988 -0
- hanzo/commands/repl.py +5 -2
- hanzo/dev.py +406 -248
- hanzo/fallback_handler.py +78 -52
- hanzo/memory_manager.py +145 -122
- hanzo/model_registry.py +399 -0
- hanzo/rate_limiter.py +59 -74
- hanzo/streaming.py +91 -70
- {hanzo-0.3.22.dist-info → hanzo-0.3.23.dist-info}/METADATA +1 -1
- {hanzo-0.3.22.dist-info → hanzo-0.3.23.dist-info}/RECORD +13 -10
- {hanzo-0.3.22.dist-info → hanzo-0.3.23.dist-info}/WHEEL +0 -0
- {hanzo-0.3.22.dist-info → hanzo-0.3.23.dist-info}/entry_points.txt +0 -0
hanzo/fallback_handler.py
CHANGED
|
@@ -6,22 +6,25 @@ Automatically tries available AI options when primary fails.
|
|
|
6
6
|
import os
|
|
7
7
|
import shutil
|
|
8
8
|
import subprocess
|
|
9
|
-
from typing import
|
|
9
|
+
from typing import Any, Dict, Optional
|
|
10
10
|
from pathlib import Path
|
|
11
11
|
|
|
12
|
+
|
|
12
13
|
class FallbackHandler:
|
|
13
14
|
"""Handles automatic fallback to available AI options."""
|
|
14
|
-
|
|
15
|
+
|
|
15
16
|
def __init__(self):
|
|
16
17
|
self.available_options = self._detect_available_options()
|
|
17
18
|
self.fallback_order = self._determine_fallback_order()
|
|
18
|
-
|
|
19
|
+
|
|
19
20
|
def _detect_available_options(self) -> Dict[str, bool]:
|
|
20
21
|
"""Detect which AI options are available."""
|
|
21
22
|
options = {
|
|
22
23
|
"openai_api": bool(os.getenv("OPENAI_API_KEY")),
|
|
23
24
|
"anthropic_api": bool(os.getenv("ANTHROPIC_API_KEY")),
|
|
24
|
-
"google_api": bool(
|
|
25
|
+
"google_api": bool(
|
|
26
|
+
os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
|
|
27
|
+
),
|
|
25
28
|
"openai_cli": shutil.which("openai") is not None,
|
|
26
29
|
"claude_cli": shutil.which("claude") is not None,
|
|
27
30
|
"gemini_cli": shutil.which("gemini") is not None,
|
|
@@ -30,11 +33,12 @@ class FallbackHandler:
|
|
|
30
33
|
"free_apis": True, # Always available (Codestral, StarCoder)
|
|
31
34
|
}
|
|
32
35
|
return options
|
|
33
|
-
|
|
36
|
+
|
|
34
37
|
def _check_ollama(self) -> bool:
|
|
35
38
|
"""Check if Ollama is running and has models."""
|
|
36
39
|
try:
|
|
37
40
|
import httpx
|
|
41
|
+
|
|
38
42
|
with httpx.Client(timeout=2.0) as client:
|
|
39
43
|
response = client.get("http://localhost:11434/api/tags")
|
|
40
44
|
if response.status_code == 200:
|
|
@@ -43,11 +47,11 @@ class FallbackHandler:
|
|
|
43
47
|
except:
|
|
44
48
|
pass
|
|
45
49
|
return False
|
|
46
|
-
|
|
50
|
+
|
|
47
51
|
def _determine_fallback_order(self) -> list:
|
|
48
52
|
"""Determine the order of fallback options based on availability."""
|
|
49
53
|
order = []
|
|
50
|
-
|
|
54
|
+
|
|
51
55
|
# Priority 1: API keys (fastest, most reliable)
|
|
52
56
|
if self.available_options["openai_api"]:
|
|
53
57
|
order.append(("openai_api", "gpt-4"))
|
|
@@ -55,7 +59,7 @@ class FallbackHandler:
|
|
|
55
59
|
order.append(("anthropic_api", "claude-3-5-sonnet"))
|
|
56
60
|
if self.available_options["google_api"]:
|
|
57
61
|
order.append(("google_api", "gemini-pro"))
|
|
58
|
-
|
|
62
|
+
|
|
59
63
|
# Priority 2: CLI tools (no API key needed)
|
|
60
64
|
if self.available_options["openai_cli"]:
|
|
61
65
|
order.append(("openai_cli", "codex"))
|
|
@@ -63,65 +67,72 @@ class FallbackHandler:
|
|
|
63
67
|
order.append(("claude_cli", "claude-desktop"))
|
|
64
68
|
if self.available_options["gemini_cli"]:
|
|
65
69
|
order.append(("gemini_cli", "gemini"))
|
|
66
|
-
|
|
70
|
+
|
|
67
71
|
# Priority 3: Local models (free, but requires setup)
|
|
68
72
|
if self.available_options["ollama"]:
|
|
69
73
|
order.append(("ollama", "local:llama3.2"))
|
|
70
74
|
if self.available_options["hanzo_ide"]:
|
|
71
75
|
order.append(("hanzo_ide", "hanzo-ide"))
|
|
72
|
-
|
|
76
|
+
|
|
73
77
|
# Priority 4: Free cloud APIs (rate limited)
|
|
74
78
|
if self.available_options["free_apis"]:
|
|
75
79
|
order.append(("free_api", "codestral-free"))
|
|
76
80
|
order.append(("free_api", "starcoder2"))
|
|
77
|
-
|
|
81
|
+
|
|
78
82
|
return order
|
|
79
|
-
|
|
83
|
+
|
|
80
84
|
def get_best_option(self) -> Optional[tuple]:
|
|
81
85
|
"""Get the best available AI option."""
|
|
82
86
|
if self.fallback_order:
|
|
83
87
|
return self.fallback_order[0]
|
|
84
88
|
return None
|
|
85
|
-
|
|
89
|
+
|
|
86
90
|
def get_next_option(self, failed_option: str) -> Optional[tuple]:
|
|
87
91
|
"""Get the next fallback option after one fails."""
|
|
88
92
|
for i, (option_type, model) in enumerate(self.fallback_order):
|
|
89
93
|
if model == failed_option and i + 1 < len(self.fallback_order):
|
|
90
94
|
return self.fallback_order[i + 1]
|
|
91
95
|
return None
|
|
92
|
-
|
|
96
|
+
|
|
93
97
|
def suggest_setup(self) -> str:
|
|
94
98
|
"""Suggest setup instructions for unavailable options."""
|
|
95
99
|
suggestions = []
|
|
96
|
-
|
|
100
|
+
|
|
97
101
|
if not self.available_options["openai_api"]:
|
|
98
102
|
suggestions.append("• Set OPENAI_API_KEY for GPT-4/GPT-5 access")
|
|
99
|
-
|
|
103
|
+
|
|
100
104
|
if not self.available_options["anthropic_api"]:
|
|
101
105
|
suggestions.append("• Set ANTHROPIC_API_KEY for Claude access")
|
|
102
|
-
|
|
106
|
+
|
|
103
107
|
if not self.available_options["ollama"]:
|
|
104
|
-
suggestions.append(
|
|
108
|
+
suggestions.append(
|
|
109
|
+
"• Install Ollama: curl -fsSL https://ollama.com/install.sh | sh"
|
|
110
|
+
)
|
|
105
111
|
suggestions.append(" Then run: ollama pull llama3.2")
|
|
106
|
-
|
|
112
|
+
|
|
107
113
|
if not self.available_options["openai_cli"]:
|
|
108
114
|
suggestions.append("• Install OpenAI CLI: pip install openai-cli")
|
|
109
|
-
|
|
115
|
+
|
|
110
116
|
if not self.available_options["claude_cli"]:
|
|
111
|
-
suggestions.append(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
suggestions.append(
|
|
118
|
+
"• Install Claude Desktop from https://claude.ai/download"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
"\n".join(suggestions) if suggestions else "All AI options are available!"
|
|
123
|
+
)
|
|
124
|
+
|
|
115
125
|
def print_status(self, console):
|
|
116
126
|
"""Print the current status of available AI options."""
|
|
117
127
|
from rich.table import Table
|
|
118
|
-
|
|
119
|
-
table = Table(
|
|
120
|
-
|
|
128
|
+
|
|
129
|
+
table = Table(
|
|
130
|
+
title="Available AI Options", show_header=True, header_style="bold magenta"
|
|
131
|
+
)
|
|
121
132
|
table.add_column("Option", style="cyan", width=20)
|
|
122
133
|
table.add_column("Status", width=10)
|
|
123
134
|
table.add_column("Model", width=20)
|
|
124
|
-
|
|
135
|
+
|
|
125
136
|
status_map = {
|
|
126
137
|
"openai_api": ("OpenAI API", "gpt-4"),
|
|
127
138
|
"anthropic_api": ("Anthropic API", "claude-3-5"),
|
|
@@ -133,17 +144,19 @@ class FallbackHandler:
|
|
|
133
144
|
"hanzo_ide": ("Hanzo IDE", "hanzo-dev"),
|
|
134
145
|
"free_apis": ("Free APIs", "codestral/starcoder"),
|
|
135
146
|
}
|
|
136
|
-
|
|
147
|
+
|
|
137
148
|
for key, available in self.available_options.items():
|
|
138
149
|
if key in status_map:
|
|
139
150
|
name, model = status_map[key]
|
|
140
151
|
status = "✅" if available else "❌"
|
|
141
152
|
table.add_row(name, status, model if available else "Not available")
|
|
142
|
-
|
|
153
|
+
|
|
143
154
|
console.print(table)
|
|
144
|
-
|
|
155
|
+
|
|
145
156
|
if self.fallback_order:
|
|
146
|
-
console.print(
|
|
157
|
+
console.print(
|
|
158
|
+
f"\n[green]Primary option: {self.fallback_order[0][1]}[/green]"
|
|
159
|
+
)
|
|
147
160
|
if len(self.fallback_order) > 1:
|
|
148
161
|
fallbacks = ", ".join([opt[1] for opt in self.fallback_order[1:]])
|
|
149
162
|
console.print(f"[yellow]Fallback options: {fallbacks}[/yellow]")
|
|
@@ -159,91 +172,104 @@ async def smart_chat(message: str, console=None) -> Optional[str]:
|
|
|
159
172
|
Returns the AI response or None if all options fail.
|
|
160
173
|
"""
|
|
161
174
|
from .rate_limiter import smart_limiter
|
|
162
|
-
|
|
175
|
+
|
|
163
176
|
handler = FallbackHandler()
|
|
164
|
-
|
|
177
|
+
|
|
165
178
|
if console:
|
|
166
179
|
console.print("\n[dim]Detecting available AI options...[/dim]")
|
|
167
|
-
|
|
180
|
+
|
|
168
181
|
best_option = handler.get_best_option()
|
|
169
182
|
if not best_option:
|
|
170
183
|
if console:
|
|
171
184
|
handler.print_status(console)
|
|
172
185
|
return None
|
|
173
|
-
|
|
186
|
+
|
|
174
187
|
option_type, model = best_option
|
|
175
|
-
|
|
188
|
+
|
|
176
189
|
# Try the primary option with rate limiting
|
|
177
190
|
try:
|
|
178
191
|
if option_type == "openai_api":
|
|
192
|
+
|
|
179
193
|
async def call_openai():
|
|
180
194
|
from openai import AsyncOpenAI
|
|
195
|
+
|
|
181
196
|
client = AsyncOpenAI()
|
|
182
197
|
response = await client.chat.completions.create(
|
|
183
198
|
model="gpt-4",
|
|
184
199
|
messages=[{"role": "user", "content": message}],
|
|
185
|
-
max_tokens=500
|
|
200
|
+
max_tokens=500,
|
|
186
201
|
)
|
|
187
202
|
return response.choices[0].message.content
|
|
188
|
-
|
|
203
|
+
|
|
189
204
|
return await smart_limiter.execute_with_limit("openai", call_openai)
|
|
190
|
-
|
|
205
|
+
|
|
191
206
|
elif option_type == "anthropic_api":
|
|
192
207
|
from anthropic import AsyncAnthropic
|
|
208
|
+
|
|
193
209
|
client = AsyncAnthropic()
|
|
194
210
|
response = await client.messages.create(
|
|
195
211
|
model="claude-3-5-sonnet-20241022",
|
|
196
212
|
messages=[{"role": "user", "content": message}],
|
|
197
|
-
max_tokens=500
|
|
213
|
+
max_tokens=500,
|
|
198
214
|
)
|
|
199
215
|
return response.content[0].text
|
|
200
|
-
|
|
216
|
+
|
|
201
217
|
elif option_type == "openai_cli":
|
|
202
218
|
# Use OpenAI CLI
|
|
203
219
|
result = subprocess.run(
|
|
204
|
-
[
|
|
220
|
+
[
|
|
221
|
+
"openai",
|
|
222
|
+
"api",
|
|
223
|
+
"chat.completions.create",
|
|
224
|
+
"-m",
|
|
225
|
+
"gpt-4",
|
|
226
|
+
"-g",
|
|
227
|
+
message,
|
|
228
|
+
],
|
|
205
229
|
capture_output=True,
|
|
206
230
|
text=True,
|
|
207
|
-
timeout=30
|
|
231
|
+
timeout=30,
|
|
208
232
|
)
|
|
209
233
|
if result.returncode == 0:
|
|
210
234
|
return result.stdout.strip()
|
|
211
|
-
|
|
235
|
+
|
|
212
236
|
elif option_type == "ollama":
|
|
213
237
|
# Use Ollama
|
|
214
238
|
import httpx
|
|
239
|
+
|
|
215
240
|
async with httpx.AsyncClient() as client:
|
|
216
241
|
response = await client.post(
|
|
217
242
|
"http://localhost:11434/api/generate",
|
|
218
243
|
json={"model": "llama3.2", "prompt": message, "stream": False},
|
|
219
|
-
timeout=30.0
|
|
244
|
+
timeout=30.0,
|
|
220
245
|
)
|
|
221
246
|
if response.status_code == 200:
|
|
222
247
|
return response.json().get("response", "")
|
|
223
|
-
|
|
248
|
+
|
|
224
249
|
elif option_type == "free_api":
|
|
225
250
|
# Try free Codestral API
|
|
226
251
|
import httpx
|
|
252
|
+
|
|
227
253
|
async with httpx.AsyncClient() as client:
|
|
228
254
|
response = await client.post(
|
|
229
255
|
"https://codestral.mistral.ai/v1/fim/completions",
|
|
230
256
|
headers={"Content-Type": "application/json"},
|
|
231
257
|
json={"prompt": message, "suffix": "", "max_tokens": 500},
|
|
232
|
-
timeout=30.0
|
|
258
|
+
timeout=30.0,
|
|
233
259
|
)
|
|
234
260
|
if response.status_code == 200:
|
|
235
261
|
return response.json().get("choices", [{}])[0].get("text", "")
|
|
236
|
-
|
|
262
|
+
|
|
237
263
|
except Exception as e:
|
|
238
264
|
if console:
|
|
239
265
|
console.print(f"[yellow]Primary option {model} failed: {e}[/yellow]")
|
|
240
266
|
console.print("[dim]Trying fallback...[/dim]")
|
|
241
|
-
|
|
267
|
+
|
|
242
268
|
# Try next fallback
|
|
243
269
|
next_option = handler.get_next_option(model)
|
|
244
270
|
if next_option:
|
|
245
271
|
# Recursively try the next option
|
|
246
272
|
handler.fallback_order.remove(best_option)
|
|
247
273
|
return await smart_chat(message, console)
|
|
248
|
-
|
|
249
|
-
return None
|
|
274
|
+
|
|
275
|
+
return None
|