Files
langcode_agent/main.py
T

731 lines
25 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
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.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.")
def load_model_from_config():
"""
Load the LLM model based on configuration.
Configuration can come from environment variables or a config file.
For simplicity, we'll use environment variables.
Expected environment variables:
- 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")
"""
provider = os.getenv("MODEL_PROVIDER", "openai").lower()
model_name = os.getenv("MODEL_NAME", "gpt-4o")
# 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 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}")
return custom_tools
def create_langcode_agent(system_prompt: Optional[str] = None):
"""
Create and return a DeepAgent instance with configurable model.
"""
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. 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 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 == 'models':
list_models()
elif command == 'clear':
clear_screen()
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:
print(f"\n❌ Error: {str(e)}")
except Exception as e:
print(f"❌ Failed to initialize agent: {str(e)}")
sys.exit(1)
if __name__ == "__main__":
main()