from typing import Optional, List from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from pydantic import BaseModel from backend.database import get_db, Tool, Integration, ToolExecution from backend.auth import get_current_user, User from backend.composio_client import get_composio from datetime import datetime router = APIRouter() class ToolResponse(BaseModel): id: str integration_id: str name: str description: str input_schema: dict = {} output_schema: Optional[dict] = None category: Optional[str] = None is_active: bool = True class ToolExecutionRequest(BaseModel): params: dict = {} connection_id: Optional[str] = None dry_run: bool = False class ToolExecutionResponse(BaseModel): execution_id: str status: str result: Optional[dict] = None latency_ms: int = 0 executed_at: str = "" error: Optional[str] = None @router.get("", response_model=List[ToolResponse]) async def list_tools( integration_id: Optional[str] = None, category: Optional[str] = None, search: Optional[str] = None, db: Session = Depends(get_db) ): composio = get_composio() if composio and not integration_id and not category and search: try: raw = composio.tools.get_raw_composio_tools(search=search, limit=100) return [_tool_to_response(t) for t in raw] except Exception: pass query = db.query(Tool).filter(Tool.is_active == True) if integration_id: query = query.filter(Tool.integration_id == integration_id) if category: query = query.filter(Tool.category == category) if search: query = query.filter( (Tool.name.ilike(f"%{search}%")) | (Tool.description.ilike(f"%{search}%")) ) tools = query.all() return [ToolResponse( id=t.id, integration_id=t.integration_id, name=t.name, description=t.description, input_schema=t.input_schema, output_schema=t.output_schema, category=t.category, is_active=t.is_active ) for t in tools] def _tool_to_response(t): t_d = t if hasattr(t, "model_dump"): t_d = t.model_dump(mode="json") elif hasattr(t, "__dict__"): t_d = {k: v for k, v in t.__dict__.items() if not k.startswith("_")} toolkit = t_d.get("toolkit", {}) or {} int_id = toolkit.get("slug", "") if isinstance(toolkit, dict) else "" return ToolResponse( id=t_d.get("slug", t_d.get("id", "")), integration_id=int_id, name=t_d.get("name", ""), description=t_d.get("description", ""), input_schema=t_d.get("input_parameters", {"type": "object", "properties": {}}), output_schema=t_d.get("output_parameters"), category=(t_d.get("tags") or ["General"])[0] if t_d.get("tags") else "General", is_active=not t_d.get("is_deprecated", False) ) @router.get("/{tool_id}", response_model=ToolResponse) async def get_tool(tool_id: str, db: Session = Depends(get_db)): composio = get_composio() if composio: try: raw = composio.tools.get_raw_composio_tools(tools=[tool_id]) if raw: return _tool_to_response(raw[0]) except Exception: pass tool = db.query(Tool).filter(Tool.id == tool_id).first() if not tool: raise HTTPException(status_code=404, detail="Tool not found") return ToolResponse( id=tool.id, integration_id=tool.integration_id, name=tool.name, description=tool.description, input_schema=tool.input_schema, output_schema=tool.output_schema, category=tool.category, is_active=tool.is_active ) @router.post("/{tool_id}/execute", response_model=ToolExecutionResponse) async def execute_tool( tool_id: str, request: ToolExecutionRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): from services.tool_executor import executor result = await executor.execute( tool_id=tool_id, params=request.params, connection_id=request.connection_id, user_id=current_user.id, source="api" ) return ToolExecutionResponse( execution_id=result["execution_id"], status=result["status"], result=result["result"], latency_ms=result["latency_ms"], executed_at=datetime.utcnow().isoformat(), error=result.get("error") ) @router.get("/executions") async def list_executions( limit: int = 20, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): executions = db.query(ToolExecution).filter( ToolExecution.user_id == current_user.id ).order_by(ToolExecution.executed_at.desc()).limit(limit).all() return [{ "id": e.id, "tool_id": e.tool_id, "status": e.status, "latency_ms": e.latency_ms, "executed_at": e.executed_at.isoformat() if e.executed_at else "", "source": e.source } for e in executions] @router.post("/sync") async def sync_tools( force: bool = Query(False), current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): from integrations.registry import sync_from_composio result = await sync_from_composio(force=force) return {"message": "Tools synced", "source": result["source"], "toolkits": result["toolkits"], "tools": result["tools"]}