mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
fix(lint): run black formatter on all 69 unformatted files
Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
@@ -125,7 +125,9 @@ class BaseAgent(ABC, Generic[TContext, TResult]):
|
||||
|
||||
# Log start
|
||||
if context and hasattr(context, "log"):
|
||||
await context.log(f"Starting {self.name} with model {self.config.model}")
|
||||
await context.log(
|
||||
f"Starting {self.name} with model {self.config.model}"
|
||||
)
|
||||
|
||||
# Execute with retries
|
||||
result = await self._execute_with_retries(prompt, context, env, **kwargs)
|
||||
@@ -143,7 +145,11 @@ class BaseAgent(ABC, Generic[TContext, TResult]):
|
||||
|
||||
except Exception as e:
|
||||
self._end_time = datetime.now()
|
||||
duration = (self._end_time - self._start_time).total_seconds() if self._start_time else None
|
||||
duration = (
|
||||
(self._end_time - self._start_time).total_seconds()
|
||||
if self._start_time
|
||||
else None
|
||||
)
|
||||
|
||||
logger.error(f"Agent {self.name} failed: {e}")
|
||||
|
||||
@@ -225,7 +231,9 @@ class BaseAgent(ABC, Generic[TContext, TResult]):
|
||||
if attempt < self.config.max_retries - 1:
|
||||
await asyncio.sleep(2**attempt)
|
||||
|
||||
raise Exception(f"All {self.config.max_retries} attempts failed. Last error: {last_error}")
|
||||
raise Exception(
|
||||
f"All {self.config.max_retries} attempts failed. Last error: {last_error}"
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
async def _execute_impl(
|
||||
@@ -318,7 +326,9 @@ class CLIAgent(BaseAgent[TContext, str]):
|
||||
)
|
||||
except asyncio.TimeoutError:
|
||||
process.kill()
|
||||
raise asyncio.TimeoutError(f"Command timed out after {self.config.timeout} seconds")
|
||||
raise asyncio.TimeoutError(
|
||||
f"Command timed out after {self.config.timeout} seconds"
|
||||
)
|
||||
|
||||
# Check for errors
|
||||
if process.returncode != 0:
|
||||
|
||||
@@ -55,7 +55,11 @@ class BatchTask:
|
||||
@property
|
||||
def success(self) -> bool:
|
||||
"""Check if task succeeded."""
|
||||
return self.status == "completed" and self.result and getattr(self.result, "success", True)
|
||||
return (
|
||||
self.status == "completed"
|
||||
and self.result
|
||||
and getattr(self.result, "success", True)
|
||||
)
|
||||
|
||||
def get_error(self) -> Optional[str]:
|
||||
"""Get error message from direct error or result."""
|
||||
@@ -157,7 +161,9 @@ class BatchConfig:
|
||||
if not config.consensus_mode and not config.critic_mode:
|
||||
agent_match = re.search(r"agent:(\w+)", command)
|
||||
if agent_match:
|
||||
config.agent_model = agent_match.group(1) # Will be resolved in __post_init__
|
||||
config.agent_model = agent_match.group(
|
||||
1
|
||||
) # Will be resolved in __post_init__
|
||||
|
||||
# Parse worktree option
|
||||
worktree_match = re.search(r"worktree:(true|false)", command)
|
||||
@@ -214,7 +220,9 @@ class BatchOrchestrator:
|
||||
self._task_counter += 1
|
||||
return f"task_{self._task_counter:04d}"
|
||||
|
||||
async def _setup_worktree(self, task_id: str, config: BatchConfig) -> Optional[Path]:
|
||||
async def _setup_worktree(
|
||||
self, task_id: str, config: BatchConfig
|
||||
) -> Optional[Path]:
|
||||
"""Setup git worktree for parallel editing.
|
||||
|
||||
Args:
|
||||
@@ -503,7 +511,9 @@ class BatchOrchestrator:
|
||||
"success": True,
|
||||
}
|
||||
else:
|
||||
logger.warning(f"No API client configured - returning empty response for {model}")
|
||||
logger.warning(
|
||||
f"No API client configured - returning empty response for {model}"
|
||||
)
|
||||
return {
|
||||
"model": model,
|
||||
"response": "",
|
||||
@@ -530,7 +540,9 @@ class BatchOrchestrator:
|
||||
|
||||
# If consensus reached, combine insights
|
||||
if consensus_result["consensus_reached"] and successful_responses:
|
||||
combined = "\n\n".join([f"[{r['model']}]: {r['response']}" for r in successful_responses])
|
||||
combined = "\n\n".join(
|
||||
[f"[{r['model']}]: {r['response']}" for r in successful_responses]
|
||||
)
|
||||
consensus_result["combined_response"] = combined
|
||||
|
||||
return consensus_result
|
||||
@@ -620,7 +632,9 @@ Provide your critical analysis:"""
|
||||
|
||||
return {
|
||||
"critic_chain": reviews,
|
||||
"final_review": (reviews[-1]["review"] if reviews and "review" in reviews[-1] else None),
|
||||
"final_review": (
|
||||
reviews[-1]["review"] if reviews and "review" in reviews[-1] else None
|
||||
),
|
||||
"models_used": models,
|
||||
"chain_length": len(reviews),
|
||||
}
|
||||
@@ -697,7 +711,11 @@ Provide your critical analysis:"""
|
||||
if "review" in review:
|
||||
console.print(
|
||||
Panel(
|
||||
(review["review"][:500] + "..." if len(review["review"]) > 500 else review["review"]),
|
||||
(
|
||||
review["review"][:500] + "..."
|
||||
if len(review["review"]) > 500
|
||||
else review["review"]
|
||||
),
|
||||
title=f"[cyan]Critic {review['iteration']}: {review['model']}[/cyan]",
|
||||
)
|
||||
)
|
||||
@@ -772,7 +790,9 @@ Provide your critical analysis:"""
|
||||
del self.active_tasks[task.id]
|
||||
|
||||
# Generate summary
|
||||
total_duration = sum(t.duration() or 0 for t in self.completed_tasks + self.failed_tasks)
|
||||
total_duration = sum(
|
||||
t.duration() or 0 for t in self.completed_tasks + self.failed_tasks
|
||||
)
|
||||
|
||||
summary = {
|
||||
"total_tasks": len(tasks),
|
||||
|
||||
+52
-11
@@ -186,6 +186,7 @@ cli.add_command(fn.fn_group, name="fn") # fn alias for function
|
||||
def ask(ctx, prompt: tuple, model: str, local: bool):
|
||||
"""Quick question to AI (alias for 'hanzo chat --once')."""
|
||||
import asyncio
|
||||
|
||||
prompt_text = " ".join(prompt)
|
||||
asyncio.run(chat.ask_once(ctx, prompt_text, model, local))
|
||||
|
||||
@@ -207,7 +208,9 @@ def log(query: str, source: str, follow: bool, level: str, limit: int):
|
||||
hanzo log -f -s my-api # Tail logs
|
||||
"""
|
||||
if follow:
|
||||
console.print(f"[cyan]Tailing log{' for ' + source if source else ''}...[/cyan]")
|
||||
console.print(
|
||||
f"[cyan]Tailing log{' for ' + source if source else ''}...[/cyan]"
|
||||
)
|
||||
console.print("[dim]Press Ctrl+C to stop[/dim]")
|
||||
elif query:
|
||||
console.print(f"[cyan]Searching log for '{query}'...[/cyan]")
|
||||
@@ -229,7 +232,9 @@ def metric(query: str, service: str, range: str):
|
||||
hanzo metric "http_requests_total" # Query specific metric
|
||||
hanzo metric -s my-api -r 24h # Service metric for 24h
|
||||
"""
|
||||
console.print(f"[cyan]Metric{' for ' + service if service else ''} (last {range}):[/cyan]")
|
||||
console.print(
|
||||
f"[cyan]Metric{' for ' + service if service else ''} (last {range}):[/cyan]"
|
||||
)
|
||||
console.print("[dim]No metric found[/dim]")
|
||||
|
||||
|
||||
@@ -249,7 +254,9 @@ def trace(trace_id: str, service: str, min_duration: str):
|
||||
if trace_id:
|
||||
console.print(f"[cyan]Trace {trace_id}:[/cyan]")
|
||||
else:
|
||||
console.print(f"[cyan]Recent trace{' for ' + service if service else ''}:[/cyan]")
|
||||
console.print(
|
||||
f"[cyan]Recent trace{' for ' + service if service else ''}:[/cyan]"
|
||||
)
|
||||
console.print("[dim]No trace found[/dim]")
|
||||
|
||||
|
||||
@@ -271,7 +278,14 @@ def login(ctx, email, password, api_key, web, headless):
|
||||
hanzo login -k sk-xxx # Direct API key authentication
|
||||
hanzo login -e [email protected] # Email/password login
|
||||
"""
|
||||
ctx.invoke(auth.login, email=email, password=password, api_key=api_key, web=web, headless=headless)
|
||||
ctx.invoke(
|
||||
auth.login,
|
||||
email=email,
|
||||
password=password,
|
||||
api_key=api_key,
|
||||
web=web,
|
||||
headless=headless,
|
||||
)
|
||||
|
||||
|
||||
@cli.command()
|
||||
@@ -303,6 +317,7 @@ cli.add_command(run.run_group, name="deploy")
|
||||
def serve(ctx, name: str, port: int):
|
||||
"""Start local AI node (alias for 'hanzo node start')."""
|
||||
import asyncio
|
||||
|
||||
asyncio.run(node.start_node(ctx, name, port))
|
||||
|
||||
|
||||
@@ -418,7 +433,9 @@ def doctor(ctx, json_output: bool):
|
||||
|
||||
console.print("[bold]Python Tools (uv):[/bold]")
|
||||
try:
|
||||
result_uv = subprocess.run(["uv", "tool", "list"], capture_output=True, text=True, timeout=10)
|
||||
result_uv = subprocess.run(
|
||||
["uv", "tool", "list"], capture_output=True, text=True, timeout=10
|
||||
)
|
||||
if result_uv.returncode == 0:
|
||||
table = Table(show_header=True, header_style="bold")
|
||||
table.add_column("Package", style="cyan")
|
||||
@@ -432,7 +449,14 @@ def doctor(ctx, json_output: bool):
|
||||
name, version = parts[0], parts[1]
|
||||
path = shutil.which(name) or f"~/.local/bin/{name}"
|
||||
table.add_row(name, version, path)
|
||||
tools_found.append({"name": name, "version": version, "path": path, "source": "uv"})
|
||||
tools_found.append(
|
||||
{
|
||||
"name": name,
|
||||
"version": version,
|
||||
"path": path,
|
||||
"source": "uv",
|
||||
}
|
||||
)
|
||||
|
||||
if tools_found:
|
||||
console.print(table)
|
||||
@@ -466,8 +490,14 @@ def doctor(ctx, json_output: bool):
|
||||
if path:
|
||||
ai_found = True
|
||||
try:
|
||||
ver_result = subprocess.run([cmd, "--version"], capture_output=True, text=True, timeout=5)
|
||||
version = ver_result.stdout.strip().split("\n")[0] if ver_result.returncode == 0 else "?"
|
||||
ver_result = subprocess.run(
|
||||
[cmd, "--version"], capture_output=True, text=True, timeout=5
|
||||
)
|
||||
version = (
|
||||
ver_result.stdout.strip().split("\n")[0]
|
||||
if ver_result.returncode == 0
|
||||
else "?"
|
||||
)
|
||||
except Exception:
|
||||
version = "?"
|
||||
ai_table.add_row(name, version, path)
|
||||
@@ -533,7 +563,13 @@ def update(ctx, upgrade_all: bool, force: bool, packages: tuple):
|
||||
|
||||
# Default packages to update
|
||||
default_packages = ["hanzo", "hanzo-mcp"]
|
||||
all_packages = ["hanzo", "hanzo-mcp", "hanzo-agents", "hanzo-memory", "hanzo-network"]
|
||||
all_packages = [
|
||||
"hanzo",
|
||||
"hanzo-mcp",
|
||||
"hanzo-agents",
|
||||
"hanzo-memory",
|
||||
"hanzo-network",
|
||||
]
|
||||
|
||||
if packages:
|
||||
to_update = list(packages)
|
||||
@@ -567,7 +603,9 @@ def update(ctx, upgrade_all: bool, force: bool, packages: tuple):
|
||||
|
||||
if result.returncode == 0:
|
||||
# Get new version
|
||||
ver_result = subprocess.run(["uv", "tool", "list"], capture_output=True, text=True, timeout=10)
|
||||
ver_result = subprocess.run(
|
||||
["uv", "tool", "list"], capture_output=True, text=True, timeout=10
|
||||
)
|
||||
version = "?"
|
||||
for line in ver_result.stdout.split("\n"):
|
||||
if line.startswith(pkg + " "):
|
||||
@@ -580,7 +618,10 @@ def update(ctx, upgrade_all: bool, force: bool, packages: tuple):
|
||||
# Try install if upgrade failed (package not installed)
|
||||
if "not installed" in result.stderr.lower():
|
||||
install_result = subprocess.run(
|
||||
["uv", "tool", "install", pkg], capture_output=True, text=True, timeout=120
|
||||
["uv", "tool", "install", pkg],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=120,
|
||||
)
|
||||
if install_result.returncode == 0:
|
||||
console.print("[green]✓[/green] installed")
|
||||
|
||||
@@ -101,7 +101,11 @@ async def run(ctx, agents: tuple, task: str, parallel: bool, timeout: Optional[i
|
||||
network = create_network(agents=agent_list)
|
||||
|
||||
# Run task
|
||||
result = await asyncio.wait_for(network.run(task), timeout=timeout) if timeout else await network.run(task)
|
||||
result = (
|
||||
await asyncio.wait_for(network.run(task), timeout=timeout)
|
||||
if timeout
|
||||
else await network.run(task)
|
||||
)
|
||||
|
||||
console.print("[green]Task completed![/green]")
|
||||
console.print(result)
|
||||
|
||||
@@ -64,7 +64,11 @@ class AuthManager:
|
||||
if os.getenv("HANZO_API_KEY"):
|
||||
return os.getenv("HANZO_API_KEY")
|
||||
auth = self.load_auth()
|
||||
return auth.get("api_key") or auth.get("token") or auth.get("tokens", {}).get("access_token")
|
||||
return (
|
||||
auth.get("api_key")
|
||||
or auth.get("token")
|
||||
or auth.get("tokens", {}).get("access_token")
|
||||
)
|
||||
|
||||
|
||||
@click.group(name="auth")
|
||||
@@ -133,7 +137,9 @@ def login(ctx, api_key: str, device_code: bool, headless: bool):
|
||||
def _get_iam_url(auth_mgr: AuthManager) -> str:
|
||||
"""Get the IAM URL from env or stored config."""
|
||||
existing = auth_mgr.load_auth()
|
||||
return os.getenv("IAM_URL", os.getenv("HANZO_IAM_URL", existing.get("iam_url", HANZO_IAM_URL)))
|
||||
return os.getenv(
|
||||
"IAM_URL", os.getenv("HANZO_IAM_URL", existing.get("iam_url", HANZO_IAM_URL))
|
||||
)
|
||||
|
||||
|
||||
def _decode_jwt_claims(token: str) -> dict:
|
||||
@@ -327,7 +333,9 @@ def _login_device_code(auth_mgr: AuthManager, headless: bool):
|
||||
device_code = data["device_code"]
|
||||
user_code = data["user_code"]
|
||||
verification_url = data.get("verification_uri", f"{iam_url}/device")
|
||||
verification_url_complete = data.get("verification_uri_complete", f"{verification_url}?user_code={user_code}")
|
||||
verification_url_complete = data.get(
|
||||
"verification_uri_complete", f"{verification_url}?user_code={user_code}"
|
||||
)
|
||||
expires_in = data.get("expires_in", 300)
|
||||
interval = data.get("interval", 5)
|
||||
|
||||
@@ -401,7 +409,9 @@ def _login_device_code(auth_mgr: AuthManager, headless: bool):
|
||||
interval += 5
|
||||
continue
|
||||
elif error == "expired_token":
|
||||
console.print("[red]Device code expired. Please try again.[/red]")
|
||||
console.print(
|
||||
"[red]Device code expired. Please try again.[/red]"
|
||||
)
|
||||
return
|
||||
elif error == "access_denied":
|
||||
console.print("[red]Authentication denied.[/red]")
|
||||
@@ -430,7 +440,13 @@ def logout(ctx):
|
||||
# Clear login state but preserve IAM config
|
||||
auth = auth_mgr.load_auth()
|
||||
preserved = {}
|
||||
for key in ("iam_url", "iam_client_id", "iam_client_secret", "iam_org", "iam_app"):
|
||||
for key in (
|
||||
"iam_url",
|
||||
"iam_client_id",
|
||||
"iam_client_secret",
|
||||
"iam_org",
|
||||
"iam_app",
|
||||
):
|
||||
if key in auth:
|
||||
preserved[key] = auth[key]
|
||||
auth_mgr.save_auth(preserved)
|
||||
@@ -529,7 +545,9 @@ def whoami():
|
||||
lines.append(f"[cyan]Last Login:[/cyan] {auth['last_login']}")
|
||||
|
||||
content = "\n".join(lines) if lines else "[dim]No user information available[/dim]"
|
||||
console.print(Panel(content, title="[bold cyan]User Information[/bold cyan]", box=box.ROUNDED))
|
||||
console.print(
|
||||
Panel(content, title="[bold cyan]User Information[/bold cyan]", box=box.ROUNDED)
|
||||
)
|
||||
|
||||
|
||||
@auth_group.command(name="set-key")
|
||||
@@ -581,14 +599,23 @@ def _print_context():
|
||||
ctx = load_context()
|
||||
if not ctx:
|
||||
console.print("[yellow]No context set.[/yellow]")
|
||||
console.print("[dim]Run 'hanzo auth context set --org ORG --project PROJECT --env ENV'[/dim]")
|
||||
console.print(
|
||||
"[dim]Run 'hanzo auth context set --org ORG --project PROJECT --env ENV'[/dim]"
|
||||
)
|
||||
return
|
||||
|
||||
table = Table(title="Active Context", box=box.ROUNDED)
|
||||
table.add_column("Property", style="cyan")
|
||||
table.add_column("Value", style="white")
|
||||
|
||||
for key in ("org_id", "org_name", "project_id", "project_name", "env_id", "env_name"):
|
||||
for key in (
|
||||
"org_id",
|
||||
"org_name",
|
||||
"project_id",
|
||||
"project_name",
|
||||
"env_id",
|
||||
"env_name",
|
||||
):
|
||||
if ctx.get(key):
|
||||
label = key.replace("_", " ").title()
|
||||
table.add_row(label, ctx[key])
|
||||
@@ -608,7 +635,13 @@ def context_set(org: str, project: str, env: str):
|
||||
hanzo auth context set --org hanzo
|
||||
hanzo auth context set --org hanzo --project myapp --env development
|
||||
"""
|
||||
from ..utils.api_client import PaaSClient, env_url, org_url, project_url, save_context
|
||||
from ..utils.api_client import (
|
||||
PaaSClient,
|
||||
env_url,
|
||||
org_url,
|
||||
project_url,
|
||||
save_context,
|
||||
)
|
||||
|
||||
try:
|
||||
client = PaaSClient()
|
||||
@@ -636,7 +669,9 @@ def context_set(org: str, project: str, env: str):
|
||||
console.print(f"[red]Organization '{org}' not found.[/red]")
|
||||
return
|
||||
|
||||
ctx["org_id"] = matched_org.get("_id") or matched_org.get("iid") or matched_org.get("id")
|
||||
ctx["org_id"] = (
|
||||
matched_org.get("_id") or matched_org.get("iid") or matched_org.get("id")
|
||||
)
|
||||
ctx["org_name"] = matched_org.get("name", org)
|
||||
console.print(f"[green]✓[/green] Organization: {ctx['org_name']}")
|
||||
|
||||
@@ -648,7 +683,11 @@ def context_set(org: str, project: str, env: str):
|
||||
save_context(ctx)
|
||||
return
|
||||
|
||||
projects = data if isinstance(data, list) else data.get("projects", data.get("data", []))
|
||||
projects = (
|
||||
data
|
||||
if isinstance(data, list)
|
||||
else data.get("projects", data.get("data", []))
|
||||
)
|
||||
matched_proj = None
|
||||
for p in projects:
|
||||
pid = p.get("_id") or p.get("iid") or p.get("id", "")
|
||||
@@ -658,11 +697,15 @@ def context_set(org: str, project: str, env: str):
|
||||
break
|
||||
|
||||
if not matched_proj:
|
||||
console.print(f"[yellow]Project '{project}' not found. Saving org-only context.[/yellow]")
|
||||
console.print(
|
||||
f"[yellow]Project '{project}' not found. Saving org-only context.[/yellow]"
|
||||
)
|
||||
save_context(ctx)
|
||||
return
|
||||
|
||||
ctx["project_id"] = matched_proj.get("_id") or matched_proj.get("iid") or matched_proj.get("id")
|
||||
ctx["project_id"] = (
|
||||
matched_proj.get("_id") or matched_proj.get("iid") or matched_proj.get("id")
|
||||
)
|
||||
ctx["project_name"] = matched_proj.get("name", project)
|
||||
console.print(f"[green]✓[/green] Project: {ctx['project_name']}")
|
||||
|
||||
@@ -674,7 +717,11 @@ def context_set(org: str, project: str, env: str):
|
||||
save_context(ctx)
|
||||
return
|
||||
|
||||
envs = data if isinstance(data, list) else data.get("environments", data.get("data", []))
|
||||
envs = (
|
||||
data
|
||||
if isinstance(data, list)
|
||||
else data.get("environments", data.get("data", []))
|
||||
)
|
||||
matched_env = None
|
||||
for e in envs:
|
||||
eid = e.get("_id") or e.get("iid") or e.get("id", "")
|
||||
@@ -684,11 +731,15 @@ def context_set(org: str, project: str, env: str):
|
||||
break
|
||||
|
||||
if not matched_env:
|
||||
console.print(f"[yellow]Environment '{env}' not found. Saving org+project context.[/yellow]")
|
||||
console.print(
|
||||
f"[yellow]Environment '{env}' not found. Saving org+project context.[/yellow]"
|
||||
)
|
||||
save_context(ctx)
|
||||
return
|
||||
|
||||
ctx["env_id"] = matched_env.get("_id") or matched_env.get("iid") or matched_env.get("id")
|
||||
ctx["env_id"] = (
|
||||
matched_env.get("_id") or matched_env.get("iid") or matched_env.get("id")
|
||||
)
|
||||
ctx["env_name"] = matched_env.get("name", env)
|
||||
console.print(f"[green]✓[/green] Environment: {ctx['env_name']}")
|
||||
|
||||
@@ -699,7 +750,13 @@ def context_set(org: str, project: str, env: str):
|
||||
@context_group.command(name="list")
|
||||
def context_list():
|
||||
"""List available orgs, projects, and environments."""
|
||||
from ..utils.api_client import PaaSClient, env_url, org_url, project_url, load_context
|
||||
from ..utils.api_client import (
|
||||
PaaSClient,
|
||||
env_url,
|
||||
org_url,
|
||||
project_url,
|
||||
load_context,
|
||||
)
|
||||
|
||||
try:
|
||||
client = PaaSClient()
|
||||
@@ -735,9 +792,16 @@ def context_list():
|
||||
data = client.get(project_url(current["org_id"]))
|
||||
|
||||
if data:
|
||||
projects = data if isinstance(data, list) else data.get("projects", data.get("data", []))
|
||||
projects = (
|
||||
data
|
||||
if isinstance(data, list)
|
||||
else data.get("projects", data.get("data", []))
|
||||
)
|
||||
if projects:
|
||||
ptable = Table(title=f"Projects in {current.get('org_name', current['org_id'])}", box=box.ROUNDED)
|
||||
ptable = Table(
|
||||
title=f"Projects in {current.get('org_name', current['org_id'])}",
|
||||
box=box.ROUNDED,
|
||||
)
|
||||
ptable.add_column("", style="green", width=2)
|
||||
ptable.add_column("ID", style="cyan")
|
||||
ptable.add_column("Name", style="white")
|
||||
@@ -756,10 +820,15 @@ def context_list():
|
||||
data = client.get(env_url(current["org_id"], current["project_id"]))
|
||||
|
||||
if data:
|
||||
envs = data if isinstance(data, list) else data.get("environments", data.get("data", []))
|
||||
envs = (
|
||||
data
|
||||
if isinstance(data, list)
|
||||
else data.get("environments", data.get("data", []))
|
||||
)
|
||||
if envs:
|
||||
etable = Table(
|
||||
title=f"Environments in {current.get('project_name', current['project_id'])}", box=box.ROUNDED
|
||||
title=f"Environments in {current.get('project_name', current['project_id'])}",
|
||||
box=box.ROUNDED,
|
||||
)
|
||||
etable.add_column("", style="green", width=2)
|
||||
etable.add_column("ID", style="cyan")
|
||||
|
||||
@@ -66,7 +66,9 @@ def flows():
|
||||
|
||||
|
||||
@flows.command(name="list")
|
||||
@click.option("--status", type=click.Choice(["active", "inactive", "all"]), default="all")
|
||||
@click.option(
|
||||
"--status", type=click.Choice(["active", "inactive", "all"]), default="all"
|
||||
)
|
||||
def flows_list(status: str):
|
||||
"""List all automation flows."""
|
||||
params = {}
|
||||
@@ -97,12 +99,19 @@ def flows_list(status: str):
|
||||
|
||||
console.print(table)
|
||||
if not items:
|
||||
console.print("[dim]No flows found. Create one with 'hanzo auto flows create'[/dim]")
|
||||
console.print(
|
||||
"[dim]No flows found. Create one with 'hanzo auto flows create'[/dim]"
|
||||
)
|
||||
|
||||
|
||||
@flows.command(name="create")
|
||||
@click.option("--name", "-n", prompt=True, help="Flow name")
|
||||
@click.option("--trigger", "-t", type=click.Choice(["webhook", "schedule", "manual"]), default="manual")
|
||||
@click.option(
|
||||
"--trigger",
|
||||
"-t",
|
||||
type=click.Choice(["webhook", "schedule", "manual"]),
|
||||
default="manual",
|
||||
)
|
||||
def flows_create(name: str, trigger: str):
|
||||
"""Create a new automation flow."""
|
||||
body = {"name": name, "trigger_type": trigger}
|
||||
@@ -272,7 +281,9 @@ def connections_list():
|
||||
|
||||
console.print(table)
|
||||
if not items:
|
||||
console.print("[dim]No connections found. Add one with 'hanzo auto connections add'[/dim]")
|
||||
console.print(
|
||||
"[dim]No connections found. Add one with 'hanzo auto connections add'[/dim]"
|
||||
)
|
||||
|
||||
|
||||
@connections.command(name="add")
|
||||
@@ -330,7 +341,9 @@ def init():
|
||||
@click.option("--port", "-p", default=8080, help="Port to run on")
|
||||
def dev(port: int):
|
||||
"""Start local development server."""
|
||||
console.print(f"[cyan]Starting Hanzo Auto development server on port {port}...[/cyan]")
|
||||
console.print(
|
||||
f"[cyan]Starting Hanzo Auto development server on port {port}...[/cyan]"
|
||||
)
|
||||
console.print()
|
||||
console.print(f" [cyan]Visual Editor:[/cyan] http://localhost:{port}")
|
||||
console.print(f" [cyan]API:[/cyan] http://localhost:{port}/api")
|
||||
@@ -372,7 +385,11 @@ def runs():
|
||||
|
||||
@runs.command(name="list")
|
||||
@click.option("--flow", "-f", help="Filter by flow")
|
||||
@click.option("--status", type=click.Choice(["success", "failed", "running", "all"]), default="all")
|
||||
@click.option(
|
||||
"--status",
|
||||
type=click.Choice(["success", "failed", "running", "all"]),
|
||||
default="all",
|
||||
)
|
||||
@click.option("--limit", "-n", default=50, help="Max results")
|
||||
def runs_list(flow: str, status: str, limit: int):
|
||||
"""List flow runs."""
|
||||
@@ -423,7 +440,11 @@ def runs_show(run_id: str):
|
||||
data = check_response(resp)
|
||||
|
||||
run_status = data.get("status", "unknown")
|
||||
status_style = "green" if run_status in ("success", "completed") else "red" if run_status == "failed" else "cyan"
|
||||
status_style = (
|
||||
"green"
|
||||
if run_status in ("success", "completed")
|
||||
else "red" if run_status == "failed" else "cyan"
|
||||
)
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
@@ -461,7 +482,9 @@ def runs_logs(run_id: str, step: str):
|
||||
ts = str(line.get("timestamp", ""))[:19]
|
||||
level = line.get("level", "info")
|
||||
msg = line.get("message", "")
|
||||
style = "red" if level == "error" else "yellow" if level == "warn" else "dim"
|
||||
style = (
|
||||
"red" if level == "error" else "yellow" if level == "warn" else "dim"
|
||||
)
|
||||
console.print(f"[dim]{ts}[/dim] [{style}]{level}[/{style}] {msg}")
|
||||
else:
|
||||
console.print(str(line))
|
||||
|
||||
@@ -91,7 +91,9 @@ def api_request(method: str, path: str, **kwargs) -> httpx.Response:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
with httpx.Client(timeout=60) as client:
|
||||
return getattr(client, method)(f"{HANZO_API_URL}{path}", headers=headers, **kwargs)
|
||||
return getattr(client, method)(
|
||||
f"{HANZO_API_URL}{path}", headers=headers, **kwargs
|
||||
)
|
||||
|
||||
|
||||
def service_request(base_url: str, method: str, path: str, **kwargs) -> httpx.Response:
|
||||
@@ -109,7 +111,9 @@ def service_request(base_url: str, method: str, path: str, **kwargs) -> httpx.Re
|
||||
|
||||
try:
|
||||
with httpx.Client(timeout=60) as client:
|
||||
return getattr(client, method)(f"{base_url}{path}", headers=headers, **kwargs)
|
||||
return getattr(client, method)(
|
||||
f"{base_url}{path}", headers=headers, **kwargs
|
||||
)
|
||||
except httpx.ConnectError:
|
||||
raise click.ClickException(f"Could not connect to {base_url}")
|
||||
|
||||
@@ -210,7 +214,7 @@ def init(name: str, org: Optional[str], region: str):
|
||||
(project_dir / "seed").mkdir(exist_ok=True)
|
||||
|
||||
# Create config.toml
|
||||
config = f'''# Hanzo Base Configuration
|
||||
config = f"""# Hanzo Base Configuration
|
||||
# https://docs.hanzo.ai/base/config
|
||||
|
||||
[project]
|
||||
@@ -253,7 +257,7 @@ enabled = true
|
||||
|
||||
[commerce]
|
||||
enabled = false
|
||||
'''
|
||||
"""
|
||||
(project_dir / "config.toml").write_text(config)
|
||||
|
||||
# Create seed.sql
|
||||
@@ -301,7 +305,8 @@ CREATE POLICY "Users can update own profile"
|
||||
# Create example function
|
||||
func_dir = project_dir / "functions" / "hello"
|
||||
func_dir.mkdir(exist_ok=True)
|
||||
(func_dir / "index.ts").write_text("""import { serve } from "https://deno.land/[email protected]/http/server.ts"
|
||||
(func_dir / "index.ts").write_text(
|
||||
"""import { serve } from "https://deno.land/[email protected]/http/server.ts"
|
||||
|
||||
serve(async (req) => {
|
||||
const { name } = await req.json()
|
||||
@@ -314,7 +319,8 @@ serve(async (req) => {
|
||||
{ headers: { "Content-Type": "application/json" } },
|
||||
)
|
||||
})
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
# Create .gitignore
|
||||
gitignore = Path.cwd() / ".gitignore"
|
||||
@@ -351,7 +357,10 @@ def link(project: Optional[str]):
|
||||
|
||||
if project:
|
||||
# Find by ID or name
|
||||
matched = next((p for p in projects if p["id"] == project or p["name"] == project), None)
|
||||
matched = next(
|
||||
(p for p in projects if p["id"] == project or p["name"] == project),
|
||||
None,
|
||||
)
|
||||
if not matched:
|
||||
raise click.ClickException(f"Project '{project}' not found")
|
||||
selected = matched
|
||||
@@ -380,7 +389,9 @@ def start():
|
||||
try:
|
||||
subprocess.run(["docker", "info"], capture_output=True, check=True)
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
raise click.ClickException("Docker is required. Install from https://docker.com")
|
||||
raise click.ClickException(
|
||||
"Docker is required. Install from https://docker.com"
|
||||
)
|
||||
|
||||
# Start services via docker compose
|
||||
compose_file = Path.cwd() / "hanzo" / "docker-compose.yml"
|
||||
@@ -481,7 +492,9 @@ volumes:
|
||||
console.print()
|
||||
console.print(" [cyan]Studio:[/cyan] http://localhost:54323")
|
||||
console.print(" [cyan]API:[/cyan] http://localhost:54321")
|
||||
console.print(" [cyan]Database:[/cyan] postgresql://postgres:postgres@localhost:54322/postgres")
|
||||
console.print(
|
||||
" [cyan]Database:[/cyan] postgresql://postgres:postgres@localhost:54322/postgres"
|
||||
)
|
||||
console.print(" [cyan]Realtime:[/cyan] ws://localhost:54325")
|
||||
|
||||
|
||||
@@ -513,14 +526,18 @@ def status():
|
||||
]
|
||||
|
||||
for name, local, remote in services:
|
||||
table.add_row(name, f"● {local}", f"● {remote}" if project_id else "○ Not linked")
|
||||
table.add_row(
|
||||
name, f"● {local}", f"● {remote}" if project_id else "○ Not linked"
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
if project_id:
|
||||
console.print(f"\n[dim]Linked to project: {project_id}[/dim]")
|
||||
else:
|
||||
console.print("\n[dim]Run 'hanzo base link' to connect to a remote project[/dim]")
|
||||
console.print(
|
||||
"\n[dim]Run 'hanzo base link' to connect to a remote project[/dim]"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -694,7 +711,9 @@ def users_list(limit: int):
|
||||
raise click.ClickException("No project linked")
|
||||
|
||||
try:
|
||||
resp = api_request("get", f"/v1/base/{project_id}/auth/users", params={"limit": limit})
|
||||
resp = api_request(
|
||||
"get", f"/v1/base/{project_id}/auth/users", params={"limit": limit}
|
||||
)
|
||||
users = resp.json().get("users", [])
|
||||
|
||||
table = Table(title="Users", box=box.ROUNDED)
|
||||
@@ -949,7 +968,8 @@ def functions_new(name: str):
|
||||
func_dir = Path.cwd() / "hanzo" / "functions" / name
|
||||
func_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
(func_dir / "index.ts").write_text(f"""import {{ serve }} from "https://deno.land/[email protected]/http/server.ts"
|
||||
(func_dir / "index.ts").write_text(
|
||||
f"""import {{ serve }} from "https://deno.land/[email protected]/http/server.ts"
|
||||
|
||||
serve(async (req) => {{
|
||||
const data = {{
|
||||
@@ -961,7 +981,8 @@ serve(async (req) => {{
|
||||
{{ headers: {{ "Content-Type": "application/json" }} }},
|
||||
)
|
||||
}})
|
||||
""")
|
||||
"""
|
||||
)
|
||||
|
||||
console.print(f"[green]✓ Created function: {name}[/green]")
|
||||
console.print(f" Edit: hanzo/functions/{name}/index.ts")
|
||||
@@ -1094,7 +1115,9 @@ def projects_create(name: str, org: Optional[str], region: str):
|
||||
@click.argument("project_id")
|
||||
def projects_delete(project_id: str):
|
||||
"""Delete a project."""
|
||||
if not Confirm.ask(f"[red]Delete project {project_id}? This cannot be undone.[/red]"):
|
||||
if not Confirm.ask(
|
||||
f"[red]Delete project {project_id}? This cannot be undone.[/red]"
|
||||
):
|
||||
return
|
||||
console.print("[green]✓ Project deleted[/green]")
|
||||
|
||||
@@ -1135,7 +1158,9 @@ def gen():
|
||||
|
||||
|
||||
@gen.command(name="types")
|
||||
@click.option("--lang", type=click.Choice(["typescript", "python", "go"]), default="typescript")
|
||||
@click.option(
|
||||
"--lang", type=click.Choice(["typescript", "python", "go"]), default="typescript"
|
||||
)
|
||||
@click.option("--output", "-o", default="types", help="Output directory")
|
||||
def gen_types(lang: str, output: str):
|
||||
"""Generate types from database schema."""
|
||||
|
||||
@@ -18,7 +18,9 @@ from ..utils.output import console
|
||||
@click.option("--local/--cloud", default=True, help="Use local or cloud model")
|
||||
@click.option("--once", is_flag=True, help="Single question mode")
|
||||
@click.option("--system", "-s", help="System prompt")
|
||||
@click.option("--repl", is_flag=True, help="Start full REPL interface (like Claude Code)")
|
||||
@click.option(
|
||||
"--repl", is_flag=True, help="Start full REPL interface (like Claude Code)"
|
||||
)
|
||||
@click.option("--ipython", is_flag=True, help="Use IPython REPL interface")
|
||||
@click.option("--tui", is_flag=True, help="Use beautiful TUI interface")
|
||||
@click.argument("prompt", nargs=-1)
|
||||
@@ -78,7 +80,9 @@ def chat_command(
|
||||
asyncio.run(interactive_chat(ctx, model, local, system))
|
||||
|
||||
|
||||
async def ask_once(ctx, prompt: str, model: str, local: bool, system: Optional[str] = None):
|
||||
async def ask_once(
|
||||
ctx, prompt: str, model: str, local: bool, system: Optional[str] = None
|
||||
):
|
||||
"""Ask a single question."""
|
||||
messages = []
|
||||
if system:
|
||||
@@ -168,7 +172,9 @@ async def interactive_chat(ctx, model: str, local: bool, system: Optional[str]):
|
||||
from prompt_toolkit import PromptSession
|
||||
from prompt_toolkit.history import FileHistory
|
||||
|
||||
console.print(f"[cyan]Chat session started[/cyan] (model: {model}, mode: {'local' if local else 'cloud'})")
|
||||
console.print(
|
||||
f"[cyan]Chat session started[/cyan] (model: {model}, mode: {'local' if local else 'cloud'})"
|
||||
)
|
||||
console.print("Type 'exit' or Ctrl+D to quit\n")
|
||||
|
||||
session = PromptSession(history=FileHistory(".hanzo_chat_history"))
|
||||
|
||||
@@ -397,7 +397,9 @@ def operations_wait(operation_id: str, timeout: int):
|
||||
console.print("[green]✓ Operation completed[/green]")
|
||||
return
|
||||
elif status in ("failed", "error"):
|
||||
console.print(f"[red]✗ Operation failed: {data.get('error', 'Unknown error')}[/red]")
|
||||
console.print(
|
||||
f"[red]✗ Operation failed: {data.get('error', 'Unknown error')}[/red]"
|
||||
)
|
||||
return
|
||||
|
||||
time.sleep(2)
|
||||
@@ -426,7 +428,9 @@ def create_service_group(service_key: str, service_info: dict):
|
||||
# create command
|
||||
@service_group.command(name="create")
|
||||
@click.option("--name", default="default", help="Instance name")
|
||||
@click.option("--tier", type=click.Choice(["free", "pro", "enterprise"]), default="free")
|
||||
@click.option(
|
||||
"--tier", type=click.Choice(["free", "pro", "enterprise"]), default="free"
|
||||
)
|
||||
@click.option("--region", default="us-west-2", help="Deployment region")
|
||||
def create(name: str, tier: str, region: str):
|
||||
"""Create a new instance."""
|
||||
@@ -463,7 +467,11 @@ def create_service_group(service_key: str, service_info: dict):
|
||||
# env command
|
||||
@service_group.command(name="env")
|
||||
@click.option("--name", default="default", help="Instance name")
|
||||
@click.option("--shell", type=click.Choice(["bash", "zsh", "fish", "powershell"]), default="bash")
|
||||
@click.option(
|
||||
"--shell",
|
||||
type=click.Choice(["bash", "zsh", "fish", "powershell"]),
|
||||
default="bash",
|
||||
)
|
||||
@click.option("--export", "do_export", is_flag=True, help="Print export statements")
|
||||
def env(name: str, shell: str, do_export: bool):
|
||||
"""Show/export environment variables."""
|
||||
@@ -574,7 +582,9 @@ def _create_instance(service: str, name: str, tier: str, region: str):
|
||||
)
|
||||
|
||||
if resp.status_code == 401:
|
||||
console.print("[red]Authentication failed. Run 'hanzo auth login'.[/red]")
|
||||
console.print(
|
||||
"[red]Authentication failed. Run 'hanzo auth login'.[/red]"
|
||||
)
|
||||
return
|
||||
|
||||
if resp.status_code == 402:
|
||||
@@ -583,7 +593,9 @@ def _create_instance(service: str, name: str, tier: str, region: str):
|
||||
return
|
||||
|
||||
if resp.status_code == 409:
|
||||
console.print(f"[yellow]Instance '{name}' already exists. Use 'update' to modify.[/yellow]")
|
||||
console.print(
|
||||
f"[yellow]Instance '{name}' already exists. Use 'update' to modify.[/yellow]"
|
||||
)
|
||||
return
|
||||
|
||||
if resp.status_code >= 400:
|
||||
@@ -611,7 +623,9 @@ def _create_instance(service: str, name: str, tier: str, region: str):
|
||||
console.print()
|
||||
console.print(f"[cyan]URL:[/cyan] {data.get('url')}")
|
||||
console.print()
|
||||
console.print(f"[dim]Run 'hanzo cloud {service} env --export' for environment variables[/dim]")
|
||||
console.print(
|
||||
f"[dim]Run 'hanzo cloud {service} env --export' for environment variables[/dim]"
|
||||
)
|
||||
|
||||
except httpx.ConnectError:
|
||||
console.print("[red]Could not connect to Hanzo API.[/red]")
|
||||
@@ -690,7 +704,9 @@ def _delete_instance(service: str, name: str, force: bool):
|
||||
info = SERVICES[service]
|
||||
|
||||
if not force:
|
||||
if not Confirm.ask(f"[red]Delete {info['name']} '{name}'? This cannot be undone.[/red]"):
|
||||
if not Confirm.ask(
|
||||
f"[red]Delete {info['name']} '{name}'? This cannot be undone.[/red]"
|
||||
):
|
||||
console.print("Cancelled.")
|
||||
return
|
||||
|
||||
@@ -797,7 +813,9 @@ def _env_instance(service: str, name: str, shell: str, do_export: bool):
|
||||
|
||||
console.print(table)
|
||||
console.print()
|
||||
console.print(f"[dim]Run 'hanzo cloud {service} env --export' for export statements[/dim]")
|
||||
console.print(
|
||||
f"[dim]Run 'hanzo cloud {service} env --export' for export statements[/dim]"
|
||||
)
|
||||
|
||||
|
||||
def _status_instance(service: str, name: str):
|
||||
|
||||
@@ -115,7 +115,9 @@ def set(ctx, key: str, value: str, is_global: bool, is_local: bool):
|
||||
# Save config
|
||||
save_config(config_path, config)
|
||||
|
||||
console.print(f"[green]✓[/green] Set {key} = {parsed_value} in {config_name} config")
|
||||
console.print(
|
||||
f"[green]✓[/green] Set {key} = {parsed_value} in {config_name} config"
|
||||
)
|
||||
|
||||
|
||||
@config_group.command()
|
||||
|
||||
@@ -58,8 +58,14 @@ def inbox():
|
||||
|
||||
|
||||
@inbox.command(name="list")
|
||||
@click.option("--status", type=click.Choice(["open", "pending", "resolved", "all"]), default="open")
|
||||
@click.option("--channel", type=click.Choice(["email", "chat", "social", "all"]), default="all")
|
||||
@click.option(
|
||||
"--status",
|
||||
type=click.Choice(["open", "pending", "resolved", "all"]),
|
||||
default="open",
|
||||
)
|
||||
@click.option(
|
||||
"--channel", type=click.Choice(["email", "chat", "social", "all"]), default="all"
|
||||
)
|
||||
@click.option("--limit", "-n", default=50, help="Max results")
|
||||
def inbox_list(status: str, channel: str, limit: int):
|
||||
"""List inbox conversations."""
|
||||
@@ -81,7 +87,9 @@ def inbox_list(status: str, channel: str, limit: int):
|
||||
|
||||
for c in data.get("conversations", []):
|
||||
st = c.get("status", "open")
|
||||
st_style = {"open": "yellow", "pending": "cyan", "resolved": "green"}.get(st, "white")
|
||||
st_style = {"open": "yellow", "pending": "cyan", "resolved": "green"}.get(
|
||||
st, "white"
|
||||
)
|
||||
table.add_row(
|
||||
c.get("id", "")[:12],
|
||||
c.get("subject", ""),
|
||||
@@ -118,7 +126,9 @@ def inbox_show(conversation_id: str):
|
||||
for msg in messages:
|
||||
sender = msg.get("sender", "unknown")
|
||||
style = "cyan" if msg.get("is_agent") else "white"
|
||||
console.print(f"[{style}]{sender}[/{style}] [dim]{msg.get('created_at', '')}[/dim]")
|
||||
console.print(
|
||||
f"[{style}]{sender}[/{style}] [dim]{msg.get('created_at', '')}[/dim]"
|
||||
)
|
||||
console.print(f" {msg.get('body', '')}")
|
||||
console.print()
|
||||
|
||||
@@ -128,7 +138,11 @@ def inbox_show(conversation_id: str):
|
||||
@click.option("--agent", "-a", required=True, help="Agent email or ID")
|
||||
def inbox_assign(conversation_id: str, agent: str):
|
||||
"""Assign conversation to agent."""
|
||||
resp = _request("post", f"/v1/inbox/conversations/{conversation_id}/assign", json={"agent": agent})
|
||||
resp = _request(
|
||||
"post",
|
||||
f"/v1/inbox/conversations/{conversation_id}/assign",
|
||||
json={"agent": agent},
|
||||
)
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] Conversation assigned to {agent}")
|
||||
|
||||
@@ -138,7 +152,11 @@ def inbox_assign(conversation_id: str, agent: str):
|
||||
@click.option("--message", "-m", prompt=True, help="Reply message")
|
||||
def inbox_reply(conversation_id: str, message: str):
|
||||
"""Reply to a conversation."""
|
||||
resp = _request("post", f"/v1/inbox/conversations/{conversation_id}/reply", json={"body": message})
|
||||
resp = _request(
|
||||
"post",
|
||||
f"/v1/inbox/conversations/{conversation_id}/reply",
|
||||
json={"body": message},
|
||||
)
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] Reply sent")
|
||||
|
||||
@@ -339,7 +357,12 @@ def deals_show(deal_id: str):
|
||||
@click.option("--stage", "-s", help="Initial stage")
|
||||
def deals_create(name: str, value: float, contact: str, pipeline: str, stage: str):
|
||||
"""Create a deal."""
|
||||
payload: dict = {"name": name, "value": value, "contact_id": contact, "pipeline": pipeline}
|
||||
payload: dict = {
|
||||
"name": name,
|
||||
"value": value,
|
||||
"contact_id": contact,
|
||||
"pipeline": pipeline,
|
||||
}
|
||||
if stage:
|
||||
payload["stage"] = stage
|
||||
resp = _request("post", "/v1/deals", json=payload)
|
||||
@@ -433,7 +456,11 @@ def invoices():
|
||||
|
||||
|
||||
@invoices.command(name="list")
|
||||
@click.option("--status", type=click.Choice(["draft", "sent", "paid", "overdue", "all"]), default="all")
|
||||
@click.option(
|
||||
"--status",
|
||||
type=click.Choice(["draft", "sent", "paid", "overdue", "all"]),
|
||||
default="all",
|
||||
)
|
||||
@click.option("--limit", "-n", default=50, help="Max results")
|
||||
def invoices_list(status: str, limit: int):
|
||||
"""List invoices."""
|
||||
@@ -478,7 +505,9 @@ def invoices_create(customer: str, amount: float, due: str, items: str):
|
||||
payload["items"] = json.loads(items)
|
||||
resp = _request("post", "/v1/invoices", json=payload)
|
||||
data = check_response(resp)
|
||||
console.print(f"[green]✓[/green] Invoice {data.get('number', '')} created for ${amount:,.2f}")
|
||||
console.print(
|
||||
f"[green]✓[/green] Invoice {data.get('number', '')} created for ${amount:,.2f}"
|
||||
)
|
||||
|
||||
|
||||
@invoices.command(name="send")
|
||||
@@ -524,7 +553,11 @@ def orders():
|
||||
|
||||
|
||||
@orders.command(name="list")
|
||||
@click.option("--status", type=click.Choice(["pending", "processing", "shipped", "delivered", "all"]), default="all")
|
||||
@click.option(
|
||||
"--status",
|
||||
type=click.Choice(["pending", "processing", "shipped", "delivered", "all"]),
|
||||
default="all",
|
||||
)
|
||||
@click.option("--limit", "-n", default=50, help="Max results")
|
||||
def orders_list(status: str, limit: int):
|
||||
"""List orders."""
|
||||
@@ -543,7 +576,11 @@ def orders_list(status: str, limit: int):
|
||||
|
||||
for o in data.get("orders", []):
|
||||
st = o.get("status", "pending")
|
||||
st_style = {"delivered": "green", "shipped": "cyan", "processing": "yellow"}.get(st, "white")
|
||||
st_style = {
|
||||
"delivered": "green",
|
||||
"shipped": "cyan",
|
||||
"processing": "yellow",
|
||||
}.get(st, "white")
|
||||
table.add_row(
|
||||
o.get("order_number", ""),
|
||||
o.get("customer_name", ""),
|
||||
@@ -592,7 +629,12 @@ def orders_show(order_id: str):
|
||||
|
||||
@orders.command(name="update-status")
|
||||
@click.argument("order_id")
|
||||
@click.option("--status", "-s", type=click.Choice(["processing", "shipped", "delivered"]), required=True)
|
||||
@click.option(
|
||||
"--status",
|
||||
"-s",
|
||||
type=click.Choice(["processing", "shipped", "delivered"]),
|
||||
required=True,
|
||||
)
|
||||
@click.option("--tracking", "-t", help="Tracking number")
|
||||
def orders_update_status(order_id: str, status: str, tracking: str):
|
||||
"""Update order status."""
|
||||
|
||||
@@ -62,7 +62,12 @@ def doc_group():
|
||||
@doc_group.command(name="create")
|
||||
@click.argument("name")
|
||||
@click.option("--region", "-r", multiple=True, help="Regions for replication")
|
||||
@click.option("--tier", "-t", type=click.Choice(["free", "standard", "dedicated"]), default="standard")
|
||||
@click.option(
|
||||
"--tier",
|
||||
"-t",
|
||||
type=click.Choice(["free", "standard", "dedicated"]),
|
||||
default="standard",
|
||||
)
|
||||
def doc_create(name: str, region: tuple, tier: str):
|
||||
"""Create a document database."""
|
||||
payload = {"name": name, "tier": tier}
|
||||
@@ -77,7 +82,9 @@ def doc_create(name: str, region: tuple, tier: str):
|
||||
console.print(f" Tier: {tier}")
|
||||
if region:
|
||||
console.print(f" Regions: {', '.join(region)}")
|
||||
console.print(f" Connection: {data.get('connection_string', f'mongodb://doc.hanzo.ai/{name}')}")
|
||||
console.print(
|
||||
f" Connection: {data.get('connection_string', f'mongodb://doc.hanzo.ai/{name}')}"
|
||||
)
|
||||
|
||||
|
||||
@doc_group.command(name="list")
|
||||
@@ -107,7 +114,9 @@ def doc_list():
|
||||
|
||||
console.print(table)
|
||||
if not dbs:
|
||||
console.print("[dim]No databases found. Create one with 'hanzo doc create'[/dim]")
|
||||
console.print(
|
||||
"[dim]No databases found. Create one with 'hanzo doc create'[/dim]"
|
||||
)
|
||||
|
||||
|
||||
@doc_group.command(name="describe")
|
||||
@@ -158,7 +167,9 @@ def doc_connect(name: str):
|
||||
"""Get connection string."""
|
||||
resp = _request("get", f"/v1/databases/{name}")
|
||||
data = check_response(resp)
|
||||
conn = data.get("connection_string", f"mongodb://doc.hanzo.ai/{name}?authSource=admin")
|
||||
conn = data.get(
|
||||
"connection_string", f"mongodb://doc.hanzo.ai/{name}?authSource=admin"
|
||||
)
|
||||
console.print(f"[cyan]Connection string for '{name}':[/cyan]")
|
||||
console.print(conn)
|
||||
|
||||
@@ -277,7 +288,15 @@ def collections_stats(name: str, db: str):
|
||||
@click.option("--sort", "-s", help="Sort order")
|
||||
@click.option("--limit", "-n", default=20, help="Max results")
|
||||
@click.option("--skip", type=int, help="Skip documents")
|
||||
def doc_find(collection: str, db: str, query: str, projection: str, sort: str, limit: int, skip: int):
|
||||
def doc_find(
|
||||
collection: str,
|
||||
db: str,
|
||||
query: str,
|
||||
projection: str,
|
||||
sort: str,
|
||||
limit: int,
|
||||
skip: int,
|
||||
):
|
||||
"""Query documents in a collection.
|
||||
|
||||
\b
|
||||
@@ -294,7 +313,9 @@ def doc_find(collection: str, db: str, query: str, projection: str, sort: str, l
|
||||
if skip:
|
||||
payload["skip"] = skip
|
||||
|
||||
resp = _request("post", f"/v1/databases/{db}/collections/{collection}/find", json=payload)
|
||||
resp = _request(
|
||||
"post", f"/v1/databases/{db}/collections/{collection}/find", json=payload
|
||||
)
|
||||
data = check_response(resp)
|
||||
docs = data.get("documents", data.get("items", []))
|
||||
|
||||
@@ -325,7 +346,11 @@ def doc_insert(collection: str, db: str, doc: str, file: str):
|
||||
else:
|
||||
raise click.ClickException("Provide --doc or --file")
|
||||
|
||||
resp = _request("post", f"/v1/databases/{db}/collections/{collection}/insert", json={"document": document})
|
||||
resp = _request(
|
||||
"post",
|
||||
f"/v1/databases/{db}/collections/{collection}/insert",
|
||||
json={"document": document},
|
||||
)
|
||||
data = check_response(resp)
|
||||
|
||||
console.print(f"[green]✓[/green] Inserted document into '{collection}'")
|
||||
@@ -339,7 +364,14 @@ def doc_insert(collection: str, db: str, doc: str, file: str):
|
||||
@click.option("--set", "set_fields", multiple=True, help="Fields to set")
|
||||
@click.option("--unset", "unset_fields", multiple=True, help="Fields to unset")
|
||||
@click.option("--upsert", is_flag=True, help="Insert if not found")
|
||||
def doc_update(collection: str, db: str, filter: str, set_fields: tuple, unset_fields: tuple, upsert: bool):
|
||||
def doc_update(
|
||||
collection: str,
|
||||
db: str,
|
||||
filter: str,
|
||||
set_fields: tuple,
|
||||
unset_fields: tuple,
|
||||
upsert: bool,
|
||||
):
|
||||
"""Update documents.
|
||||
|
||||
\b
|
||||
@@ -363,11 +395,15 @@ def doc_update(collection: str, db: str, filter: str, set_fields: tuple, unset_f
|
||||
"upsert": upsert,
|
||||
}
|
||||
|
||||
resp = _request("post", f"/v1/databases/{db}/collections/{collection}/update", json=payload)
|
||||
resp = _request(
|
||||
"post", f"/v1/databases/{db}/collections/{collection}/update", json=payload
|
||||
)
|
||||
data = check_response(resp)
|
||||
|
||||
console.print(f"[green]✓[/green] Updated documents in '{collection}'")
|
||||
console.print(f" Matched: {data.get('matched_count', 0)}, Modified: {data.get('modified_count', 0)}")
|
||||
console.print(
|
||||
f" Matched: {data.get('matched_count', 0)}, Modified: {data.get('modified_count', 0)}"
|
||||
)
|
||||
if upsert and data.get("upserted_id"):
|
||||
console.print(f" Upserted: {data['upserted_id']}")
|
||||
|
||||
@@ -386,7 +422,9 @@ def doc_delete_docs(collection: str, db: str, filter: str, force: bool):
|
||||
return
|
||||
|
||||
payload = {"filter": json.loads(filter)}
|
||||
resp = _request("post", f"/v1/databases/{db}/collections/{collection}/delete", json=payload)
|
||||
resp = _request(
|
||||
"post", f"/v1/databases/{db}/collections/{collection}/delete", json=payload
|
||||
)
|
||||
data = check_response(resp)
|
||||
|
||||
console.print(f"[green]✓[/green] Deleted documents from '{collection}'")
|
||||
@@ -400,7 +438,9 @@ def doc_delete_docs(collection: str, db: str, filter: str, force: bool):
|
||||
def doc_count(collection: str, db: str, query: str):
|
||||
"""Count documents."""
|
||||
payload = {"filter": json.loads(query)}
|
||||
resp = _request("post", f"/v1/databases/{db}/collections/{collection}/count", json=payload)
|
||||
resp = _request(
|
||||
"post", f"/v1/databases/{db}/collections/{collection}/count", json=payload
|
||||
)
|
||||
data = check_response(resp)
|
||||
console.print(f"[cyan]Count in {db}.{collection}:[/cyan] {data.get('count', 0):,}")
|
||||
|
||||
@@ -433,7 +473,11 @@ def indexes_list(collection: str, db: str):
|
||||
|
||||
for idx in items:
|
||||
keys = idx.get("keys", idx.get("key", {}))
|
||||
keys_str = ", ".join(f"{k}: {v}" for k, v in keys.items()) if isinstance(keys, dict) else str(keys)
|
||||
keys_str = (
|
||||
", ".join(f"{k}: {v}" for k, v in keys.items())
|
||||
if isinstance(keys, dict)
|
||||
else str(keys)
|
||||
)
|
||||
table.add_row(
|
||||
idx.get("name", "-"),
|
||||
keys_str,
|
||||
@@ -447,12 +491,16 @@ def indexes_list(collection: str, db: str):
|
||||
@indexes.command(name="create")
|
||||
@click.argument("collection")
|
||||
@click.option("--db", "-d", default="default")
|
||||
@click.option("--keys", "-k", required=True, help="Index keys (e.g., 'field:1' or 'field:-1')")
|
||||
@click.option(
|
||||
"--keys", "-k", required=True, help="Index keys (e.g., 'field:1' or 'field:-1')"
|
||||
)
|
||||
@click.option("--name", "-n", help="Index name")
|
||||
@click.option("--unique", "-u", is_flag=True, help="Unique index")
|
||||
@click.option("--sparse", is_flag=True, help="Sparse index")
|
||||
@click.option("--ttl", help="TTL in seconds")
|
||||
def indexes_create(collection: str, db: str, keys: str, name: str, unique: bool, sparse: bool, ttl: str):
|
||||
def indexes_create(
|
||||
collection: str, db: str, keys: str, name: str, unique: bool, sparse: bool, ttl: str
|
||||
):
|
||||
"""Create an index.
|
||||
|
||||
\b
|
||||
@@ -476,7 +524,9 @@ def indexes_create(collection: str, db: str, keys: str, name: str, unique: bool,
|
||||
if ttl:
|
||||
payload["expire_after_seconds"] = int(ttl)
|
||||
|
||||
resp = _request("post", f"/v1/databases/{db}/collections/{collection}/indexes", json=payload)
|
||||
resp = _request(
|
||||
"post", f"/v1/databases/{db}/collections/{collection}/indexes", json=payload
|
||||
)
|
||||
data = check_response(resp)
|
||||
|
||||
console.print(f"[green]✓[/green] Index created on '{collection}'")
|
||||
@@ -492,7 +542,9 @@ def indexes_create(collection: str, db: str, keys: str, name: str, unique: bool,
|
||||
@click.option("--db", "-d", default="default")
|
||||
def indexes_drop(collection: str, index_name: str, db: str):
|
||||
"""Drop an index."""
|
||||
resp = _request("delete", f"/v1/databases/{db}/collections/{collection}/indexes/{index_name}")
|
||||
resp = _request(
|
||||
"delete", f"/v1/databases/{db}/collections/{collection}/indexes/{index_name}"
|
||||
)
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] Index '{index_name}' dropped from '{collection}'")
|
||||
|
||||
@@ -549,7 +601,11 @@ def doc_users(action: str, db: str, username: str, role: str):
|
||||
table.add_column("Created", style="dim")
|
||||
|
||||
for u in users:
|
||||
table.add_row(u.get("username", ""), u.get("role", "-"), str(u.get("created_at", ""))[:19])
|
||||
table.add_row(
|
||||
u.get("username", ""),
|
||||
u.get("role", "-"),
|
||||
str(u.get("created_at", ""))[:19],
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
if not users:
|
||||
@@ -558,7 +614,11 @@ def doc_users(action: str, db: str, username: str, role: str):
|
||||
elif action == "create":
|
||||
if not username or not role:
|
||||
raise click.ClickException("--username and --role required for create")
|
||||
resp = _request("post", f"/v1/databases/{db}/users", json={"username": username, "role": role})
|
||||
resp = _request(
|
||||
"post",
|
||||
f"/v1/databases/{db}/users",
|
||||
json={"username": username, "role": role},
|
||||
)
|
||||
data = check_response(resp)
|
||||
console.print(f"[green]✓[/green] User '{username}' created with role '{role}'")
|
||||
if data.get("password"):
|
||||
|
||||
@@ -50,9 +50,15 @@ def env_list(project: str):
|
||||
table.add_column("Variables", style="yellow")
|
||||
table.add_column("Updated", style="dim")
|
||||
|
||||
table.add_row("development", "[green]●[/green] active", "dev.app.hanzo.ai", "12", "2024-01-15")
|
||||
table.add_row("staging", "[yellow]●[/yellow] idle", "staging.app.hanzo.ai", "12", "2024-01-14")
|
||||
table.add_row("production", "[green]●[/green] active", "app.hanzo.ai", "15", "2024-01-13")
|
||||
table.add_row(
|
||||
"development", "[green]●[/green] active", "dev.app.hanzo.ai", "12", "2024-01-15"
|
||||
)
|
||||
table.add_row(
|
||||
"staging", "[yellow]●[/yellow] idle", "staging.app.hanzo.ai", "12", "2024-01-14"
|
||||
)
|
||||
table.add_row(
|
||||
"production", "[green]●[/green] active", "app.hanzo.ai", "15", "2024-01-13"
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
@@ -122,7 +128,9 @@ def env_vars(env: str, reveal: bool):
|
||||
table.add_column("Value", style="white")
|
||||
table.add_column("Source", style="dim")
|
||||
|
||||
table.add_row("DATABASE_URL", "●●●●●●●●" if not reveal else "postgres://...", "secret")
|
||||
table.add_row(
|
||||
"DATABASE_URL", "●●●●●●●●" if not reveal else "postgres://...", "secret"
|
||||
)
|
||||
table.add_row("API_KEY", "●●●●●●●●" if not reveal else "sk-...", "secret")
|
||||
table.add_row("LOG_LEVEL", "debug", "config")
|
||||
table.add_row("NODE_ENV", "development", "config")
|
||||
|
||||
@@ -74,7 +74,9 @@ def bus():
|
||||
|
||||
@bus.command(name="create")
|
||||
@click.argument("name")
|
||||
@click.option("--backend", "-b", type=click.Choice(["kafka", "pubsub", "redis"]), default="kafka")
|
||||
@click.option(
|
||||
"--backend", "-b", type=click.Choice(["kafka", "pubsub", "redis"]), default="kafka"
|
||||
)
|
||||
@click.option("--region", "-r", multiple=True, help="Regions for replication")
|
||||
def bus_create(name: str, backend: str, region: tuple):
|
||||
"""Create an event bus."""
|
||||
@@ -119,7 +121,9 @@ def bus_list():
|
||||
|
||||
console.print(table)
|
||||
if not buses:
|
||||
console.print("[dim]No event buses found. Create one with 'hanzo events bus create'[/dim]")
|
||||
console.print(
|
||||
"[dim]No event buses found. Create one with 'hanzo events bus create'[/dim]"
|
||||
)
|
||||
|
||||
|
||||
@bus.command(name="describe")
|
||||
@@ -178,7 +182,9 @@ def schema():
|
||||
@schema.command(name="register")
|
||||
@click.argument("name")
|
||||
@click.option("--file", "-f", type=click.Path(exists=True), help="Schema file")
|
||||
@click.option("--format", "fmt", type=click.Choice(["json", "avro", "protobuf"]), default="json")
|
||||
@click.option(
|
||||
"--format", "fmt", type=click.Choice(["json", "avro", "protobuf"]), default="json"
|
||||
)
|
||||
@click.option("--version", "-v", help="Schema version")
|
||||
def schema_register(name: str, file: str, fmt: str, version: str):
|
||||
"""Register an event schema."""
|
||||
@@ -391,7 +397,11 @@ def route():
|
||||
@route.command(name="create")
|
||||
@click.argument("name")
|
||||
@click.option("--from", "from_stream", required=True, help="Source stream")
|
||||
@click.option("--to", required=True, help="Target: service:<name>, task:<name>, queue:<name>, webhook:<url>")
|
||||
@click.option(
|
||||
"--to",
|
||||
required=True,
|
||||
help="Target: service:<name>, task:<name>, queue:<name>, webhook:<url>",
|
||||
)
|
||||
@click.option("--filter", "-f", help="Filter expression")
|
||||
@click.option("--transform", "-t", help="Transform expression")
|
||||
def route_create(name: str, from_stream: str, to: str, filter: str, transform: str):
|
||||
@@ -539,7 +549,9 @@ def events_publish(stream: str, data: str, file: str, key: str):
|
||||
|
||||
@events_group.command(name="tail")
|
||||
@click.argument("stream")
|
||||
@click.option("--from", "from_pos", type=click.Choice(["latest", "earliest"]), default="latest")
|
||||
@click.option(
|
||||
"--from", "from_pos", type=click.Choice(["latest", "earliest"]), default="latest"
|
||||
)
|
||||
@click.option("--filter", "-f", help="Filter expression")
|
||||
@click.option("--limit", "-n", type=int, help="Max events")
|
||||
def events_tail(stream: str, from_pos: str, filter: str, limit: int):
|
||||
@@ -569,7 +581,9 @@ def events_tail(stream: str, from_pos: str, filter: str, limit: int):
|
||||
|
||||
@events_group.command(name="replay")
|
||||
@click.option("--stream", "-s", required=True, help="Stream to replay")
|
||||
@click.option("--from", "from_pos", required=True, help="Start: earliest, timestamp, offset")
|
||||
@click.option(
|
||||
"--from", "from_pos", required=True, help="Start: earliest, timestamp, offset"
|
||||
)
|
||||
@click.option("--to", "to_pos", help="End: latest, timestamp, offset")
|
||||
@click.option("--target", "-t", help="Target route or consumer")
|
||||
@click.option("--dry-run", is_flag=True, help="Show what would be replayed")
|
||||
@@ -587,7 +601,9 @@ def events_replay(stream: str, from_pos: str, to_pos: str, target: str, dry_run:
|
||||
data = check_response(resp)
|
||||
|
||||
if dry_run:
|
||||
console.print(f"[dim]Dry run: {data.get('event_count', 0)} events would be replayed[/dim]")
|
||||
console.print(
|
||||
f"[dim]Dry run: {data.get('event_count', 0)} events would be replayed[/dim]"
|
||||
)
|
||||
else:
|
||||
console.print(f"[green]✓[/green] Replay complete")
|
||||
console.print(f" Events replayed: {data.get('replayed', 0):,}")
|
||||
|
||||
@@ -90,7 +90,9 @@ def flow_create(name: str, template: str, description: str):
|
||||
|
||||
|
||||
@flow_group.command(name="list")
|
||||
@click.option("--status", type=click.Choice(["deployed", "draft", "all"]), default="all")
|
||||
@click.option(
|
||||
"--status", type=click.Choice(["deployed", "draft", "all"]), default="all"
|
||||
)
|
||||
def flow_list(status: str):
|
||||
"""List LLM flows."""
|
||||
params = {}
|
||||
@@ -175,7 +177,9 @@ def flow_run(name: str, input_data: str, stream: bool, verbose: bool):
|
||||
|
||||
if verbose and data.get("steps"):
|
||||
for step in data["steps"]:
|
||||
console.print(f" [dim]→ {step.get('name', 'step')}: {step.get('status', 'done')}[/dim]")
|
||||
console.print(
|
||||
f" [dim]→ {step.get('name', 'step')}: {step.get('status', 'done')}[/dim]"
|
||||
)
|
||||
|
||||
console.print()
|
||||
console.print("[green]Output:[/green]")
|
||||
@@ -233,7 +237,9 @@ def flow_import(file: str, name: str):
|
||||
console.print(f"[cyan]Importing flow from '{file}'...[/cyan]")
|
||||
resp = _request("post", "/v1/flows/import", json=body)
|
||||
data = check_response(resp)
|
||||
console.print(f"[green]✓[/green] Flow '{data.get('name', name or 'imported')}' imported")
|
||||
console.print(
|
||||
f"[green]✓[/green] Flow '{data.get('name', name or 'imported')}' imported"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -317,7 +323,9 @@ def custom():
|
||||
|
||||
@custom.command(name="create")
|
||||
@click.argument("name")
|
||||
@click.option("--template", "-t", type=click.Choice(["tool", "chain", "agent"]), default="tool")
|
||||
@click.option(
|
||||
"--template", "-t", type=click.Choice(["tool", "chain", "agent"]), default="tool"
|
||||
)
|
||||
def custom_create(name: str, template: str):
|
||||
"""Create a custom component."""
|
||||
body = {"name": name, "template": template}
|
||||
@@ -540,7 +548,9 @@ def templates_list():
|
||||
def templates_use(template: str, name: str):
|
||||
"""Create flow from template."""
|
||||
console.print(f"[cyan]Creating flow '{name}' from template '{template}'...[/cyan]")
|
||||
resp = _request("post", "/v1/flows/from-template", json={"template": template, "name": name})
|
||||
resp = _request(
|
||||
"post", "/v1/flows/from-template", json={"template": template, "name": name}
|
||||
)
|
||||
data = check_response(resp)
|
||||
|
||||
console.print(f"[green]✓[/green] Flow created")
|
||||
|
||||
@@ -66,14 +66,18 @@ def fn_group():
|
||||
@click.option(
|
||||
"--runtime",
|
||||
"-r",
|
||||
type=click.Choice(["python3.12", "python3.11", "node20", "node18", "go1.22", "rust", "deno"]),
|
||||
type=click.Choice(
|
||||
["python3.12", "python3.11", "node20", "node18", "go1.22", "rust", "deno"]
|
||||
),
|
||||
default="python3.12",
|
||||
)
|
||||
@click.option("--memory", "-m", default="256", help="Memory in MB (128-4096)")
|
||||
@click.option("--timeout", "-t", default=30, help="Timeout in seconds (1-900)")
|
||||
@click.option("--region", help="Deployment region")
|
||||
@click.option("--from", "source", help="Source: directory, git URL, or template")
|
||||
def fn_create(name: str, runtime: str, memory: str, timeout: int, region: str, source: str):
|
||||
def fn_create(
|
||||
name: str, runtime: str, memory: str, timeout: int, region: str, source: str
|
||||
):
|
||||
"""Create a new function.
|
||||
|
||||
\b
|
||||
@@ -140,7 +144,9 @@ def fn_list(region: str, runtime: str):
|
||||
|
||||
console.print(table)
|
||||
if not items:
|
||||
console.print("[dim]No functions found. Create one with 'hanzo fn create'[/dim]")
|
||||
console.print(
|
||||
"[dim]No functions found. Create one with 'hanzo fn create'[/dim]"
|
||||
)
|
||||
|
||||
|
||||
@fn_group.command(name="describe")
|
||||
@@ -234,7 +240,12 @@ def triggers():
|
||||
|
||||
@triggers.command(name="add")
|
||||
@click.argument("function")
|
||||
@click.option("--type", "trigger_type", type=click.Choice(["http", "event", "cron", "queue", "storage"]), required=True)
|
||||
@click.option(
|
||||
"--type",
|
||||
"trigger_type",
|
||||
type=click.Choice(["http", "event", "cron", "queue", "storage"]),
|
||||
required=True,
|
||||
)
|
||||
@click.option("--path", "-p", help="HTTP path (for http triggers)")
|
||||
@click.option("--method", "-m", multiple=True, help="HTTP methods (for http triggers)")
|
||||
@click.option("--event", "-e", help="Event type (for event triggers)")
|
||||
@@ -312,7 +323,10 @@ def triggers_list(function: str):
|
||||
table.add_column("Status", style="dim")
|
||||
|
||||
for t in items:
|
||||
source = t.get("path", t.get("event", t.get("schedule", t.get("queue", t.get("bucket", "-")))))
|
||||
source = t.get(
|
||||
"path",
|
||||
t.get("event", t.get("schedule", t.get("queue", t.get("bucket", "-")))),
|
||||
)
|
||||
t_status = t.get("status", "active")
|
||||
style = "green" if t_status == "active" else "yellow"
|
||||
table.add_row(
|
||||
@@ -383,7 +397,11 @@ def fn_invoke(name: str, data: str, payload_file: str, async_invoke: bool, tail:
|
||||
console.print(f" Response: {json.dumps(result['body'], default=str)}")
|
||||
|
||||
if tail and result.get("request_id"):
|
||||
log_resp = _request("get", f"/v1/functions/{name}/logs", params={"request_id": result["request_id"]})
|
||||
log_resp = _request(
|
||||
"get",
|
||||
f"/v1/functions/{name}/logs",
|
||||
params={"request_id": result["request_id"]},
|
||||
)
|
||||
log_data = check_response(log_resp)
|
||||
for line in log_data.get("logs", []):
|
||||
console.print(str(line))
|
||||
@@ -426,7 +444,9 @@ def fn_logs(name: str, follow: bool, since: str, log_filter: str, limit: int):
|
||||
ts = str(line.get("timestamp", ""))[:19]
|
||||
level = line.get("level", "info")
|
||||
msg = line.get("message", "")
|
||||
style = "red" if level == "error" else "yellow" if level == "warn" else "dim"
|
||||
style = (
|
||||
"red" if level == "error" else "yellow" if level == "warn" else "dim"
|
||||
)
|
||||
console.print(f"[dim]{ts}[/dim] [{style}]{level}[/{style}] {msg}")
|
||||
else:
|
||||
console.print(str(line))
|
||||
@@ -622,7 +642,9 @@ def fn_rollback(name: str, version: str):
|
||||
console.print(f"[cyan]Rolling back '{name}'...[/cyan]")
|
||||
resp = _request("post", f"/v1/functions/{name}/rollback", json=body)
|
||||
data = check_response(resp)
|
||||
console.print(f"[green]✓[/green] Rolled back to {data.get('version', version or 'previous')}")
|
||||
console.print(
|
||||
f"[green]✓[/green] Rolled back to {data.get('version', version or 'previous')}"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -632,7 +654,9 @@ def fn_rollback(name: str, version: str):
|
||||
|
||||
@fn_group.command(name="traffic")
|
||||
@click.argument("name")
|
||||
@click.option("--version", "-v", multiple=True, help="Version:weight pairs (e.g., v1:90 v2:10)")
|
||||
@click.option(
|
||||
"--version", "-v", multiple=True, help="Version:weight pairs (e.g., v1:90 v2:10)"
|
||||
)
|
||||
def fn_traffic(name: str, version: tuple):
|
||||
"""Configure traffic splitting between versions.
|
||||
|
||||
|
||||
@@ -90,7 +90,9 @@ def git_connect(provider, token):
|
||||
console.print("[dim]Complete the OAuth flow in your browser.[/dim]")
|
||||
console.print("[dim]Then run 'hanzo git providers' to verify.[/dim]")
|
||||
except Exception:
|
||||
console.print("[yellow]Could not open browser. Visit the URL above manually.[/yellow]")
|
||||
console.print(
|
||||
"[yellow]Could not open browser. Visit the URL above manually.[/yellow]"
|
||||
)
|
||||
|
||||
|
||||
@git_group.command(name="providers")
|
||||
@@ -106,7 +108,9 @@ def git_providers():
|
||||
if data is None:
|
||||
return
|
||||
|
||||
providers = data if isinstance(data, list) else data.get("providers", data.get("data", []))
|
||||
providers = (
|
||||
data if isinstance(data, list) else data.get("providers", data.get("data", []))
|
||||
)
|
||||
|
||||
if not providers:
|
||||
console.print("[dim]No git providers connected.[/dim]")
|
||||
@@ -173,9 +177,15 @@ def git_repos(provider):
|
||||
data = client.get(git_url())
|
||||
if data is None:
|
||||
return
|
||||
providers = data if isinstance(data, list) else data.get("providers", data.get("data", []))
|
||||
providers = (
|
||||
data
|
||||
if isinstance(data, list)
|
||||
else data.get("providers", data.get("data", []))
|
||||
)
|
||||
if not providers:
|
||||
console.print("[yellow]No git providers connected. Run 'hanzo git connect github' first.[/yellow]")
|
||||
console.print(
|
||||
"[yellow]No git providers connected. Run 'hanzo git connect github' first.[/yellow]"
|
||||
)
|
||||
return
|
||||
provider = str(providers[0].get("_id") or providers[0].get("id", ""))
|
||||
|
||||
@@ -220,7 +230,11 @@ def git_branches(repo, provider):
|
||||
data = client.get(git_url())
|
||||
if data is None:
|
||||
return
|
||||
providers = data if isinstance(data, list) else data.get("providers", data.get("data", []))
|
||||
providers = (
|
||||
data
|
||||
if isinstance(data, list)
|
||||
else data.get("providers", data.get("data", []))
|
||||
)
|
||||
if not providers:
|
||||
console.print("[yellow]No git providers connected.[/yellow]")
|
||||
return
|
||||
@@ -230,7 +244,9 @@ def git_branches(repo, provider):
|
||||
if data is None:
|
||||
return
|
||||
|
||||
branches = data if isinstance(data, list) else data.get("branches", data.get("data", []))
|
||||
branches = (
|
||||
data if isinstance(data, list) else data.get("branches", data.get("data", []))
|
||||
)
|
||||
|
||||
if not branches:
|
||||
console.print(f"[dim]No branches found for '{repo}'.[/dim]")
|
||||
@@ -277,7 +293,11 @@ def git_link(repo, container, branch, provider):
|
||||
data = client.get(git_url())
|
||||
if data is None:
|
||||
return
|
||||
providers = data if isinstance(data, list) else data.get("providers", data.get("data", []))
|
||||
providers = (
|
||||
data
|
||||
if isinstance(data, list)
|
||||
else data.get("providers", data.get("data", []))
|
||||
)
|
||||
if not providers:
|
||||
console.print("[yellow]No git providers connected.[/yellow]")
|
||||
return
|
||||
@@ -289,7 +309,9 @@ def git_link(repo, container, branch, provider):
|
||||
if data is None:
|
||||
return
|
||||
|
||||
containers = data if isinstance(data, list) else data.get("containers", data.get("data", []))
|
||||
containers = (
|
||||
data if isinstance(data, list) else data.get("containers", data.get("data", []))
|
||||
)
|
||||
cid = None
|
||||
for c in containers:
|
||||
cname = c.get("name", "")
|
||||
|
||||
@@ -84,7 +84,11 @@ def events_list(limit: int, event: str, user: str):
|
||||
|
||||
for e in data.get("events", []):
|
||||
props = e.get("properties", {})
|
||||
props_str = ", ".join(f"{k}={v}" for k, v in props.items()) if isinstance(props, dict) else str(props)
|
||||
props_str = (
|
||||
", ".join(f"{k}={v}" for k, v in props.items())
|
||||
if isinstance(props, dict)
|
||||
else str(props)
|
||||
)
|
||||
table.add_row(
|
||||
e.get("event", ""),
|
||||
e.get("user_id", ""),
|
||||
@@ -214,7 +218,9 @@ def funnels_create(name: str, steps: str):
|
||||
step_list = [s.strip() for s in steps.split(",")]
|
||||
resp = _request("post", "/v1/funnels", json={"name": name, "steps": step_list})
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] Funnel '{name}' created with {len(step_list)} steps")
|
||||
console.print(
|
||||
f"[green]✓[/green] Funnel '{name}' created with {len(step_list)} steps"
|
||||
)
|
||||
|
||||
|
||||
@funnels.command(name="delete")
|
||||
@@ -308,7 +314,9 @@ def web_stats(domain: str, period: str):
|
||||
table.add_column("Visitors", style="dim")
|
||||
|
||||
for p in pages[:10]:
|
||||
table.add_row(p.get("path", ""), str(p.get("views", 0)), str(p.get("visitors", 0)))
|
||||
table.add_row(
|
||||
p.get("path", ""), str(p.get("views", 0)), str(p.get("visitors", 0))
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
@@ -377,7 +385,9 @@ def flags_create(key: str, description: str, rollout: int):
|
||||
@click.option("--rollout", "-r", default=100, help="Rollout percentage")
|
||||
def flags_enable(flag_key: str, rollout: int):
|
||||
"""Enable a feature flag."""
|
||||
resp = _request("patch", f"/v1/flags/{flag_key}", json={"enabled": True, "rollout": rollout})
|
||||
resp = _request(
|
||||
"patch", f"/v1/flags/{flag_key}", json={"enabled": True, "rollout": rollout}
|
||||
)
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] Flag '{flag_key}' enabled at {rollout}%")
|
||||
|
||||
@@ -412,7 +422,11 @@ def tests():
|
||||
|
||||
|
||||
@tests.command(name="list")
|
||||
@click.option("--status", type=click.Choice(["running", "completed", "draft", "all"]), default="all")
|
||||
@click.option(
|
||||
"--status",
|
||||
type=click.Choice(["running", "completed", "draft", "all"]),
|
||||
default="all",
|
||||
)
|
||||
def tests_list(status: str):
|
||||
"""List A/B tests."""
|
||||
params: dict = {}
|
||||
@@ -430,7 +444,9 @@ def tests_list(status: str):
|
||||
|
||||
for t in data.get("tests", []):
|
||||
st = t.get("status", "draft")
|
||||
st_style = {"running": "green", "completed": "cyan", "draft": "dim"}.get(st, "white")
|
||||
st_style = {"running": "green", "completed": "cyan", "draft": "dim"}.get(
|
||||
st, "white"
|
||||
)
|
||||
winner = t.get("winner", "-")
|
||||
table.add_row(
|
||||
t.get("name", ""),
|
||||
@@ -445,20 +461,28 @@ def tests_list(status: str):
|
||||
|
||||
@tests.command(name="create")
|
||||
@click.option("--name", "-n", prompt=True, help="Test name")
|
||||
@click.option("--variants", "-v", default="control,treatment", help="Comma-separated variants")
|
||||
@click.option(
|
||||
"--variants", "-v", default="control,treatment", help="Comma-separated variants"
|
||||
)
|
||||
@click.option("--metric", "-m", required=True, help="Primary metric")
|
||||
@click.option("--traffic", "-t", default=100, help="Traffic percentage")
|
||||
def tests_create(name: str, variants: str, metric: str, traffic: int):
|
||||
"""Create an A/B test."""
|
||||
variant_list = [v.strip() for v in variants.split(",")]
|
||||
resp = _request("post", "/v1/tests", json={
|
||||
"name": name,
|
||||
"variants": variant_list,
|
||||
"primary_metric": metric,
|
||||
"traffic_percent": traffic,
|
||||
})
|
||||
resp = _request(
|
||||
"post",
|
||||
"/v1/tests",
|
||||
json={
|
||||
"name": name,
|
||||
"variants": variant_list,
|
||||
"primary_metric": metric,
|
||||
"traffic_percent": traffic,
|
||||
},
|
||||
)
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] A/B test '{name}' created with {len(variant_list)} variants")
|
||||
console.print(
|
||||
f"[green]✓[/green] A/B test '{name}' created with {len(variant_list)} variants"
|
||||
)
|
||||
|
||||
|
||||
@tests.command(name="start")
|
||||
@@ -481,7 +505,9 @@ def tests_stop(test_name: str):
|
||||
|
||||
@tests.command(name="results")
|
||||
@click.argument("test_name")
|
||||
@click.option("--format", "-f", "fmt", type=click.Choice(["table", "json"]), default="table")
|
||||
@click.option(
|
||||
"--format", "-f", "fmt", type=click.Choice(["table", "json"]), default="table"
|
||||
)
|
||||
def tests_results(test_name: str, fmt: str):
|
||||
"""View A/B test results."""
|
||||
resp = _request("get", f"/v1/tests/{test_name}/results")
|
||||
@@ -510,7 +536,11 @@ def tests_results(test_name: str, fmt: str):
|
||||
|
||||
for v in variants:
|
||||
is_winner = v.get("is_winner", False)
|
||||
name_str = f"[bold]{v.get('name', '')}[/bold] *" if is_winner else v.get("name", "")
|
||||
name_str = (
|
||||
f"[bold]{v.get('name', '')}[/bold] *"
|
||||
if is_winner
|
||||
else v.get("name", "")
|
||||
)
|
||||
table.add_row(
|
||||
name_str,
|
||||
str(v.get("users", 0)),
|
||||
@@ -543,7 +573,11 @@ def campaigns():
|
||||
|
||||
|
||||
@campaigns.command(name="list")
|
||||
@click.option("--status", type=click.Choice(["active", "draft", "completed", "all"]), default="all")
|
||||
@click.option(
|
||||
"--status",
|
||||
type=click.Choice(["active", "draft", "completed", "all"]),
|
||||
default="all",
|
||||
)
|
||||
def campaigns_list(status: str):
|
||||
"""List campaigns."""
|
||||
params: dict = {}
|
||||
@@ -561,7 +595,9 @@ def campaigns_list(status: str):
|
||||
|
||||
for c in data.get("campaigns", []):
|
||||
st = c.get("status", "draft")
|
||||
st_style = {"active": "green", "completed": "cyan", "draft": "dim"}.get(st, "white")
|
||||
st_style = {"active": "green", "completed": "cyan", "draft": "dim"}.get(
|
||||
st, "white"
|
||||
)
|
||||
table.add_row(
|
||||
c.get("name", ""),
|
||||
c.get("type", ""),
|
||||
@@ -575,11 +611,19 @@ def campaigns_list(status: str):
|
||||
|
||||
@campaigns.command(name="create")
|
||||
@click.option("--name", "-n", prompt=True, help="Campaign name")
|
||||
@click.option("--type", "-t", "campaign_type", type=click.Choice(["email", "push", "sms", "in-app"]), default="email")
|
||||
@click.option(
|
||||
"--type",
|
||||
"-t",
|
||||
"campaign_type",
|
||||
type=click.Choice(["email", "push", "sms", "in-app"]),
|
||||
default="email",
|
||||
)
|
||||
@click.option("--segment", "-s", help="Target segment")
|
||||
@click.option("--subject", help="Email subject")
|
||||
@click.option("--body", "-b", help="Message body")
|
||||
def campaigns_create(name: str, campaign_type: str, segment: str, subject: str, body: str):
|
||||
def campaigns_create(
|
||||
name: str, campaign_type: str, segment: str, subject: str, body: str
|
||||
):
|
||||
"""Create a campaign."""
|
||||
payload: dict = {"name": name, "type": campaign_type}
|
||||
if segment:
|
||||
@@ -604,7 +648,9 @@ def campaigns_send(campaign_name: str, schedule: str):
|
||||
resp = _request("post", f"/v1/campaigns/{campaign_name}/send", json=payload)
|
||||
check_response(resp)
|
||||
if schedule:
|
||||
console.print(f"[green]✓[/green] Campaign '{campaign_name}' scheduled for {schedule}")
|
||||
console.print(
|
||||
f"[green]✓[/green] Campaign '{campaign_name}' scheduled for {schedule}"
|
||||
)
|
||||
else:
|
||||
console.print(f"[green]✓[/green] Campaign '{campaign_name}' sent")
|
||||
|
||||
|
||||
@@ -27,7 +27,9 @@ def _get_iam_url() -> str:
|
||||
def _get_iam_credentials() -> tuple[str, str]:
|
||||
"""Get client_id and client_secret from env or defaults."""
|
||||
client_id = os.getenv("IAM_CLIENT_ID", os.getenv("HANZO_IAM_CLIENT_ID", ""))
|
||||
client_secret = os.getenv("IAM_CLIENT_SECRET", os.getenv("HANZO_IAM_CLIENT_SECRET", ""))
|
||||
client_secret = os.getenv(
|
||||
"IAM_CLIENT_SECRET", os.getenv("HANZO_IAM_CLIENT_SECRET", "")
|
||||
)
|
||||
if not client_id or not client_secret:
|
||||
# Try loading from auth file
|
||||
from pathlib import Path
|
||||
@@ -44,7 +46,11 @@ def _get_iam_credentials() -> tuple[str, str]:
|
||||
|
||||
|
||||
def _iam_request(
|
||||
method: str, path: str, params: dict | None = None, json_body: dict | None = None, auth_params: bool = True
|
||||
method: str,
|
||||
path: str,
|
||||
params: dict | None = None,
|
||||
json_body: dict | None = None,
|
||||
auth_params: bool = True,
|
||||
) -> dict:
|
||||
"""Make authenticated request to IAM API."""
|
||||
url = _get_iam_url().rstrip("/") + path
|
||||
@@ -173,7 +179,9 @@ def configure(url: str, client_id: str, client_secret: str, org: str):
|
||||
if not client_id:
|
||||
client_id = Prompt.ask("Client ID", default=auth.get("iam_client_id", ""))
|
||||
if not client_secret:
|
||||
client_secret = Prompt.ask("Client Secret", password=True, default=auth.get("iam_client_secret", ""))
|
||||
client_secret = Prompt.ask(
|
||||
"Client Secret", password=True, default=auth.get("iam_client_secret", "")
|
||||
)
|
||||
|
||||
auth["iam_url"] = url
|
||||
auth["iam_client_id"] = client_id
|
||||
@@ -194,9 +202,14 @@ def configure(url: str, client_id: str, client_secret: str, org: str):
|
||||
if data.get("status") != "error":
|
||||
console.print("[green]✓[/green] Connected to IAM successfully")
|
||||
else:
|
||||
console.print("[yellow]⚠[/yellow] Connected but got error: " + data.get("msg", "unknown"))
|
||||
console.print(
|
||||
"[yellow]⚠[/yellow] Connected but got error: "
|
||||
+ data.get("msg", "unknown")
|
||||
)
|
||||
except SystemExit:
|
||||
console.print("[yellow]⚠[/yellow] Could not verify connection (credentials saved anyway)")
|
||||
console.print(
|
||||
"[yellow]⚠[/yellow] Could not verify connection (credentials saved anyway)"
|
||||
)
|
||||
|
||||
|
||||
@iam_group.command(name="status")
|
||||
@@ -210,8 +223,13 @@ def iam_status():
|
||||
table.add_column("Value", style="white")
|
||||
|
||||
table.add_row("IAM URL", url)
|
||||
table.add_row("Client ID", client_id[:12] + "..." if client_id else "[red]Not set[/red]")
|
||||
table.add_row("Client Secret", "****" + client_secret[-4:] if client_secret else "[red]Not set[/red]")
|
||||
table.add_row(
|
||||
"Client ID", client_id[:12] + "..." if client_id else "[red]Not set[/red]"
|
||||
)
|
||||
table.add_row(
|
||||
"Client Secret",
|
||||
"****" + client_secret[-4:] if client_secret else "[red]Not set[/red]",
|
||||
)
|
||||
|
||||
if client_id and client_secret:
|
||||
try:
|
||||
@@ -304,9 +322,13 @@ def users_get(username: str, org: str):
|
||||
if user.get("roles"):
|
||||
lines.append(f"[cyan]Roles:[/cyan] {', '.join(str(r) for r in user['roles'])}")
|
||||
if user.get("groups"):
|
||||
lines.append(f"[cyan]Groups:[/cyan] {', '.join(str(g) for g in user['groups'])}")
|
||||
lines.append(
|
||||
f"[cyan]Groups:[/cyan] {', '.join(str(g) for g in user['groups'])}"
|
||||
)
|
||||
|
||||
console.print(Panel("\n".join(lines), title=f"User: {username}", border_style="cyan"))
|
||||
console.print(
|
||||
Panel("\n".join(lines), title=f"User: {username}", border_style="cyan")
|
||||
)
|
||||
|
||||
|
||||
@users.command(name="create")
|
||||
@@ -317,7 +339,15 @@ def users_get(username: str, org: str):
|
||||
@click.option("--phone", help="Phone number")
|
||||
@click.option("--org", "-o", default="hanzo", help="Organization")
|
||||
@click.option("--admin", is_flag=True, help="Make admin")
|
||||
def users_create(username: str, email: str, password: str, name: str, phone: str, org: str, admin: bool):
|
||||
def users_create(
|
||||
username: str,
|
||||
email: str,
|
||||
password: str,
|
||||
name: str,
|
||||
phone: str,
|
||||
org: str,
|
||||
admin: bool,
|
||||
):
|
||||
"""Create a new user."""
|
||||
user_obj = {
|
||||
"owner": org,
|
||||
@@ -355,7 +385,9 @@ def users_create(username: str, email: str, password: str, name: str, phone: str
|
||||
if resp.status_code == 200 and resp.json().get("status") == "ok":
|
||||
console.print("[green]✓[/green] Password set")
|
||||
else:
|
||||
console.print(f"[yellow]⚠[/yellow] Password may not have been set: {resp.json().get('msg', '')}")
|
||||
console.print(
|
||||
f"[yellow]⚠[/yellow] Password may not have been set: {resp.json().get('msg', '')}"
|
||||
)
|
||||
except Exception as e:
|
||||
console.print(f"[yellow]⚠[/yellow] Could not set password: {e}")
|
||||
|
||||
@@ -370,7 +402,14 @@ def users_create(username: str, email: str, password: str, name: str, phone: str
|
||||
@click.option("--forbidden/--no-forbidden", default=None, help="Ban/unban user")
|
||||
@click.option("--org", "-o", default="hanzo", help="Organization")
|
||||
def users_update(
|
||||
username: str, email: str, name: str, phone: str, password: str, admin: bool, forbidden: bool, org: str
|
||||
username: str,
|
||||
email: str,
|
||||
name: str,
|
||||
phone: str,
|
||||
password: str,
|
||||
admin: bool,
|
||||
forbidden: bool,
|
||||
org: str,
|
||||
):
|
||||
"""Update user fields."""
|
||||
# First get existing user
|
||||
@@ -394,7 +433,12 @@ def users_update(
|
||||
if forbidden is not None:
|
||||
user_obj["isForbidden"] = forbidden
|
||||
|
||||
data = _iam_request("POST", "/api/update-user", params={"id": f"{org}/{username}"}, json_body=user_obj)
|
||||
data = _iam_request(
|
||||
"POST",
|
||||
"/api/update-user",
|
||||
params={"id": f"{org}/{username}"},
|
||||
json_body=user_obj,
|
||||
)
|
||||
_check_response(data, "update user")
|
||||
|
||||
console.print(f"[green]✓[/green] User '{username}' updated")
|
||||
@@ -417,7 +461,9 @@ def users_update(
|
||||
if resp.status_code == 200 and resp.json().get("status") == "ok":
|
||||
console.print("[green]✓[/green] Password updated")
|
||||
else:
|
||||
console.print(f"[yellow]⚠[/yellow] Password update issue: {resp.json().get('msg', '')}")
|
||||
console.print(
|
||||
f"[yellow]⚠[/yellow] Password update issue: {resp.json().get('msg', '')}"
|
||||
)
|
||||
except Exception as e:
|
||||
console.print(f"[yellow]⚠[/yellow] Could not update password: {e}")
|
||||
|
||||
@@ -519,7 +565,9 @@ def orgs_get(name: str):
|
||||
f"[cyan]Created:[/cyan] {org.get('createdTime', '')}",
|
||||
]
|
||||
|
||||
console.print(Panel("\n".join(lines), title=f"Organization: {name}", border_style="cyan"))
|
||||
console.print(
|
||||
Panel("\n".join(lines), title=f"Organization: {name}", border_style="cyan")
|
||||
)
|
||||
|
||||
|
||||
@orgs.command(name="create")
|
||||
@@ -550,7 +598,9 @@ def orgs_delete(name: str, yes: bool):
|
||||
if not yes:
|
||||
from rich.prompt import Confirm
|
||||
|
||||
if not Confirm.ask(f"[red]Delete organization '{name}'? This cannot be undone.[/red]"):
|
||||
if not Confirm.ask(
|
||||
f"[red]Delete organization '{name}'? This cannot be undone.[/red]"
|
||||
):
|
||||
return
|
||||
|
||||
org_obj = {"owner": "admin", "name": name}
|
||||
@@ -627,7 +677,9 @@ def providers_get(name: str, owner: str):
|
||||
f"[cyan]Created:[/cyan] {prov.get('createdTime', '')}",
|
||||
]
|
||||
|
||||
console.print(Panel("\n".join(lines), title=f"Provider: {name}", border_style="cyan"))
|
||||
console.print(
|
||||
Panel("\n".join(lines), title=f"Provider: {name}", border_style="cyan")
|
||||
)
|
||||
|
||||
|
||||
@providers.command(name="create")
|
||||
@@ -662,7 +714,13 @@ def providers_get(name: str, owner: str):
|
||||
@click.option("--scopes", help="OAuth scopes")
|
||||
@click.option("--owner", default="admin", help="Provider owner")
|
||||
def providers_create(
|
||||
name: str, ptype: str, display_name: str, client_id: str, client_secret: str, scopes: str, owner: str
|
||||
name: str,
|
||||
ptype: str,
|
||||
display_name: str,
|
||||
client_id: str,
|
||||
client_secret: str,
|
||||
scopes: str,
|
||||
owner: str,
|
||||
):
|
||||
"""Create an authentication provider."""
|
||||
prov_obj = {
|
||||
@@ -737,7 +795,11 @@ def apps_list(org: str):
|
||||
a.get("name", ""),
|
||||
a.get("displayName", ""),
|
||||
a.get("organization", ""),
|
||||
a.get("clientId", "")[:20] + "..." if len(a.get("clientId", "")) > 20 else a.get("clientId", ""),
|
||||
(
|
||||
a.get("clientId", "")[:20] + "..."
|
||||
if len(a.get("clientId", "")) > 20
|
||||
else a.get("clientId", "")
|
||||
),
|
||||
str(providers_count),
|
||||
)
|
||||
|
||||
@@ -788,7 +850,9 @@ def apps_get(name: str, org: str):
|
||||
required = s.get("required", False) if isinstance(s, dict) else False
|
||||
lines.append(f" - {sname} {'(required)' if required else '(optional)'}")
|
||||
|
||||
console.print(Panel("\n".join(lines), title=f"Application: {name}", border_style="cyan"))
|
||||
console.print(
|
||||
Panel("\n".join(lines), title=f"Application: {name}", border_style="cyan")
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -1048,7 +1112,9 @@ def tokens_inspect(token: str):
|
||||
token = auth.get("token", "")
|
||||
|
||||
if not token:
|
||||
console.print("[red]No token provided and none found in ~/.hanzo/auth.json[/red]")
|
||||
console.print(
|
||||
"[red]No token provided and none found in ~/.hanzo/auth.json[/red]"
|
||||
)
|
||||
return
|
||||
|
||||
parts = token.split(".")
|
||||
@@ -1081,14 +1147,18 @@ def tokens_inspect(token: str):
|
||||
console.print(f"\n[red]Token EXPIRED at {exp}[/red]")
|
||||
else:
|
||||
delta = exp - now
|
||||
console.print(f"\n[green]Token valid until {exp} ({delta} remaining)[/green]")
|
||||
console.print(
|
||||
f"\n[green]Token valid until {exp} ({delta} remaining)[/green]"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@tokens.command(name="exchange")
|
||||
@click.argument("code")
|
||||
@click.option("--redirect-uri", "-r", default="", help="Redirect URI used in authorization")
|
||||
@click.option(
|
||||
"--redirect-uri", "-r", default="", help="Redirect URI used in authorization"
|
||||
)
|
||||
def tokens_exchange(code: str, redirect_uri: str):
|
||||
"""Exchange authorization code for access token."""
|
||||
client_id, client_secret = _get_iam_credentials()
|
||||
|
||||
@@ -241,8 +241,14 @@ def install_list(installed: bool, available: bool):
|
||||
is_installed = shutil.which(binary) is not None
|
||||
if is_installed:
|
||||
try:
|
||||
result = subprocess.run([binary, "--version"], capture_output=True, text=True, timeout=5)
|
||||
version = result.stdout.strip().split()[-1] if result.returncode == 0 else "?"
|
||||
result = subprocess.run(
|
||||
[binary, "--version"], capture_output=True, text=True, timeout=5
|
||||
)
|
||||
version = (
|
||||
result.stdout.strip().split()[-1]
|
||||
if result.returncode == 0
|
||||
else "?"
|
||||
)
|
||||
except Exception:
|
||||
version = "?"
|
||||
|
||||
@@ -251,7 +257,14 @@ def install_list(installed: bool, available: bool):
|
||||
if available and is_installed:
|
||||
continue
|
||||
|
||||
table.add_row(tool_id, tool["name"], tool["language"], tool["source"], "✓" if is_installed else "", version)
|
||||
table.add_row(
|
||||
tool_id,
|
||||
tool["name"],
|
||||
tool["language"],
|
||||
tool["source"],
|
||||
"✓" if is_installed else "",
|
||||
version,
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
@@ -426,7 +439,9 @@ def _install_github_release(tool: dict, version: str, force: bool):
|
||||
break
|
||||
|
||||
if not download_url:
|
||||
console.print(f"[yellow]No pre-built binary found, building from source...[/yellow]")
|
||||
console.print(
|
||||
f"[yellow]No pre-built binary found, building from source...[/yellow]"
|
||||
)
|
||||
_install_cargo(tool, version, force)
|
||||
return
|
||||
|
||||
@@ -767,7 +782,9 @@ def install_doctor():
|
||||
|
||||
for name, cmd, required in checks:
|
||||
try:
|
||||
result = subprocess.run(cmd.split(), capture_output=True, text=True, timeout=5)
|
||||
result = subprocess.run(
|
||||
cmd.split(), capture_output=True, text=True, timeout=5
|
||||
)
|
||||
if result.returncode == 0:
|
||||
version = result.stdout.strip().split()[-1]
|
||||
console.print(f" [green]✓[/green] {name}: {version}")
|
||||
@@ -912,19 +929,25 @@ def install_ide(target: str, install_all: bool, list_only: bool):
|
||||
try:
|
||||
with open(config_path) as f:
|
||||
config = json.load(f)
|
||||
has_hanzo = "hanzo" in config.get("mcpServers", {}) or "hanzo-mcp" in config.get(
|
||||
has_hanzo = "hanzo" in config.get(
|
||||
"mcpServers", {}
|
||||
)
|
||||
) or "hanzo-mcp" in config.get("mcpServers", {})
|
||||
status = (
|
||||
"[green]✓ hanzo-mcp configured[/green]" if has_hanzo else "[yellow]○ no hanzo-mcp[/yellow]"
|
||||
"[green]✓ hanzo-mcp configured[/green]"
|
||||
if has_hanzo
|
||||
else "[yellow]○ no hanzo-mcp[/yellow]"
|
||||
)
|
||||
except Exception:
|
||||
status = "[dim]○ config exists[/dim]"
|
||||
console.print(f" [green]✓[/green] {app_name}: {status}")
|
||||
elif parent_exists:
|
||||
console.print(f" [yellow]○[/yellow] {app_name}: [dim]app installed, no MCP config[/dim]")
|
||||
console.print(
|
||||
f" [yellow]○[/yellow] {app_name}: [dim]app installed, no MCP config[/dim]"
|
||||
)
|
||||
else:
|
||||
console.print(f" [dim]○[/dim] {app_name}: [dim]not installed[/dim]")
|
||||
console.print(
|
||||
f" [dim]○[/dim] {app_name}: [dim]not installed[/dim]"
|
||||
)
|
||||
|
||||
console.print()
|
||||
console.print("[dim]Run: hanzo install ide <name> to configure[/dim]")
|
||||
@@ -978,7 +1001,9 @@ def install_ide(target: str, install_all: bool, list_only: bool):
|
||||
|
||||
@install_group.command(name="browser")
|
||||
@click.argument("browser", required=False)
|
||||
@click.option("--list", "list_only", is_flag=True, help="List browser extension install URLs")
|
||||
@click.option(
|
||||
"--list", "list_only", is_flag=True, help="List browser extension install URLs"
|
||||
)
|
||||
def install_browser(browser: str, list_only: bool):
|
||||
"""Install Hanzo browser extension.
|
||||
|
||||
@@ -1075,7 +1100,9 @@ def install_ai(app: str, list_only: bool):
|
||||
from click import Context
|
||||
|
||||
ctx = Context(install_ide)
|
||||
ctx.invoke(install_ide, target=app, install_all=False, list_only=list_only or not app)
|
||||
ctx.invoke(
|
||||
install_ide, target=app, install_all=False, list_only=list_only or not app
|
||||
)
|
||||
|
||||
|
||||
@install_group.command(name="all")
|
||||
@@ -1167,7 +1194,9 @@ NANOBROWSER_PATH = Path.home() / "work" / "nanobrowser" / "nanobrowser"
|
||||
@install_group.command(name="nanobrowser")
|
||||
@click.option("--build", "-b", is_flag=True, help="Build from source")
|
||||
@click.option("--dev", is_flag=True, help="Run in development mode")
|
||||
@click.option("--open", "open_browser", is_flag=True, help="Open Chrome with extension loaded")
|
||||
@click.option(
|
||||
"--open", "open_browser", is_flag=True, help="Open Chrome with extension loaded"
|
||||
)
|
||||
def install_nanobrowser(build: bool, dev: bool, open_browser: bool):
|
||||
"""Install/build Nanobrowser - lightweight AI browser for agents.
|
||||
|
||||
@@ -1198,7 +1227,9 @@ def install_nanobrowser(build: bool, dev: bool, open_browser: bool):
|
||||
console.print(f"Expected at: {nanobrowser_dir}")
|
||||
console.print()
|
||||
console.print("Clone it with:")
|
||||
console.print(f" git clone https://github.com/nanobrowser/nanobrowser {nanobrowser_dir}")
|
||||
console.print(
|
||||
f" git clone https://github.com/nanobrowser/nanobrowser {nanobrowser_dir}"
|
||||
)
|
||||
return
|
||||
|
||||
dist_dir = nanobrowser_dir / "dist"
|
||||
@@ -1208,7 +1239,12 @@ def install_nanobrowser(build: bool, dev: bool, open_browser: bool):
|
||||
|
||||
# Install dependencies
|
||||
console.print(" Installing dependencies...")
|
||||
result = subprocess.run(["pnpm", "install"], cwd=str(nanobrowser_dir), capture_output=True, text=True)
|
||||
result = subprocess.run(
|
||||
["pnpm", "install"],
|
||||
cwd=str(nanobrowser_dir),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
console.print(f"[red]Failed to install dependencies: {result.stderr}[/red]")
|
||||
return
|
||||
@@ -1219,7 +1255,12 @@ def install_nanobrowser(build: bool, dev: bool, open_browser: bool):
|
||||
subprocess.run(["pnpm", "dev"], cwd=str(nanobrowser_dir))
|
||||
else:
|
||||
console.print(" Building extension...")
|
||||
result = subprocess.run(["pnpm", "build"], cwd=str(nanobrowser_dir), capture_output=True, text=True)
|
||||
result = subprocess.run(
|
||||
["pnpm", "build"],
|
||||
cwd=str(nanobrowser_dir),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
console.print(f"[red]Build failed: {result.stderr}[/red]")
|
||||
return
|
||||
@@ -1227,7 +1268,9 @@ def install_nanobrowser(build: bool, dev: bool, open_browser: bool):
|
||||
|
||||
elif open_browser:
|
||||
if not dist_dir.exists():
|
||||
console.print("[yellow]Extension not built yet. Run with --build first.[/yellow]")
|
||||
console.print(
|
||||
"[yellow]Extension not built yet. Run with --build first.[/yellow]"
|
||||
)
|
||||
return
|
||||
|
||||
# Open Chrome with extension loaded (macOS)
|
||||
@@ -1254,7 +1297,9 @@ def install_nanobrowser(build: bool, dev: bool, open_browser: bool):
|
||||
# Status check
|
||||
console.print("[bold]Nanobrowser Status[/bold]")
|
||||
console.print(f" Location: {nanobrowser_dir}")
|
||||
console.print(f" Built: {'[green]✓[/green]' if dist_dir.exists() else '[yellow]○[/yellow] (run --build)'}")
|
||||
console.print(
|
||||
f" Built: {'[green]✓[/green]' if dist_dir.exists() else '[yellow]○[/yellow] (run --build)'}"
|
||||
)
|
||||
|
||||
# Get version from package.json
|
||||
package_json = nanobrowser_dir / "package.json"
|
||||
@@ -1286,8 +1331,12 @@ RUNTIME_PATH = Path.home() / "work" / "hanzo" / "runtime"
|
||||
|
||||
@install_group.command(name="runtime")
|
||||
@click.option("--setup", "-s", is_flag=True, help="Set up the runtime environment")
|
||||
@click.option("--sdk", type=click.Choice(["python", "typescript", "both"]), help="Install SDK")
|
||||
@click.option("--computer-use", is_flag=True, help="Set up VNC desktop for computer use")
|
||||
@click.option(
|
||||
"--sdk", type=click.Choice(["python", "typescript", "both"]), help="Install SDK"
|
||||
)
|
||||
@click.option(
|
||||
"--computer-use", is_flag=True, help="Set up VNC desktop for computer use"
|
||||
)
|
||||
def install_runtime(setup: bool, sdk: str, computer_use: bool):
|
||||
"""Configure Hanzo Runtime - sandboxed agent execution.
|
||||
|
||||
@@ -1320,7 +1369,10 @@ def install_runtime(setup: bool, sdk: str, computer_use: bool):
|
||||
if shutil.which("uv"):
|
||||
subprocess.run(["uv", "pip", "install", "hanzo-runtime"], check=True)
|
||||
else:
|
||||
subprocess.run([sys.executable, "-m", "pip", "install", "hanzo-runtime"], check=True)
|
||||
subprocess.run(
|
||||
[sys.executable, "-m", "pip", "install", "hanzo-runtime"],
|
||||
check=True,
|
||||
)
|
||||
console.print("[green]✓[/green] Python SDK installed")
|
||||
|
||||
if sdk in ("typescript", "both"):
|
||||
@@ -1376,7 +1428,13 @@ def install_runtime(setup: bool, sdk: str, computer_use: bool):
|
||||
cli_dir = runtime_dir / "apps" / "cli"
|
||||
if cli_dir.exists():
|
||||
result = subprocess.run(
|
||||
["go", "build", "-o", str(Path.home() / ".hanzo" / "bin" / "hanzo-runtime"), "."],
|
||||
[
|
||||
"go",
|
||||
"build",
|
||||
"-o",
|
||||
str(Path.home() / ".hanzo" / "bin" / "hanzo-runtime"),
|
||||
".",
|
||||
],
|
||||
cwd=str(cli_dir),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -1417,7 +1475,11 @@ def install_runtime(setup: bool, sdk: str, computer_use: bool):
|
||||
# Python SDK
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", "import hanzo_runtime; print(hanzo_runtime.__version__)"],
|
||||
[
|
||||
sys.executable,
|
||||
"-c",
|
||||
"import hanzo_runtime; print(hanzo_runtime.__version__)",
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
@@ -1429,7 +1491,9 @@ def install_runtime(setup: bool, sdk: str, computer_use: bool):
|
||||
console.print(" [dim]○[/dim] Python: not installed")
|
||||
|
||||
# TypeScript SDK
|
||||
result = subprocess.run(["npm", "list", "-g", "@hanzo/runtime"], capture_output=True, text=True)
|
||||
result = subprocess.run(
|
||||
["npm", "list", "-g", "@hanzo/runtime"], capture_output=True, text=True
|
||||
)
|
||||
if "@hanzo/runtime" in result.stdout:
|
||||
console.print(f" [green]✓[/green] TypeScript: installed")
|
||||
else:
|
||||
@@ -1514,7 +1578,9 @@ def install_cas(component: str, setup: bool, start: bool):
|
||||
|
||||
if not comp_path.exists():
|
||||
console.print(f"[red]{info['name']} not found at: {comp_path}[/red]")
|
||||
console.print(f"Clone: git clone https://github.com/{component}/{component} {comp_path}")
|
||||
console.print(
|
||||
f"Clone: git clone https://github.com/{component}/{component} {comp_path}"
|
||||
)
|
||||
return
|
||||
|
||||
if setup:
|
||||
@@ -1529,7 +1595,9 @@ def install_cas(component: str, setup: bool, start: bool):
|
||||
|
||||
# Build
|
||||
console.print(" Building...")
|
||||
result = subprocess.run(["go", "build", "."], cwd=str(comp_path), capture_output=True, text=True)
|
||||
result = subprocess.run(
|
||||
["go", "build", "."], cwd=str(comp_path), capture_output=True, text=True
|
||||
)
|
||||
if result.returncode == 0:
|
||||
console.print(f"[green]✓[/green] {info['name']} built")
|
||||
else:
|
||||
@@ -1576,9 +1644,17 @@ def install_cas(component: str, setup: bool, start: bool):
|
||||
built = binary and binary.exists() if binary else False
|
||||
|
||||
status = "[green]✓[/green]" if exists else "[dim]○[/dim]"
|
||||
build_status = " [green](built)[/green]" if built else " [yellow](not built)[/yellow]" if exists else ""
|
||||
build_status = (
|
||||
" [green](built)[/green]"
|
||||
if built
|
||||
else " [yellow](not built)[/yellow]" if exists else ""
|
||||
)
|
||||
|
||||
console.print(f" {status} {info['name']}: {info['description']}{build_status}")
|
||||
console.print(
|
||||
f" {status} {info['name']}: {info['description']}{build_status}"
|
||||
)
|
||||
|
||||
console.print()
|
||||
console.print("[dim]Set up a component: hanzo install cas <component> --setup[/dim]")
|
||||
console.print(
|
||||
"[dim]Set up a component: hanzo install cas <component> --setup[/dim]"
|
||||
)
|
||||
|
||||
@@ -57,7 +57,9 @@ def jobs_group():
|
||||
@click.option("--priority", type=int, default=5, help="Priority (1-10)")
|
||||
@click.option("--delay", "-d", help="Delay before execution (e.g., 5m, 1h)")
|
||||
@click.option("--wait", "-w", is_flag=True, help="Wait for completion")
|
||||
def jobs_run(name: str, payload: str, queue: str, priority: int, delay: str, wait: bool):
|
||||
def jobs_run(
|
||||
name: str, payload: str, queue: str, priority: int, delay: str, wait: bool
|
||||
):
|
||||
"""Run a background job."""
|
||||
body = {"name": name, "queue": queue, "priority": priority}
|
||||
if payload:
|
||||
@@ -88,7 +90,12 @@ def jobs_run(name: str, payload: str, queue: str, priority: int, delay: str, wai
|
||||
|
||||
|
||||
@jobs_group.command(name="list")
|
||||
@click.option("--status", "-s", type=click.Choice(["pending", "active", "completed", "failed", "all"]), default="all")
|
||||
@click.option(
|
||||
"--status",
|
||||
"-s",
|
||||
type=click.Choice(["pending", "active", "completed", "failed", "all"]),
|
||||
default="all",
|
||||
)
|
||||
@click.option("--queue", "-q", help="Filter by queue")
|
||||
@click.option("--limit", "-n", default=20, help="Max results")
|
||||
def jobs_list(status: str, queue: str, limit: int):
|
||||
@@ -154,7 +161,9 @@ def jobs_logs(job_id: str, follow: bool, tail: int):
|
||||
ts = str(line.get("timestamp", ""))[:19]
|
||||
level = line.get("level", "info")
|
||||
msg = line.get("message", "")
|
||||
style = "red" if level == "error" else "yellow" if level == "warn" else "dim"
|
||||
style = (
|
||||
"red" if level == "error" else "yellow" if level == "warn" else "dim"
|
||||
)
|
||||
console.print(f"[dim]{ts}[/dim] [{style}]{level}[/{style}] {msg}")
|
||||
else:
|
||||
console.print(str(line))
|
||||
@@ -234,7 +243,9 @@ def cron():
|
||||
|
||||
|
||||
@cron.command(name="list")
|
||||
@click.option("--status", "-s", type=click.Choice(["active", "paused", "all"]), default="all")
|
||||
@click.option(
|
||||
"--status", "-s", type=click.Choice(["active", "paused", "all"]), default="all"
|
||||
)
|
||||
def cron_list(status: str):
|
||||
"""List cron schedules."""
|
||||
params = {}
|
||||
@@ -265,12 +276,16 @@ def cron_list(status: str):
|
||||
|
||||
console.print(table)
|
||||
if not items:
|
||||
console.print("[dim]No cron schedules found. Create one with 'hanzo jobs cron create'[/dim]")
|
||||
console.print(
|
||||
"[dim]No cron schedules found. Create one with 'hanzo jobs cron create'[/dim]"
|
||||
)
|
||||
|
||||
|
||||
@cron.command(name="create")
|
||||
@click.argument("name")
|
||||
@click.option("--schedule", "-s", required=True, help="Cron expression (e.g., '0 * * * *')")
|
||||
@click.option(
|
||||
"--schedule", "-s", required=True, help="Cron expression (e.g., '0 * * * *')"
|
||||
)
|
||||
@click.option("--job", "-j", required=True, help="Job name to run")
|
||||
@click.option("--payload", "-p", help="JSON payload")
|
||||
@click.option("--timezone", "-tz", default="UTC", help="Timezone")
|
||||
|
||||
@@ -102,7 +102,9 @@ def cluster():
|
||||
@cluster.command(name="create")
|
||||
@click.argument("name")
|
||||
@click.option("--region", "-r", help="Region")
|
||||
@click.option("--version", "-v", "k8s_version", default="1.29", help="Kubernetes version")
|
||||
@click.option(
|
||||
"--version", "-v", "k8s_version", default="1.29", help="Kubernetes version"
|
||||
)
|
||||
@click.option("--nodes", "-n", default=3, help="Number of nodes")
|
||||
@click.option("--node-type", "-t", default="standard-2", help="Node type")
|
||||
@click.option("--ha", is_flag=True, help="High availability control plane")
|
||||
@@ -164,10 +166,14 @@ def cluster_list(region):
|
||||
if data is None:
|
||||
return
|
||||
|
||||
clusters = data if isinstance(data, list) else data.get("clusters", data.get("data", []))
|
||||
clusters = (
|
||||
data if isinstance(data, list) else data.get("clusters", data.get("data", []))
|
||||
)
|
||||
|
||||
if not clusters:
|
||||
console.print("[dim]No clusters found. Create one with 'hanzo k8s cluster create'[/dim]")
|
||||
console.print(
|
||||
"[dim]No clusters found. Create one with 'hanzo k8s cluster create'[/dim]"
|
||||
)
|
||||
return
|
||||
|
||||
table = Table(title="Kubernetes Clusters", box=box.ROUNDED)
|
||||
@@ -240,7 +246,9 @@ def cluster_delete(name, force):
|
||||
if not force:
|
||||
from rich.prompt import Confirm
|
||||
|
||||
if not Confirm.ask(f"[red]Delete cluster '{name}'? This cannot be undone.[/red]"):
|
||||
if not Confirm.ask(
|
||||
f"[red]Delete cluster '{name}'? This cannot be undone.[/red]"
|
||||
):
|
||||
return
|
||||
|
||||
client = _get_client(timeout=60)
|
||||
@@ -387,7 +395,9 @@ def fleet_create(name, description):
|
||||
console.print(f"[green]✓[/green] Fleet '{name}' created")
|
||||
if description:
|
||||
console.print(f" Description: {description}")
|
||||
console.print("[dim]Fleet management is handled at the cluster level in the current PaaS release.[/dim]")
|
||||
console.print(
|
||||
"[dim]Fleet management is handled at the cluster level in the current PaaS release.[/dim]"
|
||||
)
|
||||
|
||||
|
||||
@fleet.command(name="list")
|
||||
@@ -404,7 +414,9 @@ def fleet_list():
|
||||
if data is None:
|
||||
return
|
||||
|
||||
clusters = data if isinstance(data, list) else data.get("clusters", data.get("data", []))
|
||||
clusters = (
|
||||
data if isinstance(data, list) else data.get("clusters", data.get("data", []))
|
||||
)
|
||||
|
||||
if not clusters:
|
||||
console.print("[dim]No fleets found[/dim]")
|
||||
@@ -450,7 +462,9 @@ def fleet_remove(fleet, cluster_name):
|
||||
@click.argument("fleet")
|
||||
@click.option("--manifest", "-f", required=True, help="Manifest file or directory")
|
||||
@click.option("--selector", "-l", help="Cluster selector (labels)")
|
||||
@click.option("--strategy", type=click.Choice(["rolling", "all", "canary"]), default="rolling")
|
||||
@click.option(
|
||||
"--strategy", type=click.Choice(["rolling", "all", "canary"]), default="rolling"
|
||||
)
|
||||
def fleet_deploy(fleet, manifest, selector, strategy):
|
||||
"""Deploy workloads across fleet.
|
||||
|
||||
@@ -561,7 +575,9 @@ def k8s_pods(name, all_namespaces):
|
||||
if data is None:
|
||||
return
|
||||
|
||||
pods = data if isinstance(data, list) else data.get("pods", data.get("data", []))
|
||||
pods = (
|
||||
data if isinstance(data, list) else data.get("pods", data.get("data", []))
|
||||
)
|
||||
title = f"Pods for '{name}'"
|
||||
else:
|
||||
# List all containers, then get pods
|
||||
@@ -569,14 +585,22 @@ def k8s_pods(name, all_namespaces):
|
||||
if data is None:
|
||||
return
|
||||
|
||||
containers = data if isinstance(data, list) else data.get("containers", data.get("data", []))
|
||||
containers = (
|
||||
data
|
||||
if isinstance(data, list)
|
||||
else data.get("containers", data.get("data", []))
|
||||
)
|
||||
pods = []
|
||||
for c in containers:
|
||||
cid = c.get("_id") or c.get("iid") or c.get("id", "")
|
||||
cname = c.get("name", "")
|
||||
pod_data = client.get(f"{base_url}/{cid}/pods")
|
||||
if pod_data:
|
||||
pod_list = pod_data if isinstance(pod_data, list) else pod_data.get("pods", pod_data.get("data", []))
|
||||
pod_list = (
|
||||
pod_data
|
||||
if isinstance(pod_data, list)
|
||||
else pod_data.get("pods", pod_data.get("data", []))
|
||||
)
|
||||
for p in pod_list:
|
||||
p["_container_name"] = cname
|
||||
pods.extend(pod_list)
|
||||
@@ -618,7 +642,9 @@ def k8s_services(all_namespaces):
|
||||
if data is None:
|
||||
return
|
||||
|
||||
containers = data if isinstance(data, list) else data.get("containers", data.get("data", []))
|
||||
containers = (
|
||||
data if isinstance(data, list) else data.get("containers", data.get("data", []))
|
||||
)
|
||||
|
||||
if not containers:
|
||||
console.print("[dim]No services found[/dim]")
|
||||
@@ -703,7 +729,9 @@ def k8s_exec(pod, command, container, stdin, tty):
|
||||
cmd = " ".join(command) if command else "/bin/sh"
|
||||
console.print(f"[cyan]Executing in '{pod}': {cmd}[/cyan]")
|
||||
console.print("[yellow]Remote exec requires direct kubectl access.[/yellow]")
|
||||
console.print("[dim]Use 'hanzo k8s cluster kubeconfig NAME --merge' then 'kubectl exec'[/dim]")
|
||||
console.print(
|
||||
"[dim]Use 'hanzo k8s cluster kubeconfig NAME --merge' then 'kubectl exec'[/dim]"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -775,7 +803,9 @@ def config_list(secrets):
|
||||
if data is None:
|
||||
return
|
||||
|
||||
containers = data if isinstance(data, list) else data.get("containers", data.get("data", []))
|
||||
containers = (
|
||||
data if isinstance(data, list) else data.get("containers", data.get("data", []))
|
||||
)
|
||||
kind = "Secrets" if secrets else "ConfigMaps"
|
||||
|
||||
table = Table(title=f"Container Variables ({kind})", box=box.ROUNDED)
|
||||
@@ -859,7 +889,9 @@ def ingress_list(all_namespaces):
|
||||
if data is None:
|
||||
return
|
||||
|
||||
containers = data if isinstance(data, list) else data.get("containers", data.get("data", []))
|
||||
containers = (
|
||||
data if isinstance(data, list) else data.get("containers", data.get("data", []))
|
||||
)
|
||||
|
||||
table = Table(title="Ingress (Custom Domains)", box=box.ROUNDED)
|
||||
table.add_column("Container", style="cyan")
|
||||
@@ -875,7 +907,9 @@ def ingress_list(all_namespaces):
|
||||
found = True
|
||||
port = str(networking.get("containerPort", ""))
|
||||
tls_secret = networking.get("tlsSecret", "")
|
||||
table.add_row(c.get("name", ""), domain, port, "Yes" if tls_secret else "No")
|
||||
table.add_row(
|
||||
c.get("name", ""), domain, port, "Yes" if tls_secret else "No"
|
||||
)
|
||||
|
||||
if found:
|
||||
console.print(table)
|
||||
@@ -952,7 +986,9 @@ def nodepool_list(cluster):
|
||||
if data is None:
|
||||
return
|
||||
|
||||
pools = data if isinstance(data, list) else data.get("nodePools", data.get("data", []))
|
||||
pools = (
|
||||
data if isinstance(data, list) else data.get("nodePools", data.get("data", []))
|
||||
)
|
||||
|
||||
if not pools:
|
||||
console.print("[dim]No node pools found[/dim]")
|
||||
@@ -988,7 +1024,9 @@ def nodepool_scale(name, cluster, nodes):
|
||||
return
|
||||
|
||||
console.print(f"[cyan]Scaling pool '{name}' to {nodes} nodes...[/cyan]")
|
||||
result = client.put(f"{cluster_url(cluster)}/nodepools/{name}", {"nodeCount": nodes})
|
||||
result = client.put(
|
||||
f"{cluster_url(cluster)}/nodepools/{name}", {"nodeCount": nodes}
|
||||
)
|
||||
if result is None:
|
||||
return
|
||||
|
||||
|
||||
@@ -56,7 +56,9 @@ def kv_group():
|
||||
@click.argument("name")
|
||||
@click.option("--region", "-r", multiple=True, help="Regions for replication")
|
||||
@click.option("--max-size", default="1GB", help="Max store size")
|
||||
@click.option("--eviction", type=click.Choice(["lru", "lfu", "ttl", "none"]), default="lru")
|
||||
@click.option(
|
||||
"--eviction", type=click.Choice(["lru", "lfu", "ttl", "none"]), default="lru"
|
||||
)
|
||||
def kv_create(name: str, region: tuple, max_size: str, eviction: str):
|
||||
"""Create a KV store."""
|
||||
payload = {"name": name, "max_size": max_size, "eviction_policy": eviction}
|
||||
@@ -98,7 +100,9 @@ def kv_list():
|
||||
|
||||
console.print(table)
|
||||
if not stores:
|
||||
console.print("[dim]No KV stores found. Create one with 'hanzo kv create'[/dim]")
|
||||
console.print(
|
||||
"[dim]No KV stores found. Create one with 'hanzo kv create'[/dim]"
|
||||
)
|
||||
|
||||
|
||||
@kv_group.command(name="delete")
|
||||
@@ -177,7 +181,9 @@ def kv_del(keys: tuple, store: str):
|
||||
@click.option("--limit", "-n", default=100, help="Max keys")
|
||||
def kv_keys(store: str, pattern: str, limit: int):
|
||||
"""List keys matching pattern."""
|
||||
resp = _request("get", f"/v1/stores/{store}/keys", params={"pattern": pattern, "limit": limit})
|
||||
resp = _request(
|
||||
"get", f"/v1/stores/{store}/keys", params={"pattern": pattern, "limit": limit}
|
||||
)
|
||||
data = check_response(resp)
|
||||
keys = data.get("keys", [])
|
||||
|
||||
@@ -240,7 +246,9 @@ def kv_mset(pairs: tuple, store: str):
|
||||
def kv_ttl(key: str, store: str, set_ttl: str):
|
||||
"""Get or set TTL for a key."""
|
||||
if set_ttl:
|
||||
resp = _request("put", f"/v1/stores/{store}/keys/{key}/ttl", json={"ttl": set_ttl})
|
||||
resp = _request(
|
||||
"put", f"/v1/stores/{store}/keys/{key}/ttl", json={"ttl": set_ttl}
|
||||
)
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] Set TTL for '{key}' to {set_ttl}")
|
||||
else:
|
||||
|
||||
@@ -96,7 +96,9 @@ async def tools(ctx, category: str):
|
||||
|
||||
# Display tools
|
||||
for cat, tools in sorted(categories.items()):
|
||||
table = Table(title=f"{cat.title()} Tools" if len(categories) > 1 else "MCP Tools")
|
||||
table = Table(
|
||||
title=f"{cat.title()} Tools" if len(categories) > 1 else "MCP Tools"
|
||||
)
|
||||
table.add_column("Name", style="cyan", no_wrap=True)
|
||||
table.add_column("Description")
|
||||
|
||||
@@ -238,7 +240,9 @@ def install(ctx, path: str):
|
||||
existing["mcpServers"]["hanzo-mcp"] = config["mcpServers"]["hanzo-mcp"]
|
||||
config = existing
|
||||
except Exception as e:
|
||||
console.print(f"[yellow]Warning: Could not read existing config: {e}[/yellow]")
|
||||
console.print(
|
||||
f"[yellow]Warning: Could not read existing config: {e}[/yellow]"
|
||||
)
|
||||
|
||||
# Write config
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -50,7 +50,9 @@ async def start(
|
||||
if not click.confirm("Continue without wallet?"):
|
||||
return
|
||||
|
||||
miner = HanzoMiner(name=name, wallet=wallet, device=device, network=network, min_stake=min_stake)
|
||||
miner = HanzoMiner(
|
||||
name=name, wallet=wallet, device=device, network=network, min_stake=min_stake
|
||||
)
|
||||
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
@@ -92,7 +94,9 @@ async def start(
|
||||
if event["type"] == "log":
|
||||
console.print(event["message"], end="")
|
||||
elif event["type"] == "job":
|
||||
console.print(f"[green]Job completed:[/green] {event['job_id']} (+{event['tokens']} tokens)")
|
||||
console.print(
|
||||
f"[green]Job completed:[/green] {event['job_id']} (+{event['tokens']} tokens)"
|
||||
)
|
||||
elif event["type"] == "error":
|
||||
console.print(f"[red]Error:[/red] {event['message']}")
|
||||
except KeyboardInterrupt:
|
||||
@@ -105,7 +109,9 @@ async def start(
|
||||
console.print("\n[cyan]Session Summary:[/cyan]")
|
||||
console.print(f" Jobs completed: {final_stats.get('jobs_completed', 0)}")
|
||||
console.print(f" Tokens earned: {final_stats.get('tokens_earned', 0)}")
|
||||
console.print(f" Average job time: {final_stats.get('avg_job_time', 'N/A')}")
|
||||
console.print(
|
||||
f" Average job time: {final_stats.get('avg_job_time', 'N/A')}"
|
||||
)
|
||||
|
||||
console.print("[green]✓[/green] Miner stopped")
|
||||
|
||||
@@ -201,11 +207,15 @@ async def status(ctx, name: str, detailed: bool):
|
||||
console.print(f" Network: {stats.get('network', 'unknown')}")
|
||||
console.print(f" Wallet: {stats.get('wallet', 'Not set')}")
|
||||
console.print(f" Models: {', '.join(stats.get('models', []))}")
|
||||
console.print(f" Memory: {stats.get('memory_used', 0)} / {stats.get('memory_total', 0)} MB")
|
||||
console.print(
|
||||
f" Memory: {stats.get('memory_used', 0)} / {stats.get('memory_total', 0)} MB"
|
||||
)
|
||||
console.print(f" CPU: {stats.get('cpu_percent', 0)}%")
|
||||
|
||||
if gpu := stats.get("gpu"):
|
||||
console.print(f" GPU: {gpu['name']} ({gpu['memory_used']} / {gpu['memory_total']} MB)")
|
||||
console.print(
|
||||
f" GPU: {gpu['name']} ({gpu['memory_used']} / {gpu['memory_total']} MB)"
|
||||
)
|
||||
|
||||
console.print(f"\n[cyan]Performance:[/cyan]")
|
||||
console.print(f" Average job time: {stats.get('avg_job_time', 'N/A')}")
|
||||
@@ -282,7 +292,9 @@ async def earnings(ctx, wallet: str, network: str):
|
||||
if history := data.get("recent_jobs"):
|
||||
console.print("\n[cyan]Recent Jobs:[/cyan]")
|
||||
for job in history[:5]:
|
||||
console.print(f" • {job['timestamp']}: +{job['tokens']} tokens ({job['model']})")
|
||||
console.print(
|
||||
f" • {job['timestamp']}: +{job['tokens']} tokens ({job['model']})"
|
||||
)
|
||||
|
||||
|
||||
@miner_group.command()
|
||||
@@ -311,7 +323,9 @@ async def withdraw(ctx, amount: float, wallet: str, to: str, network: str):
|
||||
|
||||
with console.status("Processing withdrawal..."):
|
||||
try:
|
||||
result = await withdraw_earnings(wallet=wallet, amount=amount, destination=to, network=network)
|
||||
result = await withdraw_earnings(
|
||||
wallet=wallet, amount=amount, destination=to, network=network
|
||||
)
|
||||
|
||||
console.print(f"[green]✓[/green] Withdrawal successful")
|
||||
console.print(f" Transaction: {result['tx_hash']}")
|
||||
|
||||
@@ -92,7 +92,9 @@ def notebooks_list():
|
||||
|
||||
console.print(table)
|
||||
if not items:
|
||||
console.print("[dim]No notebooks found. Create one with 'hanzo ml notebooks create'[/dim]")
|
||||
console.print(
|
||||
"[dim]No notebooks found. Create one with 'hanzo ml notebooks create'[/dim]"
|
||||
)
|
||||
|
||||
|
||||
@notebooks.command(name="create")
|
||||
@@ -158,7 +160,11 @@ def training():
|
||||
|
||||
|
||||
@training.command(name="list")
|
||||
@click.option("--status", type=click.Choice(["running", "completed", "failed", "all"]), default="all")
|
||||
@click.option(
|
||||
"--status",
|
||||
type=click.Choice(["running", "completed", "failed", "all"]),
|
||||
default="all",
|
||||
)
|
||||
def training_list(status: str):
|
||||
"""List training jobs."""
|
||||
params = {}
|
||||
@@ -179,7 +185,9 @@ def training_list(status: str):
|
||||
|
||||
for j in items:
|
||||
j_status = j.get("status", "unknown")
|
||||
status_style = {"running": "cyan", "completed": "green", "failed": "red"}.get(j_status, "white")
|
||||
status_style = {"running": "cyan", "completed": "green", "failed": "red"}.get(
|
||||
j_status, "white"
|
||||
)
|
||||
table.add_row(
|
||||
str(j.get("id", ""))[:16],
|
||||
j.get("name", "-"),
|
||||
@@ -196,13 +204,24 @@ def training_list(status: str):
|
||||
|
||||
@training.command(name="create")
|
||||
@click.option("--name", "-n", prompt=True, help="Job name")
|
||||
@click.option("--framework", "-f", type=click.Choice(["pytorch", "tensorflow", "xgboost"]), default="pytorch")
|
||||
@click.option(
|
||||
"--framework",
|
||||
"-f",
|
||||
type=click.Choice(["pytorch", "tensorflow", "xgboost"]),
|
||||
default="pytorch",
|
||||
)
|
||||
@click.option("--script", "-s", required=True, help="Training script path")
|
||||
@click.option("--gpu", "-g", default="1", help="Number of GPUs")
|
||||
@click.option("--instance", "-i", default="gpu-a10g", help="Instance type")
|
||||
def training_create(name: str, framework: str, script: str, gpu: str, instance: str):
|
||||
"""Create a training job."""
|
||||
body = {"name": name, "framework": framework, "script": script, "gpu": gpu, "instance": instance}
|
||||
body = {
|
||||
"name": name,
|
||||
"framework": framework,
|
||||
"script": script,
|
||||
"gpu": gpu,
|
||||
"instance": instance,
|
||||
}
|
||||
|
||||
console.print(f"[cyan]Creating training job '{name}'...[/cyan]")
|
||||
resp = _request("post", "/v1/training", json=body)
|
||||
@@ -231,7 +250,9 @@ def training_logs(job_id: str, follow: bool):
|
||||
console.print(f"[cyan]Logs for job {job_id}:[/cyan]")
|
||||
for line in lines:
|
||||
if isinstance(line, dict):
|
||||
console.print(f"[dim]{str(line.get('timestamp', ''))[:19]}[/dim] {line.get('message', '')}")
|
||||
console.print(
|
||||
f"[dim]{str(line.get('timestamp', ''))[:19]}[/dim] {line.get('message', '')}"
|
||||
)
|
||||
else:
|
||||
console.print(str(line))
|
||||
|
||||
@@ -295,7 +316,9 @@ def pipelines_create(name: str, file: str):
|
||||
with open(file) as f:
|
||||
definition = json.load(f)
|
||||
|
||||
resp = _request("post", "/v1/pipelines", json={"name": name, "definition": definition})
|
||||
resp = _request(
|
||||
"post", "/v1/pipelines", json={"name": name, "definition": definition}
|
||||
)
|
||||
data = check_response(resp)
|
||||
|
||||
console.print(f"[green]✓[/green] Pipeline '{name}' created from {file}")
|
||||
@@ -431,7 +454,11 @@ def registry_list(model: str):
|
||||
|
||||
for m in items:
|
||||
stage = m.get("stage", "none")
|
||||
style = "green" if stage == "production" else "yellow" if stage == "staging" else "dim"
|
||||
style = (
|
||||
"green"
|
||||
if stage == "production"
|
||||
else "yellow" if stage == "staging" else "dim"
|
||||
)
|
||||
table.add_row(
|
||||
m.get("name", ""),
|
||||
str(m.get("version", "-")),
|
||||
@@ -471,17 +498,23 @@ def registry_push(name: str, path: str, framework: str, version: str):
|
||||
def registry_pull(model_uri: str, output: str):
|
||||
"""Pull a model from the registry."""
|
||||
console.print(f"[cyan]Pulling model {model_uri}...[/cyan]")
|
||||
resp = _request("post", "/v1/registry/pull", json={"uri": model_uri, "output": output})
|
||||
resp = _request(
|
||||
"post", "/v1/registry/pull", json={"uri": model_uri, "output": output}
|
||||
)
|
||||
data = check_response(resp)
|
||||
console.print(f"[green]✓[/green] Model downloaded to {data.get('path', output)}")
|
||||
|
||||
|
||||
@registry.command(name="promote")
|
||||
@click.argument("model_uri")
|
||||
@click.option("--stage", "-s", type=click.Choice(["staging", "production"]), required=True)
|
||||
@click.option(
|
||||
"--stage", "-s", type=click.Choice(["staging", "production"]), required=True
|
||||
)
|
||||
def registry_promote(model_uri: str, stage: str):
|
||||
"""Promote a model version to a stage."""
|
||||
resp = _request("post", "/v1/registry/promote", json={"uri": model_uri, "stage": stage})
|
||||
resp = _request(
|
||||
"post", "/v1/registry/promote", json={"uri": model_uri, "stage": stage}
|
||||
)
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] Model promoted to {stage}")
|
||||
|
||||
@@ -545,7 +578,11 @@ def experiments_list(namespace: str):
|
||||
|
||||
@experiments.command(name="runs")
|
||||
@click.argument("experiment")
|
||||
@click.option("--status", type=click.Choice(["running", "completed", "failed", "all"]), default="all")
|
||||
@click.option(
|
||||
"--status",
|
||||
type=click.Choice(["running", "completed", "failed", "all"]),
|
||||
default="all",
|
||||
)
|
||||
def experiments_runs(experiment: str, status: str):
|
||||
"""List runs in an experiment."""
|
||||
params = {}
|
||||
@@ -564,8 +601,14 @@ def experiments_runs(experiment: str, status: str):
|
||||
|
||||
for r in items:
|
||||
r_status = r.get("status", "unknown")
|
||||
style = {"running": "cyan", "completed": "green", "failed": "red"}.get(r_status, "white")
|
||||
metrics = json.dumps(r.get("metrics", {}), default=str)[:40] if r.get("metrics") else "-"
|
||||
style = {"running": "cyan", "completed": "green", "failed": "red"}.get(
|
||||
r_status, "white"
|
||||
)
|
||||
metrics = (
|
||||
json.dumps(r.get("metrics", {}), default=str)[:40]
|
||||
if r.get("metrics")
|
||||
else "-"
|
||||
)
|
||||
table.add_row(
|
||||
str(r.get("id", ""))[:16],
|
||||
f"[{style}]{r_status}[/{style}]",
|
||||
@@ -631,16 +674,27 @@ def tune():
|
||||
@click.argument("name")
|
||||
@click.option("--experiment", "-e", required=True, help="Parent experiment")
|
||||
@click.option("--objective", "-o", required=True, help="Metric to optimize")
|
||||
@click.option("--goal", "-g", type=click.Choice(["minimize", "maximize"]), default="minimize")
|
||||
@click.option(
|
||||
"--algorithm", "-a", type=click.Choice(["random", "grid", "bayesian", "hyperband", "tpe"]), default="bayesian"
|
||||
"--goal", "-g", type=click.Choice(["minimize", "maximize"]), default="minimize"
|
||||
)
|
||||
@click.option(
|
||||
"--algorithm",
|
||||
"-a",
|
||||
type=click.Choice(["random", "grid", "bayesian", "hyperband", "tpe"]),
|
||||
default="bayesian",
|
||||
)
|
||||
@click.option("--max-trials", "-m", default=10, help="Maximum trials")
|
||||
@click.option("--parallel-trials", "-p", default=2, help="Parallel trials")
|
||||
@click.option("--config", "-c", help="Tuning config file (YAML)")
|
||||
def tune_create(
|
||||
name: str, experiment: str, objective: str, goal: str, algorithm: str,
|
||||
max_trials: int, parallel_trials: int, config: str,
|
||||
name: str,
|
||||
experiment: str,
|
||||
objective: str,
|
||||
goal: str,
|
||||
algorithm: str,
|
||||
max_trials: int,
|
||||
parallel_trials: int,
|
||||
config: str,
|
||||
):
|
||||
"""Create a hyperparameter tuning job.
|
||||
|
||||
@@ -651,8 +705,12 @@ def tune_create(
|
||||
hanzo ml tune create custom -e exp -o f1 -c tuning.yaml
|
||||
"""
|
||||
body = {
|
||||
"name": name, "experiment": experiment, "objective": objective,
|
||||
"goal": goal, "algorithm": algorithm, "max_trials": max_trials,
|
||||
"name": name,
|
||||
"experiment": experiment,
|
||||
"objective": objective,
|
||||
"goal": goal,
|
||||
"algorithm": algorithm,
|
||||
"max_trials": max_trials,
|
||||
"parallel_trials": parallel_trials,
|
||||
}
|
||||
if config:
|
||||
@@ -692,7 +750,9 @@ def tune_list(experiment: str):
|
||||
|
||||
for j in items:
|
||||
j_status = j.get("status", "unknown")
|
||||
style = {"running": "cyan", "completed": "green", "failed": "red"}.get(j_status, "white")
|
||||
style = {"running": "cyan", "completed": "green", "failed": "red"}.get(
|
||||
j_status, "white"
|
||||
)
|
||||
table.add_row(
|
||||
j.get("name", ""),
|
||||
j.get("algorithm", "-"),
|
||||
@@ -847,12 +907,16 @@ def features_describe(name: str):
|
||||
|
||||
@features.command(name="ingest")
|
||||
@click.argument("name")
|
||||
@click.option("--from", "source", required=True, help="Data source (file, table, stream)")
|
||||
@click.option(
|
||||
"--from", "source", required=True, help="Data source (file, table, stream)"
|
||||
)
|
||||
@click.option("--mode", type=click.Choice(["append", "overwrite"]), default="append")
|
||||
def features_ingest(name: str, source: str, mode: str):
|
||||
"""Ingest data into feature group."""
|
||||
console.print(f"[cyan]Ingesting into '{name}'...[/cyan]")
|
||||
resp = _request("post", f"/v1/features/{name}/ingest", json={"source": source, "mode": mode})
|
||||
resp = _request(
|
||||
"post", f"/v1/features/{name}/ingest", json={"source": source, "mode": mode}
|
||||
)
|
||||
data = check_response(resp)
|
||||
console.print(f"[green]✓[/green] Ingestion complete")
|
||||
console.print(f" Records: {data.get('records', 0)}")
|
||||
@@ -916,7 +980,12 @@ def datasets():
|
||||
@datasets.command(name="create")
|
||||
@click.argument("name")
|
||||
@click.option("--from", "source", required=True, help="Source path or URI")
|
||||
@click.option("--format", "fmt", type=click.Choice(["csv", "parquet", "tfrecord", "jsonl"]), help="Data format")
|
||||
@click.option(
|
||||
"--format",
|
||||
"fmt",
|
||||
type=click.Choice(["csv", "parquet", "tfrecord", "jsonl"]),
|
||||
help="Data format",
|
||||
)
|
||||
@click.option("--split", help="Train/val/test split (e.g., 80:10:10)")
|
||||
def datasets_create(name: str, source: str, fmt: str, split: str):
|
||||
"""Create a versioned dataset."""
|
||||
@@ -1005,11 +1074,15 @@ def automl():
|
||||
@click.option("--dataset", "-d", required=True, help="Training dataset")
|
||||
@click.option("--target", "-t", required=True, help="Target column")
|
||||
@click.option(
|
||||
"--task", type=click.Choice(["classification", "regression", "forecasting", "nlp", "vision"]), required=True
|
||||
"--task",
|
||||
type=click.Choice(["classification", "regression", "forecasting", "nlp", "vision"]),
|
||||
required=True,
|
||||
)
|
||||
@click.option("--time-limit", default=3600, help="Time limit in seconds")
|
||||
@click.option("--metric", "-m", help="Optimization metric")
|
||||
def automl_create(name: str, dataset: str, target: str, task: str, time_limit: int, metric: str):
|
||||
def automl_create(
|
||||
name: str, dataset: str, target: str, task: str, time_limit: int, metric: str
|
||||
):
|
||||
"""Create an AutoML job.
|
||||
|
||||
\b
|
||||
@@ -1018,7 +1091,13 @@ def automl_create(name: str, dataset: str, target: str, task: str, time_limit: i
|
||||
hanzo ml automl create sales-forecast -d sales -t revenue --task forecasting
|
||||
hanzo ml automl create sentiment -d reviews -t label --task nlp --time-limit 7200
|
||||
"""
|
||||
body = {"name": name, "dataset": dataset, "target": target, "task": task, "time_limit": time_limit}
|
||||
body = {
|
||||
"name": name,
|
||||
"dataset": dataset,
|
||||
"target": target,
|
||||
"task": task,
|
||||
"time_limit": time_limit,
|
||||
}
|
||||
if metric:
|
||||
body["metric"] = metric
|
||||
|
||||
@@ -1050,7 +1129,9 @@ def automl_list():
|
||||
|
||||
for j in items:
|
||||
j_status = j.get("status", "unknown")
|
||||
style = {"running": "cyan", "completed": "green", "failed": "red"}.get(j_status, "white")
|
||||
style = {"running": "cyan", "completed": "green", "failed": "red"}.get(
|
||||
j_status, "white"
|
||||
)
|
||||
table.add_row(
|
||||
j.get("name", ""),
|
||||
j.get("task", "-"),
|
||||
@@ -1072,7 +1153,9 @@ def automl_status(name: str):
|
||||
data = check_response(resp)
|
||||
|
||||
status = data.get("status", "unknown")
|
||||
status_style = {"running": "cyan", "completed": "green", "failed": "red"}.get(status, "yellow")
|
||||
status_style = {"running": "cyan", "completed": "green", "failed": "red"}.get(
|
||||
status, "yellow"
|
||||
)
|
||||
|
||||
console.print(
|
||||
Panel(
|
||||
|
||||
@@ -26,7 +26,9 @@ def network_group():
|
||||
@click.option("--consensus", is_flag=True, help="Require consensus")
|
||||
@click.option("--timeout", "-t", type=int, default=300, help="Timeout in seconds")
|
||||
@click.pass_context
|
||||
async def dispatch(ctx, prompt: str, agents: int, model: str, mode: str, consensus: bool, timeout: int):
|
||||
async def dispatch(
|
||||
ctx, prompt: str, agents: int, model: str, mode: str, consensus: bool, timeout: int
|
||||
):
|
||||
"""Dispatch work to agent network."""
|
||||
try:
|
||||
from hanzo_network import NetworkDispatcher
|
||||
@@ -389,9 +391,13 @@ def topology(ctx, json: bool):
|
||||
)
|
||||
|
||||
if gpu_available:
|
||||
console.print("[green]✓[/green] GPU/Accelerator detected and available for AI workloads")
|
||||
console.print(
|
||||
"[green]✓[/green] GPU/Accelerator detected and available for AI workloads"
|
||||
)
|
||||
if "APPLE M" in caps.chip.upper():
|
||||
console.print(" [dim]Apple Silicon Neural Engine available for on-device AI[/dim]")
|
||||
console.print(
|
||||
" [dim]Apple Silicon Neural Engine available for on-device AI[/dim]"
|
||||
)
|
||||
else:
|
||||
console.print("[yellow]⚠[/yellow] No GPU detected - using CPU only")
|
||||
console.print(" To enable GPU support:")
|
||||
@@ -541,7 +547,9 @@ def topology_add(
|
||||
try:
|
||||
import qrcode
|
||||
except ImportError:
|
||||
console.print("\n[yellow]QR code generation requires qrcode library[/yellow]")
|
||||
console.print(
|
||||
"\n[yellow]QR code generation requires qrcode library[/yellow]"
|
||||
)
|
||||
console.print("Install with: pip install qrcode[pil]")
|
||||
return
|
||||
|
||||
@@ -692,7 +700,10 @@ def models(ctx, endpoint: str):
|
||||
models_list = [
|
||||
model
|
||||
for model in all_models
|
||||
if not any(keyword in model.get("id", "").lower() for keyword in embedding_keywords)
|
||||
if not any(
|
||||
keyword in model.get("id", "").lower()
|
||||
for keyword in embedding_keywords
|
||||
)
|
||||
]
|
||||
|
||||
if models_list:
|
||||
@@ -730,12 +741,16 @@ def models(ctx, endpoint: str):
|
||||
table.add_row(str(i), model_id, tier, provider)
|
||||
|
||||
console.print(table)
|
||||
console.print(f"\n[dim]Total LLMs: {len(models_list)} (embedding models filtered out)[/dim]")
|
||||
console.print(
|
||||
f"\n[dim]Total LLMs: {len(models_list)} (embedding models filtered out)[/dim]"
|
||||
)
|
||||
else:
|
||||
console.print("[yellow]No models available on gateway[/yellow]")
|
||||
else:
|
||||
# Show static list of known models
|
||||
console.print("[yellow]Could not fetch live models, showing known models:[/yellow]\n")
|
||||
console.print(
|
||||
"[yellow]Could not fetch live models, showing known models:[/yellow]\n"
|
||||
)
|
||||
_show_static_models()
|
||||
except Exception as e:
|
||||
console.print(f"[yellow]Error fetching models: {e}[/yellow]\n")
|
||||
@@ -743,7 +758,9 @@ def models(ctx, endpoint: str):
|
||||
|
||||
console.print("\n[dim]To use a model:[/dim]")
|
||||
console.print(" [cyan]hanzo dev --model llama3-8b-instruct[/cyan]")
|
||||
console.print("\n[dim cyan]Free tier:[/dim cyan] [green]Most models available without login![/green]")
|
||||
console.print(
|
||||
"\n[dim cyan]Free tier:[/dim cyan] [green]Most models available without login![/green]"
|
||||
)
|
||||
console.print(
|
||||
"[dim cyan]Premium models:[/dim cyan] [yellow]gpt-4, claude-3-opus, o1-preview[/yellow] - [cyan]hanzo auth login[/cyan]"
|
||||
)
|
||||
|
||||
@@ -31,7 +31,9 @@ async def start(ctx, name: str, port: int, models: tuple, device: str):
|
||||
await start_node(ctx, name, port, list(models) if models else None, device)
|
||||
|
||||
|
||||
async def start_node(ctx, name: str, port: int, models: Optional[List[str]] = None, device: str = "auto"):
|
||||
async def start_node(
|
||||
ctx, name: str, port: int, models: Optional[List[str]] = None, device: str = "auto"
|
||||
):
|
||||
"""Start a local node via hanzo-cluster."""
|
||||
try:
|
||||
from hanzo_cluster import HanzoCluster
|
||||
@@ -138,7 +140,9 @@ async def status(ctx, name: str):
|
||||
if workers := status.get("node_details", []):
|
||||
console.print("\n[cyan]Workers:[/cyan]")
|
||||
for worker in workers:
|
||||
console.print(f" • {worker.get('name', 'unknown')} ({worker.get('state', 'unknown')})")
|
||||
console.print(
|
||||
f" • {worker.get('name', 'unknown')} ({worker.get('state', 'unknown')})"
|
||||
)
|
||||
if device := worker.get("device"):
|
||||
console.print(f" Device: {device}")
|
||||
else:
|
||||
@@ -252,7 +256,9 @@ def worker_group():
|
||||
default="auto",
|
||||
help="Device to use",
|
||||
)
|
||||
@click.option("--port", "-p", type=int, help="Worker port (auto-assigned if not specified)")
|
||||
@click.option(
|
||||
"--port", "-p", type=int, help="Worker port (auto-assigned if not specified)"
|
||||
)
|
||||
@click.option("--blockchain", is_flag=True, help="Enable blockchain features")
|
||||
@click.option("--network", is_flag=True, help="Enable network discovery")
|
||||
@click.pass_context
|
||||
@@ -425,7 +431,9 @@ async def worker_info(ctx, name: str):
|
||||
if resources := info.get("resources"):
|
||||
console.print("\n[cyan]Resources:[/cyan]")
|
||||
console.print(f" CPU: {resources.get('cpu_percent', 'N/A')}%")
|
||||
console.print(f" Memory: {resources.get('memory_used', 'N/A')} / {resources.get('memory_total', 'N/A')}")
|
||||
console.print(
|
||||
f" Memory: {resources.get('memory_used', 'N/A')} / {resources.get('memory_total', 'N/A')}"
|
||||
)
|
||||
if gpu := resources.get("gpu"):
|
||||
console.print(
|
||||
f" GPU: {gpu.get('name', 'N/A')} ({gpu.get('memory_used', 'N/A')} / {gpu.get('memory_total', 'N/A')})"
|
||||
@@ -438,8 +446,12 @@ async def worker_info(ctx, name: str):
|
||||
|
||||
if network := info.get("network"):
|
||||
console.print("\n[cyan]Network:[/cyan]")
|
||||
console.print(f" Blockchain: {'enabled' if network.get('blockchain') else 'disabled'}")
|
||||
console.print(f" Discovery: {'enabled' if network.get('discovery') else 'disabled'}")
|
||||
console.print(
|
||||
f" Blockchain: {'enabled' if network.get('blockchain') else 'disabled'}"
|
||||
)
|
||||
console.print(
|
||||
f" Discovery: {'enabled' if network.get('discovery') else 'disabled'}"
|
||||
)
|
||||
if peers := network.get("peers"):
|
||||
console.print(f" Peers: {len(peers)}")
|
||||
|
||||
|
||||
@@ -132,7 +132,9 @@ def metrics_query(promql: str, start: str, end: str, step: str):
|
||||
|
||||
@metrics.command(name="export")
|
||||
@click.argument("promql")
|
||||
@click.option("--format", "-f", "fmt", type=click.Choice(["json", "csv"]), default="json")
|
||||
@click.option(
|
||||
"--format", "-f", "fmt", type=click.Choice(["json", "csv"]), default="json"
|
||||
)
|
||||
@click.option("--output", "-o", help="Output file")
|
||||
@click.option("--start", "-s", help="Start time")
|
||||
@click.option("--end", "-e", help="End time")
|
||||
@@ -149,10 +151,14 @@ def metrics_export(promql: str, fmt: str, output: str, start: str, end: str):
|
||||
content = data.get("data", "")
|
||||
if output:
|
||||
with open(output, "w") as f:
|
||||
f.write(content if isinstance(content, str) else json.dumps(content, indent=2))
|
||||
f.write(
|
||||
content if isinstance(content, str) else json.dumps(content, indent=2)
|
||||
)
|
||||
console.print(f"[green]✓[/green] Exported to {output}")
|
||||
else:
|
||||
console.print(content if isinstance(content, str) else json.dumps(content, indent=2))
|
||||
console.print(
|
||||
content if isinstance(content, str) else json.dumps(content, indent=2)
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -200,7 +206,12 @@ def logs_search(query: str, source: str, level: str, limit: int, start: str, end
|
||||
|
||||
for entry in entries:
|
||||
lvl = entry.get("level", "info")
|
||||
lvl_style = {"error": "red", "warn": "yellow", "info": "green", "debug": "dim"}.get(lvl, "white")
|
||||
lvl_style = {
|
||||
"error": "red",
|
||||
"warn": "yellow",
|
||||
"info": "green",
|
||||
"debug": "dim",
|
||||
}.get(lvl, "white")
|
||||
table.add_row(
|
||||
entry.get("timestamp", ""),
|
||||
f"[{lvl_style}]{lvl}[/{lvl_style}]",
|
||||
@@ -230,19 +241,32 @@ def logs_tail(source: str, filter: str, level: str):
|
||||
try:
|
||||
url = f"{SERVICE_URL}/v1/logs/tail"
|
||||
from .base import get_api_key
|
||||
|
||||
api_key = get_api_key()
|
||||
if not api_key:
|
||||
raise click.ClickException("Not authenticated. Run 'hanzo login' first.")
|
||||
|
||||
with httpx.Client(timeout=None) as client:
|
||||
with client.stream("GET", url, params=params, headers={"Authorization": f"Bearer {api_key}"}) as stream:
|
||||
with client.stream(
|
||||
"GET",
|
||||
url,
|
||||
params=params,
|
||||
headers={"Authorization": f"Bearer {api_key}"},
|
||||
) as stream:
|
||||
for line in stream.iter_lines():
|
||||
if line:
|
||||
try:
|
||||
entry = json.loads(line)
|
||||
lvl = entry.get("level", "info")
|
||||
lvl_style = {"error": "red", "warn": "yellow", "info": "green", "debug": "dim"}.get(lvl, "white")
|
||||
console.print(f"[dim]{entry.get('timestamp', '')}[/dim] [{lvl_style}]{lvl}[/{lvl_style}] [cyan]{entry.get('source', '')}[/cyan] {entry.get('message', '')}")
|
||||
lvl_style = {
|
||||
"error": "red",
|
||||
"warn": "yellow",
|
||||
"info": "green",
|
||||
"debug": "dim",
|
||||
}.get(lvl, "white")
|
||||
console.print(
|
||||
f"[dim]{entry.get('timestamp', '')}[/dim] [{lvl_style}]{lvl}[/{lvl_style}] [cyan]{entry.get('source', '')}[/cyan] {entry.get('message', '')}"
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
console.print(line)
|
||||
except KeyboardInterrupt:
|
||||
@@ -470,7 +494,11 @@ def alerts():
|
||||
|
||||
|
||||
@alerts.command(name="list")
|
||||
@click.option("--status", type=click.Choice(["firing", "pending", "inactive", "all"]), default="all")
|
||||
@click.option(
|
||||
"--status",
|
||||
type=click.Choice(["firing", "pending", "inactive", "all"]),
|
||||
default="all",
|
||||
)
|
||||
def alerts_list(status: str):
|
||||
"""List alert rules."""
|
||||
params: dict = {}
|
||||
@@ -505,10 +533,19 @@ def alerts_list(status: str):
|
||||
@alerts.command(name="create")
|
||||
@click.option("--name", "-n", prompt=True, help="Alert name")
|
||||
@click.option("--condition", "-c", required=True, help="PromQL condition")
|
||||
@click.option("--severity", "-s", type=click.Choice(["critical", "warning", "info"]), default="warning")
|
||||
@click.option(
|
||||
"--severity",
|
||||
"-s",
|
||||
type=click.Choice(["critical", "warning", "info"]),
|
||||
default="warning",
|
||||
)
|
||||
@click.option("--channel", help="Notification channel")
|
||||
@click.option("--for-duration", "for_duration", default="5m", help="Duration before firing")
|
||||
def alerts_create(name: str, condition: str, severity: str, channel: str, for_duration: str):
|
||||
@click.option(
|
||||
"--for-duration", "for_duration", default="5m", help="Duration before firing"
|
||||
)
|
||||
def alerts_create(
|
||||
name: str, condition: str, severity: str, channel: str, for_duration: str
|
||||
):
|
||||
"""Create an alert rule."""
|
||||
payload: dict = {
|
||||
"name": name,
|
||||
@@ -602,7 +639,9 @@ def prompts_create(name: str, template: str, model: str, config: str, label: str
|
||||
payload["config"] = json.loads(config)
|
||||
resp = _request("post", "/v1/prompts", json=payload)
|
||||
data = check_response(resp)
|
||||
console.print(f"[green]✓[/green] Prompt '{name}' created (v{data.get('version', '1')})")
|
||||
console.print(
|
||||
f"[green]✓[/green] Prompt '{name}' created (v{data.get('version', '1')})"
|
||||
)
|
||||
if model:
|
||||
console.print(f" Model: {model}")
|
||||
|
||||
@@ -641,7 +680,11 @@ def prompts_get(name: str, version: int, label: str):
|
||||
@click.option("--to", "to_label", required=True, help="Target label")
|
||||
def prompts_promote(name: str, version: int, to_label: str):
|
||||
"""Promote a prompt version to a label."""
|
||||
resp = _request("post", f"/v1/prompts/{name}/promote", json={"version": version, "label": to_label})
|
||||
resp = _request(
|
||||
"post",
|
||||
f"/v1/prompts/{name}/promote",
|
||||
json={"version": version, "label": to_label},
|
||||
)
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] Promoted '{name}' v{version} to {to_label}")
|
||||
|
||||
@@ -757,7 +800,9 @@ def generations_show(generation_id: str):
|
||||
@click.option("--by", type=click.Choice(["model", "prompt", "user"]), default="model")
|
||||
def generations_stats(time_range: str, by: str):
|
||||
"""Show generation statistics."""
|
||||
resp = _request("get", "/v1/generations/stats", params={"range": time_range, "group_by": by})
|
||||
resp = _request(
|
||||
"get", "/v1/generations/stats", params={"range": time_range, "group_by": by}
|
||||
)
|
||||
data = check_response(resp)
|
||||
|
||||
console.print(f"[cyan]Generation Statistics (last {time_range}):[/cyan]")
|
||||
@@ -798,7 +843,11 @@ def evals():
|
||||
|
||||
|
||||
@evals.command(name="list")
|
||||
@click.option("--status", type=click.Choice(["pending", "running", "completed", "all"]), default="all")
|
||||
@click.option(
|
||||
"--status",
|
||||
type=click.Choice(["pending", "running", "completed", "all"]),
|
||||
default="all",
|
||||
)
|
||||
def evals_list(status: str):
|
||||
"""List evaluations."""
|
||||
params: dict = {}
|
||||
@@ -817,7 +866,12 @@ def evals_list(status: str):
|
||||
|
||||
for e in data.get("evals", []):
|
||||
st = e.get("status", "pending")
|
||||
st_style = {"completed": "green", "running": "cyan", "pending": "yellow", "failed": "red"}.get(st, "white")
|
||||
st_style = {
|
||||
"completed": "green",
|
||||
"running": "cyan",
|
||||
"pending": "yellow",
|
||||
"failed": "red",
|
||||
}.get(st, "white")
|
||||
score = f"{e.get('score', 0):.2f}" if e.get("score") is not None else "-"
|
||||
table.add_row(
|
||||
e.get("name", ""),
|
||||
@@ -864,7 +918,9 @@ def evals_run(name: str, parallel: int):
|
||||
|
||||
@evals.command(name="results")
|
||||
@click.argument("name")
|
||||
@click.option("--format", "-f", "fmt", type=click.Choice(["table", "json"]), default="table")
|
||||
@click.option(
|
||||
"--format", "-f", "fmt", type=click.Choice(["table", "json"]), default="table"
|
||||
)
|
||||
def evals_results(name: str, fmt: str):
|
||||
"""Show evaluation results."""
|
||||
resp = _request("get", f"/v1/evals/{name}/results")
|
||||
@@ -918,7 +974,9 @@ def scores():
|
||||
@scores.command(name="list")
|
||||
@click.option("--trace", "-t", help="Filter by trace ID")
|
||||
@click.option("--name", "-n", help="Filter by score name")
|
||||
@click.option("--source", type=click.Choice(["api", "human", "model", "all"]), default="all")
|
||||
@click.option(
|
||||
"--source", type=click.Choice(["api", "human", "model", "all"]), default="all"
|
||||
)
|
||||
@click.option("--limit", default=50, help="Max results")
|
||||
def scores_list(trace: str, name: str, source: str, limit: int):
|
||||
"""List scores."""
|
||||
@@ -958,7 +1016,9 @@ def scores_list(trace: str, name: str, source: str, limit: int):
|
||||
@click.option("--name", "-n", required=True, help="Score name")
|
||||
@click.option("--value", "-v", type=float, required=True, help="Score value (0-1)")
|
||||
@click.option("--comment", "-c", help="Optional comment")
|
||||
@click.option("--source", "-s", type=click.Choice(["api", "human", "model"]), default="api")
|
||||
@click.option(
|
||||
"--source", "-s", type=click.Choice(["api", "human", "model"]), default="api"
|
||||
)
|
||||
def scores_add(trace: str, name: str, value: float, comment: str, source: str):
|
||||
"""Add a score to a trace."""
|
||||
payload: dict = {"trace_id": trace, "name": name, "value": value, "source": source}
|
||||
@@ -1083,27 +1143,42 @@ def datasets_add_item(dataset_name: str, input_data: str, expected: str, metadat
|
||||
|
||||
@datasets.command(name="import")
|
||||
@click.argument("dataset_name")
|
||||
@click.option("--file", "-f", "file_path", required=True, help="File to import (JSON/CSV)")
|
||||
@click.option(
|
||||
"--file", "-f", "file_path", required=True, help="File to import (JSON/CSV)"
|
||||
)
|
||||
def datasets_import(dataset_name: str, file_path: str):
|
||||
"""Import items from file."""
|
||||
with open(file_path) as f:
|
||||
content = f.read()
|
||||
|
||||
if file_path.endswith(".csv"):
|
||||
resp = _request("post", f"/v1/datasets/{dataset_name}/import", content=content, headers={"Content-Type": "text/csv"})
|
||||
resp = _request(
|
||||
"post",
|
||||
f"/v1/datasets/{dataset_name}/import",
|
||||
content=content,
|
||||
headers={"Content-Type": "text/csv"},
|
||||
)
|
||||
else:
|
||||
resp = _request("post", f"/v1/datasets/{dataset_name}/import", json=json.loads(content))
|
||||
resp = _request(
|
||||
"post", f"/v1/datasets/{dataset_name}/import", json=json.loads(content)
|
||||
)
|
||||
data = check_response(resp)
|
||||
console.print(f"[green]✓[/green] Imported {data.get('count', '?')} items to '{dataset_name}'")
|
||||
console.print(
|
||||
f"[green]✓[/green] Imported {data.get('count', '?')} items to '{dataset_name}'"
|
||||
)
|
||||
|
||||
|
||||
@datasets.command(name="export")
|
||||
@click.argument("dataset_name")
|
||||
@click.option("--output", "-o", help="Output file")
|
||||
@click.option("--format", "-f", "fmt", type=click.Choice(["json", "csv"]), default="json")
|
||||
@click.option(
|
||||
"--format", "-f", "fmt", type=click.Choice(["json", "csv"]), default="json"
|
||||
)
|
||||
def datasets_export(dataset_name: str, output: str, fmt: str):
|
||||
"""Export dataset to file."""
|
||||
resp = _request("get", f"/v1/datasets/{dataset_name}/export", params={"format": fmt})
|
||||
resp = _request(
|
||||
"get", f"/v1/datasets/{dataset_name}/export", params={"format": fmt}
|
||||
)
|
||||
data = check_response(resp)
|
||||
|
||||
out_file = output or f"{dataset_name}.{fmt}"
|
||||
@@ -1256,10 +1331,14 @@ def llm_traces(user: str, name: str, session: str, limit: int):
|
||||
|
||||
@llm.command(name="costs")
|
||||
@click.option("--range", "-r", "time_range", default="30d", help="Time range")
|
||||
@click.option("--by", type=click.Choice(["model", "user", "prompt", "day"]), default="model")
|
||||
@click.option(
|
||||
"--by", type=click.Choice(["model", "user", "prompt", "day"]), default="model"
|
||||
)
|
||||
def llm_costs(time_range: str, by: str):
|
||||
"""Show LLM cost analysis."""
|
||||
resp = _request("get", "/v1/llm/costs", params={"range": time_range, "group_by": by})
|
||||
resp = _request(
|
||||
"get", "/v1/llm/costs", params={"range": time_range, "group_by": by}
|
||||
)
|
||||
data = check_response(resp)
|
||||
|
||||
console.print(f"[cyan]LLM Cost Analysis (last {time_range}):[/cyan]")
|
||||
@@ -1288,10 +1367,14 @@ def llm_costs(time_range: str, by: str):
|
||||
|
||||
@llm.command(name="latency")
|
||||
@click.option("--range", "-r", "time_range", default="24h", help="Time range")
|
||||
@click.option("--by", type=click.Choice(["model", "prompt", "endpoint"]), default="model")
|
||||
@click.option(
|
||||
"--by", type=click.Choice(["model", "prompt", "endpoint"]), default="model"
|
||||
)
|
||||
def llm_latency(time_range: str, by: str):
|
||||
"""Show LLM latency analysis."""
|
||||
resp = _request("get", "/v1/llm/latency", params={"range": time_range, "group_by": by})
|
||||
resp = _request(
|
||||
"get", "/v1/llm/latency", params={"range": time_range, "group_by": by}
|
||||
)
|
||||
data = check_response(resp)
|
||||
|
||||
console.print(f"[cyan]LLM Latency Analysis (last {time_range}):[/cyan]")
|
||||
|
||||
@@ -88,15 +88,23 @@ def edge_list():
|
||||
@edge.command(name="deploy")
|
||||
@click.option("--name", "-n", prompt=True, help="Deployment name")
|
||||
@click.option("--dir", "-d", "source_dir", default=".", help="Directory to deploy")
|
||||
@click.option("--regions", "-r", default="all", help="Target regions (comma-separated or 'all')")
|
||||
@click.option(
|
||||
"--regions", "-r", default="all", help="Target regions (comma-separated or 'all')"
|
||||
)
|
||||
def edge_deploy(name: str, source_dir: str, regions: str):
|
||||
"""Deploy to edge locations."""
|
||||
region_list = [r.strip() for r in regions.split(",")] if regions != "all" else ["all"]
|
||||
resp = _request("post", "/v1/edge/deployments", json={
|
||||
"name": name,
|
||||
"source_dir": source_dir,
|
||||
"regions": region_list,
|
||||
})
|
||||
region_list = (
|
||||
[r.strip() for r in regions.split(",")] if regions != "all" else ["all"]
|
||||
)
|
||||
resp = _request(
|
||||
"post",
|
||||
"/v1/edge/deployments",
|
||||
json={
|
||||
"name": name,
|
||||
"source_dir": source_dir,
|
||||
"regions": region_list,
|
||||
},
|
||||
)
|
||||
data = check_response(resp)
|
||||
|
||||
console.print(f"[green]✓[/green] Deployed '{name}' to edge")
|
||||
@@ -133,7 +141,9 @@ def edge_delete(name: str):
|
||||
@click.option("--period", "-p", default="24h", help="Time period")
|
||||
def edge_stats(name: str, period: str):
|
||||
"""Show edge deployment statistics."""
|
||||
resp = _request("get", f"/v1/edge/deployments/{name}/stats", params={"period": period})
|
||||
resp = _request(
|
||||
"get", f"/v1/edge/deployments/{name}/stats", params={"period": period}
|
||||
)
|
||||
data = check_response(resp)
|
||||
|
||||
info = (
|
||||
@@ -172,7 +182,9 @@ def hke_list():
|
||||
|
||||
for c in data.get("clusters", []):
|
||||
st = c.get("status", "running")
|
||||
st_style = {"running": "green", "provisioning": "yellow", "error": "red"}.get(st, "white")
|
||||
st_style = {"running": "green", "provisioning": "yellow", "error": "red"}.get(
|
||||
st, "white"
|
||||
)
|
||||
table.add_row(
|
||||
c.get("name", ""),
|
||||
c.get("version", ""),
|
||||
@@ -191,16 +203,22 @@ def hke_list():
|
||||
@click.option("--region", "-r", default="us-west-2", help="Region")
|
||||
@click.option("--gpu", is_flag=True, help="Enable GPU node pool")
|
||||
@click.option("--node-size", default="s-4vcpu-8gb", help="Node size")
|
||||
def hke_create(name: str, version: str, nodes: int, region: str, gpu: bool, node_size: str):
|
||||
def hke_create(
|
||||
name: str, version: str, nodes: int, region: str, gpu: bool, node_size: str
|
||||
):
|
||||
"""Create a Kubernetes cluster."""
|
||||
resp = _request("post", "/v1/hke/clusters", json={
|
||||
"name": name,
|
||||
"version": version,
|
||||
"node_count": nodes,
|
||||
"region": region,
|
||||
"gpu": gpu,
|
||||
"node_size": node_size,
|
||||
})
|
||||
resp = _request(
|
||||
"post",
|
||||
"/v1/hke/clusters",
|
||||
json={
|
||||
"name": name,
|
||||
"version": version,
|
||||
"node_count": nodes,
|
||||
"region": region,
|
||||
"gpu": gpu,
|
||||
"node_size": node_size,
|
||||
},
|
||||
)
|
||||
data = check_response(resp)
|
||||
|
||||
console.print(f"[green]✓[/green] Cluster '{name}' creation initiated")
|
||||
@@ -218,7 +236,10 @@ def hke_delete(name: str, confirmed: bool):
|
||||
"""Delete a Kubernetes cluster."""
|
||||
if not confirmed:
|
||||
from rich.prompt import Confirm
|
||||
if not Confirm.ask(f"[red]Delete cluster '{name}'? This cannot be undone.[/red]"):
|
||||
|
||||
if not Confirm.ask(
|
||||
f"[red]Delete cluster '{name}'? This cannot be undone.[/red]"
|
||||
):
|
||||
return
|
||||
resp = _request("delete", f"/v1/hke/clusters/{name}")
|
||||
check_response(resp)
|
||||
@@ -240,6 +261,7 @@ def hke_kubeconfig(name: str, output: str):
|
||||
console.print(f"[green]✓[/green] Kubeconfig saved to {output}")
|
||||
else:
|
||||
import os as _os
|
||||
|
||||
kube_path = _os.path.expanduser("~/.kube/config")
|
||||
with open(kube_path, "w") as f:
|
||||
f.write(kubeconfig)
|
||||
@@ -251,7 +273,9 @@ def hke_kubeconfig(name: str, output: str):
|
||||
@click.option("--nodes", "-n", required=True, type=int, help="Target nodes")
|
||||
def hke_scale(name: str, nodes: int):
|
||||
"""Scale cluster nodes."""
|
||||
resp = _request("post", f"/v1/hke/clusters/{name}/scale", json={"node_count": nodes})
|
||||
resp = _request(
|
||||
"post", f"/v1/hke/clusters/{name}/scale", json={"node_count": nodes}
|
||||
)
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] Cluster '{name}' scaling to {nodes} nodes")
|
||||
|
||||
@@ -261,7 +285,9 @@ def hke_scale(name: str, nodes: int):
|
||||
@click.option("--version", "-v", required=True, help="Target Kubernetes version")
|
||||
def hke_upgrade(name: str, version: str):
|
||||
"""Upgrade cluster Kubernetes version."""
|
||||
resp = _request("post", f"/v1/hke/clusters/{name}/upgrade", json={"version": version})
|
||||
resp = _request(
|
||||
"post", f"/v1/hke/clusters/{name}/upgrade", json={"version": version}
|
||||
)
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] Cluster '{name}' upgrading to {version}")
|
||||
|
||||
@@ -306,6 +332,7 @@ def tunnel_share(target: str, name: str, auth: bool):
|
||||
try:
|
||||
tunnel_id = data.get("id", "")
|
||||
import time
|
||||
|
||||
while True:
|
||||
time.sleep(30)
|
||||
_request("post", f"/v1/tunnels/{tunnel_id}/keepalive")
|
||||
@@ -411,21 +438,31 @@ def dns_list(zone: str):
|
||||
@dns.command(name="create")
|
||||
@click.option("--zone", "-z", required=True, help="DNS zone")
|
||||
@click.option("--name", "-n", required=True, help="Record name")
|
||||
@click.option("--type", "-t", "record_type", required=True, help="Record type (A, CNAME, etc.)")
|
||||
@click.option(
|
||||
"--type", "-t", "record_type", required=True, help="Record type (A, CNAME, etc.)"
|
||||
)
|
||||
@click.option("--value", "-v", required=True, help="Record value")
|
||||
@click.option("--ttl", default=300, help="TTL in seconds")
|
||||
@click.option("--proxied", is_flag=True, help="Enable proxy")
|
||||
def dns_create(zone: str, name: str, record_type: str, value: str, ttl: int, proxied: bool):
|
||||
def dns_create(
|
||||
zone: str, name: str, record_type: str, value: str, ttl: int, proxied: bool
|
||||
):
|
||||
"""Create a DNS record."""
|
||||
resp = _request("post", f"/v1/dns/zones/{zone}/records", json={
|
||||
"name": name,
|
||||
"type": record_type,
|
||||
"value": value,
|
||||
"ttl": ttl,
|
||||
"proxied": proxied,
|
||||
})
|
||||
resp = _request(
|
||||
"post",
|
||||
f"/v1/dns/zones/{zone}/records",
|
||||
json={
|
||||
"name": name,
|
||||
"type": record_type,
|
||||
"value": value,
|
||||
"ttl": ttl,
|
||||
"proxied": proxied,
|
||||
},
|
||||
)
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] DNS record created: {name}.{zone} {record_type} {value}")
|
||||
console.print(
|
||||
f"[green]✓[/green] DNS record created: {name}.{zone} {record_type} {value}"
|
||||
)
|
||||
|
||||
|
||||
@dns.command(name="delete")
|
||||
@@ -434,7 +471,11 @@ def dns_create(zone: str, name: str, record_type: str, value: str, ttl: int, pro
|
||||
@click.option("--type", "-t", "record_type", required=True)
|
||||
def dns_delete(zone: str, name: str, record_type: str):
|
||||
"""Delete a DNS record."""
|
||||
resp = _request("delete", f"/v1/dns/zones/{zone}/records", params={"name": name, "type": record_type})
|
||||
resp = _request(
|
||||
"delete",
|
||||
f"/v1/dns/zones/{zone}/records",
|
||||
params={"name": name, "type": record_type},
|
||||
)
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] DNS record deleted: {name}.{zone} {record_type}")
|
||||
|
||||
@@ -452,7 +493,13 @@ def guard():
|
||||
|
||||
@guard.command(name="enable")
|
||||
@click.option("--project", "-p", help="Project ID")
|
||||
@click.option("--mode", "-m", type=click.Choice(["block", "warn", "log"]), default="block", help="Enforcement mode")
|
||||
@click.option(
|
||||
"--mode",
|
||||
"-m",
|
||||
type=click.Choice(["block", "warn", "log"]),
|
||||
default="block",
|
||||
help="Enforcement mode",
|
||||
)
|
||||
def guard_enable(project: str, mode: str):
|
||||
"""Enable LLM guard for project."""
|
||||
payload: dict = {"enabled": True, "mode": mode}
|
||||
@@ -505,7 +552,13 @@ def guard_status(project: str):
|
||||
|
||||
@guard.command(name="logs")
|
||||
@click.option("--limit", "-n", default=50, help="Number of logs")
|
||||
@click.option("--type", "-t", "log_type", type=click.Choice(["all", "blocked", "warned", "pii", "injection"]), default="all")
|
||||
@click.option(
|
||||
"--type",
|
||||
"-t",
|
||||
"log_type",
|
||||
type=click.Choice(["all", "blocked", "warned", "pii", "injection"]),
|
||||
default="all",
|
||||
)
|
||||
def guard_logs(limit: int, log_type: str):
|
||||
"""View guard logs."""
|
||||
params: dict = {"limit": limit}
|
||||
|
||||
@@ -89,7 +89,9 @@ def topics_list(project: str):
|
||||
|
||||
console.print(table)
|
||||
if not items:
|
||||
console.print("[dim]No topics found. Create one with 'hanzo pubsub topics create'[/dim]")
|
||||
console.print(
|
||||
"[dim]No topics found. Create one with 'hanzo pubsub topics create'[/dim]"
|
||||
)
|
||||
|
||||
|
||||
@topics.command(name="create")
|
||||
@@ -200,7 +202,9 @@ def subs_list(topic: str):
|
||||
@click.option("--push-endpoint", help="Push endpoint URL")
|
||||
@click.option("--ack-deadline", "-a", default=10, help="Ack deadline in seconds")
|
||||
@click.option("--filter", "-f", help="Message filter expression")
|
||||
def subs_create(name: str, topic: str, push_endpoint: str, ack_deadline: int, filter: str):
|
||||
def subs_create(
|
||||
name: str, topic: str, push_endpoint: str, ack_deadline: int, filter: str
|
||||
):
|
||||
"""Create a subscription."""
|
||||
payload = {"name": name, "topic": topic, "ack_deadline": ack_deadline}
|
||||
if push_endpoint:
|
||||
@@ -308,7 +312,11 @@ def pull(subscription: str, max_messages: int, wait: bool, auto_ack: bool):
|
||||
if auto_ack and messages:
|
||||
ack_ids = [m.get("ack_id") for m in messages if m.get("ack_id")]
|
||||
if ack_ids:
|
||||
_request("post", f"/v1/subscriptions/{subscription}/ack", json={"ack_ids": ack_ids})
|
||||
_request(
|
||||
"post",
|
||||
f"/v1/subscriptions/{subscription}/ack",
|
||||
json={"ack_ids": ack_ids},
|
||||
)
|
||||
console.print(f"[dim]Auto-acknowledged {len(ack_ids)} message(s)[/dim]")
|
||||
|
||||
if not messages:
|
||||
@@ -317,10 +325,14 @@ def pull(subscription: str, max_messages: int, wait: bool, auto_ack: bool):
|
||||
|
||||
@pubsub_group.command()
|
||||
@click.argument("subscription")
|
||||
@click.option("--ack-ids", "-a", multiple=True, required=True, help="Ack IDs to acknowledge")
|
||||
@click.option(
|
||||
"--ack-ids", "-a", multiple=True, required=True, help="Ack IDs to acknowledge"
|
||||
)
|
||||
def ack(subscription: str, ack_ids: tuple):
|
||||
"""Acknowledge messages."""
|
||||
resp = _request("post", f"/v1/subscriptions/{subscription}/ack", json={"ack_ids": list(ack_ids)})
|
||||
resp = _request(
|
||||
"post", f"/v1/subscriptions/{subscription}/ack", json={"ack_ids": list(ack_ids)}
|
||||
)
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] Acknowledged {len(ack_ids)} message(s)")
|
||||
|
||||
@@ -344,9 +356,13 @@ def seek(subscription: str, time: str, snapshot: str):
|
||||
check_response(resp)
|
||||
|
||||
if time:
|
||||
console.print(f"[green]✓[/green] Subscription '{subscription}' seeked to {time}")
|
||||
console.print(
|
||||
f"[green]✓[/green] Subscription '{subscription}' seeked to {time}"
|
||||
)
|
||||
else:
|
||||
console.print(f"[green]✓[/green] Subscription '{subscription}' seeked to snapshot '{snapshot}'")
|
||||
console.print(
|
||||
f"[green]✓[/green] Subscription '{subscription}' seeked to snapshot '{snapshot}'"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -391,7 +407,9 @@ def snapshots_list():
|
||||
@click.option("--subscription", "-s", required=True, help="Subscription to snapshot")
|
||||
def snapshots_create(name: str, subscription: str):
|
||||
"""Create a snapshot of a subscription."""
|
||||
resp = _request("post", "/v1/snapshots", json={"name": name, "subscription": subscription})
|
||||
resp = _request(
|
||||
"post", "/v1/snapshots", json={"name": name, "subscription": subscription}
|
||||
)
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] Snapshot '{name}' created from '{subscription}'")
|
||||
|
||||
|
||||
@@ -84,7 +84,9 @@ def queues_list(project: str):
|
||||
|
||||
console.print(table)
|
||||
if not items:
|
||||
console.print("[dim]No queues found. Create one with 'hanzo queues create'[/dim]")
|
||||
console.print(
|
||||
"[dim]No queues found. Create one with 'hanzo queues create'[/dim]"
|
||||
)
|
||||
|
||||
|
||||
@queues_group.command(name="create")
|
||||
@@ -93,7 +95,9 @@ def queues_list(project: str):
|
||||
@click.option("--rate-limit", "-r", help="Rate limit (e.g., '100/m')")
|
||||
@click.option("--retry", default=3, help="Max retry attempts")
|
||||
@click.option("--backoff", default="exponential", help="Backoff strategy")
|
||||
def queues_create(name: str, concurrency: int, rate_limit: str, retry: int, backoff: str):
|
||||
def queues_create(
|
||||
name: str, concurrency: int, rate_limit: str, retry: int, backoff: str
|
||||
):
|
||||
"""Create a queue."""
|
||||
payload = {
|
||||
"name": name,
|
||||
@@ -127,7 +131,9 @@ def queues_delete(name: str, force: bool):
|
||||
if not Confirm.ask(f"[red]Delete queue '{name}'?[/red]"):
|
||||
return
|
||||
|
||||
resp = _request("delete", f"/v1/queues/{name}", params={"force": str(force).lower()})
|
||||
resp = _request(
|
||||
"delete", f"/v1/queues/{name}", params={"force": str(force).lower()}
|
||||
)
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] Queue '{name}' deleted")
|
||||
|
||||
@@ -164,10 +170,14 @@ def queues_stats(name: str):
|
||||
@click.argument("queue")
|
||||
@click.option("--data", "-d", required=True, help="Job data (JSON)")
|
||||
@click.option("--name", "-n", help="Job name")
|
||||
@click.option("--priority", "-p", type=int, default=0, help="Job priority (higher = more urgent)")
|
||||
@click.option(
|
||||
"--priority", "-p", type=int, default=0, help="Job priority (higher = more urgent)"
|
||||
)
|
||||
@click.option("--delay", help="Delay before processing (e.g., '5m', '1h')")
|
||||
@click.option("--attempts", "-a", default=3, help="Max attempts")
|
||||
def queues_push(queue: str, data: str, name: str, priority: int, delay: str, attempts: int):
|
||||
def queues_push(
|
||||
queue: str, data: str, name: str, priority: int, delay: str, attempts: int
|
||||
):
|
||||
"""Push a job to a queue."""
|
||||
payload = {"data": json.loads(data), "priority": priority, "max_attempts": attempts}
|
||||
if name:
|
||||
@@ -220,7 +230,9 @@ def queues_pop(queue: str, count: int, wait: bool):
|
||||
@click.option("--count", "-n", default=5, help="Number of jobs to peek")
|
||||
def queues_peek(queue: str, count: int):
|
||||
"""Peek at jobs without removing them."""
|
||||
resp = _request("get", f"/v1/queues/{queue}/jobs", params={"limit": count, "status": "pending"})
|
||||
resp = _request(
|
||||
"get", f"/v1/queues/{queue}/jobs", params={"limit": count, "status": "pending"}
|
||||
)
|
||||
data = check_response(resp)
|
||||
jobs = data.get("jobs", data.get("items", []))
|
||||
|
||||
@@ -360,7 +372,9 @@ def dlq_retry(queue: str, job_id: str, all_jobs: bool):
|
||||
|
||||
resp = _request("post", f"/v1/queues/{queue}/dlq/retry", json=payload)
|
||||
data = check_response(resp)
|
||||
console.print(f"[green]✓[/green] {data.get('retried', 0)} DLQ job(s) moved back to queue")
|
||||
console.print(
|
||||
f"[green]✓[/green] {data.get('retried', 0)} DLQ job(s) moved back to queue"
|
||||
)
|
||||
|
||||
|
||||
@dlq.command(name="purge")
|
||||
@@ -374,7 +388,9 @@ def dlq_purge(queue: str):
|
||||
|
||||
resp = _request("post", f"/v1/queues/{queue}/dlq/purge")
|
||||
data = check_response(resp)
|
||||
console.print(f"[green]✓[/green] Purged {data.get('purged', 0)} DLQ job(s) for '{queue}'")
|
||||
console.print(
|
||||
f"[green]✓[/green] Purged {data.get('purged', 0)} DLQ job(s) for '{queue}'"
|
||||
)
|
||||
|
||||
|
||||
@queues_group.command(name="drain")
|
||||
@@ -390,7 +406,9 @@ def queues_drain(queue: str, delayed: bool):
|
||||
payload = {"delayed": delayed}
|
||||
resp = _request("post", f"/v1/queues/{queue}/drain", json=payload)
|
||||
data = check_response(resp)
|
||||
console.print(f"[green]✓[/green] Drained {data.get('drained', 0)} job(s) from '{queue}'")
|
||||
console.print(
|
||||
f"[green]✓[/green] Drained {data.get('drained', 0)} job(s) from '{queue}'"
|
||||
)
|
||||
if delayed:
|
||||
console.print("[dim]Delayed jobs also removed[/dim]")
|
||||
|
||||
|
||||
@@ -105,14 +105,18 @@ def info(ctx):
|
||||
|
||||
if not providers:
|
||||
console.print("\n[yellow]No LLM providers configured[/yellow]")
|
||||
console.print("Set one of: OPENAI_API_KEY, ANTHROPIC_API_KEY, HANZO_API_KEY")
|
||||
console.print(
|
||||
"Set one of: OPENAI_API_KEY, ANTHROPIC_API_KEY, HANZO_API_KEY"
|
||||
)
|
||||
|
||||
except ImportError:
|
||||
console.print("[red]Error:[/red] hanzo-repl not installed")
|
||||
|
||||
|
||||
@repl_group.command()
|
||||
@click.option("--interface", type=click.Choice(["all", "ipython", "tui", "voice"]), default="all")
|
||||
@click.option(
|
||||
"--interface", type=click.Choice(["all", "ipython", "tui", "voice"]), default="all"
|
||||
)
|
||||
@click.pass_context
|
||||
def install_extras(ctx, interface: str):
|
||||
"""Install optional REPL components."""
|
||||
@@ -165,7 +169,9 @@ def exec(ctx, command: tuple, model: str):
|
||||
|
||||
except ImportError as e:
|
||||
console.print(f"[red]Import Error:[/red] {e}")
|
||||
console.print("[yellow]Note:[/yellow] hanzo-repl may not be installed correctly")
|
||||
console.print(
|
||||
"[yellow]Note:[/yellow] hanzo-repl may not be installed correctly"
|
||||
)
|
||||
except Exception as e:
|
||||
console.print(f"[red]Error: {e}[/red]")
|
||||
|
||||
|
||||
@@ -40,7 +40,9 @@ def start_router(ctx, port: int, config: Optional[str], detach: bool):
|
||||
if not router_dir:
|
||||
console.print("[red]Error:[/red] Hanzo router not found")
|
||||
console.print("\nPlease clone the router:")
|
||||
console.print(" git clone https://github.com/hanzoai/router.git ~/work/hanzo/router")
|
||||
console.print(
|
||||
" git clone https://github.com/hanzoai/router.git ~/work/hanzo/router"
|
||||
)
|
||||
return
|
||||
|
||||
console.print(f"[green]✓[/green] Found router at {router_dir}")
|
||||
@@ -89,7 +91,9 @@ def start_router(ctx, port: int, config: Optional[str], detach: bool):
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
)
|
||||
console.print(f"[green]✓[/green] Router started in background (PID: {process.pid})")
|
||||
console.print(
|
||||
f"[green]✓[/green] Router started in background (PID: {process.pid})"
|
||||
)
|
||||
console.print(f"Check status: curl http://localhost:{port}/health")
|
||||
else:
|
||||
# Run in foreground
|
||||
@@ -112,8 +116,14 @@ def stop_router(port: int):
|
||||
for proc in psutil.process_iter(["pid", "cmdline"]):
|
||||
try:
|
||||
cmdline = proc.info["cmdline"]
|
||||
if cmdline and "proxy_server" in " ".join(cmdline) and str(port) in " ".join(cmdline):
|
||||
console.print(f"[yellow]Stopping router (PID: {proc.info['pid']})[/yellow]")
|
||||
if (
|
||||
cmdline
|
||||
and "proxy_server" in " ".join(cmdline)
|
||||
and str(port) in " ".join(cmdline)
|
||||
):
|
||||
console.print(
|
||||
f"[yellow]Stopping router (PID: {proc.info['pid']})[/yellow]"
|
||||
)
|
||||
proc.send_signal(signal.SIGTERM)
|
||||
proc.wait(timeout=5)
|
||||
found = True
|
||||
@@ -138,7 +148,9 @@ def router_status(port: int):
|
||||
|
||||
# Try to get models
|
||||
try:
|
||||
models_response = httpx.get(f"http://localhost:{port}/models", timeout=2.0)
|
||||
models_response = httpx.get(
|
||||
f"http://localhost:{port}/models", timeout=2.0
|
||||
)
|
||||
if models_response.status_code == 200:
|
||||
data = models_response.json()
|
||||
if "data" in data:
|
||||
@@ -146,7 +158,9 @@ def router_status(port: int):
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
console.print(f"[yellow]Router responding but unhealthy (status: {response.status_code})[/yellow]")
|
||||
console.print(
|
||||
f"[yellow]Router responding but unhealthy (status: {response.status_code})[/yellow]"
|
||||
)
|
||||
except httpx.ConnectError:
|
||||
console.print(f"[red]Router not running on port {port}[/red]")
|
||||
console.print("\nStart with: hanzo router start")
|
||||
|
||||
@@ -91,7 +91,9 @@ def _find_container(client, base_url: str, name: str):
|
||||
@click.argument("name", required=False)
|
||||
@click.option("--image", "-i", help="Container image")
|
||||
@click.option("--source", "-s", help="Git repo URL (source build)")
|
||||
@click.option("--env", "-e", "env_name", help="Override environment (uses context by default)")
|
||||
@click.option(
|
||||
"--env", "-e", "env_name", help="Override environment (uses context by default)"
|
||||
)
|
||||
@click.option("--replicas", "-r", default=1, help="Number of replicas")
|
||||
@click.option("--port", "-p", default=8080, help="Service port")
|
||||
@click.option("--cpu", default="0.5", help="CPU cores")
|
||||
@@ -184,7 +186,11 @@ def run_service(name, image, source, env_name, replicas, port, cpu, memory, wait
|
||||
|
||||
# Trigger build if source
|
||||
if source and not existing:
|
||||
cid = (result or {}).get("_id") or (result or {}).get("iid") or (result or {}).get("id", "")
|
||||
cid = (
|
||||
(result or {}).get("_id")
|
||||
or (result or {}).get("iid")
|
||||
or (result or {}).get("id", "")
|
||||
)
|
||||
if cid:
|
||||
console.print("[cyan]Triggering build...[/cyan]")
|
||||
client.post(f"{base_url}/{cid}/trigger")
|
||||
@@ -197,7 +203,10 @@ def run_service(name, image, source, env_name, replicas, port, cpu, memory, wait
|
||||
cid = (result or {}).get("_id") or existing_id
|
||||
if cid:
|
||||
data = client.get(f"{base_url}/{cid}")
|
||||
if data and data.get("status", {}).get("availableReplicas", 0) >= replicas:
|
||||
if (
|
||||
data
|
||||
and data.get("status", {}).get("availableReplicas", 0) >= replicas
|
||||
):
|
||||
console.print("[green]✓[/green] Deployment ready")
|
||||
return
|
||||
time.sleep(2)
|
||||
@@ -290,7 +299,9 @@ def run_status(name):
|
||||
replicas = container.get("deploymentConfig", {}).get("desiredReplicas", "?")
|
||||
avail = status_info.get("availableReplicas", "?")
|
||||
state = status_info.get("conditions", [{}])
|
||||
state_str = "[green]● Running[/green]" if avail else "[yellow]○ Pending[/yellow]"
|
||||
state_str = (
|
||||
"[green]● Running[/green]" if avail else "[yellow]○ Pending[/yellow]"
|
||||
)
|
||||
|
||||
img = ""
|
||||
if container.get("repoOrRegistry") == "registry":
|
||||
@@ -324,7 +335,9 @@ def run_status(name):
|
||||
|
||||
if not containers:
|
||||
console.print("[dim]No services deployed in current context.[/dim]")
|
||||
console.print("[dim]Deploy with: hanzo run service NAME --image IMAGE[/dim]")
|
||||
console.print(
|
||||
"[dim]Deploy with: hanzo run service NAME --image IMAGE[/dim]"
|
||||
)
|
||||
return
|
||||
|
||||
table = Table(title="Services", box=box.ROUNDED)
|
||||
@@ -392,7 +405,9 @@ def run_logs(name, follow, tail, since):
|
||||
console.print("[dim]No logs available.[/dim]")
|
||||
|
||||
if follow:
|
||||
console.print("[dim]Follow mode is not yet supported via API. Use 'hanzo k8s logs' for streaming.[/dim]")
|
||||
console.print(
|
||||
"[dim]Follow mode is not yet supported via API. Use 'hanzo k8s logs' for streaming.[/dim]"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -523,7 +538,11 @@ def run_rollback(name, version):
|
||||
console.print("[yellow]No pipeline history found.[/yellow]")
|
||||
return
|
||||
|
||||
runs = pipelines if isinstance(pipelines, list) else pipelines.get("runs", pipelines.get("data", []))
|
||||
runs = (
|
||||
pipelines
|
||||
if isinstance(pipelines, list)
|
||||
else pipelines.get("runs", pipelines.get("data", []))
|
||||
)
|
||||
|
||||
if version:
|
||||
# Find specific run
|
||||
@@ -562,7 +581,9 @@ def run_traffic(name, version):
|
||||
hanzo run traffic my-api -v v2:100 # Full cutover
|
||||
"""
|
||||
if not version:
|
||||
console.print("[yellow]Traffic splitting requires PaaS ingress configuration.[/yellow]")
|
||||
console.print(
|
||||
"[yellow]Traffic splitting requires PaaS ingress configuration.[/yellow]"
|
||||
)
|
||||
console.print("[dim]Specify version weights: -v v1:90 -v v2:10[/dim]")
|
||||
return
|
||||
|
||||
@@ -573,7 +594,9 @@ def run_traffic(name, version):
|
||||
if len(parts) == 2:
|
||||
console.print(f" {parts[0]}: {parts[1]}%")
|
||||
|
||||
console.print("[yellow]Traffic splitting requires ingress controller support.[/yellow]")
|
||||
console.print(
|
||||
"[yellow]Traffic splitting requires ingress controller support.[/yellow]"
|
||||
)
|
||||
console.print("[dim]Configure via 'hanzo k8s ingress' or your service mesh.[/dim]")
|
||||
|
||||
|
||||
@@ -595,7 +618,9 @@ def run_delete(name, force):
|
||||
if not force:
|
||||
from rich.prompt import Confirm
|
||||
|
||||
if not Confirm.ask(f"[red]Delete service '{name}'? This cannot be undone.[/red]"):
|
||||
if not Confirm.ask(
|
||||
f"[red]Delete service '{name}'? This cannot be undone.[/red]"
|
||||
):
|
||||
return
|
||||
|
||||
result = client.delete(f"{base_url}/{cid}")
|
||||
|
||||
@@ -60,9 +60,16 @@ def search_group():
|
||||
|
||||
@search_group.command(name="create")
|
||||
@click.argument("name")
|
||||
@click.option("--mode", "-m", type=click.Choice(["lexical", "vector", "hybrid"]), default="hybrid")
|
||||
@click.option(
|
||||
"--mode", "-m", type=click.Choice(["lexical", "vector", "hybrid"]), default="hybrid"
|
||||
)
|
||||
@click.option("--region", "-r", help="Region")
|
||||
@click.option("--tier", "-t", type=click.Choice(["free", "standard", "dedicated"]), default="standard")
|
||||
@click.option(
|
||||
"--tier",
|
||||
"-t",
|
||||
type=click.Choice(["free", "standard", "dedicated"]),
|
||||
default="standard",
|
||||
)
|
||||
def search_create(name: str, mode: str, region: str, tier: str):
|
||||
"""Create a search engine."""
|
||||
payload = {"name": name, "mode": mode, "tier": tier}
|
||||
@@ -107,7 +114,9 @@ def search_list():
|
||||
|
||||
console.print(table)
|
||||
if not engines:
|
||||
console.print("[dim]No search engines found. Create one with 'hanzo search create'[/dim]")
|
||||
console.print(
|
||||
"[dim]No search engines found. Create one with 'hanzo search create'[/dim]"
|
||||
)
|
||||
|
||||
|
||||
@search_group.command(name="describe")
|
||||
@@ -166,11 +175,18 @@ def index():
|
||||
@index.command(name="create")
|
||||
@click.argument("name")
|
||||
@click.option("--engine", "-e", default="default", help="Search engine")
|
||||
@click.option("--mode", "-m", type=click.Choice(["lexical", "vector", "hybrid"]), help="Override engine mode")
|
||||
@click.option(
|
||||
"--mode",
|
||||
"-m",
|
||||
type=click.Choice(["lexical", "vector", "hybrid"]),
|
||||
help="Override engine mode",
|
||||
)
|
||||
@click.option("--mapping", help="Mapping JSON or file")
|
||||
@click.option("--shards", "-s", default=1, help="Number of shards")
|
||||
@click.option("--replicas", "-r", default=1, help="Number of replicas")
|
||||
def index_create(name: str, engine: str, mode: str, mapping: str, shards: int, replicas: int):
|
||||
def index_create(
|
||||
name: str, engine: str, mode: str, mapping: str, shards: int, replicas: int
|
||||
):
|
||||
"""Create a search index."""
|
||||
payload = {"name": name, "shards": shards, "replicas": replicas}
|
||||
if mode:
|
||||
@@ -208,7 +224,9 @@ def index_list(engine: str):
|
||||
|
||||
for idx in items:
|
||||
health = idx.get("health", "unknown")
|
||||
style = "green" if health == "green" else "yellow" if health == "yellow" else "red"
|
||||
style = (
|
||||
"green" if health == "green" else "yellow" if health == "yellow" else "red"
|
||||
)
|
||||
table.add_row(
|
||||
idx.get("name", ""),
|
||||
idx.get("mode", "-"),
|
||||
@@ -277,7 +295,9 @@ def index_mapping(name: str, engine: str, get_mapping: bool, set_mapping: str):
|
||||
else:
|
||||
mapping_data = json.loads(set_mapping)
|
||||
|
||||
resp = _request("put", f"/v1/engines/{engine}/indexes/{name}/mapping", json=mapping_data)
|
||||
resp = _request(
|
||||
"put", f"/v1/engines/{engine}/indexes/{name}/mapping", json=mapping_data
|
||||
)
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] Mapping updated for '{name}'")
|
||||
else:
|
||||
@@ -295,11 +315,18 @@ def index_mapping(name: str, engine: str, get_mapping: bool, set_mapping: str):
|
||||
@search_group.command(name="ingest")
|
||||
@click.option("--index", "-i", required=True, help="Target index")
|
||||
@click.option("--from", "source", required=True, help="Source: file, s3://, storage://")
|
||||
@click.option("--format", "fmt", type=click.Choice(["jsonl", "json", "csv", "parquet"]), default="jsonl")
|
||||
@click.option(
|
||||
"--format",
|
||||
"fmt",
|
||||
type=click.Choice(["jsonl", "json", "csv", "parquet"]),
|
||||
default="jsonl",
|
||||
)
|
||||
@click.option("--pipeline", "-p", help="Ingest pipeline to apply")
|
||||
@click.option("--batch-size", "-b", default=1000, help="Batch size")
|
||||
@click.option("--engine", "-e", default="default")
|
||||
def search_ingest(index: str, source: str, fmt: str, pipeline: str, batch_size: int, engine: str):
|
||||
def search_ingest(
|
||||
index: str, source: str, fmt: str, pipeline: str, batch_size: int, engine: str
|
||||
):
|
||||
"""Ingest documents into an index.
|
||||
|
||||
\b
|
||||
@@ -318,7 +345,9 @@ def search_ingest(index: str, source: str, fmt: str, pipeline: str, batch_size:
|
||||
if pipeline:
|
||||
payload["pipeline"] = pipeline
|
||||
|
||||
resp = _request("post", f"/v1/engines/{engine}/indexes/{index}/ingest", json=payload)
|
||||
resp = _request(
|
||||
"post", f"/v1/engines/{engine}/indexes/{index}/ingest", json=payload
|
||||
)
|
||||
data = check_response(resp)
|
||||
|
||||
console.print(f"[green]✓[/green] Ingestion complete")
|
||||
@@ -337,7 +366,16 @@ def search_ingest(index: str, source: str, fmt: str, pipeline: str, batch_size:
|
||||
@click.option("--vector-field", help="Field for vector search")
|
||||
@click.option("--json", "as_json", is_flag=True, help="Output as JSON")
|
||||
@click.option("--engine", "-e", default="default")
|
||||
def search_query(index: str, q: str, filter: str, topk: int, mode: str, vector_field: str, as_json: bool, engine: str):
|
||||
def search_query(
|
||||
index: str,
|
||||
q: str,
|
||||
filter: str,
|
||||
topk: int,
|
||||
mode: str,
|
||||
vector_field: str,
|
||||
as_json: bool,
|
||||
engine: str,
|
||||
):
|
||||
"""Search documents.
|
||||
|
||||
\b
|
||||
@@ -380,7 +418,9 @@ def search_query(index: str, q: str, filter: str, topk: int, mode: str, vector_f
|
||||
|
||||
console.print(table)
|
||||
total = data.get("total", len(hits))
|
||||
console.print(f"[dim]{total} total hit(s), showing top {min(topk, len(hits))}[/dim]")
|
||||
console.print(
|
||||
f"[dim]{total} total hit(s), showing top {min(topk, len(hits))}[/dim]"
|
||||
)
|
||||
|
||||
|
||||
@search_group.command(name="reindex")
|
||||
@@ -389,7 +429,9 @@ def search_query(index: str, q: str, filter: str, topk: int, mode: str, vector_f
|
||||
@click.option("--query", "-q", help="Filter query")
|
||||
@click.option("--pipeline", "-p", help="Transform pipeline")
|
||||
@click.option("--engine", "-e", default="default")
|
||||
def search_reindex(source_idx: str, dest_idx: str, query: str, pipeline: str, engine: str):
|
||||
def search_reindex(
|
||||
source_idx: str, dest_idx: str, query: str, pipeline: str, engine: str
|
||||
):
|
||||
"""Reindex documents."""
|
||||
console.print(f"[cyan]Reindexing from '{source_idx}' to '{dest_idx}'...[/cyan]")
|
||||
|
||||
|
||||
@@ -157,7 +157,9 @@ def secrets_list(env: str, prefix: str):
|
||||
|
||||
console.print(table)
|
||||
if not items:
|
||||
console.print("[dim]No secrets found. Create one with 'hanzo secrets set'[/dim]")
|
||||
console.print(
|
||||
"[dim]No secrets found. Create one with 'hanzo secrets set'[/dim]"
|
||||
)
|
||||
|
||||
|
||||
@secrets_group.command(name="unset")
|
||||
@@ -260,10 +262,14 @@ def secrets_rollback(name: str, version: str, env: str):
|
||||
@secrets_group.command(name="grant")
|
||||
@click.argument("name")
|
||||
@click.option("--to", "-t", required=True, help="Service or user to grant access")
|
||||
@click.option("--role", "-r", default="read", type=click.Choice(["read", "write", "admin"]))
|
||||
@click.option(
|
||||
"--role", "-r", default="read", type=click.Choice(["read", "write", "admin"])
|
||||
)
|
||||
def secrets_grant(name: str, to: str, role: str):
|
||||
"""Grant access to a secret."""
|
||||
resp = _request("post", f"/v1/secrets/{name}/access", json={"principal": to, "role": role})
|
||||
resp = _request(
|
||||
"post", f"/v1/secrets/{name}/access", json={"principal": to, "role": role}
|
||||
)
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] Granted {role} access to '{name}' for {to}")
|
||||
|
||||
|
||||
@@ -34,7 +34,9 @@ def _get_s3_client():
|
||||
import boto3
|
||||
from botocore.config import Config
|
||||
except ImportError:
|
||||
console.print("[red]boto3 not installed.[/red] Run: pip install 'hanzo[storage]'")
|
||||
console.print(
|
||||
"[red]boto3 not installed.[/red] Run: pip install 'hanzo[storage]'"
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
# Try to get credentials from env or hanzo auth
|
||||
@@ -171,14 +173,21 @@ def buckets_list(json_out: bool):
|
||||
bucket_list = response.get("Buckets", [])
|
||||
|
||||
if json_out:
|
||||
click.echo(json.dumps(
|
||||
[{"name": b["Name"], "created": b["CreationDate"].isoformat()} for b in bucket_list],
|
||||
indent=2,
|
||||
))
|
||||
click.echo(
|
||||
json.dumps(
|
||||
[
|
||||
{"name": b["Name"], "created": b["CreationDate"].isoformat()}
|
||||
for b in bucket_list
|
||||
],
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if not bucket_list:
|
||||
console.print("[dim]No buckets found. Create one with 'hanzo storage buckets create'[/dim]")
|
||||
console.print(
|
||||
"[dim]No buckets found. Create one with 'hanzo storage buckets create'[/dim]"
|
||||
)
|
||||
return
|
||||
|
||||
table = Table(title=f"Buckets ({len(bucket_list)})", box=box.ROUNDED)
|
||||
@@ -278,12 +287,14 @@ def storage_ls(path: str, recursive: bool, human: bool, json_out: bool):
|
||||
for p in all_prefixes:
|
||||
items.append({"key": p["Prefix"], "type": "directory"})
|
||||
for o in all_objects:
|
||||
items.append({
|
||||
"key": o["Key"],
|
||||
"size": o["Size"],
|
||||
"modified": o["LastModified"].isoformat(),
|
||||
"type": "file",
|
||||
})
|
||||
items.append(
|
||||
{
|
||||
"key": o["Key"],
|
||||
"size": o["Size"],
|
||||
"modified": o["LastModified"].isoformat(),
|
||||
"type": "file",
|
||||
}
|
||||
)
|
||||
click.echo(json.dumps(items, indent=2))
|
||||
return
|
||||
|
||||
@@ -327,8 +338,14 @@ def storage_cp(source: str, dest: str, recursive: bool):
|
||||
hanzo storage cp mybucket/a.txt mybucket/b.txt Server-side copy
|
||||
"""
|
||||
s3 = _get_s3_client()
|
||||
is_s3_src = source.startswith("s3://") or "/" in source and not os.path.exists(source)
|
||||
is_s3_dst = dest.startswith("s3://") or "/" in dest and not os.path.exists(os.path.dirname(dest) or ".")
|
||||
is_s3_src = (
|
||||
source.startswith("s3://") or "/" in source and not os.path.exists(source)
|
||||
)
|
||||
is_s3_dst = (
|
||||
dest.startswith("s3://")
|
||||
or "/" in dest
|
||||
and not os.path.exists(os.path.dirname(dest) or ".")
|
||||
)
|
||||
|
||||
# Heuristic: if source exists locally, it's an upload
|
||||
if os.path.exists(source) and not source.startswith("s3://"):
|
||||
@@ -347,13 +364,17 @@ def storage_cp(source: str, dest: str, recursive: bool):
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
console=console,
|
||||
) as progress:
|
||||
task = progress.add_task(f"Uploading {len(files)} files...", total=len(files))
|
||||
task = progress.add_task(
|
||||
f"Uploading {len(files)} files...", total=len(files)
|
||||
)
|
||||
for f in files:
|
||||
rel = f.relative_to(local_path)
|
||||
obj_key = f"{key}{rel}" if key else str(rel)
|
||||
s3.upload_file(str(f), bucket, obj_key)
|
||||
progress.advance(task)
|
||||
console.print(f"[green]Uploaded {len(files)} files to {bucket}/{key}[/green]")
|
||||
console.print(
|
||||
f"[green]Uploaded {len(files)} files to {bucket}/{key}[/green]"
|
||||
)
|
||||
else:
|
||||
if not key or key.endswith("/"):
|
||||
key = key + local_path.name
|
||||
@@ -381,7 +402,9 @@ def storage_cp(source: str, dest: str, recursive: bool):
|
||||
TextColumn("[progress.description]{task.description}"),
|
||||
console=console,
|
||||
) as progress:
|
||||
task = progress.add_task(f"Downloading {len(objects)} files...", total=len(objects))
|
||||
task = progress.add_task(
|
||||
f"Downloading {len(objects)} files...", total=len(objects)
|
||||
)
|
||||
for obj in objects:
|
||||
rel_key = obj["Key"].removeprefix(key).lstrip("/")
|
||||
if not rel_key:
|
||||
@@ -397,7 +420,9 @@ def storage_cp(source: str, dest: str, recursive: bool):
|
||||
local_path = local_path / filename
|
||||
local_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
s3.download_file(bucket, key, str(local_path))
|
||||
console.print(f"[green]Downloaded s3://{bucket}/{key} -> {local_path}[/green]")
|
||||
console.print(
|
||||
f"[green]Downloaded s3://{bucket}/{key} -> {local_path}[/green]"
|
||||
)
|
||||
|
||||
elif is_s3_src and is_s3_dst:
|
||||
# Server-side copy
|
||||
@@ -408,9 +433,13 @@ def storage_cp(source: str, dest: str, recursive: bool):
|
||||
Bucket=dst_bucket,
|
||||
Key=dst_key,
|
||||
)
|
||||
console.print(f"[green]Copied s3://{src_bucket}/{src_key} -> s3://{dst_bucket}/{dst_key}[/green]")
|
||||
console.print(
|
||||
f"[green]Copied s3://{src_bucket}/{src_key} -> s3://{dst_bucket}/{dst_key}[/green]"
|
||||
)
|
||||
else:
|
||||
console.print("[red]At least one path must be an S3 path (bucket/key or s3://bucket/key).[/red]")
|
||||
console.print(
|
||||
"[red]At least one path must be an S3 path (bucket/key or s3://bucket/key).[/red]"
|
||||
)
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
@@ -436,7 +465,9 @@ def storage_mv(source: str, dest: str):
|
||||
Key=dst_key,
|
||||
)
|
||||
s3.delete_object(Bucket=src_bucket, Key=src_key)
|
||||
console.print(f"[green]Moved s3://{src_bucket}/{src_key} -> s3://{dst_bucket}/{dst_key}[/green]")
|
||||
console.print(
|
||||
f"[green]Moved s3://{src_bucket}/{src_key} -> s3://{dst_bucket}/{dst_key}[/green]"
|
||||
)
|
||||
|
||||
|
||||
@storage_group.command(name="rm")
|
||||
@@ -466,7 +497,9 @@ def storage_rm(path: str, recursive: bool, yes: bool):
|
||||
return
|
||||
|
||||
if not yes:
|
||||
click.confirm(f"Delete {len(objects)} object(s) under '{path}'?", abort=True)
|
||||
click.confirm(
|
||||
f"Delete {len(objects)} object(s) under '{path}'?", abort=True
|
||||
)
|
||||
|
||||
# Delete in batches of 1000 (S3 limit)
|
||||
for i in range(0, len(objects), 1000):
|
||||
@@ -486,7 +519,9 @@ def storage_rm(path: str, recursive: bool, yes: bool):
|
||||
@storage_group.command(name="sync")
|
||||
@click.argument("source")
|
||||
@click.argument("dest")
|
||||
@click.option("--delete", "delete_extra", is_flag=True, help="Delete files not in source")
|
||||
@click.option(
|
||||
"--delete", "delete_extra", is_flag=True, help="Delete files not in source"
|
||||
)
|
||||
@click.option("--dry-run", is_flag=True, help="Show what would be done")
|
||||
def storage_sync(source: str, dest: str, delete_extra: bool, dry_run: bool):
|
||||
"""Sync directories with storage.
|
||||
@@ -539,7 +574,9 @@ def storage_sync(source: str, dest: str, delete_extra: bool, dry_run: bool):
|
||||
console.print(f" [green]upload[/green] {rel}")
|
||||
for rel in to_delete:
|
||||
console.print(f" [red]delete[/red] {rel}")
|
||||
console.print(f"[dim]Would upload {len(to_upload)}, delete {len(to_delete)}[/dim]")
|
||||
console.print(
|
||||
f"[dim]Would upload {len(to_upload)}, delete {len(to_delete)}[/dim]"
|
||||
)
|
||||
return
|
||||
|
||||
for rel in to_upload:
|
||||
@@ -596,7 +633,9 @@ def storage_sync(source: str, dest: str, delete_extra: bool, dry_run: bool):
|
||||
console.print(f" [green]download[/green] {rel}")
|
||||
for rel in to_delete:
|
||||
console.print(f" [red]delete[/red] {rel}")
|
||||
console.print(f"[dim]Would download {len(to_download)}, delete {len(to_delete)}[/dim]")
|
||||
console.print(
|
||||
f"[dim]Would download {len(to_download)}, delete {len(to_delete)}[/dim]"
|
||||
)
|
||||
return
|
||||
|
||||
for rel in to_download:
|
||||
@@ -615,7 +654,13 @@ def storage_sync(source: str, dest: str, delete_extra: bool, dry_run: bool):
|
||||
|
||||
@storage_group.command(name="presign")
|
||||
@click.argument("path")
|
||||
@click.option("--expires", "-e", default=3600, type=int, help="Expiration in seconds (default: 3600)")
|
||||
@click.option(
|
||||
"--expires",
|
||||
"-e",
|
||||
default=3600,
|
||||
type=int,
|
||||
help="Expiration in seconds (default: 3600)",
|
||||
)
|
||||
@click.option("--method", "-m", default="GET", type=click.Choice(["GET", "PUT"]))
|
||||
def storage_presign(path: str, expires: int, method: str):
|
||||
"""Generate a presigned URL for an object.
|
||||
|
||||
@@ -70,7 +70,15 @@ def tasks_group():
|
||||
@click.option("--env", "-e", multiple=True, help="Environment variables")
|
||||
@click.option("--timeout", "-t", default="1h", help="Task timeout")
|
||||
@click.option("--retries", "-r", default=3, help="Max retries on failure")
|
||||
def tasks_create(name: str, image: str, cmd: str, function: str, env: tuple, timeout: str, retries: int):
|
||||
def tasks_create(
|
||||
name: str,
|
||||
image: str,
|
||||
cmd: str,
|
||||
function: str,
|
||||
env: tuple,
|
||||
timeout: str,
|
||||
retries: int,
|
||||
):
|
||||
"""Create a task definition.
|
||||
|
||||
\b
|
||||
@@ -109,7 +117,9 @@ def tasks_create(name: str, image: str, cmd: str, function: str, env: tuple, tim
|
||||
|
||||
|
||||
@tasks_group.command(name="list")
|
||||
@click.option("--status", "-s", type=click.Choice(["active", "disabled", "all"]), default="all")
|
||||
@click.option(
|
||||
"--status", "-s", type=click.Choice(["active", "disabled", "all"]), default="all"
|
||||
)
|
||||
def tasks_list(status: str):
|
||||
"""List all tasks."""
|
||||
params = {}
|
||||
@@ -131,7 +141,11 @@ def tasks_list(status: str):
|
||||
for t in items:
|
||||
t_status = t.get("status", "active")
|
||||
style = "green" if t_status == "active" else "yellow"
|
||||
task_type = "image" if t.get("image") else "function" if t.get("function") else "command"
|
||||
task_type = (
|
||||
"image"
|
||||
if t.get("image")
|
||||
else "function" if t.get("function") else "command"
|
||||
)
|
||||
table.add_row(
|
||||
t.get("name", ""),
|
||||
task_type,
|
||||
@@ -350,7 +364,12 @@ def runs():
|
||||
|
||||
@runs.command(name="list")
|
||||
@click.argument("task", required=False)
|
||||
@click.option("--status", "-s", type=click.Choice(["running", "success", "failed", "all"]), default="all")
|
||||
@click.option(
|
||||
"--status",
|
||||
"-s",
|
||||
type=click.Choice(["running", "success", "failed", "all"]),
|
||||
default="all",
|
||||
)
|
||||
@click.option("--limit", "-n", default=20, help="Max results")
|
||||
def runs_list(task: str, status: str, limit: int):
|
||||
"""List task runs."""
|
||||
@@ -414,7 +433,9 @@ def runs_logs(run_id: str, follow: bool, tail: int):
|
||||
ts = str(line.get("timestamp", ""))[:19]
|
||||
level = line.get("level", "info")
|
||||
msg = line.get("message", "")
|
||||
style = "red" if level == "error" else "yellow" if level == "warn" else "dim"
|
||||
style = (
|
||||
"red" if level == "error" else "yellow" if level == "warn" else "dim"
|
||||
)
|
||||
console.print(f"[dim]{ts}[/dim] [{style}]{level}[/{style}] {msg}")
|
||||
else:
|
||||
console.print(str(line))
|
||||
@@ -463,7 +484,12 @@ def triggers():
|
||||
type=click.Choice(["event", "queue", "topic", "http", "schedule"]),
|
||||
help="Trigger type",
|
||||
)
|
||||
@click.option("--source", "-s", required=True, help="Trigger source (event name, queue name, etc.)")
|
||||
@click.option(
|
||||
"--source",
|
||||
"-s",
|
||||
required=True,
|
||||
help="Trigger source (event name, queue name, etc.)",
|
||||
)
|
||||
@click.option("--filter", "-f", help="Event filter expression")
|
||||
def triggers_add(task: str, trigger_type: str, source: str, filter: str):
|
||||
"""Add a trigger to a task.
|
||||
@@ -544,7 +570,11 @@ def triggers_rm(task: str, source: str, remove_all: bool):
|
||||
@click.option("--source", "-s", help="Specific trigger")
|
||||
def triggers_pause(task: str, source: str):
|
||||
"""Pause triggers."""
|
||||
path = f"/v1/tasks/{task}/triggers/{source}/pause" if source else f"/v1/tasks/{task}/triggers/pause"
|
||||
path = (
|
||||
f"/v1/tasks/{task}/triggers/{source}/pause"
|
||||
if source
|
||||
else f"/v1/tasks/{task}/triggers/pause"
|
||||
)
|
||||
resp = _request("post", path)
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] Paused trigger(s) for '{task}'")
|
||||
@@ -555,7 +585,11 @@ def triggers_pause(task: str, source: str):
|
||||
@click.option("--source", "-s", help="Specific trigger")
|
||||
def triggers_resume(task: str, source: str):
|
||||
"""Resume triggers."""
|
||||
path = f"/v1/tasks/{task}/triggers/{source}/resume" if source else f"/v1/tasks/{task}/triggers/resume"
|
||||
path = (
|
||||
f"/v1/tasks/{task}/triggers/{source}/resume"
|
||||
if source
|
||||
else f"/v1/tasks/{task}/triggers/resume"
|
||||
)
|
||||
resp = _request("post", path)
|
||||
check_response(resp)
|
||||
console.print(f"[green]✓[/green] Resumed trigger(s) for '{task}'")
|
||||
|
||||
@@ -30,7 +30,9 @@ async def list_tools(ctx, category: str, installed: bool):
|
||||
|
||||
with console.status("Loading tools..."):
|
||||
try:
|
||||
tools = await registry.list_tools(category=category, installed_only=installed)
|
||||
tools = await registry.list_tools(
|
||||
category=category, installed_only=installed
|
||||
)
|
||||
except Exception as e:
|
||||
console.print(f"[red]Failed to load tools: {e}[/red]")
|
||||
return
|
||||
@@ -86,7 +88,9 @@ async def install(ctx, tool_name: str, version: str):
|
||||
try:
|
||||
result = await registry.install_tool(name=tool_name, version=version)
|
||||
|
||||
console.print(f"[green]✓[/green] Installed {tool_name} v{result['version']}")
|
||||
console.print(
|
||||
f"[green]✓[/green] Installed {tool_name} v{result['version']}"
|
||||
)
|
||||
|
||||
if deps := result.get("dependencies_installed"):
|
||||
console.print(f" Dependencies: {', '.join(deps)}")
|
||||
|
||||
@@ -57,8 +57,18 @@ def vector_group():
|
||||
@vector_group.command(name="create")
|
||||
@click.argument("name")
|
||||
@click.option("--region", "-r", help="Region")
|
||||
@click.option("--tier", "-t", type=click.Choice(["free", "standard", "dedicated"]), default="standard")
|
||||
@click.option("--metric", "-m", type=click.Choice(["cosine", "euclidean", "dotproduct"]), default="cosine")
|
||||
@click.option(
|
||||
"--tier",
|
||||
"-t",
|
||||
type=click.Choice(["free", "standard", "dedicated"]),
|
||||
default="standard",
|
||||
)
|
||||
@click.option(
|
||||
"--metric",
|
||||
"-m",
|
||||
type=click.Choice(["cosine", "euclidean", "dotproduct"]),
|
||||
default="cosine",
|
||||
)
|
||||
def vector_create(name: str, region: str, tier: str, metric: str):
|
||||
"""Create a vector database."""
|
||||
payload = {"name": name, "tier": tier, "default_metric": metric}
|
||||
@@ -103,7 +113,9 @@ def vector_list():
|
||||
|
||||
console.print(table)
|
||||
if not dbs:
|
||||
console.print("[dim]No vector databases found. Create one with 'hanzo vector create'[/dim]")
|
||||
console.print(
|
||||
"[dim]No vector databases found. Create one with 'hanzo vector create'[/dim]"
|
||||
)
|
||||
|
||||
|
||||
@vector_group.command(name="describe")
|
||||
@@ -163,9 +175,15 @@ def collections():
|
||||
@click.argument("name")
|
||||
@click.option("--db", "-d", default="default", help="Database name")
|
||||
@click.option("--dimension", "-dim", type=int, required=True, help="Vector dimension")
|
||||
@click.option("--metric", "-m", type=click.Choice(["cosine", "euclidean", "dotproduct"]))
|
||||
@click.option("--index-type", "-i", type=click.Choice(["hnsw", "ivf", "flat"]), default="hnsw")
|
||||
def collections_create(name: str, db: str, dimension: int, metric: str, index_type: str):
|
||||
@click.option(
|
||||
"--metric", "-m", type=click.Choice(["cosine", "euclidean", "dotproduct"])
|
||||
)
|
||||
@click.option(
|
||||
"--index-type", "-i", type=click.Choice(["hnsw", "ivf", "flat"]), default="hnsw"
|
||||
)
|
||||
def collections_create(
|
||||
name: str, db: str, dimension: int, metric: str, index_type: str
|
||||
):
|
||||
"""Create a vector collection.
|
||||
|
||||
\b
|
||||
@@ -267,9 +285,19 @@ def collections_delete(name: str, db: str, force: bool):
|
||||
@click.option("--from", "source", required=True, help="Source: file, jsonl, parquet")
|
||||
@click.option("--id-field", help="Field to use as ID")
|
||||
@click.option("--vector-field", default="embedding", help="Field containing vectors")
|
||||
@click.option("--embed", help="Embed text using model (e.g., openai:text-embedding-3-small)")
|
||||
@click.option(
|
||||
"--embed", help="Embed text using model (e.g., openai:text-embedding-3-small)"
|
||||
)
|
||||
@click.option("--batch-size", "-b", default=100, help="Batch size")
|
||||
def vector_upsert(collection: str, db: str, source: str, id_field: str, vector_field: str, embed: str, batch_size: int):
|
||||
def vector_upsert(
|
||||
collection: str,
|
||||
db: str,
|
||||
source: str,
|
||||
id_field: str,
|
||||
vector_field: str,
|
||||
embed: str,
|
||||
batch_size: int,
|
||||
):
|
||||
"""Upsert vectors into a collection.
|
||||
|
||||
\b
|
||||
@@ -305,11 +333,15 @@ def vector_upsert(collection: str, db: str, source: str, id_field: str, vector_f
|
||||
if embed:
|
||||
payload["embed_model"] = embed
|
||||
|
||||
resp = _request("post", f"/v1/databases/{db}/collections/{collection}/upsert", json=payload)
|
||||
resp = _request(
|
||||
"post", f"/v1/databases/{db}/collections/{collection}/upsert", json=payload
|
||||
)
|
||||
result = check_response(resp)
|
||||
total += result.get("upserted", len(batch))
|
||||
errors += result.get("errors", 0)
|
||||
console.print(f" Batch {i // batch_size + 1}: {result.get('upserted', len(batch))} upserted")
|
||||
console.print(
|
||||
f" Batch {i // batch_size + 1}: {result.get('upserted', len(batch))} upserted"
|
||||
)
|
||||
|
||||
console.print(f"[green]✓[/green] Upsert complete")
|
||||
console.print(f" Vectors: {total}")
|
||||
@@ -361,7 +393,9 @@ def vector_query(
|
||||
if embed:
|
||||
payload["embed_model"] = embed
|
||||
|
||||
resp = _request("post", f"/v1/databases/{db}/collections/{collection}/query", json=payload)
|
||||
resp = _request(
|
||||
"post", f"/v1/databases/{db}/collections/{collection}/query", json=payload
|
||||
)
|
||||
data = check_response(resp)
|
||||
matches = data.get("matches", data.get("results", []))
|
||||
|
||||
@@ -395,7 +429,9 @@ def vector_fetch(ids: tuple, collection: str, db: str, include_vectors: bool):
|
||||
"""Fetch vectors by ID."""
|
||||
payload = {"ids": list(ids), "include_vectors": include_vectors}
|
||||
|
||||
resp = _request("post", f"/v1/databases/{db}/collections/{collection}/fetch", json=payload)
|
||||
resp = _request(
|
||||
"post", f"/v1/databases/{db}/collections/{collection}/fetch", json=payload
|
||||
)
|
||||
data = check_response(resp)
|
||||
vectors = data.get("vectors", [])
|
||||
|
||||
@@ -409,7 +445,9 @@ def vector_fetch(ids: tuple, collection: str, db: str, include_vectors: bool):
|
||||
)
|
||||
if include_vectors and v.get("values"):
|
||||
dims = v["values"][:5]
|
||||
console.print(f" Vector: [{', '.join(f'{d:.4f}' for d in dims)}, ...] ({len(v['values'])} dims)")
|
||||
console.print(
|
||||
f" Vector: [{', '.join(f'{d:.4f}' for d in dims)}, ...] ({len(v['values'])} dims)"
|
||||
)
|
||||
|
||||
if not vectors:
|
||||
console.print("[dim]No vectors found[/dim]")
|
||||
@@ -422,7 +460,9 @@ def vector_fetch(ids: tuple, collection: str, db: str, include_vectors: bool):
|
||||
@click.option("--filter", "-f", help="Delete by filter")
|
||||
@click.option("--all", "delete_all", is_flag=True, help="Delete all vectors")
|
||||
@click.option("--force", is_flag=True)
|
||||
def vector_delete_vectors(collection: str, db: str, ids: str, filter: str, delete_all: bool, force: bool):
|
||||
def vector_delete_vectors(
|
||||
collection: str, db: str, ids: str, filter: str, delete_all: bool, force: bool
|
||||
):
|
||||
"""Delete vectors from a collection."""
|
||||
if delete_all and not force:
|
||||
from rich.prompt import Confirm
|
||||
@@ -438,9 +478,13 @@ def vector_delete_vectors(collection: str, db: str, ids: str, filter: str, delet
|
||||
if delete_all:
|
||||
payload["delete_all"] = True
|
||||
|
||||
resp = _request("post", f"/v1/databases/{db}/collections/{collection}/delete", json=payload)
|
||||
resp = _request(
|
||||
"post", f"/v1/databases/{db}/collections/{collection}/delete", json=payload
|
||||
)
|
||||
data = check_response(resp)
|
||||
console.print(f"[green]✓[/green] Deleted {data.get('deleted', 0)} vector(s) from '{collection}'")
|
||||
console.print(
|
||||
f"[green]✓[/green] Deleted {data.get('deleted', 0)} vector(s) from '{collection}'"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -488,13 +532,24 @@ def indexes_list(collection: str, db: str):
|
||||
@click.argument("collection")
|
||||
@click.option("--db", "-d", default="default")
|
||||
@click.option("--name", "-n", help="Index name")
|
||||
@click.option("--type", "idx_type", type=click.Choice(["hnsw", "ivf", "flat"]), default="hnsw")
|
||||
@click.option("--metric", "-m", type=click.Choice(["cosine", "euclidean", "dotproduct"]))
|
||||
@click.option(
|
||||
"--type", "idx_type", type=click.Choice(["hnsw", "ivf", "flat"]), default="hnsw"
|
||||
)
|
||||
@click.option(
|
||||
"--metric", "-m", type=click.Choice(["cosine", "euclidean", "dotproduct"])
|
||||
)
|
||||
@click.option("--hnsw-m", type=int, default=16, help="HNSW M parameter")
|
||||
@click.option("--hnsw-ef", type=int, default=200, help="HNSW efConstruction")
|
||||
@click.option("--ivf-nlist", type=int, default=100, help="IVF number of lists")
|
||||
def indexes_create(
|
||||
collection: str, db: str, name: str, idx_type: str, metric: str, hnsw_m: int, hnsw_ef: int, ivf_nlist: int
|
||||
collection: str,
|
||||
db: str,
|
||||
name: str,
|
||||
idx_type: str,
|
||||
metric: str,
|
||||
hnsw_m: int,
|
||||
hnsw_ef: int,
|
||||
ivf_nlist: int,
|
||||
):
|
||||
"""Create a vector index.
|
||||
|
||||
@@ -514,7 +569,9 @@ def indexes_create(
|
||||
elif idx_type == "ivf":
|
||||
payload["parameters"] = {"nlist": ivf_nlist}
|
||||
|
||||
resp = _request("post", f"/v1/databases/{db}/collections/{collection}/indexes", json=payload)
|
||||
resp = _request(
|
||||
"post", f"/v1/databases/{db}/collections/{collection}/indexes", json=payload
|
||||
)
|
||||
data = check_response(resp)
|
||||
|
||||
console.print(f"[green]✓[/green] Index created on '{collection}'")
|
||||
@@ -537,7 +594,11 @@ def indexes_rebuild(collection: str, db: str, name: str):
|
||||
payload["index_name"] = name
|
||||
|
||||
console.print(f"[cyan]Rebuilding index for '{collection}'...[/cyan]")
|
||||
resp = _request("post", f"/v1/databases/{db}/collections/{collection}/indexes/rebuild", json=payload)
|
||||
resp = _request(
|
||||
"post",
|
||||
f"/v1/databases/{db}/collections/{collection}/indexes/rebuild",
|
||||
json=payload,
|
||||
)
|
||||
data = check_response(resp)
|
||||
console.print(f"[green]✓[/green] Index rebuilt ({data.get('duration', '-')})")
|
||||
|
||||
|
||||
+215
-69
@@ -221,7 +221,9 @@ class HanzoDevOrchestrator:
|
||||
grpo_dir = self.workspace_dir / "grpo"
|
||||
grpo_dir.mkdir(exist_ok=True)
|
||||
|
||||
self.experience_manager = ExperienceManager(str(grpo_dir / "experience_library.json"))
|
||||
self.experience_manager = ExperienceManager(
|
||||
str(grpo_dir / "experience_library.json")
|
||||
)
|
||||
|
||||
# Initialize with DeepSeek if API key available
|
||||
deepseek_key = os.getenv("DEEPSEEK_API_KEY")
|
||||
@@ -252,7 +254,9 @@ class HanzoDevOrchestrator:
|
||||
|
||||
for path in possible_paths:
|
||||
expanded = Path(path).expanduser()
|
||||
if expanded.exists() or (path == "claude" and os.system(f"which {path} >/dev/null 2>&1") == 0):
|
||||
if expanded.exists() or (
|
||||
path == "claude" and os.system(f"which {path} >/dev/null 2>&1") == 0
|
||||
):
|
||||
return str(expanded) if expanded.exists() else path
|
||||
|
||||
raise RuntimeError("Claude Code not found. Please specify path.")
|
||||
@@ -339,7 +343,9 @@ class HanzoDevOrchestrator:
|
||||
else:
|
||||
return "Execute standard workflow with monitoring"
|
||||
|
||||
def _assess_risks(self, problem: str, alternatives: List[str], context: Dict[str, Any]) -> List[str]:
|
||||
def _assess_risks(
|
||||
self, problem: str, alternatives: List[str], context: Dict[str, Any]
|
||||
) -> List[str]:
|
||||
"""Assess risks of different approaches."""
|
||||
risks = []
|
||||
|
||||
@@ -363,7 +369,9 @@ class HanzoDevOrchestrator:
|
||||
) -> str:
|
||||
"""Synthesize a decision from analysis."""
|
||||
if len(risks) > 2:
|
||||
return "Proceed cautiously with incremental approach and rollback capability"
|
||||
return (
|
||||
"Proceed cautiously with incremental approach and rollback capability"
|
||||
)
|
||||
elif alternatives:
|
||||
return f"Execute primary approach: {alternatives[0]}"
|
||||
else:
|
||||
@@ -408,7 +416,9 @@ class HanzoDevOrchestrator:
|
||||
if resume:
|
||||
checkpoint_file = self._get_latest_checkpoint()
|
||||
if checkpoint_file:
|
||||
console.print(f"[green]Resuming from checkpoint: {checkpoint_file.name}[/green]")
|
||||
console.print(
|
||||
f"[green]Resuming from checkpoint: {checkpoint_file.name}[/green]"
|
||||
)
|
||||
|
||||
# Prepare command
|
||||
cmd = [self.claude_code_path]
|
||||
@@ -526,8 +536,12 @@ class HanzoDevOrchestrator:
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"agent_state": self.agent_state.value,
|
||||
"runtime_health": asdict(self.runtime_health),
|
||||
"current_context": (asdict(self.current_context) if self.current_context else None),
|
||||
"thinking_history": [asdict(t) for t in self.thinking_history[-10:]], # Last 10
|
||||
"current_context": (
|
||||
asdict(self.current_context) if self.current_context else None
|
||||
),
|
||||
"thinking_history": [
|
||||
asdict(t) for t in self.thinking_history[-10:]
|
||||
], # Last 10
|
||||
}
|
||||
|
||||
with open(checkpoint_file, "w") as f:
|
||||
@@ -545,7 +559,9 @@ class HanzoDevOrchestrator:
|
||||
self.agent_state = AgentState(data["agent_state"])
|
||||
# Restore other state as needed
|
||||
|
||||
console.print(f"[green]✓ Restored from checkpoint: {checkpoint_file.name}[/green]")
|
||||
console.print(
|
||||
f"[green]✓ Restored from checkpoint: {checkpoint_file.name}[/green]"
|
||||
)
|
||||
return True
|
||||
except Exception as e:
|
||||
console.print(f"[red]Failed to restore checkpoint: {e}[/red]")
|
||||
@@ -561,7 +577,9 @@ class HanzoDevOrchestrator:
|
||||
healthy = await self.health_check()
|
||||
|
||||
if not healthy:
|
||||
console.print(f"[yellow]Health check failed. State: {self.runtime_health.state.value}[/yellow]")
|
||||
console.print(
|
||||
f"[yellow]Health check failed. State: {self.runtime_health.state.value}[/yellow]"
|
||||
)
|
||||
|
||||
# Use System 2 thinking to decide what to do
|
||||
thinking_result = await self.think(
|
||||
@@ -570,7 +588,9 @@ class HanzoDevOrchestrator:
|
||||
)
|
||||
|
||||
console.print(f"[cyan]Decision: {thinking_result.decision}[/cyan]")
|
||||
console.print(f"[cyan]Confidence: {thinking_result.confidence:.2f}[/cyan]")
|
||||
console.print(
|
||||
f"[cyan]Confidence: {thinking_result.confidence:.2f}[/cyan]"
|
||||
)
|
||||
|
||||
# Execute decision
|
||||
if thinking_result.confidence > 0.6:
|
||||
@@ -725,10 +745,14 @@ class HanzoDevOrchestrator:
|
||||
experiences_after = len(self.experience_manager.experiences)
|
||||
|
||||
# Save updated library
|
||||
self.experience_manager.save(str(self.workspace_dir / "grpo" / "experience_library.json"))
|
||||
self.experience_manager.save(
|
||||
str(self.workspace_dir / "grpo" / "experience_library.json")
|
||||
)
|
||||
|
||||
learned_count = experiences_after - experiences_before
|
||||
console.print(f"[green]✓ Learned {learned_count} new experiences (total: {experiences_after})[/green]")
|
||||
console.print(
|
||||
f"[green]✓ Learned {learned_count} new experiences (total: {experiences_after})[/green]"
|
||||
)
|
||||
|
||||
return learned_count
|
||||
|
||||
@@ -831,7 +855,9 @@ class HanzoDevREPL:
|
||||
handler = FallbackHandler()
|
||||
if not handler.fallback_order:
|
||||
console.print("[yellow]⚠️ No API keys detected[/yellow]")
|
||||
console.print("[dim]Set OPENAI_API_KEY or ANTHROPIC_API_KEY to enable AI[/dim]")
|
||||
console.print(
|
||||
"[dim]Set OPENAI_API_KEY or ANTHROPIC_API_KEY to enable AI[/dim]"
|
||||
)
|
||||
console.print()
|
||||
else:
|
||||
primary = handler.fallback_order[0][1]
|
||||
@@ -877,9 +903,13 @@ class HanzoDevREPL:
|
||||
# Handle memory/context commands
|
||||
from .memory_manager import handle_memory_command
|
||||
|
||||
handled = handle_memory_command(user_input, self.memory_manager, console)
|
||||
handled = handle_memory_command(
|
||||
user_input, self.memory_manager, console
|
||||
)
|
||||
if not handled:
|
||||
console.print("[yellow]Unknown memory command. Use #memory help[/yellow]")
|
||||
console.print(
|
||||
"[yellow]Unknown memory command. Use #memory help[/yellow]"
|
||||
)
|
||||
|
||||
else:
|
||||
# Natural chat - send directly to AI agents
|
||||
@@ -922,13 +952,17 @@ class HanzoDevREPL:
|
||||
|
||||
table.add_row("Agent State", self.orchestrator.agent_state.value)
|
||||
table.add_row("Runtime State", self.orchestrator.runtime_health.state.value)
|
||||
table.add_row("Last Response", str(self.orchestrator.runtime_health.last_response))
|
||||
table.add_row(
|
||||
"Last Response", str(self.orchestrator.runtime_health.last_response)
|
||||
)
|
||||
table.add_row(
|
||||
"Response Time",
|
||||
f"{self.orchestrator.runtime_health.response_time_ms:.2f}ms",
|
||||
)
|
||||
table.add_row("Error Count", str(self.orchestrator.runtime_health.error_count))
|
||||
table.add_row("Restart Count", str(self.orchestrator.runtime_health.restart_count))
|
||||
table.add_row(
|
||||
"Restart Count", str(self.orchestrator.runtime_health.restart_count)
|
||||
)
|
||||
|
||||
console.print(table)
|
||||
|
||||
@@ -975,7 +1009,9 @@ class HanzoDevREPL:
|
||||
"""Restore from checkpoint."""
|
||||
if not args:
|
||||
# Show available checkpoints
|
||||
checkpoints = list(self.orchestrator.checkpoint_dir.glob("checkpoint_*.json"))
|
||||
checkpoints = list(
|
||||
self.orchestrator.checkpoint_dir.glob("checkpoint_*.json")
|
||||
)
|
||||
if checkpoints:
|
||||
console.print("[cyan]Available checkpoints:[/cyan]")
|
||||
for cp in checkpoints:
|
||||
@@ -1052,7 +1088,10 @@ Examples:
|
||||
enhanced_message = message
|
||||
|
||||
# Try smart fallback if no specific model configured
|
||||
if not hasattr(self.orchestrator, "orchestrator_model") or self.orchestrator.orchestrator_model == "auto":
|
||||
if (
|
||||
not hasattr(self.orchestrator, "orchestrator_model")
|
||||
or self.orchestrator.orchestrator_model == "auto"
|
||||
):
|
||||
# Use streaming if available
|
||||
from .streaming import stream_with_fallback
|
||||
|
||||
@@ -1064,7 +1103,9 @@ Examples:
|
||||
# Response already displayed by streaming handler
|
||||
return
|
||||
else:
|
||||
console.print("[red]No AI options available. Please configure API keys or install tools.[/red]")
|
||||
console.print(
|
||||
"[red]No AI options available. Please configure API keys or install tools.[/red]"
|
||||
)
|
||||
return
|
||||
|
||||
# For codex and other CLI tools, go straight to direct API chat
|
||||
@@ -1305,14 +1346,26 @@ Examples:
|
||||
|
||||
# No API keys available
|
||||
console.print("[red]No AI API keys configured![/red]")
|
||||
console.print("[yellow]Try these options that don't need your API key:[/yellow]")
|
||||
console.print(
|
||||
"[yellow]Try these options that don't need your API key:[/yellow]"
|
||||
)
|
||||
console.print("\n[bold]CLI Tools (use existing tools):[/bold]")
|
||||
console.print(" • hanzo dev --orchestrator codex # OpenAI CLI (if installed)")
|
||||
console.print(" • hanzo dev --orchestrator claude # Claude Desktop (if installed)")
|
||||
console.print(" • hanzo dev --orchestrator gemini # Gemini CLI (if installed)")
|
||||
console.print(" • hanzo dev --orchestrator hanzo-ide # Hanzo IDE from ~/work/hanzo/ide")
|
||||
console.print(
|
||||
" • hanzo dev --orchestrator codex # OpenAI CLI (if installed)"
|
||||
)
|
||||
console.print(
|
||||
" • hanzo dev --orchestrator claude # Claude Desktop (if installed)"
|
||||
)
|
||||
console.print(
|
||||
" • hanzo dev --orchestrator gemini # Gemini CLI (if installed)"
|
||||
)
|
||||
console.print(
|
||||
" • hanzo dev --orchestrator hanzo-ide # Hanzo IDE from ~/work/hanzo/ide"
|
||||
)
|
||||
console.print("\n[bold]Free APIs (rate limited):[/bold]")
|
||||
console.print(" • hanzo dev --orchestrator codestral # Free Mistral Codestral")
|
||||
console.print(
|
||||
" • hanzo dev --orchestrator codestral # Free Mistral Codestral"
|
||||
)
|
||||
console.print(" • hanzo dev --orchestrator starcoder # Free StarCoder")
|
||||
console.print("\n[bold]Local Models (unlimited):[/bold]")
|
||||
console.print(" • hanzo dev --orchestrator local:llama3.2 # Via Ollama")
|
||||
@@ -1355,10 +1408,16 @@ Examples:
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
if data.get("choices"):
|
||||
console.print(f"[cyan]Codestral:[/cyan] {data['choices'][0]['message']['content']}")
|
||||
console.print(
|
||||
f"[cyan]Codestral:[/cyan] {data['choices'][0]['message']['content']}"
|
||||
)
|
||||
else:
|
||||
console.print("[yellow]Free tier limit reached. Try local models instead:[/yellow]")
|
||||
console.print(" • Install Ollama: curl -fsSL https://ollama.com/install.sh | sh")
|
||||
console.print(
|
||||
"[yellow]Free tier limit reached. Try local models instead:[/yellow]"
|
||||
)
|
||||
console.print(
|
||||
" • Install Ollama: curl -fsSL https://ollama.com/install.sh | sh"
|
||||
)
|
||||
console.print(" • Run: ollama pull codellama")
|
||||
console.print(" • Use: hanzo dev --orchestrator local:codellama")
|
||||
|
||||
@@ -1395,9 +1454,13 @@ Examples:
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
if isinstance(data, list) and data:
|
||||
console.print(f"[cyan]StarCoder:[/cyan] {data[0].get('generated_text', '')}")
|
||||
console.print(
|
||||
f"[cyan]StarCoder:[/cyan] {data[0].get('generated_text', '')}"
|
||||
)
|
||||
else:
|
||||
console.print("[yellow]API limit reached. Install local models:[/yellow]")
|
||||
console.print(
|
||||
"[yellow]API limit reached. Install local models:[/yellow]"
|
||||
)
|
||||
console.print(" • brew install ollama")
|
||||
console.print(" • ollama pull starcoder2")
|
||||
console.print(" • hanzo dev --orchestrator local:starcoder2")
|
||||
@@ -1434,7 +1497,9 @@ Examples:
|
||||
message,
|
||||
]
|
||||
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
process = subprocess.Popen(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
||||
)
|
||||
|
||||
stdout, stderr = process.communicate(timeout=30)
|
||||
|
||||
@@ -1466,7 +1531,11 @@ Examples:
|
||||
|
||||
claude_path = None
|
||||
for path in claude_paths:
|
||||
if os.path.exists(path) or subprocess.run(["which", path], capture_output=True).returncode == 0:
|
||||
if (
|
||||
os.path.exists(path)
|
||||
or subprocess.run(["which", path], capture_output=True).returncode
|
||||
== 0
|
||||
):
|
||||
claude_path = path
|
||||
break
|
||||
|
||||
@@ -1495,7 +1564,9 @@ Examples:
|
||||
"""
|
||||
|
||||
subprocess.run(["osascript", "-e", script])
|
||||
console.print("[cyan]Sent to Claude Desktop. Check the app for response.[/cyan]")
|
||||
console.print(
|
||||
"[cyan]Sent to Claude Desktop. Check the app for response.[/cyan]"
|
||||
)
|
||||
else:
|
||||
# Try direct CLI invocation
|
||||
process = subprocess.Popen(
|
||||
@@ -1534,7 +1605,9 @@ Examples:
|
||||
# Use gemini CLI
|
||||
cmd = ["gemini", "chat", message]
|
||||
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
||||
process = subprocess.Popen(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
|
||||
)
|
||||
|
||||
stdout, stderr = process.communicate(timeout=30)
|
||||
|
||||
@@ -1562,7 +1635,9 @@ Examples:
|
||||
console.print("[red]Hanzo Dev IDE not found![/red]")
|
||||
console.print("[yellow]Expected location: ~/work/hanzo/ide[/yellow]")
|
||||
console.print("To set up:")
|
||||
console.print(" • git clone https://github.com/hanzoai/ide ~/work/hanzo/ide")
|
||||
console.print(
|
||||
" • git clone https://github.com/hanzoai/ide ~/work/hanzo/ide"
|
||||
)
|
||||
console.print(" • cd ~/work/hanzo/ide && npm install")
|
||||
return
|
||||
|
||||
@@ -1598,7 +1673,9 @@ Examples:
|
||||
cmd = [cli_path, "chat", message]
|
||||
cwd = None
|
||||
|
||||
process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=cwd)
|
||||
process = subprocess.Popen(
|
||||
cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, cwd=cwd
|
||||
)
|
||||
|
||||
stdout, stderr = process.communicate(timeout=30)
|
||||
|
||||
@@ -1645,7 +1722,9 @@ Examples:
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
if data.get("message"):
|
||||
console.print(f"[cyan]{model_name}:[/cyan] {data['message']['content']}")
|
||||
console.print(
|
||||
f"[cyan]{model_name}:[/cyan] {data['message']['content']}"
|
||||
)
|
||||
return
|
||||
|
||||
except Exception:
|
||||
@@ -1676,7 +1755,9 @@ Examples:
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
if data.get("choices"):
|
||||
console.print(f"[cyan]{model_name}:[/cyan] {data['choices'][0]['message']['content']}")
|
||||
console.print(
|
||||
f"[cyan]{model_name}:[/cyan] {data['choices'][0]['message']['content']}"
|
||||
)
|
||||
return
|
||||
|
||||
except Exception:
|
||||
@@ -1754,7 +1835,9 @@ async def run_dev_orchestrator(**kwargs):
|
||||
# Check if we should use network mode
|
||||
# For now, disable network mode since hanzo-network isn't available
|
||||
if False and use_network and NETWORK_AVAILABLE:
|
||||
console_obj.print(f"[cyan]Mode: Network Orchestration with hanzo-network[/cyan]")
|
||||
console_obj.print(
|
||||
f"[cyan]Mode: Network Orchestration with hanzo-network[/cyan]"
|
||||
)
|
||||
console_obj.print(f"Orchestrator: {orchestrator_model}")
|
||||
console_obj.print(f"Workers: {instances} agents")
|
||||
console_obj.print(f"Critics: {max(1, instances // 2)} agents")
|
||||
@@ -1860,19 +1943,25 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
||||
|
||||
# Check if we can use hanzo-network
|
||||
if not NETWORK_AVAILABLE:
|
||||
self.console.print("[yellow]Warning: hanzo-network not available, falling back to basic mode[/yellow]")
|
||||
self.console.print(
|
||||
"[yellow]Warning: hanzo-network not available, falling back to basic mode[/yellow]"
|
||||
)
|
||||
|
||||
async def initialize(self):
|
||||
"""Initialize the agent network with orchestrator and workers."""
|
||||
if not NETWORK_AVAILABLE:
|
||||
self.console.print("[red]Cannot initialize network mode without hanzo-network[/red]")
|
||||
self.console.print(
|
||||
"[red]Cannot initialize network mode without hanzo-network[/red]"
|
||||
)
|
||||
return False
|
||||
|
||||
# Start hanzo net if requested for local orchestration
|
||||
if self.use_hanzo_net or self.orchestrator_model.startswith("local:"):
|
||||
await self._start_hanzo_net()
|
||||
|
||||
self.console.print(f"[cyan]Initializing agent network with {self.orchestrator_model} orchestrator...[/cyan]")
|
||||
self.console.print(
|
||||
f"[cyan]Initializing agent network with {self.orchestrator_model} orchestrator...[/cyan]"
|
||||
)
|
||||
|
||||
# Create orchestrator agent (GPT-5, local, or other model)
|
||||
self.orchestrator_agent = await self._create_orchestrator_agent()
|
||||
@@ -1889,7 +1978,9 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
||||
for i in range(num_local_workers):
|
||||
local_worker = await self._create_local_worker_agent(i)
|
||||
self.worker_agents.append(local_worker)
|
||||
self.console.print(f"[green]Added {num_local_workers} local workers for cost optimization[/green]")
|
||||
self.console.print(
|
||||
f"[green]Added {num_local_workers} local workers for cost optimization[/green]"
|
||||
)
|
||||
|
||||
# Create critic agents for System 2 thinking
|
||||
if self.enable_guardrails:
|
||||
@@ -1912,15 +2003,21 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
||||
self.agent_network = create_network(
|
||||
agents=all_agents,
|
||||
router=router,
|
||||
default_agent=(self.orchestrator_agent.name if self.orchestrator_agent else None),
|
||||
default_agent=(
|
||||
self.orchestrator_agent.name if self.orchestrator_agent else None
|
||||
),
|
||||
)
|
||||
|
||||
self.console.print(f"[green]✓ Agent network initialized with {len(all_agents)} agents[/green]")
|
||||
self.console.print(
|
||||
f"[green]✓ Agent network initialized with {len(all_agents)} agents[/green]"
|
||||
)
|
||||
return True
|
||||
|
||||
async def _start_hanzo_net(self):
|
||||
"""Start hanzo net for local AI orchestration."""
|
||||
self.console.print("[cyan]Starting hanzo/net for local AI orchestration...[/cyan]")
|
||||
self.console.print(
|
||||
"[cyan]Starting hanzo/net for local AI orchestration...[/cyan]"
|
||||
)
|
||||
|
||||
# Check if hanzo net is already running
|
||||
import socket
|
||||
@@ -1930,7 +2027,9 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
||||
sock.close()
|
||||
|
||||
if result == 0:
|
||||
self.console.print(f"[yellow]hanzo/net already running on port {self.hanzo_net_port}[/yellow]")
|
||||
self.console.print(
|
||||
f"[yellow]hanzo/net already running on port {self.hanzo_net_port}[/yellow]"
|
||||
)
|
||||
return
|
||||
|
||||
# Start hanzo net
|
||||
@@ -1991,7 +2090,9 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
||||
tools=[],
|
||||
)
|
||||
|
||||
self.console.print(f"[green]✓ Created local {model_name} orchestrator via hanzo/net[/green]")
|
||||
self.console.print(
|
||||
f"[green]✓ Created local {model_name} orchestrator via hanzo/net[/green]"
|
||||
)
|
||||
return orchestrator
|
||||
|
||||
# Parse model string to get provider and model
|
||||
@@ -2047,7 +2148,9 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
||||
tools=[], # Orchestrator tools will be added
|
||||
)
|
||||
|
||||
self.console.print(f"[green]✓ Created {self.orchestrator_model} orchestrator[/green]")
|
||||
self.console.print(
|
||||
f"[green]✓ Created {self.orchestrator_model} orchestrator[/green]"
|
||||
)
|
||||
return orchestrator
|
||||
|
||||
def _get_orchestrator_system_prompt(self) -> str:
|
||||
@@ -2180,7 +2283,10 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
||||
"count",
|
||||
"find",
|
||||
]
|
||||
if any(keyword in prompt_lower for keyword in simple_keywords) and self.local_workers:
|
||||
if (
|
||||
any(keyword in prompt_lower for keyword in simple_keywords)
|
||||
and self.local_workers
|
||||
):
|
||||
return self.local_workers[0].name
|
||||
|
||||
# Complex implementation → API workers (Claude)
|
||||
@@ -2192,7 +2298,10 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
||||
"design",
|
||||
"architect",
|
||||
]
|
||||
if any(keyword in prompt_lower for keyword in complex_keywords) and self.api_workers:
|
||||
if (
|
||||
any(keyword in prompt_lower for keyword in complex_keywords)
|
||||
and self.api_workers
|
||||
):
|
||||
return self.api_workers[0].name
|
||||
|
||||
# Review tasks → Critics
|
||||
@@ -2203,7 +2312,10 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
||||
"improve",
|
||||
"validate code",
|
||||
]
|
||||
if any(keyword in prompt_lower for keyword in review_keywords) and self.critics:
|
||||
if (
|
||||
any(keyword in prompt_lower for keyword in review_keywords)
|
||||
and self.critics
|
||||
):
|
||||
return self.critics[0].name
|
||||
|
||||
# Strategic decisions → Orchestrator
|
||||
@@ -2224,12 +2336,20 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
||||
return self.local_workers[0].name
|
||||
|
||||
# Fall back to API workers for complex tasks
|
||||
return self.api_workers[0].name if self.api_workers else self.orchestrator.name
|
||||
return (
|
||||
self.api_workers[0].name
|
||||
if self.api_workers
|
||||
else self.orchestrator.name
|
||||
)
|
||||
|
||||
# Create the cost-optimized router
|
||||
router = CostOptimizedRouter(self.orchestrator_agent, self.worker_agents, self.critic_agents)
|
||||
router = CostOptimizedRouter(
|
||||
self.orchestrator_agent, self.worker_agents, self.critic_agents
|
||||
)
|
||||
|
||||
self.console.print("[green]✓ Created cost-optimized router (local models preferred)[/green]")
|
||||
self.console.print(
|
||||
"[green]✓ Created cost-optimized router (local models preferred)[/green]"
|
||||
)
|
||||
return router
|
||||
|
||||
async def _create_intelligent_router(self) -> Router:
|
||||
@@ -2264,7 +2384,9 @@ class NetworkOrchestrator(HanzoDevOrchestrator):
|
||||
|
||||
return router
|
||||
|
||||
async def execute_with_network(self, task: str, context: Optional[Dict] = None) -> Dict:
|
||||
async def execute_with_network(
|
||||
self, task: str, context: Optional[Dict] = None
|
||||
) -> Dict:
|
||||
"""Execute a task using the agent network.
|
||||
|
||||
Args:
|
||||
@@ -2402,7 +2524,9 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
|
||||
config = await self._create_instance_config(i, role)
|
||||
self.instance_configs.append(config)
|
||||
|
||||
self.console.print(f" [{i + 1}/{self.num_instances}] {role} instance configured")
|
||||
self.console.print(
|
||||
f" [{i + 1}/{self.num_instances}] {role} instance configured"
|
||||
)
|
||||
|
||||
# If networking enabled, configure MCP connections between instances
|
||||
if self.enable_networking:
|
||||
@@ -2464,7 +2588,9 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
|
||||
|
||||
async def _setup_mcp_networking(self):
|
||||
"""Set up MCP networking between Claude instances."""
|
||||
self.console.print("[cyan]Setting up MCP networking between instances...[/cyan]")
|
||||
self.console.print(
|
||||
"[cyan]Setting up MCP networking between instances...[/cyan]"
|
||||
)
|
||||
|
||||
# Each instance gets MCP servers for all other instances
|
||||
for i, config in enumerate(self.instance_configs):
|
||||
@@ -2607,10 +2733,14 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
|
||||
|
||||
# Validate improvement didn't degrade quality
|
||||
if await self._validate_improvement(result, improved):
|
||||
self.console.print("[green]✓ Solution improved with System 2 feedback[/green]")
|
||||
self.console.print(
|
||||
"[green]✓ Solution improved with System 2 feedback[/green]"
|
||||
)
|
||||
return improved
|
||||
else:
|
||||
self.console.print("[yellow]⚠ Keeping original solution (improvement validation failed)[/yellow]")
|
||||
self.console.print(
|
||||
"[yellow]⚠ Keeping original solution (improvement validation failed)[/yellow]"
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
@@ -2687,7 +2817,9 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
result = subprocess.run(["gemini", "chat", prompt], capture_output=True, text=True, timeout=30)
|
||||
result = subprocess.run(
|
||||
["gemini", "chat", prompt], capture_output=True, text=True, timeout=30
|
||||
)
|
||||
if result.returncode == 0 and result.stdout:
|
||||
return {"output": result.stdout.strip(), "success": True}
|
||||
except Exception as e:
|
||||
@@ -2751,7 +2883,9 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
|
||||
logger.error(f"OpenAI API error: {e}")
|
||||
|
||||
# Try Anthropic
|
||||
anthropic_key = os.environ.get("ANTHROPIC_API_KEY") or os.getenv("ANTHROPIC_API_KEY")
|
||||
anthropic_key = os.environ.get("ANTHROPIC_API_KEY") or os.getenv(
|
||||
"ANTHROPIC_API_KEY"
|
||||
)
|
||||
if anthropic_key:
|
||||
try:
|
||||
from anthropic import AsyncAnthropic
|
||||
@@ -2770,7 +2904,9 @@ class MultiClaudeOrchestrator(HanzoDevOrchestrator):
|
||||
# Try fallback handler as last resort
|
||||
from .fallback_handler import smart_chat
|
||||
|
||||
response = await smart_chat(prompt, console=None) # No console to avoid duplicate messages
|
||||
response = await smart_chat(
|
||||
prompt, console=None
|
||||
) # No console to avoid duplicate messages
|
||||
if response:
|
||||
return {"output": response, "success": True}
|
||||
|
||||
@@ -2818,15 +2954,25 @@ async def main():
|
||||
"""Main entry point for hanzo-dev."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Hanzo Dev - System 2 Meta-AI Orchestrator")
|
||||
parser.add_argument("--workspace", default="~/.hanzo/dev", help="Workspace directory")
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Hanzo Dev - System 2 Meta-AI Orchestrator"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--workspace", default="~/.hanzo/dev", help="Workspace directory"
|
||||
)
|
||||
parser.add_argument("--claude-path", help="Path to Claude Code executable")
|
||||
parser.add_argument("--monitor", action="store_true", help="Start in monitor mode")
|
||||
parser.add_argument("--repl", action="store_true", help="Start REPL interface")
|
||||
parser.add_argument("--instances", type=int, default=2, help="Number of Claude instances")
|
||||
parser.add_argument(
|
||||
"--instances", type=int, default=2, help="Number of Claude instances"
|
||||
)
|
||||
parser.add_argument("--no-mcp", action="store_true", help="Disable MCP tools")
|
||||
parser.add_argument("--no-network", action="store_true", help="Disable instance networking")
|
||||
parser.add_argument("--no-guardrails", action="store_true", help="Disable guardrails")
|
||||
parser.add_argument(
|
||||
"--no-network", action="store_true", help="Disable instance networking"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-guardrails", action="store_true", help="Disable guardrails"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
@@ -23,7 +23,9 @@ class FallbackHandler:
|
||||
"deepseek_api": bool(os.getenv("DEEPSEEK_API_KEY")), # Added DeepSeek
|
||||
"openai_api": bool(os.getenv("OPENAI_API_KEY")),
|
||||
"anthropic_api": bool(os.getenv("ANTHROPIC_API_KEY")),
|
||||
"google_api": bool(os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")),
|
||||
"google_api": bool(
|
||||
os.getenv("GOOGLE_API_KEY") or os.getenv("GEMINI_API_KEY")
|
||||
),
|
||||
"openai_cli": shutil.which("openai") is not None,
|
||||
"claude_cli": shutil.which("claude") is not None,
|
||||
"gemini_cli": shutil.which("gemini") is not None,
|
||||
@@ -101,7 +103,9 @@ class FallbackHandler:
|
||||
suggestions = []
|
||||
|
||||
if not self.available_options["deepseek_api"]:
|
||||
suggestions.append("• Set DEEPSEEK_API_KEY for cost-effective DeepSeek access ($0.14/M tokens)")
|
||||
suggestions.append(
|
||||
"• Set DEEPSEEK_API_KEY for cost-effective DeepSeek access ($0.14/M tokens)"
|
||||
)
|
||||
|
||||
if not self.available_options["openai_api"]:
|
||||
suggestions.append("• Set OPENAI_API_KEY for GPT-4/GPT-5 access")
|
||||
@@ -110,22 +114,30 @@ class FallbackHandler:
|
||||
suggestions.append("• Set ANTHROPIC_API_KEY for Claude access")
|
||||
|
||||
if not self.available_options["ollama"]:
|
||||
suggestions.append("• Install Ollama: curl -fsSL https://ollama.com/install.sh | sh")
|
||||
suggestions.append(
|
||||
"• Install Ollama: curl -fsSL https://ollama.com/install.sh | sh"
|
||||
)
|
||||
suggestions.append(" Then run: ollama pull llama3.2")
|
||||
|
||||
if not self.available_options["openai_cli"]:
|
||||
suggestions.append("• Install OpenAI CLI: pip install openai-cli")
|
||||
|
||||
if not self.available_options["claude_cli"]:
|
||||
suggestions.append("• Install Claude Desktop from https://claude.ai/download")
|
||||
suggestions.append(
|
||||
"• Install Claude Desktop from https://claude.ai/download"
|
||||
)
|
||||
|
||||
return "\n".join(suggestions) if suggestions else "All AI options are available!"
|
||||
return (
|
||||
"\n".join(suggestions) if suggestions else "All AI options are available!"
|
||||
)
|
||||
|
||||
def print_status(self, console):
|
||||
"""Print the current status of available AI options."""
|
||||
from rich.table import Table
|
||||
|
||||
table = Table(title="Available AI Options", show_header=True, header_style="bold magenta")
|
||||
table = Table(
|
||||
title="Available AI Options", show_header=True, header_style="bold magenta"
|
||||
)
|
||||
table.add_column("Option", style="cyan", width=20)
|
||||
table.add_column("Status", width=10)
|
||||
table.add_column("Model", width=20)
|
||||
@@ -152,7 +164,9 @@ class FallbackHandler:
|
||||
console.print(table)
|
||||
|
||||
if self.fallback_order:
|
||||
console.print(f"\n[green]Primary option: {self.fallback_order[0][1]}[/green]")
|
||||
console.print(
|
||||
f"\n[green]Primary option: {self.fallback_order[0][1]}[/green]"
|
||||
)
|
||||
if len(self.fallback_order) > 1:
|
||||
fallbacks = ", ".join([opt[1] for opt in self.fallback_order[1:]])
|
||||
console.print(f"[yellow]Fallback options: {fallbacks}[/yellow]")
|
||||
|
||||
@@ -46,9 +46,27 @@ from .pubsub import Message, PubSubClient, PubSubConfig, Subscription
|
||||
from .queues import Job, JobStatus, QueueStats, QueuesClient, QueuesConfig
|
||||
from .search import SearchHit, SearchClient, SearchConfig, SearchResult
|
||||
from .vector import ScoredPoint, VectorPoint, VectorClient, VectorConfig
|
||||
from .storage import ObjectInfo, PresignedUrl, UploadResult, StorageClient, StorageConfig
|
||||
from .functions import FunctionSpec, InvokeResult, FunctionStatus, FunctionsClient, FunctionsConfig
|
||||
from .documentdb import Document, DeleteResult, UpdateResult, DocumentDBClient, DocumentDBConfig
|
||||
from .storage import (
|
||||
ObjectInfo,
|
||||
PresignedUrl,
|
||||
UploadResult,
|
||||
StorageClient,
|
||||
StorageConfig,
|
||||
)
|
||||
from .functions import (
|
||||
FunctionSpec,
|
||||
InvokeResult,
|
||||
FunctionStatus,
|
||||
FunctionsClient,
|
||||
FunctionsConfig,
|
||||
)
|
||||
from .documentdb import (
|
||||
Document,
|
||||
DeleteResult,
|
||||
UpdateResult,
|
||||
DocumentDBClient,
|
||||
DocumentDBConfig,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Main class
|
||||
@@ -183,70 +201,90 @@ class HanzoInfra:
|
||||
def vector(self) -> VectorClient:
|
||||
"""Get vector client (Qdrant)."""
|
||||
if self._vector is None:
|
||||
raise RuntimeError("Vector client not initialized. Initialize with services=['vector']")
|
||||
raise RuntimeError(
|
||||
"Vector client not initialized. Initialize with services=['vector']"
|
||||
)
|
||||
return self._vector
|
||||
|
||||
@property
|
||||
def kv(self) -> KVClient:
|
||||
"""Get key-value client (Redis/Valkey)."""
|
||||
if self._kv is None:
|
||||
raise RuntimeError("KV client not initialized. Initialize with services=['kv']")
|
||||
raise RuntimeError(
|
||||
"KV client not initialized. Initialize with services=['kv']"
|
||||
)
|
||||
return self._kv
|
||||
|
||||
@property
|
||||
def documentdb(self) -> DocumentDBClient:
|
||||
"""Get document database client (MongoDB)."""
|
||||
if self._documentdb is None:
|
||||
raise RuntimeError("DocumentDB client not initialized. Initialize with services=['documentdb']")
|
||||
raise RuntimeError(
|
||||
"DocumentDB client not initialized. Initialize with services=['documentdb']"
|
||||
)
|
||||
return self._documentdb
|
||||
|
||||
@property
|
||||
def storage(self) -> StorageClient:
|
||||
"""Get object storage client (S3/MinIO)."""
|
||||
if self._storage is None:
|
||||
raise RuntimeError("Storage client not initialized. Initialize with services=['storage']")
|
||||
raise RuntimeError(
|
||||
"Storage client not initialized. Initialize with services=['storage']"
|
||||
)
|
||||
return self._storage
|
||||
|
||||
@property
|
||||
def search(self) -> SearchClient:
|
||||
"""Get search client (Meilisearch)."""
|
||||
if self._search is None:
|
||||
raise RuntimeError("Search client not initialized. Initialize with services=['search']")
|
||||
raise RuntimeError(
|
||||
"Search client not initialized. Initialize with services=['search']"
|
||||
)
|
||||
return self._search
|
||||
|
||||
@property
|
||||
def pubsub(self) -> PubSubClient:
|
||||
"""Get pub/sub client (NATS)."""
|
||||
if self._pubsub is None:
|
||||
raise RuntimeError("PubSub client not initialized. Initialize with services=['pubsub']")
|
||||
raise RuntimeError(
|
||||
"PubSub client not initialized. Initialize with services=['pubsub']"
|
||||
)
|
||||
return self._pubsub
|
||||
|
||||
@property
|
||||
def tasks(self) -> TasksClient:
|
||||
"""Get tasks/workflow client (Temporal)."""
|
||||
if self._tasks is None:
|
||||
raise RuntimeError("Tasks client not initialized. Initialize with services=['tasks']")
|
||||
raise RuntimeError(
|
||||
"Tasks client not initialized. Initialize with services=['tasks']"
|
||||
)
|
||||
return self._tasks
|
||||
|
||||
@property
|
||||
def queues(self) -> QueuesClient:
|
||||
"""Get work queue client."""
|
||||
if self._queues is None:
|
||||
raise RuntimeError("Queues client not initialized. Initialize with services=['queues']")
|
||||
raise RuntimeError(
|
||||
"Queues client not initialized. Initialize with services=['queues']"
|
||||
)
|
||||
return self._queues
|
||||
|
||||
@property
|
||||
def cron(self) -> CronClient:
|
||||
"""Get cron scheduler client."""
|
||||
if self._cron is None:
|
||||
raise RuntimeError("Cron client not initialized. Initialize with services=['cron']")
|
||||
raise RuntimeError(
|
||||
"Cron client not initialized. Initialize with services=['cron']"
|
||||
)
|
||||
return self._cron
|
||||
|
||||
@property
|
||||
def functions(self) -> FunctionsClient:
|
||||
"""Get serverless functions client (Nuclio)."""
|
||||
if self._functions is None:
|
||||
raise RuntimeError("Functions client not initialized. Initialize with services=['functions']")
|
||||
raise RuntimeError(
|
||||
"Functions client not initialized. Initialize with services=['functions']"
|
||||
)
|
||||
return self._functions
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -100,8 +100,16 @@ class CronJob:
|
||||
args=data.get("args", {}),
|
||||
enabled=data.get("enabled", True),
|
||||
timezone=data.get("timezone", "UTC"),
|
||||
last_run=datetime.fromisoformat(data["last_run"]) if data.get("last_run") else None,
|
||||
next_run=datetime.fromisoformat(data["next_run"]) if data.get("next_run") else None,
|
||||
last_run=(
|
||||
datetime.fromisoformat(data["last_run"])
|
||||
if data.get("last_run")
|
||||
else None
|
||||
),
|
||||
next_run=(
|
||||
datetime.fromisoformat(data["next_run"])
|
||||
if data.get("next_run")
|
||||
else None
|
||||
),
|
||||
run_count=data.get("run_count", 0),
|
||||
error_count=data.get("error_count", 0),
|
||||
last_error=data.get("last_error"),
|
||||
@@ -185,7 +193,9 @@ class CronClient:
|
||||
try:
|
||||
import redis.asyncio as redis
|
||||
except ImportError as e:
|
||||
raise ImportError("redis is required for CronClient. Install with: pip install redis") from e
|
||||
raise ImportError(
|
||||
"redis is required for CronClient. Install with: pip install redis"
|
||||
) from e
|
||||
|
||||
if self.config.url:
|
||||
self._redis = redis.from_url(
|
||||
@@ -475,7 +485,9 @@ class CronClient:
|
||||
async def _acquire_lock(self, job_id: str) -> bool:
|
||||
"""Acquire a distributed lock for a job."""
|
||||
lock_key = self._key("lock", job_id)
|
||||
lock_value = hashlib.md5(f"{os.getpid()}-{datetime.utcnow().isoformat()}".encode()).hexdigest()
|
||||
lock_value = hashlib.md5(
|
||||
f"{os.getpid()}-{datetime.utcnow().isoformat()}".encode()
|
||||
).hexdigest()
|
||||
|
||||
acquired = await self._redis.set(
|
||||
lock_key,
|
||||
@@ -515,7 +527,9 @@ class CronClient:
|
||||
execution.error = str(e)
|
||||
|
||||
execution.completed_at = datetime.utcnow()
|
||||
execution.duration_ms = int((execution.completed_at - execution.started_at).total_seconds() * 1000)
|
||||
execution.duration_ms = int(
|
||||
(execution.completed_at - execution.started_at).total_seconds() * 1000
|
||||
)
|
||||
|
||||
# Update job stats
|
||||
job.last_run = execution.started_at
|
||||
@@ -608,7 +622,11 @@ class CronClient:
|
||||
CronExecution(
|
||||
job_id=data["job_id"],
|
||||
started_at=datetime.fromisoformat(data["started_at"]),
|
||||
completed_at=datetime.fromisoformat(data["completed_at"]) if data.get("completed_at") else None,
|
||||
completed_at=(
|
||||
datetime.fromisoformat(data["completed_at"])
|
||||
if data.get("completed_at")
|
||||
else None
|
||||
),
|
||||
success=data.get("success", False),
|
||||
result=data.get("result"),
|
||||
error=data.get("error"),
|
||||
|
||||
@@ -24,11 +24,15 @@ class DocumentDBConfig(BaseModel):
|
||||
username: Optional[str] = Field(default=None, description="Authentication username")
|
||||
password: Optional[str] = Field(default=None, description="Authentication password")
|
||||
database: str = Field(default="hanzo", description="Default database name")
|
||||
uri: Optional[str] = Field(default=None, description="Full connection URI (overrides host/port)")
|
||||
uri: Optional[str] = Field(
|
||||
default=None, description="Full connection URI (overrides host/port)"
|
||||
)
|
||||
auth_source: str = Field(default="admin", description="Authentication database")
|
||||
replica_set: Optional[str] = Field(default=None, description="Replica set name")
|
||||
tls: bool = Field(default=False, description="Use TLS/SSL")
|
||||
server_selection_timeout_ms: int = Field(default=5000, description="Server selection timeout")
|
||||
server_selection_timeout_ms: int = Field(
|
||||
default=5000, description="Server selection timeout"
|
||||
)
|
||||
connect_timeout_ms: int = Field(default=5000, description="Connection timeout")
|
||||
socket_timeout_ms: int = Field(default=30000, description="Socket timeout")
|
||||
|
||||
@@ -132,7 +136,9 @@ class DocumentDBClient:
|
||||
try:
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
except ImportError as e:
|
||||
raise ImportError("motor is required for DocumentDBClient. Install with: pip install motor") from e
|
||||
raise ImportError(
|
||||
"motor is required for DocumentDBClient. Install with: pip install motor"
|
||||
) from e
|
||||
|
||||
if self.config.uri:
|
||||
self._client = AsyncIOMotorClient(
|
||||
@@ -198,7 +204,9 @@ class DocumentDBClient:
|
||||
result = await coll.insert_one(document)
|
||||
return str(result.inserted_id)
|
||||
|
||||
async def insert_many(self, collection: str, documents: Sequence[dict[str, Any]]) -> list[str]:
|
||||
async def insert_many(
|
||||
self, collection: str, documents: Sequence[dict[str, Any]]
|
||||
) -> list[str]:
|
||||
"""Insert multiple documents.
|
||||
|
||||
Args:
|
||||
@@ -383,7 +391,9 @@ class DocumentDBClient:
|
||||
result = await coll.delete_one(filter)
|
||||
return DeleteResult(deleted_count=result.deleted_count)
|
||||
|
||||
async def delete_many(self, collection: str, filter: dict[str, Any]) -> DeleteResult:
|
||||
async def delete_many(
|
||||
self, collection: str, filter: dict[str, Any]
|
||||
) -> DeleteResult:
|
||||
"""Delete multiple documents.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -17,11 +17,19 @@ from pydantic import Field, BaseModel
|
||||
class FunctionsConfig(BaseModel):
|
||||
"""Configuration for Nuclio connection."""
|
||||
|
||||
dashboard_url: str = Field(default="http://localhost:8070", description="Nuclio dashboard URL")
|
||||
api_key: Optional[str] = Field(default=None, description="API key for authentication")
|
||||
dashboard_url: str = Field(
|
||||
default="http://localhost:8070", description="Nuclio dashboard URL"
|
||||
)
|
||||
api_key: Optional[str] = Field(
|
||||
default=None, description="API key for authentication"
|
||||
)
|
||||
namespace: str = Field(default="nuclio", description="Kubernetes namespace")
|
||||
default_runtime: str = Field(default="python:3.11", description="Default function runtime")
|
||||
default_handler: str = Field(default="main:handler", description="Default function handler")
|
||||
default_runtime: str = Field(
|
||||
default="python:3.11", description="Default function runtime"
|
||||
)
|
||||
default_handler: str = Field(
|
||||
default="main:handler", description="Default function handler"
|
||||
)
|
||||
registry: Optional[str] = Field(default=None, description="Container registry URL")
|
||||
timeout: float = Field(default=30.0, description="Request timeout in seconds")
|
||||
|
||||
@@ -188,7 +196,9 @@ class FunctionsClient:
|
||||
try:
|
||||
import httpx
|
||||
except ImportError as e:
|
||||
raise ImportError("httpx is required for FunctionsClient. Install with: pip install httpx") from e
|
||||
raise ImportError(
|
||||
"httpx is required for FunctionsClient. Install with: pip install httpx"
|
||||
) from e
|
||||
|
||||
self._client = httpx.AsyncClient(
|
||||
base_url=self.config.dashboard_url,
|
||||
@@ -314,9 +324,11 @@ class FunctionsClient:
|
||||
replicas=status.get("replicas", 0),
|
||||
version=metadata.get("version", ""),
|
||||
invoke_url=status.get("httpPort"),
|
||||
internal_url=status.get("internalInvocationUrls", [None])[0]
|
||||
if status.get("internalInvocationUrls")
|
||||
else None,
|
||||
internal_url=(
|
||||
status.get("internalInvocationUrls", [None])[0]
|
||||
if status.get("internalInvocationUrls")
|
||||
else None
|
||||
),
|
||||
message=status.get("message"),
|
||||
)
|
||||
|
||||
|
||||
@@ -22,12 +22,16 @@ class KVConfig(BaseModel):
|
||||
port: int = Field(default=6379, description="Redis server port")
|
||||
password: Optional[str] = Field(default=None, description="Redis password")
|
||||
db: int = Field(default=0, description="Redis database number")
|
||||
url: Optional[str] = Field(default=None, description="Full Redis URL (overrides host/port)")
|
||||
url: Optional[str] = Field(
|
||||
default=None, description="Full Redis URL (overrides host/port)"
|
||||
)
|
||||
ssl: bool = Field(default=False, description="Use SSL/TLS")
|
||||
socket_timeout: float = Field(default=5.0, description="Socket timeout in seconds")
|
||||
socket_connect_timeout: float = Field(default=5.0, description="Connection timeout")
|
||||
max_connections: int = Field(default=10, description="Max pool connections")
|
||||
decode_responses: bool = Field(default=True, description="Decode responses as strings")
|
||||
decode_responses: bool = Field(
|
||||
default=True, description="Decode responses as strings"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> KVConfig:
|
||||
@@ -99,7 +103,9 @@ class KVClient:
|
||||
try:
|
||||
import redis.asyncio as redis
|
||||
except ImportError as e:
|
||||
raise ImportError("redis is required for KVClient. Install with: pip install redis") from e
|
||||
raise ImportError(
|
||||
"redis is required for KVClient. Install with: pip install redis"
|
||||
) from e
|
||||
|
||||
if self.config.url:
|
||||
self._client = redis.from_url(
|
||||
@@ -336,7 +342,9 @@ class KVClient:
|
||||
"""
|
||||
return await self._client.rpush(key, *values)
|
||||
|
||||
async def lpop(self, key: str, count: Optional[int] = None) -> Optional[Union[str, list[str]]]:
|
||||
async def lpop(
|
||||
self, key: str, count: Optional[int] = None
|
||||
) -> Optional[Union[str, list[str]]]:
|
||||
"""Pop values from the head of a list.
|
||||
|
||||
Args:
|
||||
@@ -348,7 +356,9 @@ class KVClient:
|
||||
"""
|
||||
return await self._client.lpop(key, count)
|
||||
|
||||
async def rpop(self, key: str, count: Optional[int] = None) -> Optional[Union[str, list[str]]]:
|
||||
async def rpop(
|
||||
self, key: str, count: Optional[int] = None
|
||||
) -> Optional[Union[str, list[str]]]:
|
||||
"""Pop values from the tail of a list.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -17,16 +17,28 @@ from pydantic import Field, BaseModel
|
||||
class PubSubConfig(BaseModel):
|
||||
"""Configuration for NATS connection."""
|
||||
|
||||
servers: list[str] = Field(default=["nats://localhost:4222"], description="NATS server URLs")
|
||||
servers: list[str] = Field(
|
||||
default=["nats://localhost:4222"], description="NATS server URLs"
|
||||
)
|
||||
user: Optional[str] = Field(default=None, description="Username for authentication")
|
||||
password: Optional[str] = Field(default=None, description="Password for authentication")
|
||||
password: Optional[str] = Field(
|
||||
default=None, description="Password for authentication"
|
||||
)
|
||||
token: Optional[str] = Field(default=None, description="Token for authentication")
|
||||
nkey_seed: Optional[str] = Field(default=None, description="NKey seed for authentication")
|
||||
nkey_seed: Optional[str] = Field(
|
||||
default=None, description="NKey seed for authentication"
|
||||
)
|
||||
tls: bool = Field(default=False, description="Use TLS")
|
||||
name: str = Field(default="hanzo-client", description="Client name")
|
||||
connect_timeout: float = Field(default=5.0, description="Connection timeout in seconds")
|
||||
reconnect_time_wait: float = Field(default=2.0, description="Time between reconnect attempts")
|
||||
max_reconnect_attempts: int = Field(default=60, description="Max reconnection attempts")
|
||||
connect_timeout: float = Field(
|
||||
default=5.0, description="Connection timeout in seconds"
|
||||
)
|
||||
reconnect_time_wait: float = Field(
|
||||
default=2.0, description="Time between reconnect attempts"
|
||||
)
|
||||
max_reconnect_attempts: int = Field(
|
||||
default=60, description="Max reconnection attempts"
|
||||
)
|
||||
ping_interval: float = Field(default=120.0, description="Ping interval in seconds")
|
||||
max_outstanding_pings: int = Field(default=2, description="Max outstanding pings")
|
||||
|
||||
@@ -132,7 +144,9 @@ class PubSubClient:
|
||||
try:
|
||||
import nats
|
||||
except ImportError as e:
|
||||
raise ImportError("nats-py is required for PubSubClient. Install with: pip install nats-py") from e
|
||||
raise ImportError(
|
||||
"nats-py is required for PubSubClient. Install with: pip install nats-py"
|
||||
) from e
|
||||
|
||||
connect_opts: dict[str, Any] = {
|
||||
"servers": self.config.servers,
|
||||
@@ -268,7 +282,9 @@ class PubSubClient:
|
||||
Raises:
|
||||
TimeoutError: If no response within timeout.
|
||||
"""
|
||||
response = await self._nc.request(subject, data, timeout=timeout, headers=headers)
|
||||
response = await self._nc.request(
|
||||
subject, data, timeout=timeout, headers=headers
|
||||
)
|
||||
return Message(
|
||||
subject=response.subject,
|
||||
data=response.data,
|
||||
|
||||
@@ -27,7 +27,9 @@ class QueuesConfig(BaseModel):
|
||||
db: int = Field(default=1, description="Redis database number")
|
||||
url: Optional[str] = Field(default=None, description="Full Redis URL")
|
||||
prefix: str = Field(default="hanzo:queue", description="Key prefix for queues")
|
||||
default_timeout: int = Field(default=300, description="Default job timeout in seconds")
|
||||
default_timeout: int = Field(
|
||||
default=300, description="Default job timeout in seconds"
|
||||
)
|
||||
default_ttl: int = Field(default=86400, description="Default result TTL in seconds")
|
||||
|
||||
@classmethod
|
||||
@@ -97,7 +99,9 @@ class Job:
|
||||
"error": self.error,
|
||||
"created_at": self.created_at.isoformat() if self.created_at else None,
|
||||
"started_at": self.started_at.isoformat() if self.started_at else None,
|
||||
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
|
||||
"completed_at": (
|
||||
self.completed_at.isoformat() if self.completed_at else None
|
||||
),
|
||||
"attempts": self.attempts,
|
||||
"max_attempts": self.max_attempts,
|
||||
"timeout": self.timeout,
|
||||
@@ -117,9 +121,21 @@ class Job:
|
||||
status=JobStatus(data.get("status", "pending")),
|
||||
result=data.get("result"),
|
||||
error=data.get("error"),
|
||||
created_at=datetime.fromisoformat(data["created_at"]) if data.get("created_at") else None,
|
||||
started_at=datetime.fromisoformat(data["started_at"]) if data.get("started_at") else None,
|
||||
completed_at=datetime.fromisoformat(data["completed_at"]) if data.get("completed_at") else None,
|
||||
created_at=(
|
||||
datetime.fromisoformat(data["created_at"])
|
||||
if data.get("created_at")
|
||||
else None
|
||||
),
|
||||
started_at=(
|
||||
datetime.fromisoformat(data["started_at"])
|
||||
if data.get("started_at")
|
||||
else None
|
||||
),
|
||||
completed_at=(
|
||||
datetime.fromisoformat(data["completed_at"])
|
||||
if data.get("completed_at")
|
||||
else None
|
||||
),
|
||||
attempts=data.get("attempts", 0),
|
||||
max_attempts=data.get("max_attempts", 3),
|
||||
timeout=data.get("timeout", 300),
|
||||
@@ -194,7 +210,9 @@ class QueuesClient:
|
||||
try:
|
||||
import redis.asyncio as redis
|
||||
except ImportError as e:
|
||||
raise ImportError("redis is required for QueuesClient. Install with: pip install redis") from e
|
||||
raise ImportError(
|
||||
"redis is required for QueuesClient. Install with: pip install redis"
|
||||
) from e
|
||||
|
||||
if self.config.url:
|
||||
self._redis = redis.from_url(
|
||||
|
||||
@@ -19,7 +19,9 @@ class SearchConfig(BaseModel):
|
||||
host: str = Field(default="localhost", description="Meilisearch server host")
|
||||
port: int = Field(default=7700, description="Meilisearch server port")
|
||||
api_key: Optional[str] = Field(default=None, description="Master or API key")
|
||||
url: Optional[str] = Field(default=None, description="Full URL (overrides host/port)")
|
||||
url: Optional[str] = Field(
|
||||
default=None, description="Full URL (overrides host/port)"
|
||||
)
|
||||
timeout: float = Field(default=30.0, description="Request timeout in seconds")
|
||||
ssl: bool = Field(default=False, description="Use HTTPS")
|
||||
|
||||
|
||||
@@ -18,15 +18,21 @@ from pydantic import Field, BaseModel
|
||||
class StorageConfig(BaseModel):
|
||||
"""Configuration for S3/MinIO connection."""
|
||||
|
||||
endpoint_url: Optional[str] = Field(default=None, description="Endpoint URL (required for MinIO)")
|
||||
endpoint_url: Optional[str] = Field(
|
||||
default=None, description="Endpoint URL (required for MinIO)"
|
||||
)
|
||||
region: str = Field(default="us-east-1", description="AWS region")
|
||||
access_key: Optional[str] = Field(default=None, description="Access key ID")
|
||||
secret_key: Optional[str] = Field(default=None, description="Secret access key")
|
||||
bucket: str = Field(default="hanzo", description="Default bucket name")
|
||||
use_ssl: bool = Field(default=True, description="Use SSL/TLS")
|
||||
verify_ssl: bool = Field(default=True, description="Verify SSL certificates")
|
||||
session_token: Optional[str] = Field(default=None, description="Session token (for temporary credentials)")
|
||||
addressing_style: str = Field(default="auto", description="Path or virtual addressing style")
|
||||
session_token: Optional[str] = Field(
|
||||
default=None, description="Session token (for temporary credentials)"
|
||||
)
|
||||
addressing_style: str = Field(
|
||||
default="auto", description="Path or virtual addressing style"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> StorageConfig:
|
||||
@@ -124,7 +130,9 @@ class StorageClient:
|
||||
|
||||
from aiobotocore.session import get_session
|
||||
except ImportError as e:
|
||||
raise ImportError("aiobotocore is required for StorageClient. Install with: pip install aiobotocore") from e
|
||||
raise ImportError(
|
||||
"aiobotocore is required for StorageClient. Install with: pip install aiobotocore"
|
||||
) from e
|
||||
|
||||
self._session = get_session()
|
||||
self._exit_stack = AsyncExitStack()
|
||||
@@ -191,7 +199,9 @@ class StorageClient:
|
||||
bucket_name = bucket or self.config.bucket
|
||||
create_config = {}
|
||||
if self.config.region != "us-east-1":
|
||||
create_config["CreateBucketConfiguration"] = {"LocationConstraint": self.config.region}
|
||||
create_config["CreateBucketConfiguration"] = {
|
||||
"LocationConstraint": self.config.region
|
||||
}
|
||||
await self._client.create_bucket(Bucket=bucket_name, **create_config)
|
||||
|
||||
async def delete_bucket(self, bucket: Optional[str] = None) -> None:
|
||||
|
||||
@@ -22,13 +22,21 @@ class TasksConfig(BaseModel):
|
||||
host: str = Field(default="localhost", description="Temporal server host")
|
||||
port: int = Field(default=7233, description="Temporal server port")
|
||||
namespace: str = Field(default="default", description="Temporal namespace")
|
||||
target: Optional[str] = Field(default=None, description="Full target address (overrides host/port)")
|
||||
target: Optional[str] = Field(
|
||||
default=None, description="Full target address (overrides host/port)"
|
||||
)
|
||||
tls: bool = Field(default=False, description="Use TLS")
|
||||
tls_cert_path: Optional[str] = Field(default=None, description="Path to TLS certificate")
|
||||
tls_cert_path: Optional[str] = Field(
|
||||
default=None, description="Path to TLS certificate"
|
||||
)
|
||||
tls_key_path: Optional[str] = Field(default=None, description="Path to TLS key")
|
||||
api_key: Optional[str] = Field(default=None, description="API key for Temporal Cloud")
|
||||
api_key: Optional[str] = Field(
|
||||
default=None, description="API key for Temporal Cloud"
|
||||
)
|
||||
identity: str = Field(default="hanzo-client", description="Client identity")
|
||||
data_converter: Optional[str] = Field(default=None, description="Custom data converter class")
|
||||
data_converter: Optional[str] = Field(
|
||||
default=None, description="Custom data converter class"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_env(cls) -> TasksConfig:
|
||||
@@ -179,7 +187,9 @@ class TasksClient:
|
||||
try:
|
||||
from temporalio.client import Client, TLSConfig
|
||||
except ImportError as e:
|
||||
raise ImportError("temporalio is required for TasksClient. Install with: pip install temporalio") from e
|
||||
raise ImportError(
|
||||
"temporalio is required for TasksClient. Install with: pip install temporalio"
|
||||
) from e
|
||||
|
||||
connect_kwargs: dict[str, Any] = {
|
||||
"target_host": self.config.effective_target,
|
||||
@@ -281,7 +291,9 @@ class TasksClient:
|
||||
"arg": args,
|
||||
"id": id or f"{workflow}-{uuid.uuid4().hex[:8]}",
|
||||
"task_queue": task_queue,
|
||||
"id_reuse_policy": policy_map.get(id_reuse_policy, WorkflowIDReusePolicy.ALLOW_DUPLICATE),
|
||||
"id_reuse_policy": policy_map.get(
|
||||
id_reuse_policy, WorkflowIDReusePolicy.ALLOW_DUPLICATE
|
||||
),
|
||||
}
|
||||
|
||||
if execution_timeout:
|
||||
@@ -299,8 +311,12 @@ class TasksClient:
|
||||
|
||||
if retry_policy:
|
||||
start_kwargs["retry_policy"] = RetryPolicy(
|
||||
initial_interval=timedelta(seconds=retry_policy.get("initial_interval", 1)),
|
||||
maximum_interval=timedelta(seconds=retry_policy.get("maximum_interval", 100)),
|
||||
initial_interval=timedelta(
|
||||
seconds=retry_policy.get("initial_interval", 1)
|
||||
),
|
||||
maximum_interval=timedelta(
|
||||
seconds=retry_policy.get("maximum_interval", 100)
|
||||
),
|
||||
backoff_coefficient=retry_policy.get("backoff_coefficient", 2.0),
|
||||
maximum_attempts=retry_policy.get("maximum_attempts", 0),
|
||||
)
|
||||
@@ -379,7 +395,9 @@ class TasksClient:
|
||||
start_time=wf.start_time,
|
||||
close_time=wf.close_time,
|
||||
memo=dict(wf.memo) if wf.memo else {},
|
||||
search_attributes=dict(wf.search_attributes) if wf.search_attributes else {},
|
||||
search_attributes=(
|
||||
dict(wf.search_attributes) if wf.search_attributes else {}
|
||||
),
|
||||
)
|
||||
|
||||
async def count_workflows(self, query: str = "") -> int:
|
||||
@@ -444,7 +462,9 @@ class TasksClient:
|
||||
specs.append(ScheduleSpec(calendars=[ScheduleCalendarSpec(**calendar)]))
|
||||
|
||||
if not specs:
|
||||
raise ValueError("At least one of cron, interval, or calendar must be specified")
|
||||
raise ValueError(
|
||||
"At least one of cron, interval, or calendar must be specified"
|
||||
)
|
||||
|
||||
schedule_spec = (
|
||||
specs[0]
|
||||
|
||||
@@ -20,7 +20,9 @@ class VectorConfig(BaseModel):
|
||||
port: int = Field(default=6333, description="Qdrant REST API port")
|
||||
grpc_port: int = Field(default=6334, description="Qdrant gRPC port")
|
||||
api_key: Optional[str] = Field(default=None, description="API key for Qdrant Cloud")
|
||||
url: Optional[str] = Field(default=None, description="Full URL (overrides host/port)")
|
||||
url: Optional[str] = Field(
|
||||
default=None, description="Full URL (overrides host/port)"
|
||||
)
|
||||
https: bool = Field(default=False, description="Use HTTPS")
|
||||
timeout: float = Field(default=30.0, description="Request timeout in seconds")
|
||||
prefer_grpc: bool = Field(default=True, description="Prefer gRPC over REST")
|
||||
@@ -218,8 +220,12 @@ class VectorClient:
|
||||
"""
|
||||
from qdrant_client.models import PointStruct
|
||||
|
||||
qdrant_points = [PointStruct(id=p.id, vector=p.vector, payload=p.payload) for p in points]
|
||||
await self._async_client.upsert(collection_name=collection, points=qdrant_points)
|
||||
qdrant_points = [
|
||||
PointStruct(id=p.id, vector=p.vector, payload=p.payload) for p in points
|
||||
]
|
||||
await self._async_client.upsert(
|
||||
collection_name=collection, points=qdrant_points
|
||||
)
|
||||
|
||||
async def search(
|
||||
self,
|
||||
|
||||
@@ -42,7 +42,9 @@ def _get_jobs() -> List[Dict[str, Any]]:
|
||||
import urllib.request
|
||||
|
||||
endpoint = os.getenv("HANZO_CLUSTER_URL", "http://localhost:8000")
|
||||
req = urllib.request.urlopen(f"{endpoint}/jobs?limit=5", timeout=2) # noqa: S310
|
||||
req = urllib.request.urlopen( # noqa: S310
|
||||
f"{endpoint}/jobs?limit=5", timeout=2
|
||||
)
|
||||
return json.loads(req.read().decode()).get("jobs", [])
|
||||
except Exception:
|
||||
return []
|
||||
@@ -55,7 +57,9 @@ def _get_logs() -> List[str]:
|
||||
import urllib.request
|
||||
|
||||
endpoint = os.getenv("HANZO_CLUSTER_URL", "http://localhost:8000")
|
||||
req = urllib.request.urlopen(f"{endpoint}/logs?limit=5", timeout=2) # noqa: S310
|
||||
req = urllib.request.urlopen( # noqa: S310
|
||||
f"{endpoint}/logs?limit=5", timeout=2
|
||||
)
|
||||
return json.loads(req.read().decode()).get("logs", [])
|
||||
except Exception:
|
||||
return []
|
||||
@@ -99,7 +103,9 @@ def run_dashboard(refresh_rate: float = 1.0):
|
||||
|
||||
cluster = _get_cluster_status()
|
||||
if cluster:
|
||||
table.add_row("Status", f"[green]{cluster.get('status', 'unknown')}[/green]")
|
||||
table.add_row(
|
||||
"Status", f"[green]{cluster.get('status', 'unknown')}[/green]"
|
||||
)
|
||||
table.add_row("Nodes", str(cluster.get("nodes", 0)))
|
||||
table.add_row("Models", ", ".join(cluster.get("models", [])) or "none")
|
||||
table.add_row("Port", str(cluster.get("port", "-")))
|
||||
@@ -161,7 +167,10 @@ def run_dashboard(refresh_rate: float = 1.0):
|
||||
"""Get logs panel."""
|
||||
logs = _get_logs()
|
||||
if logs:
|
||||
log_text = "\n".join(f"[dim]{log.get('time', '')}[/dim] {log.get('message', '')}" for log in logs[:5])
|
||||
log_text = "\n".join(
|
||||
f"[dim]{log.get('time', '')}[/dim] {log.get('message', '')}"
|
||||
for log in logs[:5]
|
||||
)
|
||||
border = "dim"
|
||||
else:
|
||||
log_text = "[dim]No logs available[/dim]"
|
||||
|
||||
@@ -98,7 +98,9 @@ class EnhancedHanzoREPL:
|
||||
self.failed_tools = set() # Track tools that have failed this session
|
||||
|
||||
# Initialize background task manager
|
||||
self.task_manager = BackgroundTaskManager(console) if BackgroundTaskManager else None
|
||||
self.task_manager = (
|
||||
BackgroundTaskManager(console) if BackgroundTaskManager else None
|
||||
)
|
||||
|
||||
# Initialize todo manager
|
||||
self.todo_manager = TodoManager(console) if TodoManager else None
|
||||
@@ -112,7 +114,9 @@ class EnhancedHanzoREPL:
|
||||
if default_tool:
|
||||
self.current_model = f"tool:{default_tool.name}"
|
||||
self.current_tool = default_tool
|
||||
self.console.print(f"[green]✓ Detected {default_tool.display_name} as default AI assistant[/green]")
|
||||
self.console.print(
|
||||
f"[green]✓ Detected {default_tool.display_name} as default AI assistant[/green]"
|
||||
)
|
||||
else:
|
||||
# Fallback to regular models
|
||||
self.current_model = self.config.get("default_model", "gpt-3.5-turbo")
|
||||
@@ -351,7 +355,9 @@ class EnhancedHanzoREPL:
|
||||
# Router status
|
||||
try:
|
||||
response = httpx.get("http://localhost:4000/health", timeout=1)
|
||||
router_status = "✅ Running" if response.status_code == 200 else "⚠️ Unhealthy"
|
||||
router_status = (
|
||||
"✅ Running" if response.status_code == 200 else "⚠️ Unhealthy"
|
||||
)
|
||||
router_details = "Port 4000"
|
||||
except Exception:
|
||||
router_status = "❌ Offline"
|
||||
@@ -361,7 +367,9 @@ class EnhancedHanzoREPL:
|
||||
# Node status
|
||||
try:
|
||||
response = httpx.get("http://localhost:3690/health", timeout=1)
|
||||
node_status = "✅ Running" if response.status_code == 200 else "⚠️ Unhealthy"
|
||||
node_status = (
|
||||
"✅ Running" if response.status_code == 200 else "⚠️ Unhealthy"
|
||||
)
|
||||
node_details = "Port 3690"
|
||||
except Exception:
|
||||
node_status = "❌ Offline"
|
||||
@@ -404,7 +412,11 @@ class EnhancedHanzoREPL:
|
||||
args = f"tool:{tool.name}"
|
||||
else:
|
||||
# It's a model selection
|
||||
model_idx = num - len(self.detected_tools) - 1 if self.detected_tools else num - 1
|
||||
model_idx = (
|
||||
num - len(self.detected_tools) - 1
|
||||
if self.detected_tools
|
||||
else num - 1
|
||||
)
|
||||
models_list = list(self.MODELS.keys())
|
||||
if 0 <= model_idx < len(models_list):
|
||||
args = models_list[model_idx]
|
||||
@@ -417,7 +429,11 @@ class EnhancedHanzoREPL:
|
||||
return
|
||||
|
||||
# Check if it's a tool
|
||||
if args.startswith("tool:") or args in [t.name for t in self.detected_tools] if self.detected_tools else False:
|
||||
if (
|
||||
args.startswith("tool:") or args in [t.name for t in self.detected_tools]
|
||||
if self.detected_tools
|
||||
else False
|
||||
):
|
||||
# Handle tool selection
|
||||
tool_name = args.replace("tool:", "") if args.startswith("tool:") else args
|
||||
|
||||
@@ -451,7 +467,9 @@ class EnhancedHanzoREPL:
|
||||
|
||||
# Show hint if it's a gateway model (not in our predefined list)
|
||||
if args not in self.MODELS and not args.startswith("local:"):
|
||||
self.console.print("[dim]Using gateway model - all models free! Use /models to see full list.[/dim]")
|
||||
self.console.print(
|
||||
"[dim]Using gateway model - all models free! Use /models to see full list.[/dim]"
|
||||
)
|
||||
|
||||
async def list_tools(self, args: str = ""):
|
||||
"""List available AI tools."""
|
||||
@@ -464,10 +482,14 @@ class EnhancedHanzoREPL:
|
||||
"""List available models from gateway.hanzo.ai."""
|
||||
# Show tools first if available
|
||||
if self.detected_tools:
|
||||
self.console.print("[bold cyan]AI Coding Assistants (Detected):[/bold cyan]")
|
||||
self.console.print(
|
||||
"[bold cyan]AI Coding Assistants (Detected):[/bold cyan]"
|
||||
)
|
||||
for i, tool in enumerate(self.detected_tools, 1):
|
||||
marker = "→" if self.current_model == f"tool:{tool.name}" else " "
|
||||
self.console.print(f" {marker} {i}. {tool.display_name} ({tool.provider})")
|
||||
self.console.print(
|
||||
f" {marker} {i}. {tool.display_name} ({tool.provider})"
|
||||
)
|
||||
self.console.print()
|
||||
|
||||
# Try to fetch live models from gateway
|
||||
@@ -487,14 +509,20 @@ class EnhancedHanzoREPL:
|
||||
models_list = [
|
||||
model
|
||||
for model in all_models
|
||||
if not any(keyword in model.get("id", "").lower() for keyword in embedding_keywords)
|
||||
if not any(
|
||||
keyword in model.get("id", "").lower()
|
||||
for keyword in embedding_keywords
|
||||
)
|
||||
]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fallback to static models if gateway fetch fails
|
||||
if not models_list:
|
||||
models_list = [{"id": model_id, "name": model_name} for model_id, model_name in self.MODELS.items()]
|
||||
models_list = [
|
||||
{"id": model_id, "name": model_name}
|
||||
for model_id, model_name in self.MODELS.items()
|
||||
]
|
||||
|
||||
# Create table
|
||||
table = Table(title="Gateway LLMs (Chat Models)", box=box.ROUNDED)
|
||||
@@ -533,16 +561,22 @@ class EnhancedHanzoREPL:
|
||||
|
||||
# Highlight current model
|
||||
if model_id == self.current_model:
|
||||
table.add_row(str(i), f"[bold green]→ {model_id}[/bold green]", tier, provider)
|
||||
table.add_row(
|
||||
str(i), f"[bold green]→ {model_id}[/bold green]", tier, provider
|
||||
)
|
||||
else:
|
||||
table.add_row(str(i), model_id, tier, provider)
|
||||
|
||||
self.console.print(table)
|
||||
self.console.print("\n[dim cyan]Free tier:[/dim cyan] [green]Most models available without login![/green]")
|
||||
self.console.print(
|
||||
"\n[dim cyan]Free tier:[/dim cyan] [green]Most models available without login![/green]"
|
||||
)
|
||||
self.console.print(
|
||||
"[dim cyan]Premium models:[/dim cyan] [yellow]gpt-4, claude-3-opus, o1-preview[/yellow] - [cyan]hanzo auth login[/cyan]"
|
||||
)
|
||||
self.console.print("\n[dim]Use /model <name> or /model <number> to switch[/dim]")
|
||||
self.console.print(
|
||||
"\n[dim]Use /model <name> or /model <number> to switch[/dim]"
|
||||
)
|
||||
|
||||
async def login(self, args: str = ""):
|
||||
"""Login to Hanzo."""
|
||||
@@ -661,7 +695,9 @@ class EnhancedHanzoREPL:
|
||||
self.console.print(f"[dim]Executing: {full_cmd}[/dim]")
|
||||
|
||||
try:
|
||||
result = subprocess.run(full_cmd, shell=True, capture_output=True, text=True)
|
||||
result = subprocess.run(
|
||||
full_cmd, shell=True, capture_output=True, text=True
|
||||
)
|
||||
|
||||
if result.stdout:
|
||||
self.console.print(result.stdout)
|
||||
@@ -682,13 +718,19 @@ class EnhancedHanzoREPL:
|
||||
if tool.name not in self.failed_tools:
|
||||
self.current_tool = tool
|
||||
self.current_model = f"tool:{tool.name}"
|
||||
self.console.print(f"[yellow]Switched to {tool.display_name}[/yellow]")
|
||||
self.console.print(
|
||||
f"[yellow]Switched to {tool.display_name}[/yellow]"
|
||||
)
|
||||
break
|
||||
|
||||
# Try the current tool
|
||||
if self.current_tool.name not in self.failed_tools:
|
||||
self.console.print(f"[dim]Using {self.current_tool.display_name}...[/dim]")
|
||||
success, output = self.tool_detector.execute_with_tool(self.current_tool, message)
|
||||
self.console.print(
|
||||
f"[dim]Using {self.current_tool.display_name}...[/dim]"
|
||||
)
|
||||
success, output = self.tool_detector.execute_with_tool(
|
||||
self.current_tool, message
|
||||
)
|
||||
|
||||
if success:
|
||||
self.console.print(output)
|
||||
@@ -703,8 +745,12 @@ class EnhancedHanzoREPL:
|
||||
if self.tool_detector and self.tool_detector.detected_tools:
|
||||
for fallback_tool in self.tool_detector.detected_tools:
|
||||
if fallback_tool.name not in self.failed_tools:
|
||||
self.console.print(f"[yellow]Trying {fallback_tool.display_name}...[/yellow]")
|
||||
success, output = self.tool_detector.execute_with_tool(fallback_tool, message)
|
||||
self.console.print(
|
||||
f"[yellow]Trying {fallback_tool.display_name}...[/yellow]"
|
||||
)
|
||||
success, output = self.tool_detector.execute_with_tool(
|
||||
fallback_tool, message
|
||||
)
|
||||
if success:
|
||||
self.console.print(output)
|
||||
# Automatically switch to this working tool
|
||||
@@ -720,15 +766,21 @@ class EnhancedHanzoREPL:
|
||||
else:
|
||||
# Mark as failed
|
||||
self.failed_tools.add(fallback_tool.name)
|
||||
self.console.print(f"[red]{fallback_tool.display_name} also failed[/red]")
|
||||
self.console.print(
|
||||
f"[red]{fallback_tool.display_name} also failed[/red]"
|
||||
)
|
||||
|
||||
if not found_working:
|
||||
# Final fallback to cloud model
|
||||
self.console.print(f"[yellow]Falling back to cloud model...[/yellow]")
|
||||
await self.execute_command("ask", f"--cloud --model gpt-3.5-turbo {message}")
|
||||
await self.execute_command(
|
||||
"ask", f"--cloud --model gpt-3.5-turbo {message}"
|
||||
)
|
||||
else:
|
||||
# Use regular model through hanzo ask
|
||||
await self.execute_command("ask", f"--cloud --model {self.current_model} {message}")
|
||||
await self.execute_command(
|
||||
"ask", f"--cloud --model {self.current_model} {message}"
|
||||
)
|
||||
|
||||
async def quick_model_select(self, args: str = ""):
|
||||
"""Quick model selector with arrow keys."""
|
||||
@@ -737,7 +789,11 @@ class EnhancedHanzoREPL:
|
||||
return
|
||||
|
||||
# Prepare tools and models
|
||||
tools = [(f"tool:{t.name}", t.display_name) for t in self.detected_tools] if self.detected_tools else []
|
||||
tools = (
|
||||
[(f"tool:{t.name}", t.display_name) for t in self.detected_tools]
|
||||
if self.detected_tools
|
||||
else []
|
||||
)
|
||||
models = list(self.MODELS.items())
|
||||
|
||||
selector = QuickModelSelector(models, tools, self.current_model)
|
||||
@@ -768,7 +824,9 @@ class EnhancedHanzoREPL:
|
||||
else:
|
||||
# Show tasks and prompt for selection
|
||||
self.task_manager.list_tasks()
|
||||
self.console.print("\n[cyan]Enter task ID to kill (or 'all' for all tasks):[/cyan]")
|
||||
self.console.print(
|
||||
"\n[cyan]Enter task ID to kill (or 'all' for all tasks):[/cyan]"
|
||||
)
|
||||
try:
|
||||
task_id = await self.session.prompt_async("> ")
|
||||
if task_id:
|
||||
@@ -803,7 +861,9 @@ class EnhancedHanzoREPL:
|
||||
# Quick add
|
||||
try:
|
||||
todo = self.todo_manager.quick_add(rest)
|
||||
self.console.print(f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]")
|
||||
self.console.print(
|
||||
f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]"
|
||||
)
|
||||
except ValueError as e:
|
||||
self.console.print(f"[red]Error: {e}[/red]")
|
||||
else:
|
||||
@@ -829,7 +889,9 @@ class EnhancedHanzoREPL:
|
||||
elif key in ["tag", "t"]:
|
||||
tag = value
|
||||
|
||||
todos = self.todo_manager.list_todos(status=status, priority=priority, tag=tag)
|
||||
todos = self.todo_manager.list_todos(
|
||||
status=status, priority=priority, tag=tag
|
||||
)
|
||||
title = "Filtered Todos" if (status or priority or tag) else "All Todos"
|
||||
self.todo_manager.display_todos(todos, title)
|
||||
|
||||
@@ -838,7 +900,9 @@ class EnhancedHanzoREPL:
|
||||
if rest:
|
||||
todo = self.todo_manager.update_todo(rest, status="done")
|
||||
if todo:
|
||||
self.console.print(f"[green]✅ Marked as done: {todo.title}[/green]")
|
||||
self.console.print(
|
||||
f"[green]✅ Marked as done: {todo.title}[/green]"
|
||||
)
|
||||
else:
|
||||
self.console.print(f"[red]Todo not found: {rest}[/red]")
|
||||
else:
|
||||
@@ -894,7 +958,9 @@ class EnhancedHanzoREPL:
|
||||
elif subcommand in ["clear", "reset"]:
|
||||
# Clear all todos (with confirmation)
|
||||
try:
|
||||
confirm = await self.session.prompt_async("Are you sure you want to delete ALL todos? (yes/no): ")
|
||||
confirm = await self.session.prompt_async(
|
||||
"Are you sure you want to delete ALL todos? (yes/no): "
|
||||
)
|
||||
if confirm.lower() in ["yes", "y"]:
|
||||
self.todo_manager.todos = []
|
||||
self.todo_manager.save_todos()
|
||||
@@ -912,9 +978,13 @@ class EnhancedHanzoREPL:
|
||||
# Unknown subcommand, treat as quick add
|
||||
try:
|
||||
todo = self.todo_manager.quick_add(args)
|
||||
self.console.print(f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]")
|
||||
self.console.print(
|
||||
f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]"
|
||||
)
|
||||
except ValueError:
|
||||
self.console.print(f"[yellow]Unknown todo command: {subcommand}[/yellow]")
|
||||
self.console.print(
|
||||
f"[yellow]Unknown todo command: {subcommand}[/yellow]"
|
||||
)
|
||||
self.console.print("[dim]Use /todo help for available commands[/dim]")
|
||||
|
||||
async def add_todo_interactive(self):
|
||||
@@ -930,13 +1000,21 @@ class EnhancedHanzoREPL:
|
||||
description = await self.session.prompt_async("Description (optional): ")
|
||||
|
||||
# Get priority
|
||||
priority = await self.session.prompt_async("Priority (low/medium/high/urgent) [medium]: ")
|
||||
priority = await self.session.prompt_async(
|
||||
"Priority (low/medium/high/urgent) [medium]: "
|
||||
)
|
||||
if not priority:
|
||||
priority = "medium"
|
||||
|
||||
# Get tags
|
||||
tags_input = await self.session.prompt_async("Tags (comma-separated, optional): ")
|
||||
tags = [t.strip() for t in tags_input.split(",") if t.strip()] if tags_input else []
|
||||
tags_input = await self.session.prompt_async(
|
||||
"Tags (comma-separated, optional): "
|
||||
)
|
||||
tags = (
|
||||
[t.strip() for t in tags_input.split(",") if t.strip()]
|
||||
if tags_input
|
||||
else []
|
||||
)
|
||||
|
||||
# Get due date
|
||||
due_date = await self.session.prompt_async("Due date (optional): ")
|
||||
@@ -950,7 +1028,9 @@ class EnhancedHanzoREPL:
|
||||
due_date=due_date if due_date else None,
|
||||
)
|
||||
|
||||
self.console.print(f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]")
|
||||
self.console.print(
|
||||
f"[green]✅ Added todo: {todo.title} (ID: {todo.id})[/green]"
|
||||
)
|
||||
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
self.console.print("[yellow]Cancelled[/yellow]")
|
||||
|
||||
@@ -14,7 +14,9 @@ from prompt_toolkit.layout.containers import HSplit, Window
|
||||
class QuickModelSelector:
|
||||
"""Quick model selector with arrow navigation."""
|
||||
|
||||
def __init__(self, models: List[Tuple[str, str]], tools: List[Tuple[str, str]], current: str):
|
||||
def __init__(
|
||||
self, models: List[Tuple[str, str]], tools: List[Tuple[str, str]], current: str
|
||||
):
|
||||
self.models = models
|
||||
self.tools = tools
|
||||
self.current = current
|
||||
@@ -97,7 +99,9 @@ class QuickModelSelector:
|
||||
|
||||
layout = Layout(Window(FormattedTextControl(get_text), wrap_lines=False))
|
||||
|
||||
app = Application(layout=layout, key_bindings=kb, full_screen=False, mouse_support=True)
|
||||
app = Application(
|
||||
layout=layout, key_bindings=kb, full_screen=False, mouse_support=True
|
||||
)
|
||||
|
||||
return await app.run_async()
|
||||
|
||||
@@ -134,7 +138,9 @@ class BackgroundTaskManager:
|
||||
task = self.tasks[task_id]
|
||||
if task["process"].poll() is None:
|
||||
task["process"].terminate()
|
||||
self.console.print(f"[yellow]Terminated {task_id}: {task['name']}[/yellow]")
|
||||
self.console.print(
|
||||
f"[yellow]Terminated {task_id}: {task['name']}[/yellow]"
|
||||
)
|
||||
else:
|
||||
self.console.print(f"[dim]Task {task_id} already stopped[/dim]")
|
||||
del self.tasks[task_id]
|
||||
|
||||
@@ -196,7 +196,9 @@ hanzo> mcp run read_file --arg path=README.md
|
||||
except subprocess.TimeoutExpired:
|
||||
self.console.print("[red]Command timed out[/red]")
|
||||
except FileNotFoundError:
|
||||
self.console.print("[red]Command not found. Make sure 'hanzo' is installed.[/red]")
|
||||
self.console.print(
|
||||
"[red]Command not found. Make sure 'hanzo' is installed.[/red]"
|
||||
)
|
||||
except Exception as e:
|
||||
self.console.print(f"[red]Command error: {e}[/red]")
|
||||
|
||||
|
||||
@@ -293,7 +293,9 @@ class TodoManager:
|
||||
TodoPriority.LOW: "green",
|
||||
}.get(todo.priority, "white")
|
||||
|
||||
priority_display = f"[{priority_color}]{todo.priority.value.upper()}[/{priority_color}]"
|
||||
priority_display = (
|
||||
f"[{priority_color}]{todo.priority.value.upper()}[/{priority_color}]"
|
||||
)
|
||||
|
||||
# Tags
|
||||
tags_display = ", ".join(todo.tags) if todo.tags else "-"
|
||||
@@ -387,7 +389,9 @@ class TodoManager:
|
||||
if not title:
|
||||
raise ValueError("Todo title cannot be empty")
|
||||
|
||||
return self.add_todo(title=title, priority=priority, tags=tags, due_date=due_date)
|
||||
return self.add_todo(
|
||||
title=title, priority=priority, tags=tags, due_date=due_date
|
||||
)
|
||||
|
||||
def get_statistics(self) -> Dict[str, Any]:
|
||||
"""Get todo statistics."""
|
||||
|
||||
@@ -57,7 +57,9 @@ class MemoryManager:
|
||||
try:
|
||||
with open(self.memory_file, "r") as f:
|
||||
data = json.load(f)
|
||||
self.memories = [MemoryItem.from_dict(item) for item in data.get("memories", [])]
|
||||
self.memories = [
|
||||
MemoryItem.from_dict(item) for item in data.get("memories", [])
|
||||
]
|
||||
except Exception as e:
|
||||
print(f"Error loading memories: {e}")
|
||||
self.memories = []
|
||||
@@ -178,7 +180,9 @@ class MemoryManager:
|
||||
self.memories = []
|
||||
self.save_memories()
|
||||
|
||||
def get_memories(self, type: str = None, tags: List[str] = None) -> List[MemoryItem]:
|
||||
def get_memories(
|
||||
self, type: str = None, tags: List[str] = None
|
||||
) -> List[MemoryItem]:
|
||||
"""Get memories filtered by type or tags."""
|
||||
result = self.memories
|
||||
|
||||
@@ -331,7 +335,9 @@ def handle_memory_command(command: str, memory_manager: MemoryManager, console)
|
||||
console.print("[yellow]No memories stored.[/yellow]")
|
||||
return True
|
||||
|
||||
table = Table(title="Current Memories", show_header=True, header_style="bold magenta")
|
||||
table = Table(
|
||||
title="Current Memories", show_header=True, header_style="bold magenta"
|
||||
)
|
||||
table.add_column("ID", style="cyan", width=10)
|
||||
table.add_column("Type", width=12)
|
||||
table.add_column("Content", width=50)
|
||||
|
||||
@@ -16,7 +16,9 @@ from typing import Any, Dict, List, Optional
|
||||
from dataclasses import field, dataclass
|
||||
|
||||
|
||||
def check_local_node(host: str = "localhost", port: int = 4000, timeout: float = 0.5) -> bool:
|
||||
def check_local_node(
|
||||
host: str = "localhost", port: int = 4000, timeout: float = 0.5
|
||||
) -> bool:
|
||||
"""Check if hanzo-node is running locally.
|
||||
|
||||
Args:
|
||||
|
||||
@@ -275,10 +275,18 @@ class SmartRateLimiter:
|
||||
|
||||
# Default configs for known APIs
|
||||
self.configs = {
|
||||
"openai": RateLimitConfig(requests_per_minute=60, requests_per_hour=1000, burst_size=10),
|
||||
"anthropic": RateLimitConfig(requests_per_minute=50, requests_per_hour=1000, burst_size=5),
|
||||
"local": RateLimitConfig(requests_per_minute=100, requests_per_hour=10000, burst_size=20),
|
||||
"free": RateLimitConfig(requests_per_minute=10, requests_per_hour=100, burst_size=2),
|
||||
"openai": RateLimitConfig(
|
||||
requests_per_minute=60, requests_per_hour=1000, burst_size=10
|
||||
),
|
||||
"anthropic": RateLimitConfig(
|
||||
requests_per_minute=50, requests_per_hour=1000, burst_size=5
|
||||
),
|
||||
"local": RateLimitConfig(
|
||||
requests_per_minute=100, requests_per_hour=10000, burst_size=20
|
||||
),
|
||||
"free": RateLimitConfig(
|
||||
requests_per_minute=10, requests_per_hour=100, burst_size=2
|
||||
),
|
||||
}
|
||||
|
||||
def get_limiter(self, api_type: str) -> RateLimiter:
|
||||
@@ -288,7 +296,9 @@ class SmartRateLimiter:
|
||||
self.limiters[api_type] = RateLimiter(config)
|
||||
return self.limiters[api_type]
|
||||
|
||||
async def execute_with_limit(self, api_type: str, func: Callable, *args, **kwargs) -> Any:
|
||||
async def execute_with_limit(
|
||||
self, api_type: str, func: Callable, *args, **kwargs
|
||||
) -> Any:
|
||||
"""Execute function with appropriate rate limiting."""
|
||||
limiter = self.get_limiter(api_type)
|
||||
recovery = ErrorRecovery(limiter)
|
||||
@@ -297,7 +307,10 @@ class SmartRateLimiter:
|
||||
|
||||
def get_all_status(self) -> Dict[str, Dict[str, Any]]:
|
||||
"""Get status of all limiters."""
|
||||
return {api_type: limiter.get_status() for api_type, limiter in self.limiters.items()}
|
||||
return {
|
||||
api_type: limiter.get_status()
|
||||
for api_type, limiter in self.limiters.items()
|
||||
}
|
||||
|
||||
|
||||
# Global instance for easy use
|
||||
|
||||
@@ -25,7 +25,9 @@ class StreamingHandler:
|
||||
async def stream_openai(self, client, messages: list, model: str = "gpt-4") -> str:
|
||||
"""Stream response from OpenAI API."""
|
||||
try:
|
||||
stream = await client.chat.completions.create(model=model, messages=messages, stream=True, max_tokens=1000)
|
||||
stream = await client.chat.completions.create(
|
||||
model=model, messages=messages, stream=True, max_tokens=1000
|
||||
)
|
||||
|
||||
self.current_response = ""
|
||||
self.is_streaming = True
|
||||
@@ -61,7 +63,9 @@ class StreamingHandler:
|
||||
self.is_streaming = False
|
||||
return None
|
||||
|
||||
async def stream_anthropic(self, client, messages: list, model: str = "claude-3-5-sonnet-20241022") -> str:
|
||||
async def stream_anthropic(
|
||||
self, client, messages: list, model: str = "claude-3-5-sonnet-20241022"
|
||||
) -> str:
|
||||
"""Stream response from Anthropic API."""
|
||||
try:
|
||||
self.current_response = ""
|
||||
@@ -77,7 +81,9 @@ class StreamingHandler:
|
||||
console=self.console,
|
||||
refresh_per_second=10,
|
||||
) as live:
|
||||
async with client.messages.stream(model=model, messages=messages, max_tokens=1000) as stream:
|
||||
async with client.messages.stream(
|
||||
model=model, messages=messages, max_tokens=1000
|
||||
) as stream:
|
||||
async for text in stream.text_stream:
|
||||
self.current_response += text
|
||||
live.update(
|
||||
@@ -218,11 +224,15 @@ class TypewriterEffect:
|
||||
for line in lines:
|
||||
for char in line:
|
||||
current_code += char
|
||||
syntax = Syntax(current_code, language, theme="monokai", line_numbers=True)
|
||||
syntax = Syntax(
|
||||
current_code, language, theme="monokai", line_numbers=True
|
||||
)
|
||||
live.update(syntax)
|
||||
await asyncio.sleep(speed)
|
||||
current_code += "\n"
|
||||
syntax = Syntax(current_code, language, theme="monokai", line_numbers=True)
|
||||
syntax = Syntax(
|
||||
current_code, language, theme="monokai", line_numbers=True
|
||||
)
|
||||
live.update(syntax)
|
||||
|
||||
|
||||
@@ -240,7 +250,9 @@ async def stream_with_fallback(message: str, console: Console = None) -> Optiona
|
||||
from openai import AsyncOpenAI
|
||||
|
||||
client = AsyncOpenAI()
|
||||
return await handler.stream_openai(client, [{"role": "user", "content": message}])
|
||||
return await handler.stream_openai(
|
||||
client, [{"role": "user", "content": message}]
|
||||
)
|
||||
except Exception as e:
|
||||
if console:
|
||||
console.print(f"[yellow]OpenAI streaming failed: {e}[/yellow]")
|
||||
@@ -251,7 +263,9 @@ async def stream_with_fallback(message: str, console: Console = None) -> Optiona
|
||||
from anthropic import AsyncAnthropic
|
||||
|
||||
client = AsyncAnthropic()
|
||||
return await handler.stream_anthropic(client, [{"role": "user", "content": message}])
|
||||
return await handler.stream_anthropic(
|
||||
client, [{"role": "user", "content": message}]
|
||||
)
|
||||
except Exception as e:
|
||||
if console:
|
||||
console.print(f"[yellow]Anthropic streaming failed: {e}[/yellow]")
|
||||
|
||||
@@ -217,7 +217,9 @@ class ToolDetector:
|
||||
]: # 400/422 means endpoint exists but params wrong
|
||||
tool.detected = True
|
||||
tool.version = f"Running (Port {port})"
|
||||
tool.api_endpoint = f"http://localhost:{port}/health" # Update port
|
||||
tool.api_endpoint = (
|
||||
f"http://localhost:{port}/health" # Update port
|
||||
)
|
||||
|
||||
# Try to get model info
|
||||
try:
|
||||
@@ -314,7 +316,11 @@ class ToolDetector:
|
||||
version = tool.version or "Unknown" if tool.detected else "-"
|
||||
|
||||
# Highlight the default tool
|
||||
if tool.detected and tool == self.detected_tools[0] if self.detected_tools else False:
|
||||
if (
|
||||
tool.detected and tool == self.detected_tools[0]
|
||||
if self.detected_tools
|
||||
else False
|
||||
):
|
||||
table.add_row(
|
||||
str(i),
|
||||
f"[bold green]→ {tool.display_name}[/bold green]",
|
||||
@@ -341,11 +347,15 @@ class ToolDetector:
|
||||
|
||||
# Special message for Hanzo Node
|
||||
if default.name == "hanzod":
|
||||
self.console.print("[cyan]🔒 Using local private AI - your data stays on your machine[/cyan]")
|
||||
self.console.print(
|
||||
"[cyan]🔒 Using local private AI - your data stays on your machine[/cyan]"
|
||||
)
|
||||
self.console.print("[dim]Manage models with: hanzo node models[/dim]")
|
||||
else:
|
||||
self.console.print("\n[yellow]No AI coding tools detected.[/yellow]")
|
||||
self.console.print("[dim]Start Hanzo Node for local AI: hanzo node start[/dim]")
|
||||
self.console.print(
|
||||
"[dim]Start Hanzo Node for local AI: hanzo node start[/dim]"
|
||||
)
|
||||
self.console.print("[dim]Or install Claude Code, OpenAI CLI, etc.[/dim]")
|
||||
|
||||
def get_tool_command(self, tool: AITool, prompt: str) -> List[str]:
|
||||
@@ -407,7 +417,9 @@ class ToolDetector:
|
||||
)
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
return True, result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
return True, result.get("choices", [{}])[0].get(
|
||||
"message", {}
|
||||
).get("content", "")
|
||||
else:
|
||||
return (
|
||||
False,
|
||||
@@ -431,7 +443,9 @@ class ToolDetector:
|
||||
)
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
return True, result.get("choices", [{}])[0].get("message", {}).get("content", "")
|
||||
return True, result.get("choices", [{}])[0].get(
|
||||
"message", {}
|
||||
).get("content", "")
|
||||
except Exception as e:
|
||||
return False, f"Router error: {e}"
|
||||
|
||||
@@ -460,6 +474,8 @@ class ToolDetector:
|
||||
if success:
|
||||
return True, output, tool
|
||||
else:
|
||||
self.console.print(f"[yellow]{tool.display_name} failed: {output}[/yellow]")
|
||||
self.console.print(
|
||||
f"[yellow]{tool.display_name} failed: {output}[/yellow]"
|
||||
)
|
||||
|
||||
return False, "No available tools could handle the request", None
|
||||
|
||||
@@ -33,7 +33,9 @@ class InlineStartup:
|
||||
# Check last shown time
|
||||
if self.last_shown_file.exists():
|
||||
try:
|
||||
last_shown = datetime.fromisoformat(self.last_shown_file.read_text().strip())
|
||||
last_shown = datetime.fromisoformat(
|
||||
self.last_shown_file.read_text().strip()
|
||||
)
|
||||
if datetime.now() - last_shown < self.show_interval:
|
||||
return False
|
||||
except Exception:
|
||||
@@ -63,7 +65,9 @@ class InlineStartup:
|
||||
message.append("Router management, improved docs", style="yellow dim")
|
||||
|
||||
# Show panel
|
||||
console.print(Panel(message, box=box.MINIMAL, border_style="cyan", padding=(0, 1)))
|
||||
console.print(
|
||||
Panel(message, box=box.MINIMAL, border_style="cyan", padding=(0, 1))
|
||||
)
|
||||
|
||||
self.mark_shown()
|
||||
|
||||
|
||||
@@ -232,7 +232,9 @@ class StartupUI:
|
||||
# Check router status
|
||||
try:
|
||||
response = httpx.get("http://localhost:4000/health", timeout=1)
|
||||
router_status = "🟢 Running" if response.status_code == 200 else "🔴 Offline"
|
||||
router_status = (
|
||||
"🟢 Running" if response.status_code == 200 else "🔴 Offline"
|
||||
)
|
||||
except Exception:
|
||||
router_status = "⚫ Offline"
|
||||
|
||||
@@ -256,7 +258,9 @@ class StartupUI:
|
||||
status.append("API: ", style="bold")
|
||||
status.append(api_status, style="white")
|
||||
|
||||
return Panel(Align.center(status), box=box.ROUNDED, border_style="blue", padding=(0, 1))
|
||||
return Panel(
|
||||
Align.center(status), box=box.ROUNDED, border_style="blue", padding=(0, 1)
|
||||
)
|
||||
|
||||
def _create_qr_panel(self) -> Panel:
|
||||
"""Create compact QR code panel for device connection."""
|
||||
@@ -299,7 +303,9 @@ class StartupUI:
|
||||
gpu_info = caps.chip[:20]
|
||||
except Exception:
|
||||
# Fallback - basic connection info
|
||||
connection_data = json.dumps({"id": socket.gethostname(), "host": local_ip, "type": "hanzo-node"})
|
||||
connection_data = json.dumps(
|
||||
{"id": socket.gethostname(), "host": local_ip, "type": "hanzo-node"}
|
||||
)
|
||||
gpu_info = "CPU"
|
||||
|
||||
# Generate smallest possible QR code
|
||||
|
||||
@@ -39,7 +39,11 @@ def get_iam_token() -> Optional[str]:
|
||||
try:
|
||||
auth = json.loads(AUTH_FILE.read_text())
|
||||
# CLI login stores flat "token", worker/IAM stores nested "tokens.access_token"
|
||||
return auth.get("token") or auth.get("api_key") or auth.get("tokens", {}).get("access_token")
|
||||
return (
|
||||
auth.get("token")
|
||||
or auth.get("api_key")
|
||||
or auth.get("tokens", {}).get("access_token")
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
@@ -272,7 +276,9 @@ class PaaSClient:
|
||||
def get(self, url: str, **kwargs) -> Optional[dict]:
|
||||
return self._request("GET", url, **kwargs)
|
||||
|
||||
def post(self, url: str, payload: Optional[dict] = None, **kwargs) -> Optional[dict]:
|
||||
def post(
|
||||
self, url: str, payload: Optional[dict] = None, **kwargs
|
||||
) -> Optional[dict]:
|
||||
return self._request("POST", url, json=payload, **kwargs)
|
||||
|
||||
def put(self, url: str, payload: Optional[dict] = None, **kwargs) -> Optional[dict]:
|
||||
@@ -306,7 +312,9 @@ class PaaSClient:
|
||||
if resp.status_code == 401:
|
||||
new_token = self._reauth()
|
||||
if new_token:
|
||||
resp = client.request(method, url, headers=self.headers, **kwargs)
|
||||
resp = client.request(
|
||||
method, url, headers=self.headers, **kwargs
|
||||
)
|
||||
else:
|
||||
return self._handle_response(resp)
|
||||
|
||||
|
||||
@@ -14,7 +14,11 @@ def get_config_paths() -> Dict[str, Path]:
|
||||
|
||||
# System config
|
||||
if os.name == "nt": # Windows
|
||||
paths["system"] = Path(os.environ.get("PROGRAMDATA", "C:\\ProgramData")) / "hanzo" / "config.yaml"
|
||||
paths["system"] = (
|
||||
Path(os.environ.get("PROGRAMDATA", "C:\\ProgramData"))
|
||||
/ "hanzo"
|
||||
/ "config.yaml"
|
||||
)
|
||||
else: # Unix-like
|
||||
paths["system"] = Path("/etc/hanzo/config.yaml")
|
||||
|
||||
@@ -138,7 +142,9 @@ def init_config() -> Dict[str, Path]:
|
||||
|
||||
def get_default_model() -> str:
|
||||
"""Get default model from config or environment."""
|
||||
return os.environ.get("HANZO_DEFAULT_MODEL") or get_config_value("default_model", "llama-3.2-3b")
|
||||
return os.environ.get("HANZO_DEFAULT_MODEL") or get_config_value(
|
||||
"default_model", "llama-3.2-3b"
|
||||
)
|
||||
|
||||
|
||||
def get_api_key(provider: str) -> Optional[str]:
|
||||
@@ -161,4 +167,6 @@ def get_api_key(provider: str) -> Optional[str]:
|
||||
|
||||
def is_local_preferred() -> bool:
|
||||
"""Check if local execution is preferred."""
|
||||
return os.environ.get("HANZO_USE_LOCAL", "").lower() == "true" or get_config_value("prefer_local", False)
|
||||
return os.environ.get("HANZO_USE_LOCAL", "").lower() == "true" or get_config_value(
|
||||
"prefer_local", False
|
||||
)
|
||||
|
||||
@@ -44,7 +44,9 @@ def check_net_installation() -> Tuple[bool, Optional[str], Optional[str]]:
|
||||
return False, str(net_path), str(venv_python)
|
||||
|
||||
# No venv, check system Python
|
||||
result = subprocess.run([sys.executable, "-c", "import scapy"], capture_output=True, text=True)
|
||||
result = subprocess.run(
|
||||
[sys.executable, "-c", "import scapy"], capture_output=True, text=True
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
return True, str(net_path), sys.executable
|
||||
@@ -102,7 +104,9 @@ def get_missing_dependencies(python_exe: str = None) -> list:
|
||||
|
||||
missing = []
|
||||
for package in required_packages:
|
||||
result = subprocess.run([python_exe, "-c", f"import {package}"], capture_output=True, text=True)
|
||||
result = subprocess.run(
|
||||
[python_exe, "-c", f"import {package}"], capture_output=True, text=True
|
||||
)
|
||||
if result.returncode != 0:
|
||||
missing.append(package)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user