scrapeRL / backend /app /api /routes /plugins.py
NeerajCodz's picture
fix: GitHub trending CSV output returns correct columns
4ece098
"""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},
}