Files
langcode_agent/main.py
T

1176 lines
43 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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_core.messages 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: List = []
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 uses custom OllamaToolAgent with tool calling support.
For other providers with full tool support uses deepagents.
"""
# Try to get config info for Ollama detection
is_ollama = False
if 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":
is_ollama = True
# Use OllamaToolAgent for Ollama with tool calling
if is_ollama and not use_simple_agent:
try:
# Force reload the module
import importlib
import ollama_agent
importlib.reload(ollama_agent)
from ollama_agent import create_ollama_tool_agent
model = load_model_from_config()
custom_tools = get_custom_tools()
# Enhance with memory and planning tools if available
if ENHANCEMENTS_AVAILABLE:
try:
memory_tools = create_simple_memory_tools(simple_memory)
custom_tools.extend(memory_tools)
except Exception:
pass
try:
planning_tools = enhanced_planner.create_planning_tools()
custom_tools.extend(planning_tools)
except Exception:
pass
print("🔧 Using OllamaToolAgent with tool calling")
return create_ollama_tool_agent(
model=model,
tools=custom_tools,
system_prompt=system_prompt,
include_builtin_tools=True
)
except Exception as e:
print(f"⚠️ Could not load OllamaToolAgent: {e}")
print("⚠️ Falling back to simple agent")
use_simple_agent = True
elif is_ollama:
print("⚠️ Using simple chat agent (Ollama detected)")
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 (works for both simple and tool agent)
print(f"MAIN: result keys={result.keys()}")
if "response" in result:
# OllamaToolAgent returns a separate response field
response_text = result["response"]
else:
# Simple agent returns the last message
ai_message = result["messages"][-1]
response_text = ai_message.content if hasattr(ai_message, 'content') else str(ai_message)
print(f"MAIN: response_text={response_text[:100] if response_text else 'EMPTY'}...")
print(f"\n🤖 LangCode: {response_text}")
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()