#!/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.") 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_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}") 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. 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 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 == '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()