From 5ac35ff8ced12ff0629a605426d0b90b9f9d6840 Mon Sep 17 00:00:00 2001 From: Paulo Date: Sun, 27 Sep 2026 22:16:53 +0200 Subject: [PATCH] Answer the decision with Druks gate controls The decision page drew its own outcome form and posted it to Panel's record_outcome route, which answered the RecordDecision gate. Druks now draws the controls for a subject's parked gate from the gate itself, so Panel asks for the call in-app and shows ui.GateControls. - RecordDecision parks with an in_app ask. Druks offers the three DecisionAction values as controls and a note box, because the gate declares both. The ask shows the run's latest artifact, the moderator's synthesis. - The decision page shows ui.GateControls(decision) in "The call" until an outcome is recorded. The page no longer reads the run status. - The record_outcome route and DecisionOutcomeRequest go. The operator's answer reaches the workflow through the Druks gate route. --- druks_panel/pages.py | 48 +++++++--------------------------------- druks_panel/routes.py | 26 +++------------------- druks_panel/schemas.py | 7 ------ druks_panel/workflows.py | 6 +---- 4 files changed, 12 insertions(+), 75 deletions(-) diff --git a/druks_panel/pages.py b/druks_panel/pages.py index a1e5fcc..8189b64 100644 --- a/druks_panel/pages.py +++ b/druks_panel/pages.py @@ -1,5 +1,4 @@ from druks import ui -from druks.workflows import SubjectStatus from druks_panel.contracts import AdvisorAssessment, ModeratorSynthesis from druks_panel.models import Decision @@ -49,8 +48,8 @@ def _synthesis(decision: Decision) -> ui.Section: ) -def _human_call(decision: Decision, status: SubjectStatus) -> ui.Section: - """The human gate: the form while the run waits, the record after.""" +def _human_call(decision: Decision) -> ui.Section: + """The human gate: the controls while the run waits, the record after.""" if decision.outcome: return ui.Section( title="The call", @@ -64,42 +63,12 @@ def _human_call(decision: Decision, status: SubjectStatus) -> ui.Section: ui.Quote(decision.outcome_note or "Nobody left a note."), ], ) - if status.is_parked: - return ui.Section( - title="The call", - blocks=[ - ui.Form( - description="The run stays parked until you record the outcome.", - fields=[ - ui.RadioField( - name="action", - label="Outcome", - options=[ - ui.Option("Proceed", value="proceed"), - ui.Option("Revise", value="revise"), - ui.Option("Pass", value="pass"), - ], - is_required=True, - ), - ui.TextAreaField( - name="note", - label="Note", - rows=3, - help_text="What the record must say about this call.", - ), - ], - action=ui.Action( - label="Record the decision", - operation="record_outcome", - arguments={"decision_id": decision.id}, - tone="primary", - ), - ) - ], - ) return ui.Section( title="The call", - blocks=[ui.Text("The run parks here after the moderator reports.")], + blocks=[ + ui.Text("After the moderator reports, the run waits here for your call."), + ui.GateControls(decision), + ], ) @@ -187,13 +156,12 @@ async def new_decision(): @ui.page("/decisions/{decision_id}", subject=Decision) async def decision(decision_id: int): decision = await Decision.get(id=decision_id) - status = await decision.get_status() assessments = [AdvisorAssessment.model_validate(item) for item in decision.assessments] return ui.Page( decision.title, description=decision.question, # The whole page follows: an operator watches each advisor land. A - # parked run publishes nothing, so a redraw cannot wipe the form below. + # parked run publishes nothing, so a redraw cannot wipe a half-written note. follows=decision, blocks=[ ui.Facts( @@ -242,6 +210,6 @@ async def decision(decision_id: int): ), ), _synthesis(decision), - _human_call(decision, status), + _human_call(decision), ], ) diff --git a/druks_panel/routes.py b/druks_panel/routes.py index f076696..a77e0e4 100644 --- a/druks_panel/routes.py +++ b/druks_panel/routes.py @@ -1,14 +1,8 @@ -from druks.workflows import WorkflowError -from fastapi import APIRouter, HTTPException, status +from fastapi import APIRouter, status from druks_panel.models import Decision -from druks_panel.schemas import ( - CreateDecisionRequest, - CreateDecisionResponse, - DecisionOutcomeRequest, - DecisionSummary, -) -from druks_panel.workflows import Deliberate, RecordDecision +from druks_panel.schemas import CreateDecisionRequest, CreateDecisionResponse, DecisionSummary +from druks_panel.workflows import Deliberate # Every APIRouter declared here mounts under /api/panel. router = APIRouter(prefix="/decisions") @@ -34,17 +28,3 @@ async def create_decision(body: CreateDecisionRequest) -> CreateDecisionResponse ) run_id = await Deliberate.start(subject=decision) return CreateDecisionResponse(id=decision.id, run_id=run_id) - - -@router.post( - "/{decision_id}/outcome", - status_code=status.HTTP_202_ACCEPTED, - operation_id="record_outcome", -) -async def record_outcome(decision_id: int, body: DecisionOutcomeRequest) -> dict[str, str]: - decision = await Decision.get(id=decision_id) - try: - await RecordDecision.answer(decision, action=body.action, note=body.note) - except WorkflowError as error: - raise HTTPException(status.HTTP_409_CONFLICT, str(error)) from error - return {"result": "recorded"} diff --git a/druks_panel/schemas.py b/druks_panel/schemas.py index b7ed4e6..94d82f1 100644 --- a/druks_panel/schemas.py +++ b/druks_panel/schemas.py @@ -14,13 +14,6 @@ class CreateDecisionRequest(BaseModel): context: str = Field(default="", max_length=4000) -class DecisionOutcomeRequest(BaseModel): - model_config = ConfigDict(str_strip_whitespace=True) - - action: DecisionAction - note: str = Field(default="", max_length=2000) - - class CreateDecisionResponse(Schema): id: int run_id: str diff --git a/druks_panel/workflows.py b/druks_panel/workflows.py index e09ac90..408519f 100644 --- a/druks_panel/workflows.py +++ b/druks_panel/workflows.py @@ -55,11 +55,7 @@ async def run_multistep(self) -> dict[str, str]: ) reply = await RecordDecision.wait( - input_request={ - "presentation": "external", - "label": "Record the decision", - "url": f"/panel/decisions/{decision.id}", - } + input_request={"presentation": "in_app", "label": "Record the decision"} ) await self.persist_outcome(action=reply.action, note=reply.note) await self.announce("decision.recorded", action=reply.action)