- Add dynamic_graph_builder.py that constructs LangGraph graphs at runtime
from frontend CouncilBlueprint JSON (no more hardcoded graphs in production)
- Add PostgreSQL persistence via SQLAlchemy async with Blueprint model
- Add blueprint CRUD endpoints (POST/GET/PUT/DELETE /api/councils/)
- Add POST /api/councils/{id}/run to execute blueprints dynamically
- Add Alembic migration infrastructure with initial blueprints table
- Add database.py with async engine and SQLite fallback for dev/test
- Fix missing typing-extensions and add aiosqlite dependency
- Add 42 new tests (80/80 total passing) covering dynamic graph building,
blueprint service CRUD, and API integration
https://claude.ai/code/session_014yZUxrPsgZbvkebXbCXR4U
153 lines
4.5 KiB
Python
153 lines
4.5 KiB
Python
"""
|
|
REST API routes for council blueprint CRUD.
|
|
|
|
Endpoints:
|
|
GET /api/councils/ — List all blueprints
|
|
POST /api/councils/ — Create a new blueprint
|
|
GET /api/councils/{id} — Get a specific blueprint
|
|
PUT /api/councils/{id} — Update a blueprint
|
|
DELETE /api/councils/{id} — Delete a blueprint
|
|
"""
|
|
|
|
from typing import List, Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel, Field
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from database import get_session
|
|
from services.blueprint_service import (
|
|
create_blueprint,
|
|
delete_blueprint,
|
|
get_blueprint,
|
|
list_blueprints,
|
|
update_blueprint,
|
|
)
|
|
|
|
blueprint_router = APIRouter()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Request / Response Models
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class AgentTools(BaseModel):
|
|
webSearch: bool = False
|
|
pdfReader: bool = False
|
|
|
|
|
|
class BlueprintNodeSchema(BaseModel):
|
|
id: str
|
|
label: str
|
|
systemPrompt: str = ""
|
|
model: str = "claude-3-5-sonnet"
|
|
tools: AgentTools = Field(default_factory=AgentTools)
|
|
position: dict = Field(default_factory=lambda: {"x": 0, "y": 0})
|
|
|
|
|
|
class BlueprintEdgeSchema(BaseModel):
|
|
id: str
|
|
source: str
|
|
target: str
|
|
type: str = "linear"
|
|
condition: Optional[str] = None
|
|
|
|
|
|
class BlueprintCreateRequest(BaseModel):
|
|
version: int = 1
|
|
name: str = Field(..., min_length=1, max_length=255)
|
|
nodes: List[BlueprintNodeSchema]
|
|
edges: List[BlueprintEdgeSchema] = []
|
|
id: Optional[str] = None
|
|
|
|
|
|
class BlueprintUpdateRequest(BaseModel):
|
|
name: Optional[str] = Field(None, min_length=1, max_length=255)
|
|
nodes: Optional[List[BlueprintNodeSchema]] = None
|
|
edges: Optional[List[BlueprintEdgeSchema]] = None
|
|
|
|
|
|
class BlueprintResponse(BaseModel):
|
|
id: str
|
|
version: int
|
|
name: str
|
|
nodes: list
|
|
edges: list
|
|
createdAt: Optional[str] = None
|
|
updatedAt: Optional[str] = None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Endpoints
|
|
# ---------------------------------------------------------------------------
|
|
|
|
@blueprint_router.get("/councils/", response_model=List[BlueprintResponse])
|
|
async def list_all_blueprints(
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""List all council blueprints."""
|
|
blueprints = await list_blueprints(session)
|
|
return [bp.to_dict() for bp in blueprints]
|
|
|
|
|
|
@blueprint_router.post(
|
|
"/councils/",
|
|
response_model=BlueprintResponse,
|
|
status_code=201,
|
|
)
|
|
async def create_new_blueprint(
|
|
request: BlueprintCreateRequest,
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Create a new council blueprint."""
|
|
bp = await create_blueprint(
|
|
session=session,
|
|
name=request.name,
|
|
nodes=[n.model_dump() for n in request.nodes],
|
|
edges=[e.model_dump() for e in request.edges],
|
|
blueprint_id=request.id,
|
|
version=request.version,
|
|
)
|
|
return bp.to_dict()
|
|
|
|
|
|
@blueprint_router.get("/councils/{blueprint_id}", response_model=BlueprintResponse)
|
|
async def get_single_blueprint(
|
|
blueprint_id: str,
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Retrieve a specific blueprint by ID."""
|
|
bp = await get_blueprint(session, blueprint_id)
|
|
if bp is None:
|
|
raise HTTPException(status_code=404, detail=f"Blueprint '{blueprint_id}' not found.")
|
|
return bp.to_dict()
|
|
|
|
|
|
@blueprint_router.put("/councils/{blueprint_id}", response_model=BlueprintResponse)
|
|
async def update_existing_blueprint(
|
|
blueprint_id: str,
|
|
request: BlueprintUpdateRequest,
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Update an existing blueprint."""
|
|
bp = await update_blueprint(
|
|
session=session,
|
|
blueprint_id=blueprint_id,
|
|
name=request.name,
|
|
nodes=[n.model_dump() for n in request.nodes] if request.nodes is not None else None,
|
|
edges=[e.model_dump() for e in request.edges] if request.edges is not None else None,
|
|
)
|
|
if bp is None:
|
|
raise HTTPException(status_code=404, detail=f"Blueprint '{blueprint_id}' not found.")
|
|
return bp.to_dict()
|
|
|
|
|
|
@blueprint_router.delete("/councils/{blueprint_id}", status_code=204)
|
|
async def delete_existing_blueprint(
|
|
blueprint_id: str,
|
|
session: AsyncSession = Depends(get_session),
|
|
):
|
|
"""Delete a blueprint by ID."""
|
|
deleted = await delete_blueprint(session, blueprint_id)
|
|
if not deleted:
|
|
raise HTTPException(status_code=404, detail=f"Blueprint '{blueprint_id}' not found.")
|