generated from coulomb/repo-seed
Task flow engine implementation
This commit is contained in:
@@ -6,6 +6,7 @@ from sqlalchemy import func, select, text
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from api.database import get_session, engine
|
||||
from api.flow_defs import assertion_result_to_dict, load_flow
|
||||
from api.models.capability_request import CapabilityRequest
|
||||
from api.models.contribution import Contribution, ContributionStatus, ContributionType
|
||||
from api.models.decision import Decision, DecisionStatus, DecisionType
|
||||
@@ -17,7 +18,7 @@ from api.models.sbom_entry import SBOMEntry
|
||||
from api.models.task import Task, TaskPriority, TaskStatus
|
||||
from api.models.technical_debt import TechnicalDebt
|
||||
from api.models.topic import Topic, TopicStatus
|
||||
from api.models.workstream import Workstream, WorkstreamStatus
|
||||
from api.models.workstream import Workstream
|
||||
from api.models.workstream_dependency import WorkstreamDependency
|
||||
from api.schemas.decision import DecisionRead
|
||||
from api.schemas.domain import DomainSummary
|
||||
@@ -35,6 +36,7 @@ from api.schemas.task import TaskRead
|
||||
from api.schemas.topic import TopicWithWorkstreams
|
||||
from api.schemas.workstream import WorkstreamRead, WorkstreamWithTaskCounts, WorkstreamWithDeps
|
||||
from api.schemas.workstream_dependency import WorkstreamDepStub
|
||||
from task_flow_engine import FlowEngine
|
||||
|
||||
router = APIRouter(prefix="/state", tags=["state"])
|
||||
|
||||
@@ -69,7 +71,7 @@ async def get_summary(session: AsyncSession = Depends(get_session)) -> StateSumm
|
||||
|
||||
open_ws_rows = await session.execute(
|
||||
select(Workstream)
|
||||
.where(Workstream.status.in_([WorkstreamStatus.active, WorkstreamStatus.blocked]))
|
||||
.where(Workstream.status.in_(["active", "blocked"]))
|
||||
.order_by(Workstream.due_date.asc().nullslast(), Workstream.created_at)
|
||||
)
|
||||
open_ws = list(open_ws_rows.scalars().all())
|
||||
@@ -128,6 +130,27 @@ async def get_summary(session: AsyncSession = Depends(get_session)) -> StateSumm
|
||||
description=d.description,
|
||||
))
|
||||
|
||||
workstream_flow = load_flow("workstream")
|
||||
flow_engine = FlowEngine()
|
||||
effective_status: dict = {}
|
||||
blocked_reasons: dict = {}
|
||||
for w in open_ws:
|
||||
flow_obj = {
|
||||
"status": w.status,
|
||||
"workstation": w.status,
|
||||
"tasks": [{"status": _value(t.status)} for t in w.tasks],
|
||||
"dependencies": [
|
||||
{"workstation": ws_lookup[d.to_workstream_id].status}
|
||||
for d in dep_rows
|
||||
if d.from_workstream_id == w.id and d.to_workstream_id in ws_lookup
|
||||
],
|
||||
}
|
||||
flow_result = flow_engine.evaluate(flow_obj, workstream_flow)
|
||||
effective_status[w.id] = "blocked" if flow_result.exit_blocked else w.status
|
||||
blocked_reasons[w.id] = [
|
||||
assertion_result_to_dict(item) for item in flow_result.blocking_assertions
|
||||
]
|
||||
|
||||
# Totals — one GROUP BY per table
|
||||
topic_counts = {r[0]: r[1] for r in await session.execute(
|
||||
select(Topic.status, func.count()).group_by(Topic.status)
|
||||
@@ -150,10 +173,10 @@ async def get_summary(session: AsyncSession = Depends(get_session)) -> StateSumm
|
||||
total=sum(topic_counts.values()),
|
||||
),
|
||||
workstreams=WorkstreamTotals(
|
||||
active=ws_counts.get(WorkstreamStatus.active, 0),
|
||||
blocked=ws_counts.get(WorkstreamStatus.blocked, 0),
|
||||
completed=ws_counts.get(WorkstreamStatus.completed, 0),
|
||||
archived=ws_counts.get(WorkstreamStatus.archived, 0),
|
||||
active=sum(1 for status in effective_status.values() if status == "active"),
|
||||
blocked=sum(1 for status in effective_status.values() if status == "blocked"),
|
||||
completed=ws_counts.get("completed", 0),
|
||||
archived=ws_counts.get("archived", 0),
|
||||
total=sum(ws_counts.values()),
|
||||
),
|
||||
tasks=TaskTotals(
|
||||
@@ -226,7 +249,10 @@ async def get_summary(session: AsyncSession = Depends(get_session)) -> StateSumm
|
||||
open_capability_requests=open_cap_req_count,
|
||||
open_workstreams=[
|
||||
WorkstreamWithDeps(
|
||||
**WorkstreamRead.model_validate(w).model_dump(),
|
||||
**{
|
||||
**WorkstreamRead.model_validate(w).model_dump(),
|
||||
"status": effective_status.get(w.id, w.status),
|
||||
},
|
||||
tasks_total=sum(task_per_ws.get(w.id, {}).values()),
|
||||
tasks_todo=task_per_ws.get(w.id, {}).get(TaskStatus.todo, 0),
|
||||
tasks_in_progress=task_per_ws.get(w.id, {}).get(TaskStatus.in_progress, 0),
|
||||
@@ -234,6 +260,7 @@ async def get_summary(session: AsyncSession = Depends(get_session)) -> StateSumm
|
||||
tasks_done=task_per_ws.get(w.id, {}).get(TaskStatus.done, 0),
|
||||
depends_on=dep_index.get(w.id, {}).get("depends_on", []),
|
||||
blocks=dep_index.get(w.id, {}).get("blocks", []),
|
||||
blocked_reasons=blocked_reasons.get(w.id, []),
|
||||
)
|
||||
for w in open_ws
|
||||
],
|
||||
@@ -259,7 +286,7 @@ async def _build_domain_summaries(session: AsyncSession) -> list[DomainSummary]:
|
||||
for domain_id, cnt in await session.execute(
|
||||
select(Topic.domain_id, func.count(Workstream.id))
|
||||
.join(Workstream, Workstream.topic_id == Topic.id)
|
||||
.where(Workstream.status == WorkstreamStatus.active)
|
||||
.where(Workstream.status == "active")
|
||||
.group_by(Topic.domain_id)
|
||||
):
|
||||
ws_per_domain[domain_id] = cnt
|
||||
@@ -357,14 +384,14 @@ async def _derive_next_steps(session: AsyncSession) -> list[NextStep]:
|
||||
all_done = True
|
||||
for to_id in to_ws_ids:
|
||||
to_ws = await session.get(Workstream, to_id)
|
||||
if to_ws is None or to_ws.status != WorkstreamStatus.completed:
|
||||
if to_ws is None or to_ws.status != "completed":
|
||||
all_done = False
|
||||
break
|
||||
if not all_done:
|
||||
continue
|
||||
|
||||
from_ws = await session.get(Workstream, from_ws_id)
|
||||
if from_ws is None or from_ws.status not in (WorkstreamStatus.active, WorkstreamStatus.blocked):
|
||||
if from_ws is None or from_ws.status not in ("active", "blocked"):
|
||||
continue
|
||||
|
||||
todo_rows = await session.execute(
|
||||
@@ -414,6 +441,10 @@ async def _get_domain_slug_for_workstream(ws: Workstream | None, session: AsyncS
|
||||
return domain.slug if domain else None
|
||||
|
||||
|
||||
def _value(item):
|
||||
return item.value if hasattr(item, "value") else item
|
||||
|
||||
|
||||
@router.get("/next_steps", response_model=list[NextStep])
|
||||
async def get_next_steps(session: AsyncSession = Depends(get_session)) -> list[NextStep]:
|
||||
"""Derive contextual next-action suggestions from current hub state.
|
||||
|
||||
Reference in New Issue
Block a user