hanzo 0.3.23__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,377 @@
1
+ """Authentication commands."""
2
+
3
+ import os
4
+ import json
5
+ from pathlib import Path
6
+ from datetime import datetime
7
+ from typing import Optional
8
+
9
+ import click
10
+ from rich.prompt import Prompt
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+ from rich import box
14
+
15
+ from ..utils.output import console
16
+
17
+
18
+ class AuthManager:
19
+ """Manage Hanzo authentication."""
20
+
21
+ def __init__(self):
22
+ self.config_dir = Path.home() / ".hanzo"
23
+ self.auth_file = self.config_dir / "auth.json"
24
+
25
+ def load_auth(self) -> dict:
26
+ """Load authentication data."""
27
+ if self.auth_file.exists():
28
+ try:
29
+ return json.loads(self.auth_file.read_text())
30
+ except:
31
+ pass
32
+ return {}
33
+
34
+ def save_auth(self, auth: dict):
35
+ """Save authentication data."""
36
+ self.config_dir.mkdir(exist_ok=True)
37
+ self.auth_file.write_text(json.dumps(auth, indent=2))
38
+
39
+ def is_authenticated(self) -> bool:
40
+ """Check if authenticated."""
41
+ if os.getenv("HANZO_API_KEY"):
42
+ return True
43
+ auth = self.load_auth()
44
+ return bool(auth.get("api_key") or auth.get("logged_in"))
45
+
46
+ def get_api_key(self) -> Optional[str]:
47
+ """Get API key."""
48
+ if os.getenv("HANZO_API_KEY"):
49
+ return os.getenv("HANZO_API_KEY")
50
+ auth = self.load_auth()
51
+ return auth.get("api_key")
52
+
53
+
54
+ @click.group(name="auth")
55
+ def auth_group():
56
+ """Manage Hanzo authentication."""
57
+ pass
58
+
59
+
60
+ @auth_group.command()
61
+ @click.option("--email", "-e", help="Email address")
62
+ @click.option("--password", "-p", help="Password (not recommended, use prompt)")
63
+ @click.option("--api-key", "-k", help="API key for direct authentication")
64
+ @click.option("--sso", is_flag=True, help="Use SSO authentication")
65
+ @click.pass_context
66
+ async def login(ctx, email: str, password: str, api_key: str, sso: bool):
67
+ """Login to Hanzo AI."""
68
+ try:
69
+ from hanzoai.auth import HanzoAuth
70
+ except ImportError:
71
+ console.print("[red]Error:[/red] hanzoai not installed")
72
+ console.print("Install with: pip install hanzo")
73
+ return
74
+
75
+ auth = HanzoAuth()
76
+
77
+ try:
78
+ if api_key:
79
+ # Direct API key authentication
80
+ console.print("Authenticating with API key...")
81
+ result = await auth.login_with_api_key(api_key)
82
+ elif sso:
83
+ # SSO authentication via browser
84
+ console.print("Opening browser for SSO login...")
85
+ console.print("If browser doesn't open, visit: https://iam.hanzo.ai/login")
86
+ result = await auth.login_with_sso()
87
+ else:
88
+ # Email/password authentication
89
+ if not email:
90
+ email = Prompt.ask("Email")
91
+ if not password:
92
+ password = Prompt.ask("Password", password=True)
93
+
94
+ console.print("Authenticating...")
95
+ result = await auth.login(email, password)
96
+
97
+ # Save credentials
98
+ config_dir = Path.home() / ".hanzo"
99
+ config_dir.mkdir(exist_ok=True)
100
+
101
+ config_file = config_dir / "auth.json"
102
+ await auth.save_credentials(config_file)
103
+
104
+ # Also set environment variable if API key is available
105
+ if api_key or result.get("api_key"):
106
+ key = api_key or result.get("api_key")
107
+ console.print(f"\n[dim]To use in environment:[/dim]")
108
+ console.print(f"export HANZO_API_KEY={key}")
109
+
110
+ console.print(f"[green]✓[/green] Logged in as {result.get('email', 'user')}")
111
+
112
+ # Check organization
113
+ if org := result.get("organization"):
114
+ console.print(f" Organization: {org}")
115
+
116
+ # Check permissions
117
+ if permissions := result.get("permissions"):
118
+ console.print(f" Permissions: {', '.join(permissions)}")
119
+
120
+ except Exception as e:
121
+ console.print(f"[red]Login failed: {e}[/red]")
122
+
123
+
124
+ @auth_group.command()
125
+ @click.pass_context
126
+ async def logout(ctx):
127
+ """Logout from Hanzo AI."""
128
+ auth_mgr = AuthManager()
129
+
130
+ if not auth_mgr.is_authenticated():
131
+ console.print("[yellow]Not logged in[/yellow]")
132
+ return
133
+
134
+ try:
135
+ # Try using hanzoai if available
136
+ try:
137
+ from hanzoai.auth import HanzoAuth
138
+ hanzo_auth = HanzoAuth()
139
+ await hanzo_auth.logout()
140
+ except ImportError:
141
+ pass # hanzoai not installed, just clear local auth
142
+
143
+ # Clear local auth
144
+ auth_mgr.save_auth({})
145
+
146
+ console.print("[green]✓[/green] Logged out successfully")
147
+
148
+ except Exception as e:
149
+ console.print(f"[red]Logout failed: {e}[/red]")
150
+
151
+
152
+ @auth_group.command()
153
+ @click.pass_context
154
+ async def status(ctx):
155
+ """Show authentication status."""
156
+ auth_mgr = AuthManager()
157
+
158
+ # Create status table
159
+ table = Table(title="Authentication Status", box=box.ROUNDED)
160
+ table.add_column("Property", style="cyan")
161
+ table.add_column("Value", style="white")
162
+
163
+ if auth_mgr.is_authenticated():
164
+ auth = auth_mgr.load_auth()
165
+
166
+ table.add_row("Status", "✅ Authenticated")
167
+
168
+ # Show auth method
169
+ if os.getenv("HANZO_API_KEY"):
170
+ table.add_row("Method", "Environment Variable")
171
+ api_key = os.getenv("HANZO_API_KEY")
172
+ table.add_row("API Key", f"{api_key[:8]}...{api_key[-4:]}")
173
+ elif auth.get("api_key"):
174
+ table.add_row("Method", "API Key")
175
+ table.add_row("API Key", f"{auth['api_key'][:8]}...")
176
+ elif auth.get("email"):
177
+ console.print(f" Email: {email}")
178
+ if org := creds.get("organization"):
179
+ console.print(f" Organization: {org}")
180
+
181
+ # Verify credentials are still valid
182
+ with console.status("Verifying credentials..."):
183
+ try:
184
+ user_info = await auth.get_user_info()
185
+ console.print("[green]✓[/green] Credentials are valid")
186
+
187
+ # Show usage stats if available
188
+ if usage := user_info.get("usage"):
189
+ console.print("\n[cyan]Usage:[/cyan]")
190
+ console.print(f" API calls: {usage.get('api_calls', 0)}")
191
+ console.print(f" Tokens: {usage.get('tokens', 0)}")
192
+ if quota := usage.get("quota"):
193
+ console.print(
194
+ f" Quota: {usage.get('tokens', 0)} / {quota}"
195
+ )
196
+ except Exception:
197
+ console.print("[yellow]![/yellow] Credentials may be expired")
198
+ console.print("Run 'hanzo auth login' to refresh")
199
+
200
+ except Exception as e:
201
+ console.print(f"[red]Error reading credentials: {e}[/red]")
202
+ else:
203
+ console.print("[yellow]![/yellow] Not logged in")
204
+ console.print("Run 'hanzo auth login' to authenticate")
205
+
206
+
207
+ @auth_group.command()
208
+ @click.option("--name", "-n", required=True, help="API key name")
209
+ @click.option(
210
+ "--permissions", "-p", multiple=True, help="Permissions (e.g., read, write, admin)"
211
+ )
212
+ @click.option("--expires", "-e", help="Expiration (e.g., 30d, 1y, never)")
213
+ @click.pass_context
214
+ async def create_key(ctx, name: str, permissions: tuple, expires: str):
215
+ """Create a new API key."""
216
+ try:
217
+ from hanzoai.auth import HanzoAuth
218
+ except ImportError:
219
+ console.print("[red]Error:[/red] hanzoai not installed")
220
+ return
221
+
222
+ auth = HanzoAuth()
223
+
224
+ # Ensure authenticated
225
+ if not await auth.is_authenticated():
226
+ console.print("[red]Error:[/red] Not authenticated")
227
+ console.print("Run 'hanzo auth login' first")
228
+ return
229
+
230
+ with console.status(f"Creating API key '{name}'..."):
231
+ try:
232
+ result = await auth.create_api_key(
233
+ name=name,
234
+ permissions=list(permissions) if permissions else None,
235
+ expires=expires,
236
+ )
237
+
238
+ key = result.get("key")
239
+ console.print(f"[green]✓[/green] Created API key: {name}")
240
+ console.print(
241
+ f"\n[yellow]Save this key - it won't be shown again:[/yellow]"
242
+ )
243
+ console.print(f"{key}")
244
+ console.print(f"\nTo use:")
245
+ console.print(f"export HANZO_API_KEY={key}")
246
+
247
+ except Exception as e:
248
+ console.print(f"[red]Failed to create key: {e}[/red]")
249
+
250
+
251
+ @auth_group.command()
252
+ @click.pass_context
253
+ async def list_keys(ctx):
254
+ """List your API keys."""
255
+ try:
256
+ from hanzoai.auth import HanzoAuth
257
+ except ImportError:
258
+ console.print("[red]Error:[/red] hanzoai not installed")
259
+ return
260
+
261
+ auth = HanzoAuth()
262
+
263
+ # Ensure authenticated
264
+ if not await auth.is_authenticated():
265
+ console.print("[red]Error:[/red] Not authenticated")
266
+ console.print("Run 'hanzo auth login' first")
267
+ return
268
+
269
+ with console.status("Loading API keys..."):
270
+ try:
271
+ keys = await auth.list_api_keys()
272
+
273
+ if keys:
274
+ from rich.table import Table
275
+
276
+ table = Table(title="API Keys")
277
+ table.add_column("Name", style="cyan")
278
+ table.add_column("Created", style="green")
279
+ table.add_column("Last Used", style="yellow")
280
+ table.add_column("Permissions", style="blue")
281
+ table.add_column("Status", style="magenta")
282
+
283
+ for key in keys:
284
+ table.add_row(
285
+ key.get("name", "unknown"),
286
+ key.get("created_at", "unknown"),
287
+ key.get("last_used", "never"),
288
+ ", ".join(key.get("permissions", [])),
289
+ key.get("status", "active"),
290
+ )
291
+
292
+ console.print(table)
293
+ else:
294
+ console.print("[yellow]No API keys found[/yellow]")
295
+ console.print("Create one with: hanzo auth create-key")
296
+
297
+ except Exception as e:
298
+ console.print(f"[red]Failed to list keys: {e}[/red]")
299
+
300
+
301
+ @auth_group.command()
302
+ @click.argument("name")
303
+ @click.pass_context
304
+ async def revoke_key(ctx, name: str):
305
+ """Revoke an API key."""
306
+ try:
307
+ from hanzoai.auth import HanzoAuth
308
+ except ImportError:
309
+ console.print("[red]Error:[/red] hanzoai not installed")
310
+ return
311
+
312
+ auth = HanzoAuth()
313
+
314
+ # Ensure authenticated
315
+ if not await auth.is_authenticated():
316
+ console.print("[red]Error:[/red] Not authenticated")
317
+ console.print("Run 'hanzo auth login' first")
318
+ return
319
+
320
+ if click.confirm(f"Revoke API key '{name}'?"):
321
+ with console.status(f"Revoking key '{name}'..."):
322
+ try:
323
+ await auth.revoke_api_key(name)
324
+ console.print(f"[green]✓[/green] Revoked API key: {name}")
325
+ except Exception as e:
326
+ console.print(f"[red]Failed to revoke key: {e}[/red]")
327
+
328
+
329
+ @auth_group.command()
330
+ @click.pass_context
331
+ async def whoami(ctx):
332
+ """Show current user information."""
333
+ try:
334
+ from hanzoai.auth import HanzoAuth
335
+ except ImportError:
336
+ console.print("[red]Error:[/red] hanzoai not installed")
337
+ return
338
+
339
+ auth = HanzoAuth()
340
+
341
+ # Check if authenticated
342
+ if not await auth.is_authenticated():
343
+ console.print("[yellow]Not authenticated[/yellow]")
344
+
345
+ # Check if API key is set
346
+ if os.environ.get("HANZO_API_KEY"):
347
+ console.print("HANZO_API_KEY is set but may be invalid")
348
+ else:
349
+ console.print("Run 'hanzo auth login' to authenticate")
350
+ return
351
+
352
+ with console.status("Loading user information..."):
353
+ try:
354
+ user = await auth.get_user_info()
355
+
356
+ console.print(f"[cyan]User Information:[/cyan]")
357
+ console.print(f" ID: {user.get('id', 'unknown')}")
358
+ console.print(f" Email: {user.get('email', 'unknown')}")
359
+ console.print(f" Name: {user.get('name', 'unknown')}")
360
+
361
+ if org := user.get("organization"):
362
+ console.print(f"\n[cyan]Organization:[/cyan]")
363
+ console.print(f" Name: {org.get('name', 'unknown')}")
364
+ console.print(f" Role: {org.get('role', 'member')}")
365
+
366
+ if teams := user.get("teams"):
367
+ console.print(f"\n[cyan]Teams:[/cyan]")
368
+ for team in teams:
369
+ console.print(f" • {team}")
370
+
371
+ if perms := user.get("permissions"):
372
+ console.print(f"\n[cyan]Permissions:[/cyan]")
373
+ for perm in sorted(perms):
374
+ console.print(f" • {perm}")
375
+
376
+ except Exception as e:
377
+ console.print(f"[red]Error: {e}[/red]")
hanzo/commands/chat.py CHANGED
@@ -1,11 +1,14 @@
1
1
  """Chat command for interactive AI conversations."""
