hanzo 0.3.24__py3-none-any.whl → 0.3.25__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/__init__.py +2 -2
- hanzo/cli.py +13 -5
- hanzo/commands/auth.py +206 -266
- hanzo/commands/auth_broken.py +377 -0
- hanzo/commands/chat.py +3 -0
- hanzo/interactive/enhanced_repl.py +513 -0
- hanzo/interactive/repl.py +2 -2
- hanzo/ui/__init__.py +13 -0
- hanzo/ui/inline_startup.py +136 -0
- hanzo/ui/startup.py +350 -0
- {hanzo-0.3.24.dist-info → hanzo-0.3.25.dist-info}/METADATA +1 -1
- {hanzo-0.3.24.dist-info → hanzo-0.3.25.dist-info}/RECORD +14 -9
- {hanzo-0.3.24.dist-info → hanzo-0.3.25.dist-info}/WHEEL +0 -0
- {hanzo-0.3.24.dist-info → hanzo-0.3.25.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
"""Enhanced REPL with model selection and authentication."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import httpx
|
|
6
|
+
import asyncio
|
|
7
|
+
from typing import Optional, Dict, Any
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.markdown import Markdown
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
from rich.text import Text
|
|
16
|
+
from rich import box
|
|
17
|
+
from prompt_toolkit import PromptSession
|
|
18
|
+
from prompt_toolkit.history import FileHistory
|
|
19
|
+
from prompt_toolkit.completion import WordCompleter
|
|
20
|
+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
21
|
+
from prompt_toolkit.formatted_text import HTML
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EnhancedHanzoREPL:
|
|
25
|
+
"""Enhanced REPL with model selection and authentication."""
|
|
26
|
+
|
|
27
|
+
# Available models
|
|
28
|
+
MODELS = {
|
|
29
|
+
# OpenAI
|
|
30
|
+
"gpt-4": "OpenAI GPT-4",
|
|
31
|
+
"gpt-4-turbo": "OpenAI GPT-4 Turbo",
|
|
32
|
+
"gpt-3.5-turbo": "OpenAI GPT-3.5 Turbo",
|
|
33
|
+
|
|
34
|
+
# Anthropic
|
|
35
|
+
"claude-3-opus": "Anthropic Claude 3 Opus",
|
|
36
|
+
"claude-3-sonnet": "Anthropic Claude 3 Sonnet",
|
|
37
|
+
"claude-3-haiku": "Anthropic Claude 3 Haiku",
|
|
38
|
+
"claude-2.1": "Anthropic Claude 2.1",
|
|
39
|
+
|
|
40
|
+
# Google
|
|
41
|
+
"gemini-pro": "Google Gemini Pro",
|
|
42
|
+
"gemini-pro-vision": "Google Gemini Pro Vision",
|
|
43
|
+
|
|
44
|
+
# Meta
|
|
45
|
+
"llama2-70b": "Meta Llama 2 70B",
|
|
46
|
+
"llama2-13b": "Meta Llama 2 13B",
|
|
47
|
+
"llama2-7b": "Meta Llama 2 7B",
|
|
48
|
+
"codellama-34b": "Meta Code Llama 34B",
|
|
49
|
+
|
|
50
|
+
# Mistral
|
|
51
|
+
"mistral-medium": "Mistral Medium",
|
|
52
|
+
"mistral-small": "Mistral Small",
|
|
53
|
+
"mixtral-8x7b": "Mixtral 8x7B",
|
|
54
|
+
|
|
55
|
+
# Local models
|
|
56
|
+
"local:llama2": "Local Llama 2",
|
|
57
|
+
"local:mistral": "Local Mistral",
|
|
58
|
+
"local:phi-2": "Local Phi-2",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
def __init__(self, console: Optional[Console] = None):
|
|
62
|
+
self.console = console or Console()
|
|
63
|
+
self.config_dir = Path.home() / ".hanzo"
|
|
64
|
+
self.config_file = self.config_dir / "config.json"
|
|
65
|
+
self.auth_file = self.config_dir / "auth.json"
|
|
66
|
+
|
|
67
|
+
# Load configuration
|
|
68
|
+
self.config = self.load_config()
|
|
69
|
+
self.auth = self.load_auth()
|
|
70
|
+
|
|
71
|
+
# Current model
|
|
72
|
+
self.current_model = self.config.get("default_model", "gpt-3.5-turbo")
|
|
73
|
+
|
|
74
|
+
# Setup session
|
|
75
|
+
self.session = PromptSession(
|
|
76
|
+
history=FileHistory(str(self.config_dir / ".repl_history")),
|
|
77
|
+
auto_suggest=AutoSuggestFromHistory(),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# Commands
|
|
81
|
+
self.commands = {
|
|
82
|
+
"help": self.show_help,
|
|
83
|
+
"exit": self.exit_repl,
|
|
84
|
+
"quit": self.exit_repl,
|
|
85
|
+
"clear": self.clear_screen,
|
|
86
|
+
"status": self.show_status,
|
|
87
|
+
"model": self.change_model,
|
|
88
|
+
"models": self.list_models,
|
|
89
|
+
"login": self.login,
|
|
90
|
+
"logout": self.logout,
|
|
91
|
+
"config": self.show_config,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
self.running = False
|
|
95
|
+
|
|
96
|
+
def load_config(self) -> Dict[str, Any]:
|
|
97
|
+
"""Load configuration from file."""
|
|
98
|
+
if self.config_file.exists():
|
|
99
|
+
try:
|
|
100
|
+
return json.loads(self.config_file.read_text())
|
|
101
|
+
except:
|
|
102
|
+
pass
|
|
103
|
+
return {}
|
|
104
|
+
|
|
105
|
+
def save_config(self):
|
|
106
|
+
"""Save configuration to file."""
|
|
107
|
+
self.config_dir.mkdir(exist_ok=True)
|
|
108
|
+
self.config_file.write_text(json.dumps(self.config, indent=2))
|
|
109
|
+
|
|
110
|
+
def load_auth(self) -> Dict[str, Any]:
|
|
111
|
+
"""Load authentication data."""
|
|
112
|
+
if self.auth_file.exists():
|
|
113
|
+
try:
|
|
114
|
+
return json.loads(self.auth_file.read_text())
|
|
115
|
+
except:
|
|
116
|
+
pass
|
|
117
|
+
return {}
|
|
118
|
+
|
|
119
|
+
def save_auth(self):
|
|
120
|
+
"""Save authentication data."""
|
|
121
|
+
self.config_dir.mkdir(exist_ok=True)
|
|
122
|
+
self.auth_file.write_text(json.dumps(self.auth, indent=2))
|
|
123
|
+
|
|
124
|
+
def get_prompt(self) -> str:
|
|
125
|
+
"""Get the simple prompt."""
|
|
126
|
+
# We'll use a simple > prompt, the box is handled by prompt_toolkit
|
|
127
|
+
return "> "
|
|
128
|
+
|
|
129
|
+
def is_authenticated(self) -> bool:
|
|
130
|
+
"""Check if user is authenticated."""
|
|
131
|
+
# Check for API key
|
|
132
|
+
if os.getenv("HANZO_API_KEY"):
|
|
133
|
+
return True
|
|
134
|
+
|
|
135
|
+
# Check auth file
|
|
136
|
+
if self.auth.get("api_key"):
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
# Check if logged in
|
|
140
|
+
if self.auth.get("logged_in"):
|
|
141
|
+
return True
|
|
142
|
+
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
def get_model_info(self):
|
|
146
|
+
"""Get current model info string."""
|
|
147
|
+
# Determine provider from model name
|
|
148
|
+
model = self.current_model
|
|
149
|
+
if model.startswith("gpt"):
|
|
150
|
+
provider = "openai"
|
|
151
|
+
elif model.startswith("claude"):
|
|
152
|
+
provider = "anthropic"
|
|
153
|
+
elif model.startswith("gemini"):
|
|
154
|
+
provider = "google"
|
|
155
|
+
elif model.startswith("llama") or model.startswith("codellama"):
|
|
156
|
+
provider = "meta"
|
|
157
|
+
elif model.startswith("mistral") or model.startswith("mixtral"):
|
|
158
|
+
provider = "mistral"
|
|
159
|
+
elif model.startswith("local:"):
|
|
160
|
+
provider = "local"
|
|
161
|
+
else:
|
|
162
|
+
provider = "unknown"
|
|
163
|
+
|
|
164
|
+
# Auth status
|
|
165
|
+
auth_status = "🔓" if self.is_authenticated() else "🔒"
|
|
166
|
+
|
|
167
|
+
return f"[dim]model: {provider}/{model} {auth_status}[/dim]"
|
|
168
|
+
|
|
169
|
+
async def run(self):
|
|
170
|
+
"""Run the enhanced REPL."""
|
|
171
|
+
self.running = True
|
|
172
|
+
|
|
173
|
+
# Setup completer
|
|
174
|
+
commands = list(self.commands.keys())
|
|
175
|
+
models = list(self.MODELS.keys())
|
|
176
|
+
cli_commands = ["chat", "ask", "agent", "node", "mcp", "network",
|
|
177
|
+
"auth", "config", "tools", "miner", "serve", "net",
|
|
178
|
+
"dev", "router"]
|
|
179
|
+
|
|
180
|
+
completer = WordCompleter(
|
|
181
|
+
commands + models + cli_commands,
|
|
182
|
+
ignore_case=True,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
while self.running:
|
|
186
|
+
try:
|
|
187
|
+
# Show model info above prompt
|
|
188
|
+
self.console.print(self.get_model_info())
|
|
189
|
+
|
|
190
|
+
# Get input with simple prompt
|
|
191
|
+
command = await self.session.prompt_async(
|
|
192
|
+
self.get_prompt(),
|
|
193
|
+
completer=completer
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if not command.strip():
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
# Handle slash commands
|
|
200
|
+
if command.startswith("/"):
|
|
201
|
+
await self.handle_slash_command(command[1:])
|
|
202
|
+
continue
|
|
203
|
+
|
|
204
|
+
# Parse command
|
|
205
|
+
parts = command.strip().split(maxsplit=1)
|
|
206
|
+
cmd = parts[0].lower()
|
|
207
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
208
|
+
|
|
209
|
+
# Execute command
|
|
210
|
+
if cmd in self.commands:
|
|
211
|
+
await self.commands[cmd](args)
|
|
212
|
+
elif cmd in cli_commands:
|
|
213
|
+
await self.execute_command(cmd, args)
|
|
214
|
+
else:
|
|
215
|
+
# Treat as chat message
|
|
216
|
+
await self.chat_with_ai(command)
|
|
217
|
+
|
|
218
|
+
except KeyboardInterrupt:
|
|
219
|
+
continue
|
|
220
|
+
except EOFError:
|
|
221
|
+
break
|
|
222
|
+
except Exception as e:
|
|
223
|
+
self.console.print(f"[red]Error: {e}[/red]")
|
|
224
|
+
|
|
225
|
+
async def handle_slash_command(self, command: str):
|
|
226
|
+
"""Handle slash commands like /model, /status, etc."""
|
|
227
|
+
parts = command.strip().split(maxsplit=1)
|
|
228
|
+
cmd = parts[0].lower()
|
|
229
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
230
|
+
|
|
231
|
+
# Map slash commands to regular commands
|
|
232
|
+
slash_map = {
|
|
233
|
+
"m": "model",
|
|
234
|
+
"s": "status",
|
|
235
|
+
"h": "help",
|
|
236
|
+
"q": "quit",
|
|
237
|
+
"c": "clear",
|
|
238
|
+
"models": "models",
|
|
239
|
+
"login": "login",
|
|
240
|
+
"logout": "logout",
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
mapped_cmd = slash_map.get(cmd, cmd)
|
|
244
|
+
|
|
245
|
+
if mapped_cmd in self.commands:
|
|
246
|
+
await self.commands[mapped_cmd](args)
|
|
247
|
+
else:
|
|
248
|
+
self.console.print(f"[yellow]Unknown command: /{cmd}[/yellow]")
|
|
249
|
+
self.console.print("[dim]Type /help for available commands[/dim]")
|
|
250
|
+
|
|
251
|
+
async def show_status(self, args: str = ""):
|
|
252
|
+
"""Show comprehensive status."""
|
|
253
|
+
# Create status table
|
|
254
|
+
table = Table(title="System Status", box=box.ROUNDED)
|
|
255
|
+
table.add_column("Component", style="cyan")
|
|
256
|
+
table.add_column("Status", style="green")
|
|
257
|
+
table.add_column("Details", style="dim")
|
|
258
|
+
|
|
259
|
+
# Authentication status
|
|
260
|
+
if self.is_authenticated():
|
|
261
|
+
auth_status = "✅ Authenticated"
|
|
262
|
+
auth_details = self.auth.get("email", "API Key configured")
|
|
263
|
+
else:
|
|
264
|
+
auth_status = "❌ Not authenticated"
|
|
265
|
+
auth_details = "Run /login to authenticate"
|
|
266
|
+
table.add_row("Authentication", auth_status, auth_details)
|
|
267
|
+
|
|
268
|
+
# Current model
|
|
269
|
+
model_name = self.MODELS.get(self.current_model, self.current_model)
|
|
270
|
+
table.add_row("Current Model", f"🤖 {self.current_model}", model_name)
|
|
271
|
+
|
|
272
|
+
# Router status
|
|
273
|
+
try:
|
|
274
|
+
response = httpx.get("http://localhost:4000/health", timeout=1)
|
|
275
|
+
router_status = "✅ Running" if response.status_code == 200 else "⚠️ Unhealthy"
|
|
276
|
+
router_details = "Port 4000"
|
|
277
|
+
except:
|
|
278
|
+
router_status = "❌ Offline"
|
|
279
|
+
router_details = "Run 'hanzo router start'"
|
|
280
|
+
table.add_row("Router", router_status, router_details)
|
|
281
|
+
|
|
282
|
+
# Node status
|
|
283
|
+
try:
|
|
284
|
+
response = httpx.get("http://localhost:8000/health", timeout=1)
|
|
285
|
+
node_status = "✅ Running" if response.status_code == 200 else "⚠️ Unhealthy"
|
|
286
|
+
node_details = "Port 8000"
|
|
287
|
+
except:
|
|
288
|
+
node_status = "❌ Offline"
|
|
289
|
+
node_details = "Run 'hanzo node start'"
|
|
290
|
+
table.add_row("Node", node_status, node_details)
|
|
291
|
+
|
|
292
|
+
# API endpoints
|
|
293
|
+
if os.getenv("HANZO_API_KEY"):
|
|
294
|
+
api_status = "✅ Configured"
|
|
295
|
+
api_details = "Using Hanzo Cloud API"
|
|
296
|
+
else:
|
|
297
|
+
api_status = "⚠️ Not configured"
|
|
298
|
+
api_details = "Set HANZO_API_KEY environment variable"
|
|
299
|
+
table.add_row("Cloud API", api_status, api_details)
|
|
300
|
+
|
|
301
|
+
self.console.print(table)
|
|
302
|
+
|
|
303
|
+
# Show additional info
|
|
304
|
+
if self.auth.get("last_login"):
|
|
305
|
+
self.console.print(f"\n[dim]Last login: {self.auth['last_login']}[/dim]")
|
|
306
|
+
|
|
307
|
+
async def change_model(self, args: str = ""):
|
|
308
|
+
"""Change the current model."""
|
|
309
|
+
if not args:
|
|
310
|
+
# Show model selection menu
|
|
311
|
+
await self.list_models("")
|
|
312
|
+
self.console.print("\n[cyan]Enter model name or number:[/cyan]")
|
|
313
|
+
|
|
314
|
+
# Get selection
|
|
315
|
+
try:
|
|
316
|
+
selection = await self.session.prompt_async("> ")
|
|
317
|
+
|
|
318
|
+
# Handle numeric selection
|
|
319
|
+
if selection.isdigit():
|
|
320
|
+
models_list = list(self.MODELS.keys())
|
|
321
|
+
idx = int(selection) - 1
|
|
322
|
+
if 0 <= idx < len(models_list):
|
|
323
|
+
args = models_list[idx]
|
|
324
|
+
else:
|
|
325
|
+
self.console.print("[red]Invalid selection[/red]")
|
|
326
|
+
return
|
|
327
|
+
else:
|
|
328
|
+
args = selection
|
|
329
|
+
except (KeyboardInterrupt, EOFError):
|
|
330
|
+
return
|
|
331
|
+
|
|
332
|
+
# Validate model
|
|
333
|
+
if args not in self.MODELS and not args.startswith("local:"):
|
|
334
|
+
self.console.print(f"[red]Unknown model: {args}[/red]")
|
|
335
|
+
self.console.print("[dim]Use /models to see available models[/dim]")
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
# Change model
|
|
339
|
+
self.current_model = args
|
|
340
|
+
self.config["default_model"] = args
|
|
341
|
+
self.save_config()
|
|
342
|
+
|
|
343
|
+
model_name = self.MODELS.get(args, args)
|
|
344
|
+
self.console.print(f"[green]✅ Switched to {model_name}[/green]")
|
|
345
|
+
|
|
346
|
+
async def list_models(self, args: str = ""):
|
|
347
|
+
"""List available models."""
|
|
348
|
+
table = Table(title="Available Models", box=box.ROUNDED)
|
|
349
|
+
table.add_column("#", style="dim")
|
|
350
|
+
table.add_column("Model ID", style="cyan")
|
|
351
|
+
table.add_column("Name", style="white")
|
|
352
|
+
table.add_column("Provider", style="yellow")
|
|
353
|
+
|
|
354
|
+
for i, (model_id, model_name) in enumerate(self.MODELS.items(), 1):
|
|
355
|
+
# Extract provider
|
|
356
|
+
if model_id.startswith("gpt"):
|
|
357
|
+
provider = "OpenAI"
|
|
358
|
+
elif model_id.startswith("claude"):
|
|
359
|
+
provider = "Anthropic"
|
|
360
|
+
elif model_id.startswith("gemini"):
|
|
361
|
+
provider = "Google"
|
|
362
|
+
elif model_id.startswith("llama") or model_id.startswith("codellama"):
|
|
363
|
+
provider = "Meta"
|
|
364
|
+
elif model_id.startswith("mistral") or model_id.startswith("mixtral"):
|
|
365
|
+
provider = "Mistral"
|
|
366
|
+
elif model_id.startswith("local:"):
|
|
367
|
+
provider = "Local"
|
|
368
|
+
else:
|
|
369
|
+
provider = "Other"
|
|
370
|
+
|
|
371
|
+
# Highlight current model
|
|
372
|
+
if model_id == self.current_model:
|
|
373
|
+
table.add_row(
|
|
374
|
+
str(i),
|
|
375
|
+
f"[bold green]→ {model_id}[/bold green]",
|
|
376
|
+
f"[bold]{model_name}[/bold]",
|
|
377
|
+
provider
|
|
378
|
+
)
|
|
379
|
+
else:
|
|
380
|
+
table.add_row(str(i), model_id, model_name, provider)
|
|
381
|
+
|
|
382
|
+
self.console.print(table)
|
|
383
|
+
self.console.print("\n[dim]Use /model <name> or /model <number> to switch[/dim]")
|
|
384
|
+
|
|
385
|
+
async def login(self, args: str = ""):
|
|
386
|
+
"""Login to Hanzo."""
|
|
387
|
+
self.console.print("[cyan]Hanzo Authentication[/cyan]\n")
|
|
388
|
+
|
|
389
|
+
# Check if already logged in
|
|
390
|
+
if self.is_authenticated():
|
|
391
|
+
self.console.print("[yellow]Already authenticated[/yellow]")
|
|
392
|
+
if self.auth.get("email"):
|
|
393
|
+
self.console.print(f"Logged in as: {self.auth['email']}")
|
|
394
|
+
return
|
|
395
|
+
|
|
396
|
+
# Get credentials
|
|
397
|
+
try:
|
|
398
|
+
# Email
|
|
399
|
+
email = await self.session.prompt_async("Email: ")
|
|
400
|
+
|
|
401
|
+
# Password (hidden)
|
|
402
|
+
from prompt_toolkit import prompt
|
|
403
|
+
password = prompt("Password: ", is_password=True)
|
|
404
|
+
|
|
405
|
+
# Attempt login
|
|
406
|
+
self.console.print("\n[dim]Authenticating...[/dim]")
|
|
407
|
+
|
|
408
|
+
# TODO: Implement actual authentication
|
|
409
|
+
# For now, simulate successful login
|
|
410
|
+
await asyncio.sleep(1)
|
|
411
|
+
|
|
412
|
+
# Save auth
|
|
413
|
+
self.auth["email"] = email
|
|
414
|
+
self.auth["logged_in"] = True
|
|
415
|
+
self.auth["last_login"] = datetime.now().isoformat()
|
|
416
|
+
self.save_auth()
|
|
417
|
+
|
|
418
|
+
self.console.print("[green]✅ Successfully logged in![/green]")
|
|
419
|
+
|
|
420
|
+
except (KeyboardInterrupt, EOFError):
|
|
421
|
+
self.console.print("\n[yellow]Login cancelled[/yellow]")
|
|
422
|
+
|
|
423
|
+
async def logout(self, args: str = ""):
|
|
424
|
+
"""Logout from Hanzo."""
|
|
425
|
+
if not self.is_authenticated():
|
|
426
|
+
self.console.print("[yellow]Not logged in[/yellow]")
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
# Clear auth
|
|
430
|
+
self.auth = {}
|
|
431
|
+
self.save_auth()
|
|
432
|
+
|
|
433
|
+
# Clear environment variable if set
|
|
434
|
+
if "HANZO_API_KEY" in os.environ:
|
|
435
|
+
del os.environ["HANZO_API_KEY"]
|
|
436
|
+
|
|
437
|
+
self.console.print("[green]✅ Successfully logged out[/green]")
|
|
438
|
+
|
|
439
|
+
async def show_config(self, args: str = ""):
|
|
440
|
+
"""Show current configuration."""
|
|
441
|
+
config_text = json.dumps(self.config, indent=2)
|
|
442
|
+
self.console.print(Panel(config_text, title="Configuration", box=box.ROUNDED))
|
|
443
|
+
|
|
444
|
+
async def show_help(self, args: str = ""):
|
|
445
|
+
"""Show enhanced help."""
|
|
446
|
+
help_text = """
|
|
447
|
+
# Hanzo Enhanced REPL
|
|
448
|
+
|
|
449
|
+
## Slash Commands:
|
|
450
|
+
- `/model [name]` - Change AI model (or `/m`)
|
|
451
|
+
- `/models` - List available models
|
|
452
|
+
- `/status` - Show system status (or `/s`)
|
|
453
|
+
- `/login` - Login to Hanzo Cloud
|
|
454
|
+
- `/logout` - Logout from Hanzo
|
|
455
|
+
- `/config` - Show configuration
|
|
456
|
+
- `/help` - Show this help (or `/h`)
|
|
457
|
+
- `/clear` - Clear screen (or `/c`)
|
|
458
|
+
- `/quit` - Exit REPL (or `/q`)
|
|
459
|
+
|
|
460
|
+
## Model Selection:
|
|
461
|
+
- Use `/model gpt-4` to switch to GPT-4
|
|
462
|
+
- Use `/model 3` to select model by number
|
|
463
|
+
- Current model shown in prompt: `hanzo [gpt] >`
|
|
464
|
+
|
|
465
|
+
## Authentication:
|
|
466
|
+
- 🔓 = Authenticated (logged in or API key set)
|
|
467
|
+
- 🔒 = Not authenticated
|
|
468
|
+
- Use `/login` to authenticate with Hanzo Cloud
|
|
469
|
+
|
|
470
|
+
## Tips:
|
|
471
|
+
- Type any message to chat with current model
|
|
472
|
+
- Use Tab for command completion
|
|
473
|
+
- Use Up/Down arrows for history
|
|
474
|
+
"""
|
|
475
|
+
self.console.print(Markdown(help_text))
|
|
476
|
+
|
|
477
|
+
async def clear_screen(self, args: str = ""):
|
|
478
|
+
"""Clear the screen."""
|
|
479
|
+
self.console.clear()
|
|
480
|
+
|
|
481
|
+
async def exit_repl(self, args: str = ""):
|
|
482
|
+
"""Exit the REPL."""
|
|
483
|
+
self.running = False
|
|
484
|
+
self.console.print("[yellow]Goodbye! 👋[/yellow]")
|
|
485
|
+
|
|
486
|
+
async def execute_command(self, cmd: str, args: str):
|
|
487
|
+
"""Execute a CLI command."""
|
|
488
|
+
# Import here to avoid circular imports
|
|
489
|
+
import subprocess
|
|
490
|
+
|
|
491
|
+
full_cmd = f"hanzo {cmd} {args}".strip()
|
|
492
|
+
self.console.print(f"[dim]Executing: {full_cmd}[/dim]")
|
|
493
|
+
|
|
494
|
+
try:
|
|
495
|
+
result = subprocess.run(
|
|
496
|
+
full_cmd,
|
|
497
|
+
shell=True,
|
|
498
|
+
capture_output=True,
|
|
499
|
+
text=True
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
if result.stdout:
|
|
503
|
+
self.console.print(result.stdout)
|
|
504
|
+
if result.stderr:
|
|
505
|
+
self.console.print(f"[red]{result.stderr}[/red]")
|
|
506
|
+
|
|
507
|
+
except Exception as e:
|
|
508
|
+
self.console.print(f"[red]Error executing command: {e}[/red]")
|
|
509
|
+
|
|
510
|
+
async def chat_with_ai(self, message: str):
|
|
511
|
+
"""Chat with AI using current model."""
|
|
512
|
+
# Default to cloud mode to avoid needing local server
|
|
513
|
+
await self.execute_command("ask", f"--cloud --model {self.current_model} {message}")
|
hanzo/interactive/repl.py
CHANGED
hanzo/ui/__init__.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Inline startup notifications for Hanzo commands.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.text import Text
|
|
13
|
+
from rich import box
|
|
14
|
+
|
|
15
|
+
console = Console()
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class InlineStartup:
|
|
19
|
+
"""Lightweight inline startup notifications."""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
self.config_dir = Path.home() / ".hanzo"
|
|
23
|
+
self.last_shown_file = self.config_dir / ".last_inline_shown"
|
|
24
|
+
self.show_interval = timedelta(hours=24) # Show once per day
|
|
25
|
+
|
|
26
|
+
def should_show(self) -> bool:
|
|
27
|
+
"""Check if we should show inline startup."""
|
|
28
|
+
# Check environment variable
|
|
29
|
+
if os.environ.get("HANZO_NO_STARTUP") == "1":
|
|
30
|
+
return False
|
|
31
|
+
|
|
32
|
+
# Check last shown time
|
|
33
|
+
if self.last_shown_file.exists():
|
|
34
|
+
try:
|
|
35
|
+
last_shown = datetime.fromisoformat(
|
|
36
|
+
self.last_shown_file.read_text().strip()
|
|
37
|
+
)
|
|
38
|
+
if datetime.now() - last_shown < self.show_interval:
|
|
39
|
+
return False
|
|
40
|
+
except:
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
return True
|
|
44
|
+
|
|
45
|
+
def mark_shown(self):
|
|
46
|
+
"""Mark inline startup as shown."""
|
|
47
|
+
self.config_dir.mkdir(exist_ok=True)
|
|
48
|
+
self.last_shown_file.write_text(datetime.now().isoformat())
|
|
49
|
+
|
|
50
|
+
def show_mini(self, command: str = None):
|
|
51
|
+
"""Show mini inline startup."""
|
|
52
|
+
if not self.should_show():
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
# Build message
|
|
56
|
+
message = Text()
|
|
57
|
+
message.append("✨ ", style="yellow")
|
|
58
|
+
message.append("Hanzo AI ", style="bold cyan")
|
|
59
|
+
message.append("v0.3.23", style="green")
|
|
60
|
+
|
|
61
|
+
# Add what's new teaser
|
|
62
|
+
message.append(" • ", style="dim")
|
|
63
|
+
message.append("What's new: ", style="dim")
|
|
64
|
+
message.append("Router management, improved docs", style="yellow dim")
|
|
65
|
+
|
|
66
|
+
# Show panel
|
|
67
|
+
console.print(
|
|
68
|
+
Panel(
|
|
69
|
+
message,
|
|
70
|
+
box=box.MINIMAL,
|
|
71
|
+
border_style="cyan",
|
|
72
|
+
padding=(0, 1)
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
self.mark_shown()
|
|
77
|
+
|
|
78
|
+
def show_command_hint(self, command: str):
|
|
79
|
+
"""Show command-specific hints."""
|
|
80
|
+
hints = {
|
|
81
|
+
"chat": "💡 Tip: Use --model to change AI model, --router for local proxy",
|
|
82
|
+
"node": "💡 Tip: Run 'hanzo node start' to enable local AI inference",
|
|
83
|
+
"router": "💡 Tip: Router provides unified access to 100+ LLM providers",
|
|
84
|
+
"repl": "💡 Tip: REPL combines Python with AI assistance",
|
|
85
|
+
"agent": "💡 Tip: Agents can work in parallel with 'hanzo agent swarm'"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
hint = hints.get(command)
|
|
89
|
+
if hint and os.environ.get("HANZO_SHOW_HINTS") != "0":
|
|
90
|
+
console.print(f"[dim]{hint}[/dim]")
|
|
91
|
+
|
|
92
|
+
def show_status_bar(self):
|
|
93
|
+
"""Show a compact status bar."""
|
|
94
|
+
items = []
|
|
95
|
+
|
|
96
|
+
# Check router
|
|
97
|
+
try:
|
|
98
|
+
import httpx
|
|
99
|
+
response = httpx.get("http://localhost:4000/health", timeout=0.5)
|
|
100
|
+
if response.status_code == 200:
|
|
101
|
+
items.append("[green]Router ✓[/green]")
|
|
102
|
+
except:
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
# Check node
|
|
106
|
+
try:
|
|
107
|
+
import httpx
|
|
108
|
+
response = httpx.get("http://localhost:8000/health", timeout=0.5)
|
|
109
|
+
if response.status_code == 200:
|
|
110
|
+
items.append("[green]Node ✓[/green]")
|
|
111
|
+
except:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
# Check API key
|
|
115
|
+
if os.environ.get("HANZO_API_KEY"):
|
|
116
|
+
items.append("[green]API ✓[/green]")
|
|
117
|
+
else:
|
|
118
|
+
items.append("[yellow]API ⚠[/yellow]")
|
|
119
|
+
|
|
120
|
+
if items:
|
|
121
|
+
status = " • ".join(items)
|
|
122
|
+
console.print(f"[dim]Status: {status}[/dim]")
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def show_inline_startup(command: str = None):
|
|
126
|
+
"""Show inline startup notification."""
|
|
127
|
+
startup = InlineStartup()
|
|
128
|
+
startup.show_mini(command)
|
|
129
|
+
if command:
|
|
130
|
+
startup.show_command_hint(command)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def show_status():
|
|
134
|
+
"""Show compact status bar."""
|
|
135
|
+
startup = InlineStartup()
|
|
136
|
+
startup.show_status_bar()
|