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
95
backend/services/blueprint_service.py
Normal file
95
backend/services/blueprint_service.py
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
"""
|
||||
Blueprint Service — CRUD operations for council blueprints.
|
||||
|
||||
Handles persistence of blueprints to PostgreSQL via SQLAlchemy async sessions.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models.blueprint import Blueprint
|
||||
|
||||
|
||||
async def create_blueprint(
|
||||
session: AsyncSession,
|
||||
name: str,
|
||||
nodes: list,
|
||||
edges: list,
|
||||
blueprint_id: Optional[str] = None,
|
||||
version: int = 1,
|
||||
) -> Blueprint:
|
||||
"""Create and persist a new blueprint."""
|
||||
bp = Blueprint(
|
||||
name=name,
|
||||
version=version,
|
||||
nodes=nodes,
|
||||
edges=edges,
|
||||
)
|
||||
if blueprint_id:
|
||||
bp.id = blueprint_id
|
||||
|
||||
session.add(bp)
|
||||
await session.commit()
|
||||
await session.refresh(bp)
|
||||
return bp
|
||||
|
||||
|
||||
async def get_blueprint(
|
||||
session: AsyncSession,
|
||||
blueprint_id: str,
|
||||
) -> Optional[Blueprint]:
|
||||
"""Retrieve a blueprint by ID."""
|
||||
result = await session.execute(
|
||||
select(Blueprint).where(Blueprint.id == blueprint_id)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
|
||||
async def list_blueprints(session: AsyncSession) -> List[Blueprint]:
|
||||
"""Retrieve all blueprints, ordered by most recently updated."""
|
||||
result = await session.execute(
|
||||
select(Blueprint).order_by(Blueprint.updated_at.desc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def update_blueprint(
|
||||
session: AsyncSession,
|
||||
blueprint_id: str,
|
||||
name: Optional[str] = None,
|
||||
nodes: Optional[list] = None,
|
||||
edges: Optional[list] = None,
|
||||
) -> Optional[Blueprint]:
|
||||
"""Update an existing blueprint. Returns None if not found."""
|
||||
bp = await get_blueprint(session, blueprint_id)
|
||||
if bp is None:
|
||||
return None
|
||||
|
||||
if name is not None:
|
||||
bp.name = name
|
||||
if nodes is not None:
|
||||
bp.nodes = nodes
|
||||
if edges is not None:
|
||||
bp.edges = edges
|
||||
bp.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
await session.commit()
|
||||
await session.refresh(bp)
|
||||
return bp
|
||||
|
||||
|
||||
async def delete_blueprint(
|
||||
session: AsyncSession,
|
||||
blueprint_id: str,
|
||||
) -> bool:
|
||||
"""Delete a blueprint by ID. Returns True if deleted, False if not found."""
|
||||
bp = await get_blueprint(session, blueprint_id)
|
||||
if bp is None:
|
||||
return False
|
||||
|
||||
await session.delete(bp)
|
||||
await session.commit()
|
||||
return True
|
||||
Loading…
Add table
Add a link
Reference in a new issue