Sets up the full backend foundation for CouncilOS:
- CouncilState TypedDict with all required fields and LangGraph reducers
- Three agent nodes: master_agent (drafts), critic_agent (scores + routes),
writer_agent (final polish)
- LangGraph graph with cyclic rework loop: Master → Critic → (score < 8:
back to Master | score ≥ 8: Writer → END)
- Safety valve: MAX_ITERATIONS=5 prevents infinite loops
- FastAPI app with REST endpoints (POST /api/councils/run, GET /api/councils/run/{id})
and WebSocket endpoint (/ws/council/{run_id}) for real-time agent status events
- In-memory RunStore for Phase 1 (PostgreSQL-backed in Phase 3)
- pytest test suite: state, routing logic, critic parser, agent nodes, API endpoints
- .env.example, .gitignore, docker-compose.yml, Dockerfile
https://claude.ai/code/session_01RfMpt3TbMjZEtK3CAyP5iQ
135 lines
4.2 KiB
Python
135 lines
4.2 KiB
Python
"""
|
|
REST API routes for CouncilOS.
|
|
|
|
Endpoints:
|
|
POST /api/councils/run — Start a new council run (async, returns run_id)
|
|
GET /api/councils/run/{run_id} — Poll the status/result of a run
|
|
GET /api/health — Health check
|
|
"""
|
|
|
|
import uuid
|
|
from typing import Optional
|
|
from fastapi import APIRouter, HTTPException, BackgroundTasks
|
|
from pydantic import BaseModel, Field
|
|
|
|
from services.graph_builder import run_council_async
|
|
from api.run_store import run_store
|
|
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Request / Response Models
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class CouncilRunRequest(BaseModel):
|
|
input_topic: str = Field(
|
|
...,
|
|
min_length=1,
|
|
max_length=10_000,
|
|
description="The user's prompt or document content for the council to work on.",
|
|
examples=["Erkläre die wichtigsten Konzepte des maschinellen Lernens für Einsteiger."],
|
|
)
|
|
|
|
|
|
class CouncilRunResponse(BaseModel):
|
|
run_id: str
|
|
status: str # "pending" | "running" | "completed" | "failed"
|
|
message: str
|
|
|
|
|
|
class CouncilResultResponse(BaseModel):
|
|
run_id: str
|
|
status: str
|
|
final_draft: Optional[str] = None
|
|
critic_score: Optional[float] = None
|
|
iteration_count: Optional[int] = None
|
|
error: Optional[str] = None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Endpoints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@router.get("/health")
|
|
async def health_check():
|
|
"""Health check endpoint."""
|
|
return {"status": "ok", "service": "CouncilOS API"}
|
|
|
|
|
|
@router.post("/councils/run", response_model=CouncilRunResponse, status_code=202)
|
|
async def start_council_run(
|
|
request: CouncilRunRequest,
|
|
background_tasks: BackgroundTasks,
|
|
):
|
|
"""
|
|
Start a new council run.
|
|
|
|
The run executes asynchronously in the background. Poll
|
|
GET /api/councils/run/{run_id} for the result, or connect to the
|
|
WebSocket at /ws/council/{run_id} for real-time updates.
|
|
"""
|
|
run_id = str(uuid.uuid4())
|
|
|
|
# Register the run as pending in the in-memory store
|
|
run_store.create(run_id, request.input_topic)
|
|
|
|
# Schedule the graph execution as a background task
|
|
background_tasks.add_task(_execute_run, run_id, request.input_topic)
|
|
|
|
return CouncilRunResponse(
|
|
run_id=run_id,
|
|
status="pending",
|
|
message=f"Council run started. Connect to /ws/council/{run_id} for live updates.",
|
|
)
|
|
|
|
|
|
@router.get("/councils/run/{run_id}", response_model=CouncilResultResponse)
|
|
async def get_council_result(run_id: str):
|
|
"""
|
|
Retrieve the current status or final result of a council run.
|
|
"""
|
|
run = run_store.get(run_id)
|
|
if run is None:
|
|
raise HTTPException(status_code=404, detail=f"Run '{run_id}' not found.")
|
|
|
|
return CouncilResultResponse(
|
|
run_id=run_id,
|
|
status=run["status"],
|
|
final_draft=run.get("final_draft"),
|
|
critic_score=run.get("critic_score"),
|
|
iteration_count=run.get("iteration_count"),
|
|
error=run.get("error"),
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Internal helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def _execute_run(run_id: str, input_topic: str) -> None:
|
|
"""
|
|
Background task that runs the LangGraph council and updates the run store.
|
|
"""
|
|
run_store.update(run_id, {"status": "running"})
|
|
try:
|
|
final_state = await run_council_async(
|
|
input_topic=input_topic,
|
|
run_id=run_id,
|
|
on_node_event=lambda nid, node: run_store.update(
|
|
nid, {"active_node": node}
|
|
),
|
|
)
|
|
run_store.update(
|
|
run_id,
|
|
{
|
|
"status": "completed",
|
|
"final_draft": final_state.get("current_draft"),
|
|
"critic_score": final_state.get("critic_score"),
|
|
"iteration_count": final_state.get("iteration_count"),
|
|
"active_node": "done",
|
|
},
|
|
)
|
|
except Exception as exc: # noqa: BLE001
|
|
run_store.update(run_id, {"status": "failed", "error": str(exc)})
|