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: #44
This commit was merged in pull request #44.
This commit is contained in:
251
backend/api/routes_colony.py
Normal file
251
backend/api/routes_colony.py
Normal file
@@ -0,0 +1,251 @@
|
||||
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),
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user