- 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
95 lines
2.3 KiB
Python
95 lines
2.3 KiB
Python
"""
|
|
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
|