#38 Ipad app production readiness, Colony orchestration, Social posting Co-authored-by: Sayan Datta <sayan@Sayans-MacBook-Air.local> Reviewed-on: #44
252 lines
9.5 KiB
Python
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),
|
|
},
|
|
}
|