"""Plugin management API routes.""" from typing import Any from fastapi import APIRouter, HTTPException from pydantic import BaseModel, Field from app.plugins.registry import ( get_all_plugins, get_all_tools, get_plugin, get_plugin_summary, get_tool, get_tools_by_category, PluginCategory, ) router = APIRouter(prefix="/plugins", tags=["plugins"]) # Plugin registry - available plugins PLUGIN_REGISTRY = { # API Providers "apis": [ { "id": "openai-api", "name": "OpenAI API", "category": "apis", "description": "GPT-4, GPT-4o, and embedding models", "version": "1.0.0", "size": "50KB", "installed": False, "requires_key": True, }, { "id": "anthropic-api", "name": "Anthropic API", "category": "apis", "description": "Claude 3.5 Sonnet and Opus models", "version": "1.0.0", "size": "45KB", "installed": False, "requires_key": True, }, { "id": "google-api", "name": "Google Gemini API", "category": "apis", "description": "Gemini Pro and Flash models", "version": "1.0.0", "size": "48KB", "installed": True, # Pre-installed "requires_key": True, }, { "id": "groq-api", "name": "Groq API", "category": "apis", "description": "Fast inference for open models", "version": "1.0.0", "size": "40KB", "installed": True, # Pre-installed "requires_key": True, }, { "id": "nvidia-api", "name": "NVIDIA API", "category": "apis", "description": "DeepSeek, Nemotron, and Llama models via NVIDIA", "version": "1.0.0", "size": "44KB", "installed": True, # Pre-installed "requires_key": True, }, { "id": "ollama-api", "name": "Ollama (Local)", "category": "apis", "description": "Run models locally with Ollama", "version": "1.0.0", "size": "35KB", "installed": False, "requires_key": False, }, ], # MCP Tools "mcps": [ { "id": "mcp-browser", "name": "Browser Tools", "category": "mcps", "description": "Playwright-based browser automation", "version": "1.0.0", "size": "120KB", "installed": True, "requires_key": False, }, { "id": "mcp-search", "name": "Search Tools", "category": "mcps", "description": "Google, Bing, DuckDuckGo search", "version": "1.0.0", "size": "80KB", "installed": True, "requires_key": False, }, { "id": "mcp-html", "name": "HTML Processing", "category": "mcps", "description": "Parse, extract, and transform HTML", "version": "1.0.0", "size": "60KB", "installed": True, "requires_key": False, }, { "id": "mcp-python-sandbox", "name": "Python Sandbox Executor", "category": "mcps", "description": "Run sandboxed Python analysis for datasets and pages", "version": "1.0.0", "size": "95KB", "installed": True, "requires_key": False, }, { "id": "mcp-screenshot", "name": "Screenshot Tools", "category": "mcps", "description": "Capture and analyze page screenshots", "version": "1.0.0", "size": "90KB", "installed": False, "requires_key": False, }, { "id": "mcp-pdf", "name": "PDF Tools", "category": "mcps", "description": "Extract data from PDF documents", "version": "1.0.0", "size": "150KB", "installed": False, "requires_key": False, }, { "id": "mcp-database", "name": "Database Tools", "category": "mcps", "description": "Connect to SQL/NoSQL databases", "version": "1.0.0", "size": "100KB", "installed": False, "requires_key": False, }, ], # Data Processors "processors": [ { "id": "proc-json", "name": "JSON Processor", "category": "processors", "description": "Parse and transform JSON data", "version": "1.0.0", "size": "25KB", "installed": True, "requires_key": False, }, { "id": "proc-csv", "name": "CSV Processor", "category": "processors", "description": "Parse and export CSV files", "version": "1.0.0", "size": "30KB", "installed": True, "requires_key": False, }, { "id": "proc-python", "name": "Python Analysis Processor", "category": "processors", "description": "Execute safe Python transformations on extracted data", "version": "1.0.0", "size": "55KB", "installed": True, "requires_key": False, }, { "id": "proc-pandas", "name": "Pandas Processor", "category": "processors", "description": "Tabular analysis and aggregation with pandas", "version": "1.0.0", "size": "130KB", "installed": True, "requires_key": False, }, { "id": "proc-numpy", "name": "NumPy Processor", "category": "processors", "description": "Numerical analysis and statistics with NumPy", "version": "1.0.0", "size": "90KB", "installed": True, "requires_key": False, }, { "id": "proc-bs4", "name": "BeautifulSoup Processor", "category": "processors", "description": "Advanced HTML parsing and link/content analysis via bs4", "version": "1.0.0", "size": "45KB", "installed": True, "requires_key": False, }, { "id": "python_sandbox", "name": "Python Sandbox", "category": "processors", "description": "Execute Python code in secure sandbox environment", "version": "1.0.0", "size": "85KB", "installed": True, "requires_key": False, }, { "id": "proc-excel", "name": "Excel Processor", "category": "processors", "description": "Read/write Excel files", "version": "1.0.0", "size": "500KB", "installed": False, "requires_key": False, }, { "id": "proc-xml", "name": "XML Processor", "category": "processors", "description": "Parse and transform XML data", "version": "1.0.0", "size": "40KB", "installed": False, "requires_key": False, }, ], } # Track installed plugins (in-memory, would be persistent in production) _installed_plugins: set[str] = { "google-api", "groq-api", "nvidia-api", "mcp-browser", "mcp-search", "mcp-html", "mcp-python-sandbox", "proc-json", "proc-csv", "proc-python", "proc-pandas", "proc-numpy", "proc-bs4", } class PluginAction(BaseModel): """Request to install/uninstall a plugin.""" plugin_id: str = Field(..., description="Plugin identifier") class PluginResponse(BaseModel): """Plugin information response.""" id: str name: str category: str description: str version: str size: str installed: bool requires_key: bool @router.get("") @router.get("/") async def list_plugins(category: str | None = None) -> dict[str, Any]: """List all available plugins, optionally filtered by category.""" result = {} for cat, plugins in PLUGIN_REGISTRY.items(): if category and cat != category: continue result[cat] = [ {**plugin, "installed": plugin["id"] in _installed_plugins} for plugin in plugins ] # Calculate stats total = sum(len(plugins) for plugins in PLUGIN_REGISTRY.values()) installed = len(_installed_plugins) return { "plugins": result, "categories": list(PLUGIN_REGISTRY.keys()), "stats": { "total": total, "installed": installed, "available": total - installed, }, } @router.get("/installed") async def list_installed_plugins() -> dict[str, Any]: """List only installed plugins.""" installed = [] for plugins in PLUGIN_REGISTRY.values(): for plugin in plugins: if plugin["id"] in _installed_plugins: installed.append({**plugin, "installed": True}) return { "plugins": installed, "count": len(installed), } @router.get("/categories") async def get_categories() -> dict[str, Any]: """Get plugin categories with descriptions.""" return { "categories": [ { "id": "apis", "name": "API Providers", "description": "LLM and AI service providers", "icon": "🔌", }, { "id": "mcps", "name": "MCP Tools", "description": "Model Context Protocol tools", "icon": "🔧", }, { "id": "processors", "name": "Data Processors", "description": "Data transformation tools", "icon": "📊", }, ], } # ============================================================================== # Tool Registry Endpoints (must be before /{plugin_id} catch-all) # ============================================================================== @router.get("/tools") async def list_tools(category: str | None = None) -> dict[str, Any]: """List all available tools from plugin registry.""" if category: try: cat = PluginCategory(category) tools = get_tools_by_category(cat) except ValueError: tools = [] else: tools = get_all_tools() return { "tools": [ { "name": t.name, "description": t.description, "category": t.category.value, "parameters": t.parameters, "returns": t.returns, } for t in tools ], "count": len(tools), } @router.get("/tools/{tool_name:path}") async def get_tool_details(tool_name: str) -> dict[str, Any]: """Get details about a specific tool.""" tool = get_tool(tool_name) if not tool: raise HTTPException(status_code=404, detail=f"Tool not found: {tool_name}") return { "name": tool.name, "description": tool.description, "category": tool.category.value, "parameters": tool.parameters, "returns": tool.returns, "examples": tool.examples, } @router.get("/registry") async def get_registry_endpoint() -> dict[str, Any]: """Get full plugin registry with all tools.""" plugins = get_all_plugins() return { "plugins": [ { "id": p.id, "name": p.name, "description": p.description, "category": p.category.value, "version": p.version, "enabled": p.enabled, "tools": [ { "name": t.name, "description": t.description, "parameters": t.parameters, "returns": t.returns, } for t in p.tools ], "tools_count": len(p.tools), } for p in plugins ], "summary": get_plugin_summary(), } @router.get("/summary") async def get_summary_endpoint() -> dict[str, Any]: """Get summary of plugins and tools.""" return get_plugin_summary() @router.get("/{plugin_id}") async def get_plugin_by_id(plugin_id: str) -> PluginResponse: """Get details about a specific plugin.""" for plugins in PLUGIN_REGISTRY.values(): for plugin in plugins: if plugin["id"] == plugin_id: return PluginResponse(**{**plugin, "installed": plugin_id in _installed_plugins}) raise HTTPException(status_code=404, detail=f"Plugin not found: {plugin_id}") @router.post("/install") async def install_plugin(action: PluginAction) -> dict[str, Any]: """Install a plugin.""" plugin_id = action.plugin_id # Find the plugin plugin = None for plugins in PLUGIN_REGISTRY.values(): for p in plugins: if p["id"] == plugin_id: plugin = p break if not plugin: raise HTTPException(status_code=404, detail=f"Plugin not found: {plugin_id}") if plugin_id in _installed_plugins: return { "status": "already_installed", "message": f"Plugin {plugin['name']} is already installed", "plugin": {**plugin, "installed": True}, } # Install the plugin (in production, this would download/configure) _installed_plugins.add(plugin_id) return { "status": "success", "message": f"Plugin {plugin['name']} installed successfully", "plugin": {**plugin, "installed": True}, } @router.post("/uninstall") async def uninstall_plugin(action: PluginAction) -> dict[str, Any]: """Uninstall a plugin.""" plugin_id = action.plugin_id # Find the plugin plugin = None for plugins in PLUGIN_REGISTRY.values(): for p in plugins: if p["id"] == plugin_id: plugin = p break if not plugin: raise HTTPException(status_code=404, detail=f"Plugin not found: {plugin_id}") if plugin_id not in _installed_plugins: return { "status": "not_installed", "message": f"Plugin {plugin['name']} is not installed", "plugin": {**plugin, "installed": False}, } # Check if it's a core plugin core_plugins = { "mcp-browser", "mcp-search", "mcp-html", "mcp-python-sandbox", "proc-json", "proc-python", "proc-pandas", "proc-numpy", "proc-bs4", } if plugin_id in core_plugins: raise HTTPException( status_code=400, detail=f"Cannot uninstall core plugin: {plugin['name']}", ) # Uninstall the plugin _installed_plugins.discard(plugin_id) return { "status": "success", "message": f"Plugin {plugin['name']} uninstalled successfully", "plugin": {**plugin, "installed": False}, }