#!/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_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.") 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 - 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 ") 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 ") 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 , install , uninstall , list, details ") 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()