2
2
 
3
3
  import asyncio
4
+ import os
4
5
  from typing import Optional
5
6
 
6
7
  import click
7
8
  import httpx
8
9
  from rich.markdown import Markdown
10
+ from rich.panel import Panel
11
+ from rich import box
9
12
 
10
13
  from ..utils.output import console
11
14
 
@@ -88,20 +91,32 @@ async def ask_once(
88
91
 
89
92
  try:
90
93
  if local:
91
- # Use local cluster
92
- base_url = "http://localhost:8000"
93
-
94
- # Check if cluster is running
95
- try:
96
- async with httpx.AsyncClient() as client:
97
- await client.get(f"{base_url}/health")
98
- except httpx.ConnectError:
94
+ # Try router first, then fall back to local node
95
+ base_urls = [
96
+ "http://localhost:4000", # Hanzo router default port
97
+ "http://localhost:8000", # Local node port
98
+ ]
99
+
100
+ base_url = None
101
+ for url in base_urls:
102
+ try:
103
+ async with httpx.AsyncClient() as client:
104
+ await client.get(f"{url}/health", timeout=1.0)
105
+ base_url = url
106
+ break
107
+ except (httpx.ConnectError, httpx.TimeoutException):
108
+ continue
109
+
110
+ if not base_url:
99
111
  console.print(
100
- "[yellow]Local cluster not running. Start with: hanzo serve[/yellow]"
112
+ "[yellow]No local AI server running.[/yellow]\n"
113
+ "Start one of:\n"
114
+ " • Hanzo router: hanzo router start\n"
115
+ " • Local node: hanzo serve"
101
116
  )
102
117
  return
103
118
 
104
- # Make request to local cluster
119
+ # Make request to local node
105
120
  async with httpx.AsyncClient() as client:
106
121
  response = await client.post(
107
122
  f"{base_url}/v1/chat/completions",
@@ -113,16 +128,31 @@ async def ask_once(
113
128
  else:
114
129
  # Use cloud API
115
130
  try:
116
- from hanzoai import completion
131
+ # Try different import paths
132
+ try:
133
+ from hanzoai import completion
134
+ except ImportError:
135
+ try:
136
+ from pkg.hanzoai import completion
137
+ except ImportError:
138
+ # Fallback to using litellm directly
139
+ import litellm
140
+ def completion(**kwargs):
141
+ import os
142
+ api_key = os.getenv("HANZO_API_KEY")
143
+ if api_key:
144
+ kwargs["api_key"] = api_key
145
+ kwargs["api_base"] = "https://api.hanzo.ai/v1"
146
+ return litellm.completion(**kwargs)
117
147
 
118
148
  result = completion(
119
149
  model=f"anthropic/{model}" if "claude" in model else model,
120
150
  messages=messages,
121
151
  )
122
152
  content = result.choices[0].message.content
123
- except ImportError:
124
- console.print("[red]Error:[/red] hanzoai not installed")
125
- console.print("Install with: pip install hanzo[all]")
153
+ except ImportError as e:
154
+ console.print(f"[red]Error:[/red] Missing dependencies: {e}")
155
+ console.print("Install with: pip install litellm")
126
156
  return
127
157
 
128
158
  # Display response
@@ -166,7 +196,7 @@ async def interactive_chat(ctx, model: str, local: bool, system: Optional[str]):
166
196
  console.print("AI: ", end="")
167
197
  with console.status(""):
168
198
  if local:
169
- # Use local cluster
199
+ # Use local node
170
200
  async with httpx.AsyncClient() as client:
171
201
  response = await client.post(
172
202
  "http://localhost:8000/v1/chat/completions",
@@ -181,7 +211,21 @@ async def interactive_chat(ctx, model: str, local: bool, system: Optional[str]):
181
211
  content = result["choices"][0]["message"]["content"]
182
212
  else:
183
213
  # Use cloud API
184
- from hanzoai import completion
214
+ try:
215
+ from hanzoai import completion
216
+ except ImportError:
217
+ try:
218
+ from pkg.hanzoai import completion
219
+ except ImportError:
220
+ # Fallback to using litellm directly
221
+ import litellm
222
+ def completion(**kwargs):
223
+ import os
224
+ api_key = os.getenv("HANZO_API_KEY")
225
+ if api_key:
226
+ kwargs["api_key"] = api_key
227
+ kwargs["api_base"] = "https://api.hanzo.ai/v1"
228
+ return litellm.completion(**kwargs)
185
229
 
186
230
  result = completion(
187
231
  model=f"anthropic/{model}" if "claude" in model else model,