Files
Unraid-AppOps-Gateway-Public/backend/app/api/routes_integrations.py
T

151 lines
5.1 KiB
Python

from __future__ import annotations
from fastapi import APIRouter, Depends, Header, HTTPException, Query
from pydantic import BaseModel, ConfigDict, Field
from sqlmodel import Session
from ..core.config import settings
from ..core.security import n8n_auth, operator_auth
from ..db.session import get_session
from ..mcp_server import runtime_status as mcp_runtime_status
from ..services import knowledge_service, n8n_integration
router = APIRouter(prefix="/api/integrations")
n8n_router = APIRouter(prefix="/api/integrations/n8n", dependencies=[Depends(n8n_auth)])
class PreflightRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
action: str = Field(min_length=1, max_length=160)
target: str = Field(min_length=1, max_length=240)
class ApprovalRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
action: str = Field(min_length=1, max_length=160)
target: str = Field(min_length=1, max_length=240)
reason: str = Field(min_length=3, max_length=1000)
plan: list[str] = Field(default_factory=list, max_length=30)
class KnowledgeRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
question: str = Field(min_length=2, max_length=500)
limit: int = Field(default=6, ge=1, le=10)
class HeartbeatRequest(BaseModel):
model_config = ConfigDict(extra="forbid")
workflowId: str = Field(min_length=1, max_length=120)
workflowVersion: str = Field(default="1", max_length=40)
branch: str = Field(default="unknown", max_length=40)
processedEvents: int = Field(default=0, ge=0, le=100_000)
backlogRemaining: bool = False
confidence: str = Field(default="", max_length=20)
@router.get("/status", dependencies=[Depends(operator_auth)])
def status(session: Session = Depends(get_session)):
rag = knowledge_service.index_status(session)
mcp_runtime = mcp_runtime_status()
n8n_runtime = n8n_integration.heartbeat_status(session)
return {
"mcp": {
"enabled": settings.mcp_enabled,
"authenticated": bool(settings.mcp_token),
"transport": "streamable_http",
"endpoint": "/mcp",
"scope": "governed_operations",
"canRequestApproval": True,
"canExecuteApproved": True,
"requiresHumanApproval": True,
"maxConcurrentExecutions": 1,
**mcp_runtime,
},
"rag": rag,
"n8n": {
"enabled": settings.n8n_enabled,
"authenticated": bool(settings.n8n_token),
"endpoint": "/api/integrations/n8n",
"canExecute": False,
"capabilities": [
"event_feed",
"knowledge_query",
"preflight",
"create_approval",
"read_approval",
"read_job",
],
**n8n_runtime,
},
}
@n8n_router.post("/heartbeat")
def heartbeat(payload: HeartbeatRequest, session: Session = Depends(get_session)):
return n8n_integration.record_heartbeat(session, payload.model_dump())
@n8n_router.get("/events")
def events(
cursor: str | None = Query(default=None, max_length=600),
limit: int = Query(default=100, ge=1, le=250),
session: Session = Depends(get_session),
):
try:
return n8n_integration.event_feed(session, cursor=cursor, limit=limit)
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
@n8n_router.post("/knowledge/query")
def knowledge_query(payload: KnowledgeRequest, session: Session = Depends(get_session)):
try:
return knowledge_service.query(session, payload.question, limit=payload.limit)
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
@n8n_router.post("/preflight")
def preflight(payload: PreflightRequest, session: Session = Depends(get_session)):
return n8n_integration.preflight(session, payload.action, payload.target)
@n8n_router.post("/approvals")
def create_approval(
payload: ApprovalRequest,
idempotency_key: str = Header(
alias="Idempotency-Key", min_length=16, max_length=128
),
session: Session = Depends(get_session),
):
try:
return n8n_integration.request_approval(
session,
action=payload.action,
target=payload.target,
reason=payload.reason,
plan=payload.plan,
idempotency_key=idempotency_key,
)
except PermissionError as exc:
raise HTTPException(status_code=403, detail=str(exc)) from exc
except ValueError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
@n8n_router.get("/approvals/{approval_id}")
def approval(approval_id: str, session: Session = Depends(get_session)):
result = n8n_integration.approval_status(session, approval_id)
if not result:
raise HTTPException(status_code=404, detail="approval_not_found")
return result
@n8n_router.get("/jobs/{job_id}")
def job(job_id: str, session: Session = Depends(get_session)):
result = n8n_integration.job_status(session, job_id)
if not result:
raise HTTPException(status_code=404, detail="job_not_found")
return result