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.

@@ -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
@@ -45,9 +45,9 @@ class HanzoREPL:
45
45
 
46
46
  while self.running:
47
47
  try:
48
- # Get input
48
+ # Get input with simple prompt
49
49
  command = await self.session.prompt_async(
50
- "hanzo> ", completer=completer
50
+ "> ", completer=completer
51
51
  )
52
52
 
53
53
  if not command.strip():
hanzo/ui/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """
2
+ Hanzo UI components for CLI.
3
+ """
4
+
5
+ from .startup import show_startup, StartupUI
6
+ from .inline_startup import show_inline_startup, show_status
7
+
8
+ __all__ = [
9
+ "show_startup",
10
+ "StartupUI",
11
+ "show_inline_startup",
12
+ "show_status"
13
+ ]
@@ -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()