mirror of
https://github.com/hanzoai/python-sdk.git
synced 2026-10-23 20:23:00 +00:00
feat(agent): agentic proxy with agent integration
Enhanced agent tool architecture: - 8 CLI agents: claude, codex, gemini, grok, qwen, vibe, code, dev - Auto-detects agent environment for seamless auth - Shares hanzo-mcp config with spawned agents - Lightweight - no heavy litellm dependency Essential tools reorganization: - Added agent, think, critic to essential tools - Removed llm/consensus from essential (heavy deps, opt-in) - HEAVY_TOOLS constant for tracking opt-in tools Config sharing: - HANZO_AGENT_PARENT=true passed to child agents - MCP config env vars propagated - API keys shared when appropriate
This commit is contained in:
@@ -11,10 +11,12 @@ from hanzo_mcp.tools.common.mode import (
|
||||
|
||||
# Essential system tools that are ALWAYS enabled regardless of mode
|
||||
# These are core infrastructure tools that should never be disabled
|
||||
# NOTE: llm and consensus are NOT essential (heavy litellm dependency)
|
||||
ESSENTIAL_SYSTEM_TOOLS: Set[str] = {
|
||||
# Core LLM infrastructure
|
||||
"llm", # Primary LLM interaction
|
||||
"consensus", # Multi-model consensus
|
||||
# Reasoning (lightweight)
|
||||
"think", # Structured thinking
|
||||
"critic", # Critical analysis
|
||||
"agent", # Lightweight agent spawning
|
||||
# Configuration and mode management
|
||||
"config", # System configuration
|
||||
"mode", # Mode switching
|
||||
@@ -27,6 +29,13 @@ ESSENTIAL_SYSTEM_TOOLS: Set[str] = {
|
||||
"memory", # Unified memory operations
|
||||
}
|
||||
|
||||
# Heavy tools that are disabled by default (opt-in)
|
||||
# These require large dependencies like litellm
|
||||
HEAVY_TOOLS: Set[str] = {
|
||||
"llm", # Requires litellm (~100MB+ deps)
|
||||
"consensus", # Requires litellm
|
||||
}
|
||||
|
||||
|
||||
class ModeLoader:
|
||||
"""Loads and manages tool modes for dynamic configuration."""
|
||||
|
||||
@@ -99,6 +99,7 @@ class PersonalityRegistry:
|
||||
|
||||
|
||||
# Essential tools that are always available in every mode
|
||||
# NOTE: llm/consensus are NOT essential (heavy litellm dependency - opt-in only)
|
||||
ESSENTIAL_TOOLS = [
|
||||
# File operations
|
||||
"read",
|
||||
@@ -112,12 +113,10 @@ ESSENTIAL_TOOLS = [
|
||||
"open",
|
||||
# Memory
|
||||
"memory",
|
||||
# Reasoning
|
||||
# Reasoning (lightweight)
|
||||
"think",
|
||||
"critic",
|
||||
# LLM infrastructure (always available)
|
||||
"llm",
|
||||
"consensus",
|
||||
"agent", # Lightweight agent spawning (claude, codex, etc.)
|
||||
# Configuration
|
||||
"config",
|
||||
"mode",
|
||||
@@ -125,6 +124,13 @@ ESSENTIAL_TOOLS = [
|
||||
"tool",
|
||||
]
|
||||
|
||||
# Heavy tools (require large dependencies like litellm)
|
||||
# These are opt-in only, not included by default
|
||||
HEAVY_TOOLS = [
|
||||
"llm", # Requires litellm (~100MB deps)
|
||||
"consensus", # Requires litellm
|
||||
]
|
||||
|
||||
# Common tool sets for reuse
|
||||
UNIX_TOOLS = ["search", "find", "dag", "ps", "zsh"]
|
||||
BUILD_TOOLS = ["dag", "npx", "uvx", "ps"]
|
||||
|
||||
@@ -4,11 +4,10 @@ from pathlib import Path
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
|
||||
from hanzo_tools.agent import TOOLS, register_tools
|
||||
from hanzo_tools.agent.unified_agent_tool import UnifiedAgentTool
|
||||
from hanzo_tools.agent.iching_tool import IChingTool
|
||||
from hanzo_tools.agent.review_tool import ReviewTool
|
||||
from hanzo_tools.agent.unified_agent_tool import UnifiedAgentTool
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -39,10 +38,18 @@ class TestUnifiedAgentTools:
|
||||
def test_unified_agent_tool_agents(self):
|
||||
"""Test UnifiedAgentTool has expected agents."""
|
||||
tool = UnifiedAgentTool()
|
||||
# Core agents
|
||||
assert "claude" in tool.AGENTS
|
||||
assert "codex" in tool.AGENTS
|
||||
assert "gemini" in tool.AGENTS
|
||||
assert "grok" in tool.AGENTS
|
||||
# Extended agents
|
||||
assert "qwen" in tool.AGENTS
|
||||
assert "vibe" in tool.AGENTS
|
||||
assert "code" in tool.AGENTS
|
||||
assert "dev" in tool.AGENTS
|
||||
# Total 8 agents
|
||||
assert len(tool.AGENTS) == 8
|
||||
|
||||
def test_unified_agent_tool_list_agents(self):
|
||||
"""Test agent listing."""
|
||||
@@ -88,6 +95,30 @@ class TestUnifiedAgentTools:
|
||||
assert iching.name == "iching"
|
||||
assert review.name == "review"
|
||||
|
||||
def test_agent_config_sharing(self):
|
||||
"""Test agent shares MCP config with spawned agents."""
|
||||
tool = UnifiedAgentTool()
|
||||
config = tool._mcp_config
|
||||
# Config is a dict
|
||||
assert isinstance(config, dict)
|
||||
|
||||
def test_claude_env_detection(self):
|
||||
"""Test Claude Code environment detection."""
|
||||
tool = UnifiedAgentTool()
|
||||
env = tool._claude_env
|
||||
# Should have expected keys
|
||||
assert "running_in_claude" in env
|
||||
assert "session_id" in env
|
||||
assert "auth_token" in env
|
||||
assert "api_key" in env
|
||||
|
||||
def test_default_agent_selection(self):
|
||||
"""Test default agent selection logic."""
|
||||
tool = UnifiedAgentTool()
|
||||
default = tool._get_default_agent()
|
||||
# Should return a valid agent name
|
||||
assert default in tool.AGENTS
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
|
||||
@@ -21,24 +21,24 @@ import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Core consolidated tools
|
||||
from .unified_agent_tool import UnifiedAgentTool
|
||||
from .critic_tool import CriticTool # Legacy - use reasoning.critic instead
|
||||
from .iching_tool import IChingTool
|
||||
from .review_tool import ReviewTool
|
||||
from .grok_cli_tool import GrokCLITool
|
||||
|
||||
# Legacy imports for backwards compatibility
|
||||
from .cli_agent_base import CLIAgentBase
|
||||
from .claude_cli_tool import ClaudeCLITool
|
||||
from .codex_cli_tool import CodexCLITool
|
||||
from .claude_cli_tool import ClaudeCLITool
|
||||
from .gemini_cli_tool import GeminiCLITool
|
||||
from .grok_cli_tool import GrokCLITool
|
||||
from .critic_tool import CriticTool # Legacy - use reasoning.critic instead
|
||||
from .unified_agent_tool import UnifiedAgentTool
|
||||
|
||||
# Export list for tool discovery - consolidated to 3 tools
|
||||
# Note: critic is from hanzo-tools-reasoning, not here
|
||||
TOOLS = [
|
||||
UnifiedAgentTool, # Unified agent runner
|
||||
IChingTool, # I Ching wisdom
|
||||
ReviewTool, # Code review
|
||||
IChingTool, # I Ching wisdom
|
||||
ReviewTool, # Code review
|
||||
]
|
||||
|
||||
__all__ = [
|
||||
|
||||
@@ -29,15 +29,16 @@ with warnings.catch_warnings():
|
||||
from pydantic import Field
|
||||
from openai.types.chat import ChatCompletionMessageParam
|
||||
from mcp.server.fastmcp import Context as MCPContext
|
||||
from hanzo_tools.jupyter import get_read_only_jupyter_tools
|
||||
from hanzo_tools.filesystem import get_read_only_filesystem_tools
|
||||
|
||||
from hanzo_tools.core import (
|
||||
BaseTool,
|
||||
PermissionManager,
|
||||
ToolContext,
|
||||
create_tool_context,
|
||||
PermissionManager,
|
||||
auto_timeout,
|
||||
create_tool_context,
|
||||
)
|
||||
from hanzo_tools.jupyter import get_read_only_jupyter_tools
|
||||
from hanzo_tools.filesystem import get_read_only_filesystem_tools
|
||||
|
||||
from .prompt import (
|
||||
get_default_model,
|
||||
|
||||
@@ -89,9 +89,9 @@ except ImportError:
|
||||
self.metadata = metadata or {}
|
||||
|
||||
|
||||
from hanzo_tools.core import BaseTool, PermissionManager, create_tool_context
|
||||
from hanzo_tools.jupyter import get_read_only_jupyter_tools
|
||||
from hanzo_tools.filesystem import Edit, get_read_only_filesystem_tools
|
||||
from hanzo_tools.core import BaseTool, PermissionManager, create_tool_context
|
||||
|
||||
from .critic_tool import CriticTool
|
||||
from .iching_tool import IChingTool
|
||||
|
||||
@@ -54,7 +54,8 @@ class ClaudeDesktopAuth:
|
||||
# Check if claude command is available
|
||||
try:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"which", "claude",
|
||||
"which",
|
||||
"claude",
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
@@ -267,12 +267,13 @@ class CodeAuthManager:
|
||||
# Check if claude command exists
|
||||
try:
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"which", "claude",
|
||||
"which",
|
||||
"claude",
|
||||
stdout=asyncio.subprocess.DEVNULL,
|
||||
stderr=asyncio.subprocess.DEVNULL,
|
||||
)
|
||||
await asyncio.wait_for(process.wait(), timeout=5)
|
||||
|
||||
|
||||
if process.returncode == 0:
|
||||
# Claude command exists, update its config
|
||||
claude_config = Path.home() / ".claude" / "config.json"
|
||||
@@ -296,9 +297,10 @@ class CodeAuthManager:
|
||||
"""Update claude command configuration if needed (sync wrapper)."""
|
||||
# Use sync check for existence, skip if not found
|
||||
import shutil
|
||||
|
||||
if not shutil.which("claude"):
|
||||
return
|
||||
|
||||
|
||||
claude_config = Path.home() / ".claude" / "config.json"
|
||||
if claude_config.exists():
|
||||
try:
|
||||
|
||||
@@ -44,10 +44,7 @@ except ImportError:
|
||||
"""Metaclass that raises ImportError when stub class is instantiated."""
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
raise ImportError(
|
||||
f"{cls.__name__} requires hanzo-agents package. "
|
||||
"Install with: pip install hanzo-agents"
|
||||
)
|
||||
raise ImportError(f"{cls.__name__} requires hanzo-agents package. Install with: pip install hanzo-agents")
|
||||
|
||||
class Agent(metaclass=_StubMeta):
|
||||
"""Stub - requires hanzo-agents."""
|
||||
@@ -104,10 +101,7 @@ def _stub_factory(name: str):
|
||||
"""Create a stub function that raises ImportError."""
|
||||
|
||||
def stub(*args, **kwargs):
|
||||
raise ImportError(
|
||||
f"{name} requires hanzo-agents package. "
|
||||
"Install with: pip install hanzo-agents"
|
||||
)
|
||||
raise ImportError(f"{name} requires hanzo-agents package. Install with: pip install hanzo-agents")
|
||||
|
||||
stub.__name__ = name
|
||||
return stub
|
||||
@@ -160,9 +154,9 @@ except ImportError:
|
||||
"""Stub - requires hanzo-agents."""
|
||||
|
||||
|
||||
from hanzo_tools.core import BaseTool, PermissionManager, create_tool_context
|
||||
from hanzo_tools.jupyter import get_read_only_jupyter_tools
|
||||
from hanzo_tools.filesystem import Edit, get_read_only_filesystem_tools
|
||||
from hanzo_tools.core import BaseTool, PermissionManager, create_tool_context
|
||||
|
||||
from .agent_tool import MCPAgent
|
||||
|
||||
|
||||
@@ -1,10 +1,26 @@
|
||||
"""Unified agent tool - run any agent by name.
|
||||
"""Unified agent tool - lightweight agent spawning.
|
||||
|
||||
Single tool that dispatches to installed CLI agents (claude, codex, gemini, grok)
|
||||
and orchestrates multi-agent workflows.
|
||||
Single tool that dispatches to installed CLI agents and supports:
|
||||
- claude: Claude Code CLI (default when running in Claude Code)
|
||||
- codex: OpenAI Codex CLI
|
||||
- gemini: Google Gemini CLI
|
||||
- grok: xAI Grok CLI
|
||||
- qwen: Alibaba Qwen CLI
|
||||
- vibe: Vibe coding agent
|
||||
- code: Hanzo Code agent
|
||||
- dev: Hanzo Dev agent (default)
|
||||
|
||||
Key features:
|
||||
- Auto-detects Claude Code environment and uses same auth
|
||||
- Shares hanzo-mcp config with spawned agents
|
||||
- Lightweight - no heavy dependencies
|
||||
"""
|
||||
|
||||
from typing import Optional, List, Dict, Any, Literal, final, override, Annotated
|
||||
import os
|
||||
import json
|
||||
import asyncio
|
||||
from typing import List, Literal, Optional, Annotated, final, override
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import Field
|
||||
from mcp.server import FastMCP
|
||||
@@ -12,22 +28,85 @@ from mcp.server.fastmcp import Context as MCPContext
|
||||
|
||||
from hanzo_tools.core import BaseTool, auto_timeout, create_tool_context
|
||||
|
||||
|
||||
Action = Annotated[
|
||||
Literal[
|
||||
"run", # Run a specific agent
|
||||
"list", # List available agents
|
||||
"status", # Check agent status
|
||||
"run", # Run a specific agent
|
||||
"list", # List available agents
|
||||
"status", # Check agent status
|
||||
"config", # Show/set agent config
|
||||
],
|
||||
Field(description="Agent action to perform"),
|
||||
]
|
||||
|
||||
|
||||
def detect_claude_code_env() -> dict:
|
||||
"""Detect if running inside Claude Code and get auth info.
|
||||
|
||||
Returns dict with:
|
||||
running_in_claude: bool
|
||||
session_id: Optional[str]
|
||||
auth_token: Optional[str]
|
||||
api_key: Optional[str]
|
||||
"""
|
||||
result = {
|
||||
"running_in_claude": False,
|
||||
"session_id": None,
|
||||
"auth_token": None,
|
||||
"api_key": None,
|
||||
}
|
||||
|
||||
# Check Claude Code environment markers
|
||||
if os.environ.get("CLAUDE_CODE") or os.environ.get("CLAUDE_SESSION_ID"):
|
||||
result["running_in_claude"] = True
|
||||
result["session_id"] = os.environ.get("CLAUDE_SESSION_ID")
|
||||
|
||||
# Check for Claude auth
|
||||
if os.environ.get("ANTHROPIC_API_KEY"):
|
||||
result["api_key"] = os.environ.get("ANTHROPIC_API_KEY")
|
||||
|
||||
# Check Claude Desktop config for OAuth tokens
|
||||
claude_config_path = Path.home() / "Library/Application Support/Claude/claude_desktop_config.json"
|
||||
if claude_config_path.exists():
|
||||
try:
|
||||
with open(claude_config_path) as f:
|
||||
config = json.load(f)
|
||||
if "mcpServers" in config and "hanzo-mcp" in config["mcpServers"]:
|
||||
result["running_in_claude"] = True
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def get_mcp_config() -> dict:
|
||||
"""Get current hanzo-mcp config to share with spawned agents."""
|
||||
config = {}
|
||||
|
||||
# Get relevant environment variables
|
||||
mcp_env_vars = [
|
||||
"HANZO_MCP_MODE",
|
||||
"HANZO_MCP_ALLOWED_PATHS",
|
||||
"HANZO_MCP_ENABLED_TOOLS",
|
||||
"HANZO_MCP_PERSONA",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"OPENAI_API_KEY",
|
||||
]
|
||||
|
||||
for var in mcp_env_vars:
|
||||
if os.environ.get(var):
|
||||
config[var] = os.environ.get(var)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
@final
|
||||
class UnifiedAgentTool(BaseTool):
|
||||
"""Unified agent tool for running CLI agents.
|
||||
|
||||
Dispatches to: claude, codex, gemini, grok or custom agents.
|
||||
Lightweight agent spawning that:
|
||||
- Auto-detects Claude Code environment
|
||||
- Shares hanzo-mcp config with child agents
|
||||
- Supports multiple agent backends
|
||||
"""
|
||||
|
||||
name = "agent"
|
||||
@@ -36,56 +115,123 @@ class UnifiedAgentTool(BaseTool):
|
||||
AGENTS = {
|
||||
"claude": {
|
||||
"command": "claude",
|
||||
"description": "Anthropic Claude Code CLI",
|
||||
"args": ["-p"], # Use -p for print mode (non-interactive)
|
||||
"description": "Anthropic Claude Code CLI (recommended when in Claude)",
|
||||
"check": ["claude", "--version"],
|
||||
"env_key": "ANTHROPIC_API_KEY",
|
||||
"priority": 1, # Highest priority when in Claude env
|
||||
},
|
||||
"codex": {
|
||||
"command": "codex",
|
||||
"args": [],
|
||||
"description": "OpenAI Codex CLI",
|
||||
"check": ["codex", "--version"],
|
||||
"env_key": "OPENAI_API_KEY",
|
||||
"priority": 2,
|
||||
},
|
||||
"gemini": {
|
||||
"command": "gemini",
|
||||
"args": [],
|
||||
"description": "Google Gemini CLI",
|
||||
"check": ["gemini", "--version"],
|
||||
"env_key": "GOOGLE_API_KEY",
|
||||
"priority": 3,
|
||||
},
|
||||
"grok": {
|
||||
"command": "grok",
|
||||
"args": [],
|
||||
"description": "xAI Grok CLI",
|
||||
"check": ["grok", "--version"],
|
||||
"env_key": "XAI_API_KEY",
|
||||
"priority": 4,
|
||||
},
|
||||
"qwen": {
|
||||
"command": "qwen",
|
||||
"args": [],
|
||||
"description": "Alibaba Qwen CLI",
|
||||
"check": ["qwen", "--version"],
|
||||
"env_key": "DASHSCOPE_API_KEY",
|
||||
"priority": 5,
|
||||
},
|
||||
"vibe": {
|
||||
"command": "vibe",
|
||||
"args": [],
|
||||
"description": "Vibe coding agent",
|
||||
"check": ["vibe", "--version"],
|
||||
"priority": 6,
|
||||
},
|
||||
"code": {
|
||||
"command": "hanzo-code",
|
||||
"args": [],
|
||||
"description": "Hanzo Code agent",
|
||||
"check": ["hanzo-code", "--version"],
|
||||
"priority": 7,
|
||||
},
|
||||
"dev": {
|
||||
"command": "hanzo-dev",
|
||||
"args": [],
|
||||
"description": "Hanzo Dev agent (full development assistant)",
|
||||
"check": ["hanzo-dev", "--version"],
|
||||
"priority": 8,
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._claude_env = detect_claude_code_env()
|
||||
self._mcp_config = get_mcp_config()
|
||||
|
||||
@property
|
||||
@override
|
||||
def description(self) -> str:
|
||||
return """Run AI agents by name.
|
||||
default_agent = self._get_default_agent()
|
||||
return f"""Run AI agents by name. Lightweight agent spawning.
|
||||
|
||||
Actions:
|
||||
- run: Execute an agent with a prompt
|
||||
- run: Execute an agent with a prompt (default: {default_agent})
|
||||
- list: List available agents
|
||||
- status: Check if an agent is available
|
||||
- status: Check agent availability
|
||||
- config: Show/set agent configuration
|
||||
|
||||
Agents: claude, codex, gemini, grok
|
||||
Agents: claude, codex, gemini, grok, qwen, vibe, code, dev
|
||||
|
||||
Examples:
|
||||
agent run --name claude --prompt "Explain this code"
|
||||
agent run --name codex --prompt "Write a function" --cwd /path/to/project
|
||||
agent run --prompt "Explain this code" # Uses default agent
|
||||
agent run --name claude --prompt "Review this PR"
|
||||
agent run --name dev --prompt "Fix the build" --cwd /project
|
||||
agent list
|
||||
agent status --name claude
|
||||
agent status
|
||||
|
||||
{"⚡ Running in Claude Code - claude agent uses same auth" if self._claude_env["running_in_claude"] else ""}
|
||||
"""
|
||||
|
||||
def _get_default_agent(self) -> str:
|
||||
"""Get the default agent based on environment."""
|
||||
# If running in Claude Code, prefer claude
|
||||
if self._claude_env.get("running_in_claude"):
|
||||
return "claude"
|
||||
|
||||
# Otherwise check which agents are configured
|
||||
for name, config in sorted(self.AGENTS.items(), key=lambda x: x[1].get("priority", 99)):
|
||||
env_key = config.get("env_key")
|
||||
if env_key and os.environ.get(env_key):
|
||||
return name
|
||||
|
||||
# Default fallback
|
||||
return "dev"
|
||||
|
||||
@override
|
||||
@auto_timeout("agent")
|
||||
async def call(
|
||||
self,
|
||||
ctx: MCPContext,
|
||||
action: str = "list",
|
||||
action: str = "run",
|
||||
name: Optional[str] = None,
|
||||
prompt: Optional[str] = None,
|
||||
cwd: Optional[str] = None,
|
||||
args: Optional[List[str]] = None,
|
||||
timeout: int = 300,
|
||||
share_config: bool = True,
|
||||
**kwargs,
|
||||
) -> str:
|
||||
"""Execute agent operation."""
|
||||
@@ -96,31 +242,75 @@ Examples:
|
||||
return self._list_agents()
|
||||
elif action == "status":
|
||||
return await self._check_status(name)
|
||||
elif action == "config":
|
||||
return self._show_config()
|
||||
elif action == "run":
|
||||
return await self._run_agent(name, prompt, cwd, args, timeout)
|
||||
# Use default agent if not specified
|
||||
agent_name = name or self._get_default_agent()
|
||||
return await self._run_agent(agent_name, prompt, cwd, args, timeout, share_config)
|
||||
else:
|
||||
return f"Unknown action: {action}. Use: run, list, status"
|
||||
return f"Unknown action: {action}. Use: run, list, status, config"
|
||||
|
||||
def _list_agents(self) -> str:
|
||||
"""List available agents."""
|
||||
default = self._get_default_agent()
|
||||
lines = ["Available agents:"]
|
||||
for name, config in self.AGENTS.items():
|
||||
lines.append(f" • {name}: {config['description']}")
|
||||
|
||||
for name, config in sorted(self.AGENTS.items(), key=lambda x: x[1].get("priority", 99)):
|
||||
marker = " (default)" if name == default else ""
|
||||
lines.append(f" • {name}: {config['description']}{marker}")
|
||||
|
||||
lines.append("")
|
||||
lines.append("Usage: agent run --name <agent> --prompt 'your prompt'")
|
||||
lines.append("Usage: agent run --prompt 'your prompt'")
|
||||
lines.append(f" agent run --name <agent> --prompt 'prompt'")
|
||||
|
||||
if self._claude_env.get("running_in_claude"):
|
||||
lines.append("")
|
||||
lines.append("⚡ Running in Claude Code - using same authentication")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _show_config(self) -> str:
|
||||
"""Show current agent configuration."""
|
||||
lines = ["Agent Configuration:"]
|
||||
lines.append(f" Default agent: {self._get_default_agent()}")
|
||||
lines.append(f" Running in Claude: {self._claude_env.get('running_in_claude', False)}")
|
||||
|
||||
if self._claude_env.get("session_id"):
|
||||
lines.append(f" Claude session: {self._claude_env['session_id'][:8]}...")
|
||||
|
||||
lines.append("")
|
||||
lines.append("MCP Config (shared with spawned agents):")
|
||||
for key, value in self._mcp_config.items():
|
||||
# Mask sensitive values
|
||||
if "KEY" in key or "TOKEN" in key:
|
||||
display = value[:8] + "..." if len(value) > 8 else "***"
|
||||
else:
|
||||
display = value
|
||||
lines.append(f" {key}: {display}")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
async def _check_status(self, name: Optional[str]) -> str:
|
||||
"""Check if an agent is available."""
|
||||
import asyncio
|
||||
|
||||
"""Check if agents are available."""
|
||||
if not name:
|
||||
# Check all agents
|
||||
results = []
|
||||
for agent_name, config in self.AGENTS.items():
|
||||
for agent_name, config in sorted(self.AGENTS.items(), key=lambda x: x[1].get("priority", 99)):
|
||||
available = await self._is_available(config["check"])
|
||||
status = "✓ available" if available else "✗ not found"
|
||||
|
||||
# Check for API key
|
||||
env_key = config.get("env_key")
|
||||
has_key = bool(env_key and os.environ.get(env_key))
|
||||
|
||||
if available:
|
||||
key_status = "✓ key" if has_key else "○ no key"
|
||||
status = f"✓ installed ({key_status})"
|
||||
else:
|
||||
status = "✗ not found"
|
||||
|
||||
results.append(f" {agent_name}: {status}")
|
||||
|
||||
return "Agent status:\n" + "\n".join(results)
|
||||
|
||||
if name not in self.AGENTS:
|
||||
@@ -134,8 +324,6 @@ Examples:
|
||||
|
||||
async def _is_available(self, check_cmd: List[str]) -> bool:
|
||||
"""Check if a command is available."""
|
||||
import asyncio
|
||||
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*check_cmd,
|
||||
@@ -149,18 +337,14 @@ Examples:
|
||||
|
||||
async def _run_agent(
|
||||
self,
|
||||
name: Optional[str],
|
||||
name: str,
|
||||
prompt: Optional[str],
|
||||
cwd: Optional[str],
|
||||
args: Optional[List[str]],
|
||||
timeout: int,
|
||||
share_config: bool,
|
||||
) -> str:
|
||||
"""Run an agent with a prompt."""
|
||||
import asyncio
|
||||
import os
|
||||
|
||||
if not name:
|
||||
return "Error: name required for run action"
|
||||
if not prompt:
|
||||
return "Error: prompt required for run action"
|
||||
|
||||
@@ -169,18 +353,28 @@ Examples:
|
||||
|
||||
config = self.AGENTS[name]
|
||||
command = config["command"]
|
||||
default_args = config.get("args", [])
|
||||
|
||||
# Build command
|
||||
cmd_args = [command, prompt]
|
||||
cmd_args = [command] + default_args + [prompt]
|
||||
if args:
|
||||
cmd_args.extend(args)
|
||||
|
||||
# Build environment with shared MCP config
|
||||
env = os.environ.copy()
|
||||
if share_config:
|
||||
env.update(self._mcp_config)
|
||||
# Mark that this is a child agent
|
||||
env["HANZO_AGENT_PARENT"] = "true"
|
||||
env["HANZO_AGENT_NAME"] = name
|
||||
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*cmd_args,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=cwd or os.getcwd(),
|
||||
env=env,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -202,7 +396,17 @@ Examples:
|
||||
return f"Agent {name} timed out after {timeout}s"
|
||||
|
||||
except FileNotFoundError:
|
||||
return f"Agent {name} not found. Install it or check PATH."
|
||||
# Provide helpful installation instructions
|
||||
install_hints = {
|
||||
"claude": "npm install -g @anthropic-ai/claude-code",
|
||||
"codex": "npm install -g @openai/codex",
|
||||
"gemini": "pip install google-generativeai",
|
||||
"grok": "pip install xai-grok",
|
||||
"dev": "pip install hanzo-dev",
|
||||
"code": "pip install hanzo-code",
|
||||
}
|
||||
hint = install_hints.get(name, f"Install the {name} CLI")
|
||||
return f"Agent {name} not found.\n\nTo install: {hint}"
|
||||
except Exception as e:
|
||||
return f"Error running {name}: {e}"
|
||||
|
||||
@@ -212,15 +416,22 @@ Examples:
|
||||
|
||||
@mcp_server.tool()
|
||||
async def agent(
|
||||
action: Action = "list",
|
||||
name: Annotated[Optional[str], Field(description="Agent name: claude, codex, gemini, grok")] = None,
|
||||
action: Action = "run",
|
||||
name: Annotated[
|
||||
Optional[str], Field(description="Agent: claude, codex, gemini, grok, qwen, vibe, code, dev")
|
||||
] = None,
|
||||
prompt: Annotated[Optional[str], Field(description="Prompt for the agent")] = None,
|
||||
cwd: Annotated[Optional[str], Field(description="Working directory")] = None,
|
||||
args: Annotated[Optional[List[str]], Field(description="Additional arguments")] = None,
|
||||
timeout: Annotated[int, Field(description="Timeout in seconds")] = 300,
|
||||
share_config: Annotated[bool, Field(description="Share hanzo-mcp config with agent")] = True,
|
||||
ctx: MCPContext = None,
|
||||
) -> str:
|
||||
"""Run AI agents: claude, codex, gemini, grok."""
|
||||
"""Run AI agents: claude, codex, gemini, grok, qwen, vibe, code, dev.
|
||||
|
||||
Lightweight agent spawning with shared MCP config.
|
||||
Auto-detects Claude Code environment for seamless auth.
|
||||
"""
|
||||
return await tool_instance.call(
|
||||
ctx,
|
||||
action=action,
|
||||
@@ -229,4 +440,5 @@ Examples:
|
||||
cwd=cwd,
|
||||
args=args,
|
||||
timeout=timeout,
|
||||
share_config=share_config,
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,22 +11,22 @@ Performance optimizations:
|
||||
- Async-first design with no blocking in event loop
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import base64
|
||||
import functools
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import base64
|
||||
import asyncio
|
||||
import functools
|
||||
import subprocess
|
||||
from typing import Any, Literal, Optional, Annotated, final, override
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any, Literal, Optional, final, override
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from pydantic import Field
|
||||
from mcp.server import FastMCP
|
||||
from mcp.server.fastmcp import Context as MCPContext
|
||||
from pydantic import Field
|
||||
|
||||
from hanzo_tools.core import BaseTool, PermissionManager, auto_timeout
|
||||
|
||||
@@ -37,28 +37,56 @@ _EXECUTOR = ThreadPoolExecutor(max_workers=4, thread_name_prefix="computer_")
|
||||
# All supported actions
|
||||
Action = Literal[
|
||||
# Mouse actions
|
||||
"click", "double_click", "right_click", "middle_click",
|
||||
"move", "move_relative", "drag", "drag_relative", "scroll",
|
||||
"click",
|
||||
"double_click",
|
||||
"right_click",
|
||||
"middle_click",
|
||||
"move",
|
||||
"move_relative",
|
||||
"drag",
|
||||
"drag_relative",
|
||||
"scroll",
|
||||
# Keyboard actions
|
||||
"type", "write", "press", "key_down", "key_up", "hotkey",
|
||||
"type",
|
||||
"write",
|
||||
"press",
|
||||
"key_down",
|
||||
"key_up",
|
||||
"hotkey",
|
||||
# Screen actions
|
||||
"screenshot", "screenshot_region",
|
||||
"screenshot",
|
||||
"screenshot_region",
|
||||
# Image location
|
||||
"locate", "locate_all", "locate_center", "wait_for_image", "wait_while_image",
|
||||
"locate",
|
||||
"locate_all",
|
||||
"locate_center",
|
||||
"wait_for_image",
|
||||
"wait_while_image",
|
||||
# Pixel operations
|
||||
"pixel", "pixel_matches",
|
||||
"pixel",
|
||||
"pixel_matches",
|
||||
# Window management
|
||||
"get_active_window", "list_windows", "focus_window",
|
||||
"get_active_window",
|
||||
"list_windows",
|
||||
"focus_window",
|
||||
# Screen management
|
||||
"get_screens", "screen_size", "current_screen",
|
||||
"get_screens",
|
||||
"screen_size",
|
||||
"current_screen",
|
||||
# Region helpers
|
||||
"define_region", "region_screenshot", "region_locate",
|
||||
"define_region",
|
||||
"region_screenshot",
|
||||
"region_locate",
|
||||
# Timing/flow
|
||||
"sleep", "countdown", "set_pause", "set_failsafe",
|
||||
"sleep",
|
||||
"countdown",
|
||||
"set_pause",
|
||||
"set_failsafe",
|
||||
# Batch operations
|
||||
"batch",
|
||||
# Info
|
||||
"info", "position",
|
||||
"info",
|
||||
"position",
|
||||
]
|
||||
|
||||
|
||||
@@ -568,11 +596,13 @@ Examples:
|
||||
displays = []
|
||||
for gpu in data.get("SPDisplaysDataType", []):
|
||||
for disp in gpu.get("spdisplays_ndrvs", []):
|
||||
displays.append({
|
||||
"name": disp.get("_name", "Unknown"),
|
||||
"resolution": disp.get("_spdisplays_resolution", "Unknown"),
|
||||
"main": disp.get("spdisplays_main") == "spdisplays_yes",
|
||||
})
|
||||
displays.append(
|
||||
{
|
||||
"name": disp.get("_name", "Unknown"),
|
||||
"resolution": disp.get("_spdisplays_resolution", "Unknown"),
|
||||
"main": disp.get("spdisplays_main") == "spdisplays_yes",
|
||||
}
|
||||
)
|
||||
return json.dumps(displays, indent=2)
|
||||
except Exception as e:
|
||||
pass
|
||||
@@ -611,16 +641,16 @@ Examples:
|
||||
pg = self._ensure_pyautogui()
|
||||
pos = pg.position()
|
||||
size = pg.size()
|
||||
return json.dumps({
|
||||
"size": {"width": size.width, "height": size.height},
|
||||
"mouse": {"x": pos.x, "y": pos.y},
|
||||
})
|
||||
return json.dumps(
|
||||
{
|
||||
"size": {"width": size.width, "height": size.height},
|
||||
"mouse": {"x": pos.x, "y": pos.y},
|
||||
}
|
||||
)
|
||||
|
||||
# ========== Image Location Methods ==========
|
||||
|
||||
def _locate(
|
||||
self, image_path: str, confidence: float, region: tuple | None
|
||||
) -> str:
|
||||
def _locate(self, image_path: str, confidence: float, region: tuple | None) -> str:
|
||||
pg = self._ensure_pyautogui()
|
||||
path = Path(image_path)
|
||||
if not path.exists():
|
||||
@@ -636,12 +666,18 @@ Examples:
|
||||
location = pg.locateOnScreen(str(path), **kwargs)
|
||||
if location:
|
||||
center = pg.center(location)
|
||||
return json.dumps({
|
||||
"found": True,
|
||||
"center": {"x": center.x, "y": center.y},
|
||||
"box": {"left": location.left, "top": location.top,
|
||||
"width": location.width, "height": location.height},
|
||||
})
|
||||
return json.dumps(
|
||||
{
|
||||
"found": True,
|
||||
"center": {"x": center.x, "y": center.y},
|
||||
"box": {
|
||||
"left": location.left,
|
||||
"top": location.top,
|
||||
"width": location.width,
|
||||
"height": location.height,
|
||||
},
|
||||
}
|
||||
)
|
||||
else:
|
||||
return json.dumps({"found": False, "message": "Image not found on screen"})
|
||||
except Exception as e:
|
||||
@@ -662,11 +698,12 @@ Examples:
|
||||
results = []
|
||||
for loc in locations:
|
||||
center = pg.center(loc)
|
||||
results.append({
|
||||
"center": {"x": center.x, "y": center.y},
|
||||
"box": {"left": loc.left, "top": loc.top,
|
||||
"width": loc.width, "height": loc.height},
|
||||
})
|
||||
results.append(
|
||||
{
|
||||
"center": {"x": center.x, "y": center.y},
|
||||
"box": {"left": loc.left, "top": loc.top, "width": loc.width, "height": loc.height},
|
||||
}
|
||||
)
|
||||
return json.dumps({"found": len(results), "locations": results})
|
||||
except Exception as e:
|
||||
return f"Error locating images: {str(e)}"
|
||||
@@ -690,9 +727,7 @@ Examples:
|
||||
except Exception as e:
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
async def _wait_for_image(
|
||||
self, image_path: str, timeout: float, confidence: float, run
|
||||
) -> str:
|
||||
async def _wait_for_image(self, image_path: str, timeout: float, confidence: float, run) -> str:
|
||||
"""Wait until image appears on screen."""
|
||||
path = Path(image_path)
|
||||
if not path.exists():
|
||||
@@ -704,19 +739,19 @@ Examples:
|
||||
data = json.loads(result) if result.startswith("{") else {}
|
||||
if data.get("found"):
|
||||
elapsed = time.time() - start
|
||||
return json.dumps({
|
||||
"found": True,
|
||||
"x": data["x"],
|
||||
"y": data["y"],
|
||||
"elapsed": round(elapsed, 2),
|
||||
})
|
||||
return json.dumps(
|
||||
{
|
||||
"found": True,
|
||||
"x": data["x"],
|
||||
"y": data["y"],
|
||||
"elapsed": round(elapsed, 2),
|
||||
}
|
||||
)
|
||||
await asyncio.sleep(0.1) # Faster polling
|
||||
|
||||
return json.dumps({"found": False, "timeout": timeout})
|
||||
|
||||
async def _wait_while_image(
|
||||
self, image_path: str, timeout: float, confidence: float, run
|
||||
) -> str:
|
||||
async def _wait_while_image(self, image_path: str, timeout: float, confidence: float, run) -> str:
|
||||
"""Wait while image is visible on screen."""
|
||||
path = Path(image_path)
|
||||
if not path.exists():
|
||||
@@ -744,22 +779,20 @@ Examples:
|
||||
except Exception as e:
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
def _pixel_matches(
|
||||
self, x: int, y: int, color: tuple[int, int, int], tolerance: int
|
||||
) -> str:
|
||||
def _pixel_matches(self, x: int, y: int, color: tuple[int, int, int], tolerance: int) -> str:
|
||||
pg = self._ensure_pyautogui()
|
||||
try:
|
||||
screenshot = pg.screenshot(region=(x, y, 1, 1))
|
||||
pixel = screenshot.getpixel((0, 0))
|
||||
matches = all(
|
||||
abs(pixel[i] - color[i]) <= tolerance for i in range(3)
|
||||
matches = all(abs(pixel[i] - color[i]) <= tolerance for i in range(3))
|
||||
return json.dumps(
|
||||
{
|
||||
"matches": matches,
|
||||
"expected": {"r": color[0], "g": color[1], "b": color[2]},
|
||||
"actual": {"r": pixel[0], "g": pixel[1], "b": pixel[2]},
|
||||
"tolerance": tolerance,
|
||||
}
|
||||
)
|
||||
return json.dumps({
|
||||
"matches": matches,
|
||||
"expected": {"r": color[0], "g": color[1], "b": color[2]},
|
||||
"actual": {"r": pixel[0], "g": pixel[1], "b": pixel[2]},
|
||||
"tolerance": tolerance,
|
||||
})
|
||||
except Exception as e:
|
||||
return f"Error: {str(e)}"
|
||||
|
||||
@@ -768,7 +801,7 @@ Examples:
|
||||
def _get_active_window(self) -> str:
|
||||
"""Get frontmost window info using AppleScript."""
|
||||
try:
|
||||
script = '''
|
||||
script = """
|
||||
tell application "System Events"
|
||||
set frontApp to first application process whose frontmost is true
|
||||
set appName to name of frontApp
|
||||
@@ -782,7 +815,7 @@ Examples:
|
||||
return appName & "|||" & "" & "|||0,0|||0,0"
|
||||
end try
|
||||
end tell
|
||||
'''
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["osascript", "-e", script],
|
||||
capture_output=True,
|
||||
@@ -794,12 +827,14 @@ Examples:
|
||||
if len(parts) >= 4:
|
||||
pos = parts[2].split(",")
|
||||
size = parts[3].split(",")
|
||||
return json.dumps({
|
||||
"app": parts[0],
|
||||
"title": parts[1],
|
||||
"position": {"x": int(pos[0]), "y": int(pos[1])},
|
||||
"size": {"width": int(size[0]), "height": int(size[1])},
|
||||
})
|
||||
return json.dumps(
|
||||
{
|
||||
"app": parts[0],
|
||||
"title": parts[1],
|
||||
"position": {"x": int(pos[0]), "y": int(pos[1])},
|
||||
"size": {"width": int(size[0]), "height": int(size[1])},
|
||||
}
|
||||
)
|
||||
return json.dumps({"error": "Could not get active window"})
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
@@ -807,7 +842,7 @@ Examples:
|
||||
def _list_windows(self) -> str:
|
||||
"""List all windows using AppleScript."""
|
||||
try:
|
||||
script = '''
|
||||
script = """
|
||||
set windowList to ""
|
||||
tell application "System Events"
|
||||
set allProcesses to application processes whose visible is true
|
||||
@@ -825,7 +860,7 @@ Examples:
|
||||
end repeat
|
||||
end tell
|
||||
return windowList
|
||||
'''
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["osascript", "-e", script],
|
||||
capture_output=True,
|
||||
@@ -840,12 +875,14 @@ Examples:
|
||||
if len(parts) >= 4:
|
||||
pos = parts[2].split(",")
|
||||
size = parts[3].split(",")
|
||||
windows.append({
|
||||
"app": parts[0],
|
||||
"title": parts[1],
|
||||
"position": {"x": int(pos[0]), "y": int(pos[1])},
|
||||
"size": {"width": int(size[0]), "height": int(size[1])},
|
||||
})
|
||||
windows.append(
|
||||
{
|
||||
"app": parts[0],
|
||||
"title": parts[1],
|
||||
"position": {"x": int(pos[0]), "y": int(pos[1])},
|
||||
"size": {"width": int(size[0]), "height": int(size[1])},
|
||||
}
|
||||
)
|
||||
return json.dumps({"windows": windows, "count": len(windows)})
|
||||
return json.dumps({"error": "Could not list windows", "windows": []})
|
||||
except Exception as e:
|
||||
@@ -950,7 +987,9 @@ Examples:
|
||||
elif action == "type":
|
||||
return await run(self._type_text, params.get("text", ""), params.get("interval", 0.02))
|
||||
elif action == "write":
|
||||
return await run(self._write_text, params.get("text", ""), params.get("clear", False), params.get("interval", 0.02))
|
||||
return await run(
|
||||
self._write_text, params.get("text", ""), params.get("clear", False), params.get("interval", 0.02)
|
||||
)
|
||||
elif action == "press":
|
||||
return await run(self._press_key, params.get("key", ""))
|
||||
elif action == "key_down":
|
||||
@@ -978,13 +1017,15 @@ Examples:
|
||||
pg = self._ensure_pyautogui()
|
||||
size = pg.size()
|
||||
pos = pg.position()
|
||||
return json.dumps({
|
||||
"screen": {"width": size.width, "height": size.height},
|
||||
"mouse": {"x": pos.x, "y": pos.y},
|
||||
"pause": self._pause,
|
||||
"failsafe": self._failsafe,
|
||||
"regions": list(self._defined_regions.keys()),
|
||||
})
|
||||
return json.dumps(
|
||||
{
|
||||
"screen": {"width": size.width, "height": size.height},
|
||||
"mouse": {"x": pos.x, "y": pos.y},
|
||||
"pause": self._pause,
|
||||
"failsafe": self._failsafe,
|
||||
"regions": list(self._defined_regions.keys()),
|
||||
}
|
||||
)
|
||||
|
||||
def _get_position(self) -> str:
|
||||
pg = self._ensure_pyautogui()
|
||||
@@ -1028,14 +1069,28 @@ Examples:
|
||||
return await tool_self.call(
|
||||
ctx,
|
||||
action=action,
|
||||
x=x, y=y, dx=dx, dy=dy,
|
||||
text=text, key=key, keys=keys,
|
||||
amount=amount, duration=duration, interval=interval,
|
||||
region=region, clear=clear,
|
||||
image_path=image_path, confidence=confidence, timeout=timeout,
|
||||
color=tuple(color) if color else None, tolerance=tolerance,
|
||||
title=title, use_regex=use_regex,
|
||||
name=name, width=width, height=height,
|
||||
x=x,
|
||||
y=y,
|
||||
dx=dx,
|
||||
dy=dy,
|
||||
text=text,
|
||||
key=key,
|
||||
keys=keys,
|
||||
amount=amount,
|
||||
duration=duration,
|
||||
interval=interval,
|
||||
region=region,
|
||||
clear=clear,
|
||||
image_path=image_path,
|
||||
confidence=confidence,
|
||||
timeout=timeout,
|
||||
color=tuple(color) if color else None,
|
||||
tolerance=tolerance,
|
||||
title=title,
|
||||
use_regex=use_regex,
|
||||
name=name,
|
||||
width=width,
|
||||
height=height,
|
||||
value=value,
|
||||
actions=actions,
|
||||
)
|
||||
|
||||
@@ -154,6 +154,7 @@ class FileSystemTool(BaseTool, ABC):
|
||||
"""
|
||||
if permission_manager is None:
|
||||
from hanzo_tools.core.permissions import PermissionManager
|
||||
|
||||
permission_manager = PermissionManager()
|
||||
self.permission_manager = permission_manager
|
||||
|
||||
|
||||
@@ -80,17 +80,17 @@ __all__ = [
|
||||
|
||||
def get_read_only_filesystem_tools(permission_manager) -> list:
|
||||
"""Get read-only filesystem tools for sandboxed agents.
|
||||
|
||||
|
||||
Returns tools that can only read files, not modify them:
|
||||
- read: Read file contents
|
||||
- tree: View directory structure
|
||||
- find: Find files by pattern
|
||||
- search: Search file contents
|
||||
- ast: Code structure analysis
|
||||
|
||||
|
||||
Args:
|
||||
permission_manager: PermissionManager instance
|
||||
|
||||
|
||||
Returns:
|
||||
List of instantiated read-only tools
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Edit tool - find and replace in files."""
|
||||
|
||||
from typing import Optional, Annotated
|
||||
from typing import Optional, Annotated
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Read tool - read file contents."""
|
||||
|
||||
from typing import Optional, Any, Annotated
|
||||
from typing import Any, Optional, Annotated
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
@@ -6,7 +6,6 @@ from typing import Optional, Annotated
|
||||
from pathlib import Path
|
||||
|
||||
import aiofiles
|
||||
|
||||
from pydantic import Field
|
||||
from mcp.server import FastMCP
|
||||
from mcp.server.fastmcp import Context as MCPContext
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Write tool - write/create files."""
|
||||
|
||||
from typing import Optional, Annotated
|
||||
from typing import Optional, Annotated
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import Field
|
||||
|
||||
@@ -34,13 +34,13 @@ __all__ = [
|
||||
|
||||
def get_read_only_jupyter_tools(permission_manager) -> list:
|
||||
"""Get read-only jupyter tools for sandboxed agents.
|
||||
|
||||
|
||||
Returns tools that can read jupyter notebooks:
|
||||
- jupyter: Read and analyze notebook contents
|
||||
|
||||
|
||||
Args:
|
||||
permission_manager: PermissionManager instance
|
||||
|
||||
|
||||
Returns:
|
||||
List of instantiated read-only tools
|
||||
"""
|
||||
|
||||
@@ -15,7 +15,6 @@ Provides a unified `memory` tool for all memory and knowledge operations:
|
||||
from mcp.server import FastMCP
|
||||
|
||||
from hanzo_tools.core import BaseTool, ToolRegistry
|
||||
from hanzo_tools.memory.unified_memory_tool import UnifiedMemoryTool
|
||||
|
||||
# Legacy imports for backwards compatibility
|
||||
from hanzo_tools.memory.memory_tools import (
|
||||
@@ -33,6 +32,7 @@ from hanzo_tools.memory.knowledge_tools import (
|
||||
SummarizeToMemoryTool,
|
||||
ManageKnowledgeBasesTool,
|
||||
)
|
||||
from hanzo_tools.memory.unified_memory_tool import UnifiedMemoryTool
|
||||
|
||||
__all__ = [
|
||||
# Primary unified tool
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Single tool with action parameter replaces 9 separate memory tools.
|
||||
"""
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, List, Dict, Any, Literal, final, override, Annotated
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Annotated, final, override
|
||||
|
||||
from pydantic import Field
|
||||
from mcp.server import FastMCP
|
||||
@@ -25,6 +25,7 @@ def _check_memory_available() -> bool:
|
||||
if MEMORY_AVAILABLE is None:
|
||||
try:
|
||||
import hanzo_memory # noqa: F401
|
||||
|
||||
MEMORY_AVAILABLE = True
|
||||
except ImportError:
|
||||
MEMORY_AVAILABLE = False
|
||||
@@ -38,22 +39,23 @@ def _get_lazy_memory_service() -> "MemoryService":
|
||||
if not _check_memory_available():
|
||||
raise ImportError("hanzo-memory required. Install: pip install hanzo-memory")
|
||||
from hanzo_memory.services.memory import get_memory_service
|
||||
|
||||
_memory_service = get_memory_service()
|
||||
return _memory_service
|
||||
|
||||
|
||||
Action = Annotated[
|
||||
Literal[
|
||||
"recall", # Search memories
|
||||
"create", # Store new memories
|
||||
"update", # Update existing memories
|
||||
"delete", # Delete memories
|
||||
"manage", # Atomic create/update/delete
|
||||
"facts", # Recall facts from knowledge bases
|
||||
"store", # Store facts in knowledge bases
|
||||
"summarize", # Summarize and store
|
||||
"kb", # Manage knowledge bases
|
||||
"list", # List memories/knowledge bases
|
||||
"recall", # Search memories
|
||||
"create", # Store new memories
|
||||
"update", # Update existing memories
|
||||
"delete", # Delete memories
|
||||
"manage", # Atomic create/update/delete
|
||||
"facts", # Recall facts from knowledge bases
|
||||
"store", # Store facts in knowledge bases
|
||||
"summarize", # Summarize and store
|
||||
"kb", # Manage knowledge bases
|
||||
"list", # List memories/knowledge bases
|
||||
],
|
||||
Field(description="Memory action to perform"),
|
||||
]
|
||||
@@ -296,6 +298,7 @@ Examples:
|
||||
|
||||
try:
|
||||
from hanzo_memory.services.knowledge import get_knowledge_service
|
||||
|
||||
ks = get_knowledge_service()
|
||||
|
||||
results = []
|
||||
@@ -326,6 +329,7 @@ Examples:
|
||||
|
||||
try:
|
||||
from hanzo_memory.services.knowledge import get_knowledge_service
|
||||
|
||||
ks = get_knowledge_service()
|
||||
|
||||
stored = []
|
||||
@@ -366,6 +370,7 @@ Examples:
|
||||
"""Manage knowledge bases."""
|
||||
try:
|
||||
from hanzo_memory.services.knowledge import get_knowledge_service
|
||||
|
||||
ks = get_knowledge_service()
|
||||
|
||||
if action == "list":
|
||||
|
||||
@@ -125,9 +125,7 @@ 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)
|
||||
@@ -145,11 +143,7 @@ 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}")
|
||||
|
||||
@@ -231,9 +225,7 @@ 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(
|
||||
@@ -326,9 +318,7 @@ 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,11 +55,7 @@ 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."""
|
||||
@@ -161,9 +157,7 @@ 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)
|
||||
@@ -220,9 +214,7 @@ 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:
|
||||
@@ -536,9 +528,7 @@ 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
|
||||
@@ -627,9 +617,7 @@ 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),
|
||||
}
|
||||
@@ -706,11 +694,7 @@ 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]",
|
||||
)
|
||||
)
|
||||
@@ -785,9 +769,7 @@ 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),
|
||||
|
||||
+14
-42
@@ -115,15 +115,9 @@ def serve(ctx, name: str, port: int):
|
||||
|
||||
@cli.command()
|
||||
@click.option("--name", "-n", help="Node name (auto-generated if not provided)")
|
||||
@click.option(
|
||||
"--port", "-p", default=52415, help="Node port (default: 52415 for hanzo/net)"
|
||||
)
|
||||
@click.option(
|
||||
"--network", default="local", help="Network to join (mainnet/testnet/local)"
|
||||
)
|
||||
@click.option(
|
||||
"--models", "-m", multiple=True, help="Models to serve (e.g., llama-3.2-3b)"
|
||||
)
|
||||
@click.option("--port", "-p", default=52415, help="Node port (default: 52415 for hanzo/net)")
|
||||
@click.option("--network", default="local", help="Network to join (mainnet/testnet/local)")
|
||||
@click.option("--models", "-m", multiple=True, help="Models to serve (e.g., llama-3.2-3b)")
|
||||
@click.option("--max-jobs", type=int, default=10, help="Max concurrent jobs")
|
||||
@click.pass_context
|
||||
def net(ctx, name: str, port: int, network: str, models: tuple, max_jobs: int):
|
||||
@@ -137,15 +131,9 @@ def net(ctx, name: str, port: int, network: str, models: tuple, max_jobs: int):
|
||||
|
||||
@cli.command()
|
||||
@click.option("--name", "-n", help="Node name (auto-generated if not provided)")
|
||||
@click.option(
|
||||
"--port", "-p", default=52415, help="Node port (default: 52415 for hanzo/net)"
|
||||
)
|
||||
@click.option(
|
||||
"--network", default="local", help="Network to join (mainnet/testnet/local)"
|
||||
)
|
||||
@click.option(
|
||||
"--models", "-m", multiple=True, help="Models to serve (e.g., llama-3.2-3b)"
|
||||
)
|
||||
@click.option("--port", "-p", default=52415, help="Node port (default: 52415 for hanzo/net)")
|
||||
@click.option("--network", default="local", help="Network to join (mainnet/testnet/local)")
|
||||
@click.option("--models", "-m", multiple=True, help="Models to serve (e.g., llama-3.2-3b)")
|
||||
@click.option("--max-jobs", type=int, default=10, help="Max concurrent jobs")
|
||||
@click.pass_context
|
||||
def node(ctx, name: str, port: int, network: str, models: tuple, max_jobs: int):
|
||||
@@ -180,15 +168,9 @@ def node(ctx, name: str, port: int, network: str, models: tuple, max_jobs: int):
|
||||
@click.option("--repl", is_flag=True, help="Start REPL interface (default)")
|
||||
@click.option("--instances", type=int, default=2, help="Number of worker agents")
|
||||
@click.option("--mcp-tools", is_flag=True, default=True, help="Enable all MCP tools")
|
||||
@click.option(
|
||||
"--network-mode", is_flag=True, default=True, help="Network agents together"
|
||||
)
|
||||
@click.option(
|
||||
"--guardrails", is_flag=True, default=True, help="Enable code quality guardrails"
|
||||
)
|
||||
@click.option(
|
||||
"--use-network/--no-network", default=True, help="Use hanzo-network if available"
|
||||
)
|
||||
@click.option("--network-mode", is_flag=True, default=True, help="Network agents together")
|
||||
@click.option("--guardrails", is_flag=True, default=True, help="Enable code quality guardrails")
|
||||
@click.option("--use-network/--no-network", default=True, help="Use hanzo-network if available")
|
||||
@click.option(
|
||||
"--use-hanzo-net",
|
||||
is_flag=True,
|
||||
@@ -266,9 +248,7 @@ def dev(
|
||||
console.print(f" Router Endpoint: {orch_config.router.endpoint}")
|
||||
if orch_config.codex:
|
||||
console.print(f" Codex Model: {orch_config.codex.model}")
|
||||
console.print(
|
||||
f" Cost Optimization: {'Enabled' if orch_config.enable_cost_optimization else 'Disabled'}"
|
||||
)
|
||||
console.print(f" Cost Optimization: {'Enabled' if orch_config.enable_cost_optimization else 'Disabled'}")
|
||||
console.print()
|
||||
|
||||
asyncio.run(
|
||||
@@ -351,9 +331,7 @@ async def start_compute_node(
|
||||
|
||||
console.print(f"\n[green]✓[/green] Node initialized")
|
||||
console.print(f" Port: {port}")
|
||||
console.print(
|
||||
f" Models: {', '.join(models) if models else 'auto-detect'}"
|
||||
)
|
||||
console.print(f" Models: {', '.join(models) if models else 'auto-detect'}")
|
||||
console.print("\n[bold green]Hanzo Net is running![/bold green]")
|
||||
console.print("WebUI: http://localhost:52415")
|
||||
console.print("API: http://localhost:52415/v1/chat/completions")
|
||||
@@ -375,9 +353,7 @@ async def start_compute_node(
|
||||
stop_task = asyncio.create_task(stop_event.wait())
|
||||
|
||||
# Wait for either net to complete or stop signal
|
||||
done, pending = await asyncio.wait(
|
||||
[net_task, stop_task], return_when=asyncio.FIRST_COMPLETED
|
||||
)
|
||||
done, pending = await asyncio.wait([net_task, stop_task], return_when=asyncio.FIRST_COMPLETED)
|
||||
|
||||
# Cancel pending tasks
|
||||
for task in pending:
|
||||
@@ -413,15 +389,11 @@ async def start_compute_node(
|
||||
env["NET_MODELS"] = ",".join(models)
|
||||
if name:
|
||||
env["NET_NODE_NAME"] = name
|
||||
env["PYTHONPATH"] = (
|
||||
os.path.join(net_path, "src") + ":" + env.get("PYTHONPATH", "")
|
||||
)
|
||||
env["PYTHONPATH"] = os.path.join(net_path, "src") + ":" + env.get("PYTHONPATH", "")
|
||||
|
||||
console.print(f"\n[green]✓[/green] Starting net node")
|
||||
console.print(f" Port: {port}")
|
||||
console.print(
|
||||
f" Models: {', '.join(models) if models else 'auto-detect'}"
|
||||
)
|
||||
console.print(f" Models: {', '.join(models) if models else 'auto-detect'}")
|
||||
console.print("\n[bold green]Hanzo Net is running![/bold green]")
|
||||
console.print("WebUI: http://localhost:52415")
|
||||
console.print("API: http://localhost:52415/v1/chat/completions")
|
||||
|
||||
@@ -88,11 +88,7 @@ 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)
|
||||
|
||||
@@ -127,9 +127,7 @@ def login(ctx, email: str, password: str, api_key: str, sso: bool):
|
||||
|
||||
hanzo_auth = HanzoAuth()
|
||||
# Email auth not implemented yet
|
||||
console.print(
|
||||
"[yellow]Email authentication not yet implemented[/yellow]"
|
||||
)
|
||||
console.print("[yellow]Email authentication not yet implemented[/yellow]")
|
||||
console.print("[dim]Saving credentials locally for development[/dim]")
|
||||
|
||||
auth = {
|
||||
@@ -249,9 +247,7 @@ def whoami():
|
||||
|
||||
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")
|
||||
|
||||
@@ -18,9 +18,7 @@ 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)
|
||||
@@ -80,9 +78,7 @@ 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:
|
||||
@@ -172,9 +168,7 @@ 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"))
|
||||
|
||||
@@ -115,9 +115,7 @@ 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()
|
||||
|
||||
@@ -96,9 +96,7 @@ 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")
|
||||
|
||||
@@ -241,9 +239,7 @@ 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,9 +50,7 @@ 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(),
|
||||
@@ -94,9 +92,7 @@ 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:
|
||||
@@ -109,9 +105,7 @@ 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")
|
||||
|
||||
@@ -207,15 +201,11 @@ 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')}")
|
||||
@@ -292,9 +282,7 @@ 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()
|
||||
@@ -323,9 +311,7 @@ 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']}")
|
||||
|
||||
@@ -26,9 +26,7 @@ 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
|
||||
@@ -391,13 +389,9 @@ 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:")
|
||||
@@ -547,9 +541,7 @@ 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
|
||||
|
||||
@@ -700,10 +692,7 @@ 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:
|
||||
@@ -741,16 +730,12 @@ 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")
|
||||
@@ -758,9 +743,7 @@ 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,9 +31,7 @@ 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
|
||||
@@ -140,9 +138,7 @@ 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:
|
||||
@@ -256,9 +252,7 @@ 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
|
||||
@@ -431,9 +425,7 @@ 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')})"
|
||||
@@ -446,12 +438,8 @@ 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)}")
|
||||
|
||||
|
||||
@@ -105,18 +105,14 @@ 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."""
|
||||
@@ -169,9 +165,7 @@ 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,9 +40,7 @@ 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}")
|
||||
@@ -91,9 +89,7 @@ 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
|
||||
@@ -116,14 +112,8 @@ 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
|
||||
@@ -148,9 +138,7 @@ 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:
|
||||
@@ -158,9 +146,7 @@ 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")
|
||||
|
||||
@@ -30,9 +30,7 @@ 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
|
||||
@@ -88,9 +86,7 @@ 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)}")
|
||||
|
||||
+69
-215
@@ -221,9 +221,7 @@ 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")
|
||||
@@ -254,9 +252,7 @@ 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.")
|
||||
@@ -343,9 +339,7 @@ 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 = []
|
||||
|
||||
@@ -369,9 +363,7 @@ 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:
|
||||
@@ -416,9 +408,7 @@ 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]
|
||||
@@ -536,12 +526,8 @@ 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:
|
||||
@@ -559,9 +545,7 @@ 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]")
|
||||
@@ -577,9 +561,7 @@ 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(
|
||||
@@ -588,9 +570,7 @@ 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:
|
||||
@@ -744,14 +724,10 @@ 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
|
||||
|
||||
@@ -854,9 +830,7 @@ 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]
|
||||
@@ -902,13 +876,9 @@ 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
|
||||
@@ -951,17 +921,13 @@ 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)
|
||||
|
||||
@@ -1008,9 +974,7 @@ 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:
|
||||
@@ -1087,10 +1051,7 @@ 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
|
||||
|
||||
@@ -1102,9 +1063,7 @@ 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
|
||||
@@ -1345,26 +1304,14 @@ 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")
|
||||
@@ -1407,16 +1354,10 @@ 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")
|
||||
|
||||
@@ -1453,13 +1394,9 @@ 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")
|
||||
@@ -1496,9 +1433,7 @@ 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)
|
||||
|
||||
@@ -1530,11 +1465,7 @@ 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
|
||||
|
||||
@@ -1563,9 +1494,7 @@ 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(
|
||||
@@ -1604,9 +1533,7 @@ 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)
|
||||
|
||||
@@ -1634,9 +1561,7 @@ 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
|
||||
|
||||
@@ -1672,9 +1597,7 @@ 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)
|
||||
|
||||
@@ -1721,9 +1644,7 @@ 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:
|
||||
@@ -1754,9 +1675,7 @@ 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:
|
||||
@@ -1834,9 +1753,7 @@ 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")
|
||||
@@ -1942,25 +1859,19 @@ 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()
|
||||
@@ -1977,9 +1888,7 @@ 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:
|
||||
@@ -2002,21 +1911,15 @@ 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
|
||||
@@ -2026,9 +1929,7 @@ 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
|
||||
@@ -2089,9 +1990,7 @@ 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
|
||||
@@ -2147,9 +2046,7 @@ 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:
|
||||
@@ -2282,10 +2179,7 @@ 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)
|
||||
@@ -2297,10 +2191,7 @@ 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
|
||||
@@ -2311,10 +2202,7 @@ 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
|
||||
@@ -2335,20 +2223,12 @@ 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:
|
||||
@@ -2383,9 +2263,7 @@ 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:
|
||||
@@ -2523,9 +2401,7 @@ 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:
|
||||
@@ -2587,9 +2463,7 @@ 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):
|
||||
@@ -2732,14 +2606,10 @@ 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
|
||||
|
||||
@@ -2816,9 +2686,7 @@ 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:
|
||||
@@ -2882,9 +2750,7 @@ 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
|
||||
@@ -2903,9 +2769,7 @@ 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}
|
||||
|
||||
@@ -2948,25 +2812,15 @@ 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,9 +23,7 @@ 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,
|
||||
@@ -103,9 +101,7 @@ 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")
|
||||
@@ -114,30 +110,22 @@ 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)
|
||||
@@ -164,9 +152,7 @@ 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]")
|
||||
|
||||
@@ -98,9 +98,7 @@ 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
|
||||
@@ -114,9 +112,7 @@ 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")
|
||||
@@ -355,9 +351,7 @@ 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"
|
||||
@@ -410,11 +404,7 @@ 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]
|
||||
@@ -427,11 +417,7 @@ 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
|
||||
|
||||
@@ -465,9 +451,7 @@ 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."""
|
||||
@@ -480,14 +464,10 @@ 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
|
||||
@@ -507,20 +487,14 @@ 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)
|
||||
@@ -559,22 +533,16 @@ 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."""
|
||||
@@ -697,9 +665,7 @@ 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)
|
||||
@@ -720,19 +686,13 @@ 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)
|
||||
@@ -747,12 +707,8 @@ 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
|
||||
@@ -768,21 +724,15 @@ 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."""
|
||||
@@ -791,11 +741,7 @@ 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)
|
||||
@@ -826,9 +772,7 @@ 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:
|
||||
@@ -863,9 +807,7 @@ 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:
|
||||
@@ -891,9 +833,7 @@ 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)
|
||||
|
||||
@@ -902,9 +842,7 @@ 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:
|
||||
@@ -960,9 +898,7 @@ 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()
|
||||
@@ -980,13 +916,9 @@ 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):
|
||||
@@ -1002,21 +934,13 @@ 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): ")
|
||||
@@ -1030,9 +954,7 @@ 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,9 +14,7 @@ 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
|
||||
@@ -99,9 +97,7 @@ 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()
|
||||
|
||||
@@ -138,9 +134,7 @@ 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,9 +196,7 @@ 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,9 +293,7 @@ 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 "-"
|
||||
@@ -389,9 +387,7 @@ 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,9 +57,7 @@ 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 = []
|
||||
@@ -180,9 +178,7 @@ 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
|
||||
|
||||
@@ -335,9 +331,7 @@ 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,9 +16,7 @@ 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,18 +275,10 @@ 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:
|
||||
@@ -296,9 +288,7 @@ 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)
|
||||
@@ -307,10 +297,7 @@ 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,9 +25,7 @@ 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
|
||||
@@ -63,9 +61,7 @@ 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 = ""
|
||||
@@ -81,9 +77,7 @@ 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(
|
||||
@@ -224,15 +218,11 @@ 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)
|
||||
|
||||
|
||||
@@ -250,9 +240,7 @@ 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]")
|
||||
@@ -263,9 +251,7 @@ 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,9 +217,7 @@ 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:
|
||||
@@ -316,11 +314,7 @@ 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]",
|
||||
@@ -347,15 +341,11 @@ 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]:
|
||||
@@ -417,9 +407,7 @@ 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,
|
||||
@@ -443,9 +431,7 @@ 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}"
|
||||
|
||||
@@ -474,8 +460,6 @@ 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,9 +33,7 @@ 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:
|
||||
@@ -65,9 +63,7 @@ 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,9 +232,7 @@ 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"
|
||||
|
||||
@@ -258,9 +256,7 @@ 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."""
|
||||
@@ -303,9 +299,7 @@ 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
|
||||
|
||||
@@ -14,11 +14,7 @@ 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")
|
||||
|
||||
@@ -142,9 +138,7 @@ 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]:
|
||||
@@ -167,6 +161,4 @@ 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,9 +44,7 @@ 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
|
||||
@@ -104,9 +102,7 @@ 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