Implement Phase 3: dynamic graph builder, blueprint persistence, and CRUD API
- 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
This commit is contained in:
parent
89ba3aacd4
commit
437db4ca68
16 changed files with 1698 additions and 11 deletions
153
backend/api/blueprint_routes.py
Normal file
153
backend/api/blueprint_routes.py
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
"""
|
||||
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.")
|
||||
Loading…
Add table
Add a link
Reference in a new issue