#!/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 - 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 ") 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 == '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 or preference get ") elif args[0] == 'stats': memory_stats() else: print("❌ Sottocomando sconosciuto o argomenti mancanti") print("Sottocomandi disponibili: remember , recall , preference set , preference get , 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 \"\"") 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 \"\"") 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 ..., list, progress , start , complete ") 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) 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"\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()