151 lines
5.1 KiB
Python
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
|