Files
Project_Velocity/backend/api/routes_colony.py
sayan eeb684b46c feat: Ipad app production readiness, Colony orchestration, Social posting (#44)
#38 Ipad app production readiness, Colony orchestration, Social posting

Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local>
Reviewed-on: sagnik/Project_Velocity#44
2026-05-03 18:30:38 +05:30

252 lines
9.5 KiB
Python

from __future__ import annotations
import uuid
from typing import Any, Literal
import httpx
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from pydantic import BaseModel, Field
from backend.auth.dependencies import UserPrincipal, get_current_user
from backend.services.colony_gateway import ColonyConfigurationError, ColonyGateway, ColonyGatewayError
from backend.services.colony_repository import ColonyRepository
router = APIRouter()
MissionType = Literal["oracle_advisory", "crm_lead_intelligence", "catalyst_strategy_brief"]
RiskLevel = Literal["low", "medium", "high"]
SensitivityClass = Literal["public", "internal", "confidential"]
class MissionCreateRequest(BaseModel):
mission_type: MissionType
user_goal: str = Field(..., min_length=1, max_length=2000)
normalized_goal: str | None = Field(default=None, max_length=2000)
origin_surface: str = Field(default="api", min_length=1, max_length=128)
actor_role: str | None = Field(default=None, max_length=128)
risk_level: RiskLevel = "low"
sensitivity_class: SensitivityClass = "internal"
time_budget_ms: int = Field(default=30000, gt=0, le=300000)
token_budget: int = Field(default=4096, gt=0, le=200000)
context_refs: dict[str, Any] = Field(default_factory=dict)
requested_outputs: list[str] = Field(default_factory=list)
payload: dict[str, Any] = Field(default_factory=dict)
class ApprovalRequest(BaseModel):
reason: str | None = Field(default=None, max_length=2000)
def _get_repo(request: Request) -> ColonyRepository:
pool = getattr(request.app.state, "db_pool", None)
if pool is None:
raise HTTPException(status_code=503, detail="Database unavailable.")
return ColonyRepository(pool)
def _serialize_mission(row: dict[str, Any], dispatch: dict[str, Any] | None = None) -> dict[str, Any]:
data = {
"mission_id": str(row["mission_id"]),
"tenant_id": row["tenant_id"],
"mission_type": row["mission_type"],
"origin_surface": row["origin_surface"],
"actor_id": row["actor_id"],
"actor_role": row["actor_role"],
"risk_level": row["risk_level"],
"sensitivity_class": row["sensitivity_class"],
"status": row["status"],
"review_status": row["review_status"],
"time_budget_ms": row["time_budget_ms"],
"token_budget": row["token_budget"],
"user_goal": row["user_goal"],
"normalized_goal": row["normalized_goal"],
"context_refs": row["context_refs"] or {},
"requested_outputs": row["requested_outputs"] or [],
"payload": row["payload"] or {},
"created_at": row["created_at"].isoformat() if row.get("created_at") else None,
"updated_at": row["updated_at"].isoformat() if row.get("updated_at") else None,
"completed_at": row["completed_at"].isoformat() if row.get("completed_at") else None,
}
if dispatch is not None:
data["dispatch"] = dispatch
return data
@router.post("/missions", status_code=status.HTTP_201_CREATED, summary="Create and dispatch a colony mission")
async def create_mission(
body: MissionCreateRequest,
request: Request,
user: UserPrincipal = Depends(get_current_user),
) -> dict[str, Any]:
repo = _get_repo(request)
try:
gateway = ColonyGateway()
except ColonyConfigurationError as exc:
raise HTTPException(status_code=503, detail=str(exc)) from exc
mission_id = str(uuid.uuid4())
mission = {
"mission_id": mission_id,
"mission_type": body.mission_type,
"origin_surface": body.origin_surface,
"tenant_id": user.tenant_id,
"actor_id": user.user_id,
"actor_role": body.actor_role or user.role,
"risk_level": body.risk_level,
"sensitivity_class": body.sensitivity_class,
"time_budget_ms": body.time_budget_ms,
"token_budget": body.token_budget,
"user_goal": body.user_goal,
"normalized_goal": body.normalized_goal or body.user_goal,
"context_refs": body.context_refs,
"requested_outputs": body.requested_outputs,
"payload": body.payload,
}
row = await repo.create_mission(mission)
await repo.log_event(
mission_id=mission_id,
tenant_id=user.tenant_id,
event_type="mission_created",
actor=user.user_id,
detail={"mission_type": body.mission_type},
)
try:
dispatch = await gateway.dispatch_mission(mission)
except ColonyGatewayError as exc:
failed = await repo.update_status(mission_id, user.tenant_id, "dispatch_failed")
await repo.log_event(
mission_id=mission_id,
tenant_id=user.tenant_id,
event_type="mission_dispatch_failed",
actor=user.user_id,
detail={"error": str(exc)},
)
raise HTTPException(
status_code=502,
detail={
"message": str(exc),
"mission": _serialize_mission(failed or row),
},
) from exc
queued = await repo.update_status(mission_id, user.tenant_id, "queued")
await repo.log_event(
mission_id=mission_id,
tenant_id=user.tenant_id,
event_type="mission_dispatched",
actor=user.user_id,
detail={"dispatch": dispatch},
)
return {"status": "ok", "data": _serialize_mission(queued or row, dispatch=dispatch)}
@router.get("/missions", summary="List colony missions for the authenticated tenant")
async def list_missions(
request: Request,
limit: int = Query(default=50, ge=1, le=200),
offset: int = Query(default=0, ge=0),
user: UserPrincipal = Depends(get_current_user),
) -> dict[str, Any]:
rows = await _get_repo(request).list_missions(user.tenant_id, limit=limit, offset=offset)
return {"status": "ok", "data": [_serialize_mission(row) for row in rows], "meta": {"count": len(rows)}}
@router.get("/missions/{mission_id}", summary="Get a colony mission")
async def get_mission(
mission_id: str,
request: Request,
user: UserPrincipal = Depends(get_current_user),
) -> dict[str, Any]:
row = await _get_repo(request).get_mission(mission_id, user.tenant_id)
if row is None:
raise HTTPException(status_code=404, detail=f"Mission '{mission_id}' not found.")
return {"status": "ok", "data": _serialize_mission(row)}
@router.get("/missions/{mission_id}/artifacts", summary="Get mission tasks, results, and writeback proposals")
async def get_artifacts(
mission_id: str,
request: Request,
user: UserPrincipal = Depends(get_current_user),
) -> dict[str, Any]:
artifacts = await _get_repo(request).artifacts(mission_id, user.tenant_id)
if not artifacts:
raise HTTPException(status_code=404, detail=f"Mission '{mission_id}' not found.")
return {"status": "ok", "data": artifacts}
@router.post("/missions/{mission_id}/approve", summary="Approve all pending writeback proposals for a mission")
async def approve_writebacks(
mission_id: str,
body: ApprovalRequest,
request: Request,
user: UserPrincipal = Depends(get_current_user),
) -> dict[str, Any]:
del body
repo = _get_repo(request)
if await repo.get_mission(mission_id, user.tenant_id) is None:
raise HTTPException(status_code=404, detail=f"Mission '{mission_id}' not found.")
count = await repo.approve_pending_writebacks(mission_id, user.tenant_id, user.user_id)
if count == 0:
raise HTTPException(status_code=404, detail="No pending writeback proposals.")
await repo.log_event(
mission_id=mission_id,
tenant_id=user.tenant_id,
event_type="writeback_approved",
actor=user.user_id,
detail={"approved": count},
)
return {"status": "ok", "data": {"approved": count}}
@router.post("/missions/{mission_id}/reject", summary="Reject all pending writeback proposals for a mission")
async def reject_writebacks(
mission_id: str,
body: ApprovalRequest,
request: Request,
user: UserPrincipal = Depends(get_current_user),
) -> dict[str, Any]:
repo = _get_repo(request)
if await repo.get_mission(mission_id, user.tenant_id) is None:
raise HTTPException(status_code=404, detail=f"Mission '{mission_id}' not found.")
count = await repo.reject_pending_writebacks(
mission_id,
user.tenant_id,
user.user_id,
body.reason or "Rejected by operator.",
)
if count == 0:
raise HTTPException(status_code=404, detail="No pending writeback proposals.")
await repo.log_event(
mission_id=mission_id,
tenant_id=user.tenant_id,
event_type="writeback_rejected",
actor=user.user_id,
detail={"rejected": count, "reason": body.reason},
)
return {"status": "ok", "data": {"rejected": count}}
@router.get("/health", summary="Check colony root persistence and orchestrator connectivity")
async def colony_health(
request: Request,
user: UserPrincipal = Depends(get_current_user),
) -> dict[str, Any]:
repo = _get_repo(request)
try:
gateway = ColonyGateway()
service = await gateway.health()
except (ColonyConfigurationError, ColonyGatewayError, httpx.HTTPError) as exc: # type: ignore[name-defined]
raise HTTPException(status_code=503, detail=str(exc)) from exc
rows = await repo.list_missions(user.tenant_id, limit=1, offset=0)
return {
"status": "ok",
"data": {
"tenant_id": user.tenant_id,
"root_db": "connected",
"orchestrator": service,
"has_missions": bool(rows),
},
}