- Crea create_simple_agent() per provider senza tool-calling - Detecta automaticamente Ollama e usa agente semplice - Aggiunge comando /reset per pulire cronologia
1130 lines
42 KiB
Python
1130 lines
42 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
LangCode Agent - A configurable agent harness using DeepAgents
|
||
Supports multiple LLM providers via LangChain's init_chat_model
|
||
Enhanced with unified provider tools and custom tools
|
||
"""
|
||
import os
|
||
import sys
|
||
import json
|
||
from typing import Optional, List, Dict, Any
|
||
from deepagents import create_deep_agent
|
||
from langchain.chat_models import init_chat_model
|
||
from langchain_core.messages import HumanMessage, SystemMessage
|
||
from langchain_core.tools import Tool
|
||
|
||
# Import custom tools
|
||
try:
|
||
from tools import get_proxmox_tools
|
||
from tools.provider_tools import get_provider_tools, get_all_provider_tools
|
||
TOOLS_AVAILABLE = True
|
||
except ImportError:
|
||
TOOLS_AVAILABLE = False
|
||
print("Warning: Custom tools not available. Install required dependencies.")
|
||
|
||
# Import configuration manager
|
||
try:
|
||
from config_manager import config_manager
|
||
CONFIG_AVAILABLE = True
|
||
except ImportError:
|
||
CONFIG_AVAILABLE = False
|
||
print("Warning: Configuration manager not available.")
|
||
|
||
# Import skill manager
|
||
try:
|
||
from skill_manager import skill_manager
|
||
SKILL_MANAGER_AVAILABLE = True
|
||
except ImportError:
|
||
SKILL_MANAGER_AVAILABLE = False
|
||
print("Warning: Skill manager not available.")
|
||
|
||
# Import enhancement modules
|
||
try:
|
||
from enhancements.simple_memory import simple_memory, create_simple_memory_tools
|
||
from enhancements.planning_enhancement import enhanced_planner
|
||
ENHANCEMENTS_AVAILABLE = True
|
||
except ImportError:
|
||
ENHANCEMENTS_AVAILABLE = False
|
||
print("Warning: Enhancement modules not available.")
|
||
|
||
# Import json for memory preference handling
|
||
import json
|
||
|
||
|
||
def load_model_from_config():
|
||
"""
|
||
Load the LLM model based on configuration.
|
||
Priority: 1) config_manager active config, 2) environment variables
|
||
|
||
Expected environment variables (fallback):
|
||
- OPENAI_API_KEY: For OpenAI models
|
||
- OLLAMA_BASE_URL: For Ollama (e.g., https://ollama.solarpi.net)
|
||
- MODEL_PROVIDER: One of "openai", "ollama", "openrouter", etc.
|
||
- MODEL_NAME: The specific model name (e.g., "gpt-4o", "gemma2:9b")
|
||
"""
|
||
# Try to use config_manager first
|
||
if CONFIG_AVAILABLE:
|
||
active_name = config_manager.get_active_configuration()
|
||
if active_name:
|
||
config = config_manager.get_configuration(active_name)
|
||
if config:
|
||
provider = config.get("provider", "ollama").lower()
|
||
model_name = config.get("model_name", "gemma2:9b")
|
||
base_url = config.get("base_url", "http://localhost:11434")
|
||
api_key = config.get("api_key")
|
||
|
||
print(f"🔗 Caricamento modello da config_manager: {active_name}")
|
||
print(f" Provider: {provider}")
|
||
print(f" Modello: {model_name}")
|
||
print(f" URL: {base_url}")
|
||
|
||
if provider == "ollama":
|
||
return init_chat_model(f"ollama:{model_name}", base_url=base_url)
|
||
elif provider == "openai":
|
||
return init_chat_model(f"openai:{model_name}", api_key=api_key)
|
||
elif provider == "openrouter":
|
||
return init_chat_model(
|
||
f"openai:{model_name}",
|
||
base_url=base_url,
|
||
api_key=api_key
|
||
)
|
||
|
||
# Fallback to environment variables
|
||
provider = os.getenv("MODEL_PROVIDER", "openai").lower()
|
||
model_name = os.getenv("MODEL_NAME", "gpt-4o")
|
||
|
||
print(f"⚠️ Usando variabili d'ambiente (nessuna config attiva)")
|
||
print(f" Provider: {provider}")
|
||
print(f" Modello: {model_name}")
|
||
|
||
# Configure based on provider
|
||
if provider == "openai":
|
||
# Uses OPENAI_API_KEY from environment
|
||
return init_chat_model(f"openai:{model_name}")
|
||
elif provider == "ollama":
|
||
# For Ollama, we need to specify the base URL
|
||
base_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
|
||
return init_chat_model(f"ollama:{model_name}", base_url=base_url)
|
||
elif provider == "openrouter":
|
||
# For OpenRouter, we need OPENROUTER_API_KEY and optionally base URL
|
||
# Note: LangChain's init_chat_model for openrouter might require specific setup
|
||
# As of now, we assume it's supported via openai-compatible API
|
||
base_url = os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1")
|
||
api_key = os.getenv("OPENROUTER_API_KEY")
|
||
if not api_key:
|
||
raise ValueError("OPENROUTER_API_KEY environment variable is required for OpenRouter")
|
||
# Using openai-compatible API
|
||
return init_chat_model(
|
||
f"openai:{model_name}",
|
||
base_url=base_url,
|
||
api_key=api_key
|
||
)
|
||
else:
|
||
# Fallback to openai for unknown providers (or add more as needed)
|
||
return init_chat_model(f"openai:{model_name}")
|
||
|
||
|
||
def get_mcp_tools() -> List[Tool]:
|
||
"""
|
||
Get MCP tools from configured MCP servers
|
||
|
||
Returns:
|
||
List of MCP-derived LangChain Tool objects
|
||
"""
|
||
from mcp_client import MCPClient, get_mcp_tools_from_client
|
||
|
||
mcp_tools = []
|
||
|
||
if not CONFIG_AVAILABLE:
|
||
return mcp_tools
|
||
|
||
try:
|
||
# Get all MCP server configurations
|
||
# We'll look for configurations with provider="mcp"
|
||
all_configs = config_manager.get_configurations()
|
||
mcp_configs = {
|
||
name: config for name, config in all_configs.items()
|
||
if config.get("provider") == "mcp"
|
||
}
|
||
|
||
for name, config in mcp_configs.items():
|
||
try:
|
||
server_url = config.get("server_url")
|
||
if not server_url:
|
||
print(f"⚠️ MCP configuration '{name}' missing server_url")
|
||
continue
|
||
|
||
headers = {}
|
||
api_key = config.get("api_key")
|
||
if api_key:
|
||
headers["Authorization"] = f"Bearer {api_key}"
|
||
|
||
mcp_client = MCPClient(
|
||
server_url=server_url,
|
||
headers=headers,
|
||
timeout=config.get("timeout", 30)
|
||
)
|
||
|
||
tools = get_mcp_tools_from_client(mcp_client)
|
||
mcp_tools.extend(tools)
|
||
print(f"✅ Added {len(tools)} tools from MCP server '{name}'")
|
||
except Exception as e:
|
||
print(f"⚠️ Could not load tools from MCP server '{name}': {e}")
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ Error loading MCP tools: {e}")
|
||
|
||
return mcp_tools
|
||
|
||
|
||
def get_skill_tools() -> List[Tool]:
|
||
"""
|
||
Get skill management tools for the LangCode agent
|
||
|
||
Returns:
|
||
List of skill management LangChain Tool objects
|
||
"""
|
||
from skill_manager import skill_manager
|
||
|
||
skill_tools = []
|
||
|
||
# Search for skills
|
||
def search_skills_func(query: str, limit: int = 10) -> str:
|
||
try:
|
||
skills = skill_manager.search_skills(query, limit)
|
||
if not skills:
|
||
return f"No skills found for query: '{query}'"
|
||
|
||
result = f"Found {len(skills)} skills:\n"
|
||
for i, skill in enumerate(skills, 1):
|
||
result += f"{i}. {skill.get('name', 'Unknown')}: {skill.get('description', 'No description')}\n"
|
||
return result
|
||
except Exception as e:
|
||
return f"Error searching skills: {str(e)}"
|
||
|
||
# Install a skill
|
||
def install_skill_func(skill_name: str) -> str:
|
||
try:
|
||
success, message = skill_manager.install_skill(skill_name)
|
||
return message
|
||
except Exception as e:
|
||
return f"Error installing skill: {str(e)}"
|
||
|
||
# Uninstall a skill
|
||
def uninstall_skill_func(skill_name: str) -> str:
|
||
try:
|
||
success, message = skill_manager.uninstall_skill(skill_name)
|
||
return message
|
||
except Exception as e:
|
||
return f"Error uninstalling skill: {str(e)}"
|
||
|
||
# List installed skills
|
||
def list_installed_skills_func() -> str:
|
||
try:
|
||
skills = skill_manager.list_installed_skills()
|
||
if not skills:
|
||
return "No skills installed"
|
||
|
||
result = f"Installed skills ({len(skills)}):\n"
|
||
for i, skill in enumerate(skills, 1):
|
||
result += f"{i}. {skill.get('name', 'Unknown')}: {skill.get('description', 'No description')}\n"
|
||
return result
|
||
except Exception as e:
|
||
return f"Error listing installed skills: {str(e)}"
|
||
|
||
# Get skill details
|
||
def get_skill_details_func(skill_name: str) -> str:
|
||
try:
|
||
details = skill_manager.get_skill_details(skill_name)
|
||
if not details:
|
||
return f"Skill '{skill_name}' not found or no details available"
|
||
|
||
# Format the details nicely
|
||
result = f"Skill: {details.get('name', 'Unknown')}\n"
|
||
result += f"Description: {details.get('description', 'No description')}\n"
|
||
|
||
# Add other relevant details if available
|
||
if 'version' in details:
|
||
result += f"Version: {details['version']}\n"
|
||
if 'author' in details:
|
||
result += f"Author: {details['author']}\n"
|
||
if 'category' in details:
|
||
result += f"Category: {details['category']}\n"
|
||
|
||
return result
|
||
except Exception as e:
|
||
return f"Error getting skill details: {str(e)}"
|
||
|
||
# Create skill management tools
|
||
skill_tools.extend([
|
||
Tool(
|
||
name="search_skills",
|
||
description="Search for agent skills in the skills.sh registry. Use this to find skills by keyword or description.",
|
||
func=search_skills_func
|
||
),
|
||
Tool(
|
||
name="install_skill",
|
||
description="Install a skill from the skills.sh registry. Provide the skill name to install.",
|
||
func=install_skill_func
|
||
),
|
||
Tool(
|
||
name="uninstall_skill",
|
||
description="Uninstall an installed skill. Provide the skill name to remove.",
|
||
func=uninstall_skill_func
|
||
),
|
||
Tool(
|
||
name="list_installed_skills",
|
||
description="List all currently installed skills in the local skills directory.",
|
||
func=list_installed_skills_func
|
||
),
|
||
Tool(
|
||
name="get_skill_details",
|
||
description="Get detailed information about a specific skill. Provide the skill name to get details.",
|
||
func=get_skill_details_func
|
||
)
|
||
])
|
||
|
||
return skill_tools
|
||
|
||
|
||
def create_simple_agent(system_prompt: Optional[str] = None):
|
||
"""
|
||
Create a simple chat agent for providers without full tool-calling support (e.g., Ollama).
|
||
This agent just uses direct chat without advanced tool calling features.
|
||
"""
|
||
from langchain.schema import HumanMessage, AIMessage, SystemMessage
|
||
from langchain.chat_models import init_chat_model
|
||
|
||
model = load_model_from_config()
|
||
|
||
class SimpleAgent:
|
||
"""Simple agent wrapper for Ollama and similar providers."""
|
||
|
||
def __init__(self, llm, prompt):
|
||
self.llm = llm
|
||
self.messages = []
|
||
if prompt:
|
||
self.messages.append(SystemMessage(content=prompt))
|
||
|
||
def invoke(self, input_dict):
|
||
user_message = input_dict.get("messages", [])[-1]
|
||
if hasattr(user_message, 'content'):
|
||
user_content = user_message.content
|
||
else:
|
||
user_content = str(user_message)
|
||
|
||
# Add to history
|
||
self.messages.append(HumanMessage(content=user_content))
|
||
|
||
# Get response
|
||
response = self.llm.invoke(self.messages)
|
||
self.messages.append(response)
|
||
|
||
return {"messages": self.messages}
|
||
|
||
def clear_history(self):
|
||
"""Clear conversation history but keep system prompt."""
|
||
system_msg = self.messages[0] if self.messages and isinstance(self.messages[0], SystemMessage) else None
|
||
self.messages = [system_msg] if system_msg else []
|
||
|
||
if system_prompt is None:
|
||
system_prompt = """You are LangCode, a helpful AI assistant.
|
||
You can answer questions, write code, explain concepts, and help with programming tasks.
|
||
Be concise and helpful. Respond in Italian when the user writes in Italian."""
|
||
|
||
return SimpleAgent(model, system_prompt)
|
||
|
||
|
||
def get_custom_tools() -> List[Tool]:
|
||
"""
|
||
Get custom tools for the LangCode agent
|
||
|
||
Returns:
|
||
List of custom tools
|
||
"""
|
||
custom_tools = []
|
||
|
||
if not TOOLS_AVAILABLE:
|
||
return custom_tools
|
||
|
||
# Add Proxmox tools if configured
|
||
proxmox_host = os.getenv("PROXMOX_HOST")
|
||
if proxmox_host:
|
||
proxmox_user = os.getenv("PROXMOX_USER", "root")
|
||
proxmox_password = os.getenv("PROXMOX_PASSWORD")
|
||
proxmox_token = os.getenv("PROXMOX_TOKEN")
|
||
|
||
try:
|
||
proxmox_tools = get_proxmox_tools(
|
||
host=proxmox_host,
|
||
user=proxmox_user,
|
||
password=proxmox_password,
|
||
api_token=proxmox_token
|
||
)
|
||
custom_tools.extend(proxmox_tools)
|
||
print(f"✅ Added {len(proxmox_tools)} Proxmox tools")
|
||
except Exception as e:
|
||
print(f"⚠️ Could not load Proxmox tools: {e}")
|
||
|
||
# Add unified provider tools for all configured providers
|
||
try:
|
||
provider_tools = get_all_provider_tools()
|
||
custom_tools.extend(provider_tools)
|
||
print(f"✅ Added {len(provider_tools)} provider tools")
|
||
except Exception as e:
|
||
print(f"⚠️ Could not load provider tools: {e}")
|
||
|
||
# Add MCP tools from configured servers
|
||
try:
|
||
mcp_tools = get_mcp_tools()
|
||
custom_tools.extend(mcp_tools)
|
||
print(f"✅ Added {len(mcp_tools)} MCP tools")
|
||
except Exception as e:
|
||
print(f"⚠️ Could not load MCP tools: {e}")
|
||
|
||
# Add skill management tools
|
||
try:
|
||
skill_tools = get_skill_tools()
|
||
custom_tools.extend(skill_tools)
|
||
print(f"✅ Added {len(skill_tools)} skill management tools")
|
||
except Exception as e:
|
||
print(f"⚠️ Could not load skill tools: {e}")
|
||
|
||
# Add enhancement tools
|
||
try:
|
||
if ENHANCEMENTS_AVAILABLE:
|
||
# Add simple memory tools
|
||
memory_tools = create_simple_memory_tools(simple_memory)
|
||
custom_tools.extend(memory_tools)
|
||
print(f"✅ Added {len(memory_tools)} simple memory tools")
|
||
|
||
# Add planning tools
|
||
planning_tools = enhanced_planner.create_planning_tools()
|
||
custom_tools.extend(planning_tools)
|
||
print(f"✅ Added {len(planning_tools)} planning tools")
|
||
else:
|
||
print("⚠️ Enhancement modules not available, skipping enhancement tools")
|
||
except Exception as e:
|
||
print(f"⚠️ Could not load enhancement tools: {e}")
|
||
|
||
return custom_tools
|
||
|
||
|
||
def create_langcode_agent(system_prompt: Optional[str] = None, use_simple_agent: bool = False):
|
||
"""
|
||
Create and return an agent instance with configurable model.
|
||
|
||
For Ollama and other providers without full tool-calling support,
|
||
uses a simple chat agent instead of deepagents.
|
||
"""
|
||
# Detect if we need simple agent (for Ollama without tool support)
|
||
if not use_simple_agent and CONFIG_AVAILABLE:
|
||
active_name = config_manager.get_active_configuration()
|
||
if active_name:
|
||
config = config_manager.get_configuration(active_name)
|
||
if config and config.get("provider") == "ollama":
|
||
# Ollama doesn't fully support bind_tools in deepagents
|
||
# Check if we have tools - if so, use simple agent
|
||
custom_tools = get_custom_tools()
|
||
if len(custom_tools) > 0:
|
||
print("⚠️ Using simple chat agent (Ollama detected without full tool support)")
|
||
use_simple_agent = True
|
||
|
||
if use_simple_agent:
|
||
return create_simple_agent(system_prompt)
|
||
|
||
# Use deepagents for providers with full tool-calling support
|
||
if system_prompt is None:
|
||
system_prompt = """You are LangCode, a helpful AI agent that can plan, use tools,
|
||
manage context, and delegate to subagents. You are designed to be configurable
|
||
to work with various LLM providers.
|
||
|
||
You have access to:
|
||
1. Unified provider tools for managing LLM providers (list models, test connections, etc.)
|
||
2. Custom tools for managing Proxmox virtual machines
|
||
3. MCP tools for accessing external services and skills registries (like skills.sh)
|
||
4. Skill management tools for discovering, installing, and managing agent skills
|
||
5. Enhancement tools for persistent memory and advanced planning
|
||
6. Standard DeepAgents tools (planning, filesystem, shell, subagents)
|
||
|
||
Use these tools when asked about:
|
||
- LLM provider management (listing models, testing connections, setting defaults)
|
||
- System administration and virtualization
|
||
- AI model management and configuration"""
|
||
|
||
model = load_model_from_config()
|
||
|
||
# Get custom tools
|
||
custom_tools = get_custom_tools()
|
||
|
||
# Create the agent with custom tools
|
||
agent = create_deep_agent(
|
||
model=model,
|
||
tools=custom_tools, # Add custom tools to the agent
|
||
system_prompt=system_prompt
|
||
)
|
||
|
||
return agent
|
||
|
||
|
||
def show_help():
|
||
"""Show available commands"""
|
||
print("""
|
||
📖 Comandi disponibili (prefissati con /):
|
||
|
||
/help, /? - Mostra questo messaggio di aiuto
|
||
/status - Mostra lo stato del sistema
|
||
/config list - Elenca tutte le configurazioni
|
||
/config active - Mostra la configurazione attiva
|
||
/config test - Testa la connessione alla configurazione attiva
|
||
/config use <name> - Cambia la configurazione attiva
|
||
/models - Elenca i modelli disponibili per il provider attivo
|
||
/clear - Pulisce lo schermo
|
||
/exit, /quit - Esci dall'agente
|
||
|
||
💡 Per chattare con l'agente, scrivi semplicemente il tuo messaggio senza "/".
|
||
Esempio: "Ciao, chi sei?" oppure "Elenca i file nella directory corrente"
|
||
""")
|
||
|
||
|
||
def show_status():
|
||
"""Show system status"""
|
||
print("\n📊 Stato del Sistema:")
|
||
print("=" * 40)
|
||
|
||
# Show provider info
|
||
provider = os.getenv("MODEL_PROVIDER", "non configurato")
|
||
model = os.getenv("MODEL_NAME", "non configurato")
|
||
print(f"Provider: {provider}")
|
||
print(f"Modello: {model}")
|
||
|
||
# Show Ollama URL if configured
|
||
if provider == "ollama":
|
||
ollama_url = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
|
||
print(f"Ollama URL: {ollama_url}")
|
||
|
||
# Show configuration status
|
||
if CONFIG_AVAILABLE:
|
||
active_config = config_manager.get_active_configuration()
|
||
if active_config:
|
||
print(f"Configurazione attiva: {active_config}")
|
||
else:
|
||
print("Configurazione attiva: Nessuna")
|
||
|
||
configs = config_manager.get_configurations()
|
||
print(f"Configurazioni totali: {len(configs)}")
|
||
|
||
# Show tools status
|
||
if TOOLS_AVAILABLE:
|
||
print("Strumenti personalizzati: Disponibili")
|
||
else:
|
||
print("Strumenti personalizzati: Non disponibili")
|
||
|
||
|
||
def list_configurations():
|
||
"""List all configurations"""
|
||
if not CONFIG_AVAILABLE:
|
||
print("❌ Gestore configurazioni non disponibile.")
|
||
return
|
||
|
||
configs = config_manager.get_configurations()
|
||
active = config_manager.get_active_configuration()
|
||
|
||
if not configs:
|
||
print("📭 Nessuna configurazione trovata.")
|
||
return
|
||
|
||
print("\n📋 Configurazioni disponibili:")
|
||
print("=" * 50)
|
||
|
||
for name, config in configs.items():
|
||
marker = "✅" if name == active else " "
|
||
provider = config.get("provider", "unknown")
|
||
model = config.get("model_name", "unknown")
|
||
print(f"{marker} {name}: {provider} - {model}")
|
||
|
||
|
||
def show_active_configuration():
|
||
"""Show active configuration"""
|
||
if not CONFIG_AVAILABLE:
|
||
print("❌ Gestore configurazioni non disponibile.")
|
||
return
|
||
|
||
active_name = config_manager.get_active_configuration()
|
||
if not active_name:
|
||
print("📭 Nessuna configurazione attiva.")
|
||
return
|
||
|
||
config = config_manager.get_configuration(active_name)
|
||
if not config:
|
||
print(f"❌ Configurazione '{active_name}' non trovata.")
|
||
return
|
||
|
||
print(f"\n✅ Configurazione attiva: {active_name}")
|
||
print(f" Provider: {config.get('provider', 'unknown')}")
|
||
print(f" Modello: {config.get('model_name', 'unknown')}")
|
||
|
||
if config.get('base_url'):
|
||
print(f" URL: {config.get('base_url')}")
|
||
|
||
|
||
def test_active_configuration():
|
||
"""Test connection to active configuration"""
|
||
if not CONFIG_AVAILABLE:
|
||
print("❌ Gestore configurazioni non disponibile.")
|
||
return
|
||
|
||
active_name = config_manager.get_active_configuration()
|
||
if not active_name:
|
||
print("📭 Nessuna configurazione attiva da testare.")
|
||
return
|
||
|
||
print(f"\n🔍 Test connessione a '{active_name}'...")
|
||
success, message = config_manager.test_configuration_connection(active_name)
|
||
|
||
if success:
|
||
print(f"✅ {message}")
|
||
else:
|
||
print(f"❌ {message}")
|
||
|
||
|
||
def use_configuration(name: str):
|
||
"""Set active configuration"""
|
||
if not CONFIG_AVAILABLE:
|
||
print("❌ Gestore configurazioni non disponibile.")
|
||
return
|
||
|
||
if not config_manager.configuration_exists(name):
|
||
print(f"❌ Configurazione '{name}' non trovata.")
|
||
print("Usa '/config list' per vedere le configurazioni disponibili.")
|
||
return
|
||
|
||
if config_manager.set_active_configuration(name):
|
||
print(f"✅ Configurazione '{name}' impostata come attiva.")
|
||
else:
|
||
print(f"❌ Errore nell'impostare la configurazione '{name}'.")
|
||
|
||
|
||
def list_models():
|
||
"""List available models for current provider"""
|
||
provider = os.getenv("MODEL_PROVIDER", "").lower()
|
||
|
||
if not provider:
|
||
print("❌ Nessun provider configurato.")
|
||
return
|
||
|
||
print(f"\n📋 Modelli disponibili per {provider}:")
|
||
print("=" * 40)
|
||
|
||
# This is a simplified version - in a real implementation,
|
||
# you would call the provider's API to list models
|
||
if provider == "ollama":
|
||
print("Per elencare i modelli Ollama, chiedi all'agente:")
|
||
print(" 'Elenca i modelli disponibili su Ollama'")
|
||
elif provider == "openai":
|
||
print("Modelli OpenAI comuni:")
|
||
print(" - gpt-4o")
|
||
print(" - gpt-4o-mini")
|
||
print(" - gpt-4-turbo")
|
||
print(" - gpt-3.5-turbo")
|
||
elif provider == "openrouter":
|
||
print("Per elencare i modelli OpenRouter, chiedi all'agente:")
|
||
print(" 'Elenca i modelli disponibili su OpenRouter'")
|
||
else:
|
||
print(f"Provider '{provider}' non riconosciuto.")
|
||
|
||
|
||
def clear_screen():
|
||
"""Clear the screen"""
|
||
os.system('cls' if os.name == 'nt' else 'clear')
|
||
|
||
|
||
def skills_search(query: str):
|
||
"""Search for skills"""
|
||
from skill_manager import skill_manager
|
||
|
||
try:
|
||
skills = skill_manager.search_skills(query)
|
||
if not skills:
|
||
print(f"No skills found for query: '{query}'")
|
||
return
|
||
|
||
print(f"\nFound {len(skills)} skills:")
|
||
print("=" * 50)
|
||
for i, skill in enumerate(skills, 1):
|
||
print(f"{i}. {skill.get('name', 'Unknown')}")
|
||
print(f" Description: {skill.get('description', 'No description')}")
|
||
if 'version' in skill:
|
||
print(f" Version: {skill['version']}")
|
||
if 'author' in skill:
|
||
print(f" Author: {skill['author']}")
|
||
print()
|
||
except Exception as e:
|
||
print(f"Error searching skills: {str(e)}")
|
||
|
||
|
||
def skills_install(skill_name: str):
|
||
"""Install a skill"""
|
||
from skill_manager import skill_manager
|
||
|
||
try:
|
||
success, message = skill_manager.install_skill(skill_name)
|
||
if success:
|
||
print(f"✅ {message}")
|
||
else:
|
||
print(f"❌ {message}")
|
||
except Exception as e:
|
||
print(f"Error installing skill: {str(e)}")
|
||
|
||
|
||
def skills_uninstall(skill_name: str):
|
||
"""Uninstall a skill"""
|
||
from skill_manager import skill_manager
|
||
|
||
try:
|
||
success, message = skill_manager.uninstall_skill(skill_name)
|
||
if success:
|
||
print(f"✅ {message}")
|
||
else:
|
||
print(f"❌ {message}")
|
||
except Exception as e:
|
||
print(f"Error uninstalling skill: {str(e)}")
|
||
|
||
|
||
def skills_list():
|
||
"""List installed skills"""
|
||
from skill_manager import skill_manager
|
||
|
||
try:
|
||
skills = skill_manager.list_installed_skills()
|
||
if not skills:
|
||
print("No skills installed")
|
||
return
|
||
|
||
print(f"\nInstalled skills ({len(skills)}):")
|
||
print("=" * 50)
|
||
for i, skill in enumerate(skills, 1):
|
||
print(f"{i}. {skill.get('name', 'Unknown')}")
|
||
print(f" Description: {skill.get('description', 'No description')}")
|
||
if 'version' in skill:
|
||
print(f" Version: {skill['version']}")
|
||
if 'author' in skill:
|
||
print(f" Author: {skill['author']}")
|
||
print(f" Path: {skill.get('path', 'Unknown')}")
|
||
print()
|
||
except Exception as e:
|
||
print(f"Error listing installed skills: {str(e)}")
|
||
|
||
|
||
def skills_details(skill_name: str):
|
||
"""Show skill details"""
|
||
from skill_manager import skill_manager
|
||
|
||
try:
|
||
details = skill_manager.get_skill_details(skill_name)
|
||
if not details:
|
||
print(f"Skill '{skill_name}' not found or no details available")
|
||
return
|
||
|
||
print(f"\nSkill Details: {details.get('name', 'Unknown')}")
|
||
print("=" * 50)
|
||
print(f"Description: {details.get('description', 'No description')}")
|
||
if 'version' in details:
|
||
print(f"Version: {details['version']}")
|
||
if 'author' in details:
|
||
print(f"Author: {details['author']}")
|
||
if 'category' in details:
|
||
print(f"Category: {details['category']}")
|
||
if 'repository' in details:
|
||
print(f"Repository: {details['repository']}")
|
||
if 'homepage' in details:
|
||
print(f"Homepage: {details['homepage']}")
|
||
print()
|
||
except Exception as e:
|
||
print(f"Error getting skill details: {str(e)}")
|
||
|
||
|
||
def memory_remember(text: str):
|
||
"""Remember a fact for future reference"""
|
||
from enhancements.simple_memory import simple_memory
|
||
try:
|
||
# Clean the text - remove any quotes
|
||
clean_text = _clean_text(text)
|
||
if not clean_text:
|
||
print("❌ Errore: il testo non può essere vuoto")
|
||
return
|
||
# Default category is "general"
|
||
simple_memory.add_fact(clean_text, "general")
|
||
print(f"✅ Remembered: {clean_text[:100]}{'...' if len(clean_text) > 100 else ''}")
|
||
except Exception as e:
|
||
print(f"❌ Error remembering: {e}")
|
||
|
||
def memory_recall(query: str):
|
||
"""Recall facts by searching memory"""
|
||
from enhancements.simple_memory import simple_memory
|
||
try:
|
||
results = simple_memory.search_facts(query)
|
||
if not results:
|
||
print(f"No memories found for: {query}")
|
||
return
|
||
|
||
print(f"\nFound {len(results)} memories:")
|
||
print("=" * 50)
|
||
for i, fact in enumerate(results[:10], 1): # Limit to 10 results
|
||
print(f"{i}. {fact['fact']}")
|
||
print(f" Category: {fact['category']}")
|
||
print(f" Time: {fact['timestamp']}")
|
||
print()
|
||
except Exception as e:
|
||
print(f"Error recalling: {e}")
|
||
|
||
def memory_set_preference(key: str, value: str):
|
||
"""Set a user preference"""
|
||
from enhancements.simple_memory import simple_memory
|
||
try:
|
||
# Try to parse value as JSON for complex types
|
||
try:
|
||
parsed_val = json.loads(value)
|
||
except:
|
||
parsed_val = value
|
||
simple_memory.set_preference(key, parsed_val)
|
||
print(f"✅ Set preference '{key}' = {parsed_val}")
|
||
except Exception as e:
|
||
print(f"Error setting preference: {e}")
|
||
|
||
def memory_preference_get(key: str):
|
||
"""Get a user preference"""
|
||
from enhancements.simple_memory import simple_memory
|
||
try:
|
||
val = simple_memory.get_preference(key)
|
||
if val is not None:
|
||
print(f"Preference '{key}': {val}")
|
||
else:
|
||
print(f"Preference '{key}' not found")
|
||
except Exception as e:
|
||
print(f"Error getting preference: {e}")
|
||
|
||
def memory_stats():
|
||
"""Get memory statistics"""
|
||
from enhancements.simple_memory import simple_memory
|
||
try:
|
||
stats = simple_memory.get_memory_stats()
|
||
print("\nMemory Statistics:")
|
||
print("=" * 30)
|
||
print(f"Total Conversations: {stats['total_conversations']}")
|
||
print(f"Total Facts: {stats['total_facts']}")
|
||
print(f"Total Preferences: {stats['total_preferences']}")
|
||
print(f"Total Skills Used: {stats['total_skills_used']}")
|
||
print(f"Last Updated: {stats['last_updated'] or 'Never'}")
|
||
except Exception as e:
|
||
print(f"Error getting memory stats: {e}")
|
||
|
||
def _strip_quotes(s: str) -> str:
|
||
"""Strip surrounding quotes from a string"""
|
||
s = s.strip()
|
||
if len(s) >= 2:
|
||
if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")):
|
||
return s[1:-1]
|
||
return s
|
||
|
||
def _clean_text(s: str) -> str:
|
||
"""Remove all quotes (single and double) from a string"""
|
||
return s.replace('"', '').replace("'", '').strip()
|
||
|
||
def plan_create(name: str, tasks_string: str):
|
||
"""Create a plan with multiple tasks"""
|
||
from enhancements.planning_enhancement import enhanced_planner
|
||
try:
|
||
# Strip quotes from name
|
||
name = _strip_quotes(name)
|
||
# Split tasks by semicolon and clean up
|
||
task_descriptions = [_strip_quotes(t.strip()) for t in tasks_string.split(';') if t.strip()]
|
||
if not task_descriptions:
|
||
print("❌ No tasks provided")
|
||
return
|
||
|
||
plan_id = enhanced_planner.create_plan(name, task_descriptions)
|
||
print(f"✅ Created plan '{name}' with ID: {plan_id}")
|
||
print(f" Tasks: {len(task_descriptions)}")
|
||
for i, desc in enumerate(task_descriptions, 1):
|
||
print(f" {i}. {desc}")
|
||
except Exception as e:
|
||
print(f"❌ Error creating plan: {e}")
|
||
|
||
def plan_list():
|
||
"""List all plans"""
|
||
from enhancements.planning_enhancement import enhanced_planner
|
||
try:
|
||
if not enhanced_planner.plans:
|
||
print("No plans created yet")
|
||
return
|
||
|
||
print(f"\nPlans ({len(enhanced_planner.plans)}):")
|
||
print("=" * 50)
|
||
for plan_id, task_ids in enhanced_planner.plans.items():
|
||
task_count = len(task_ids)
|
||
print(f"Plan ID: {plan_id}")
|
||
print(f" Tasks: {task_count}")
|
||
# Show first few tasks
|
||
for i, task_id in enumerate(task_ids[:3], 1):
|
||
if task_id in enhanced_planner.tasks:
|
||
task = enhanced_planner.tasks[task_id]
|
||
print(f" {i}. [{task.status.value}] {task.description}")
|
||
else:
|
||
print(f" {i}. [unknown] Task {task_id} not found")
|
||
if len(task_ids) > 3:
|
||
print(f" ... and {len(task_ids) - 3} more tasks")
|
||
print()
|
||
except Exception as e:
|
||
print(f"Error listing plans: {e}")
|
||
|
||
def plan_progress(plan_id: str):
|
||
"""Get progress information for a plan"""
|
||
from enhancements.planning_enhancement import enhanced_planner
|
||
try:
|
||
progress = enhanced_planner.get_plan_progress(plan_id)
|
||
if "error" in progress:
|
||
print(f"❌ {progress['error']}")
|
||
return
|
||
|
||
print(f"\nPlan {plan_id} Progress:")
|
||
print("=" * 40)
|
||
print(f"Total Tasks: {progress['total_tasks']}")
|
||
print(f"Completed: {progress['completed_tasks']}")
|
||
print(f"Failed: {progress['failed_tasks']}")
|
||
print(f"In Progress: {progress['in_progress_tasks']}")
|
||
print(f"Pending: {progress['pending_tasks']}")
|
||
print(f"Progress: {progress['progress_percentage']:.1f}%")
|
||
except Exception as e:
|
||
print(f"Error getting plan progress: {e}")
|
||
|
||
def plan_start(task_id: str):
|
||
"""Start working on a task"""
|
||
from enhancements.planning_enhancement import enhanced_planner
|
||
try:
|
||
if enhanced_planner.start_task(task_id):
|
||
print(f"✅ Started task {task_id}")
|
||
else:
|
||
print(f"❌ Failed to start task {task_id} - check dependencies or task ID")
|
||
except Exception as e:
|
||
print(f"Error starting task: {e}")
|
||
|
||
def plan_complete(task_id: str):
|
||
"""Mark a task as completed"""
|
||
from enhancements.planning_enhancement import enhanced_planner
|
||
try:
|
||
if enhanced_planner.complete_task(task_id):
|
||
print(f"✅ Completed task {task_id}")
|
||
else:
|
||
print(f"❌ Failed to complete task {task_id} - check task ID or status")
|
||
except Exception as e:
|
||
print(f"Error completing task: {e}")
|
||
|
||
def process_command(user_input: str) -> bool:
|
||
"""
|
||
Process a command starting with "/"
|
||
|
||
Returns:
|
||
True if it was a command, False otherwise
|
||
"""
|
||
if not user_input.startswith('/'):
|
||
return False
|
||
|
||
# Remove the "/" and split into command and arguments
|
||
command_line = user_input[1:].strip()
|
||
parts = command_line.split()
|
||
command = parts[0].lower() if parts else ""
|
||
args = parts[1:] if len(parts) > 1 else []
|
||
|
||
# Process commands
|
||
if command in ['help', '?']:
|
||
show_help()
|
||
elif command == 'status':
|
||
show_status()
|
||
elif command == 'config':
|
||
if not args:
|
||
print("❌ Specifica un sottocomando: list, active, test, use <name>")
|
||
elif args[0] == 'list':
|
||
list_configurations()
|
||
elif args[0] == 'active':
|
||
show_active_configuration()
|
||
elif args[0] == 'test':
|
||
test_active_configuration()
|
||
elif args[0] == 'use' and len(args) > 1:
|
||
use_configuration(args[1])
|
||
else:
|
||
print(f"❌ Sottocomando sconosciuto: {args[0]}")
|
||
print("Sottocomandi disponibili: list, active, test, use <name>")
|
||
elif command == 'skills':
|
||
if not args:
|
||
print("❌ Specifica un sottocomando: search, install, uninstall, list, details")
|
||
elif args[0] == 'search' and len(args) > 1:
|
||
skills_search(' '.join(args[1:]))
|
||
elif args[0] == 'install' and len(args) > 1:
|
||
skills_install(args[1])
|
||
elif args[0] == 'uninstall' and len(args) > 1:
|
||
skills_uninstall(args[1])
|
||
elif args[0] == 'list':
|
||
skills_list()
|
||
elif args[0] == 'details' and len(args) > 1:
|
||
skills_details(args[1])
|
||
else:
|
||
print("❌ Sottocomando sconosciuto o argomenti mancanti")
|
||
print("Sottocomandi disponibili: search <query>, install <name>, uninstall <name>, list, details <name>")
|
||
elif command == 'memory':
|
||
if not args:
|
||
print("❌ Specifica un sottocomando: remember, recall, preference, stats")
|
||
elif args[0] == 'remember' and len(args) > 1:
|
||
memory_remember(' '.join(args[1:]))
|
||
elif args[0] == 'recall' and len(args) > 1:
|
||
memory_recall(' '.join(args[1:]))
|
||
elif args[0] == 'preference':
|
||
if len(args) >= 4 and args[1] == 'set':
|
||
# Join everything after 'set' as the value (key is args[2], value is args[3:])
|
||
key = args[2]
|
||
value = ' '.join(args[3:])
|
||
memory_set_preference(key, value)
|
||
elif len(args) == 3 and args[1] == 'get':
|
||
key = args[2]
|
||
memory_preference_get(key)
|
||
else:
|
||
print("❌ Usage: preference set <key> <value> or preference get <key>")
|
||
elif args[0] == 'stats':
|
||
memory_stats()
|
||
else:
|
||
print("❌ Sottocomando sconosciuto o argomenti mancanti")
|
||
print("Sottocomandi disponibili: remember <text>, recall <query>, preference set <key> <value>, preference get <key>, stats")
|
||
elif command == 'plan':
|
||
if not args:
|
||
print("❌ Specifica un sottocomando: create, list, progress, start, complete")
|
||
elif args[0] == 'create':
|
||
# Handle plan creation with special parsing for quoted strings
|
||
# Format: /plan create "Plan Name" "Task 1; Task 2; Task 3"
|
||
# Or: /plan create PlanName "Task1; Task2; Task3"
|
||
import re
|
||
# Get the raw part after "plan create"
|
||
raw_part = command_line[len("plan create "):]
|
||
|
||
# Find all quoted strings (handles both " and ' quotes)
|
||
quoted_pattern = r'"([^"]*)"|\'([^\']*)\''
|
||
matches = re.findall(quoted_pattern, raw_part)
|
||
|
||
if len(matches) >= 2:
|
||
# Both name and tasks are quoted
|
||
name = matches[0][0] or matches[0][1]
|
||
tasks = matches[1][0] or matches[1][1]
|
||
plan_create(name, tasks)
|
||
elif len(matches) == 1:
|
||
# Only one quoted string - it's the tasks, name is the first unquoted word
|
||
tasks = matches[0][0] or matches[0][1]
|
||
# Find the name as the first word before the quoted part
|
||
unquoted_start = raw_part.find('"') if '"' in raw_part else raw_part.find("'")
|
||
name = raw_part[:unquoted_start].strip()
|
||
if name and tasks:
|
||
plan_create(name, tasks)
|
||
else:
|
||
print("❌ Usage: /plan create <nome> \"<task1; task2; ...>\"")
|
||
else:
|
||
# No quotes - use simple split
|
||
if len(args) >= 3:
|
||
name = args[1]
|
||
tasks = ' '.join(args[2:])
|
||
plan_create(name, tasks)
|
||
else:
|
||
print("❌ Usage: /plan create <nome> \"<task1; task2; ...>\"")
|
||
elif args[0] == 'list':
|
||
plan_list()
|
||
elif args[0] == 'progress' and len(args) > 1:
|
||
plan_progress(args[1])
|
||
elif args[0] == 'start' and len(args) > 1:
|
||
plan_start(args[1])
|
||
elif args[0] == 'complete' and len(args) > 1:
|
||
plan_complete(args[1])
|
||
else:
|
||
print("❌ Sottocomando sconosciuto o argomenti mancanti")
|
||
print("Sottocomandi disponibili: create <name> <task1> <task2> ..., list, progress <plan_id>, start <task_id>, complete <task_id>")
|
||
elif command == 'models':
|
||
list_models()
|
||
elif command == 'clear':
|
||
clear_screen()
|
||
elif command == 'reset':
|
||
# Reset conversation history (for simple agent)
|
||
if hasattr(agent, 'clear_history'):
|
||
agent.clear_history()
|
||
print("✅ Cronologia conversazione pulita!")
|
||
else:
|
||
print("ℹ️ Reset non disponibile per questo tipo di agent.")
|
||
elif command in ['exit', 'quit']:
|
||
print("👋 Goodbye!")
|
||
sys.exit(0)
|
||
else:
|
||
print(f"❌ Comando sconosciuto: /{command}")
|
||
print("Digita /help per vedere i comandi disponibili.")
|
||
|
||
return True
|
||
|
||
|
||
def main():
|
||
"""
|
||
Main entry point for the LangCode agent.
|
||
"""
|
||
print("🚀 Initializing LangCode Agent...")
|
||
|
||
try:
|
||
agent = create_langcode_agent()
|
||
print("✅ Agent initialized successfully!")
|
||
|
||
# Show available tools
|
||
print("\n🔧 Available tools:")
|
||
print(" • Provider tools: list models, test connections, set defaults")
|
||
print(" • Proxmox tools: manage VMs, snapshots, resources")
|
||
print(" • Standard tools: planning, filesystem, shell, subagents")
|
||
|
||
print("\n💬 You can now interact with the agent.")
|
||
print("📌 Comandi speciali (prefissati con /):")
|
||
print(" /help - Mostra i comandi disponibili")
|
||
print(" /status - Mostra lo stato del sistema")
|
||
print(" /config - Gestisci le configurazioni")
|
||
print(" /exit - Esci dall'agente")
|
||
print("\n💡 Per chattare, scrivi semplicemente il tuo messaggio.")
|
||
|
||
while True:
|
||
user_input = input("\n👤 You: ").strip()
|
||
|
||
if not user_input:
|
||
continue
|
||
|
||
# Check if it's a command (starts with /)
|
||
if process_command(user_input):
|
||
continue
|
||
|
||
# Otherwise, treat it as a chat message
|
||
try:
|
||
result = agent.invoke({"messages": [HumanMessage(content=user_input)]})
|
||
# Extract the AI's response
|
||
ai_message = result["messages"][-1]
|
||
print(f"\n🤖 LangCode: {ai_message.content}")
|
||
except Exception as e:
|
||
error_msg = str(e)
|
||
if not error_msg:
|
||
error_msg = f"{type(e).__name__} (nessun messaggio)"
|
||
print(f"\n❌ Errore: {error_msg}")
|
||
|
||
# Suggerimenti basati sul tipo di errore
|
||
exc_type = type(e).__name__
|
||
if "Connection" in exc_type or "Connect" in str(e):
|
||
print("💡 Suggerimento: Verifica che il server Ollama sia raggiungibile.")
|
||
print(f" URL: {os.getenv('OLLAMA_BASE_URL', 'http://localhost:11434')}")
|
||
elif "Timeout" in exc_type or "timeout" in str(e).lower():
|
||
print("💡 Suggerimento: Il server è lento. Prova con un modello più piccolo.")
|
||
elif "model" in str(e).lower() or "Model" in exc_type:
|
||
print("💡 Suggerimento: Il modello specificato potrebbe non esistere.")
|
||
print(f" Modello: {os.getenv('MODEL_NAME', 'non impostato')}")
|
||
|
||
except Exception as e:
|
||
print(f"❌ Failed to initialize agent: {str(e)}")
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main() |