KI-Konzil/backend/services/blueprint_service.py
Claude 437db4ca68
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
2026-02-21 10:28:27 +00:00

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