Adds the load-bearing structural feature of the FlowPilot migration: a
"What we know" panel that holds confirmed facts for a session, fed by AI
[PROMOTE] markers and engineer-added notes. Facts feed the resolution
note preview (Phase 3) and survive across turns via stable UUIDs assigned
to pending_task_lane items.
Backend:
- FactSynthesisService: create/update/soft-delete facts with atomic
state_version bumps; LLM-backed synthesize_from_question/check on the
fact_synthesis (Haiku) action tier per Section 6.6.
- /api/v1/ai-sessions/{id}/facts CRUD + /facts/promote (proposed_text or
via synthesis). PATCH returns 403 for question/diagnostic_check facts
(edit the source item instead, Section 7.3).
- unified_chat_service: [PROMOTE] marker parser (JSON-block per Section
8.1 spec drift note), stable-UUID assignment for pending_task_lane
questions/actions preserved by exact text/label match across turns.
- ASSISTANT_SYSTEM_PROMPT: documents [PROMOTE] format, when to/not to
emit, hallucination guardrails, source_ref handling.
- 17 tests covering parser, stable IDs, service validation, CRUD,
editability rule, both promote modes, 422 null-synthesis path,
state_version invariant.
Frontend:
- src/components/pilot/sections/{WhatWeKnow,WhatWeKnowItem,AddNoteButton}
— green-gradient section above Questions, dashed-circle check, inline
edit/delete gated by the server's editable flag.
- TaskLane gains a whatWeKnowSlot prop (existing assistant/ folder kept
per the doc's "rename is opportunistic" guidance).
- AssistantChatPage fetches facts on selectChat and refetches after each
chat send (so [PROMOTE]-synthesized facts appear immediately); auto-
opens the lane when facts exist.
Verification: end-to-end smoke against the local docker stack confirms
all five endpoints (list/create/patch/delete/promote) plus the 403
editability rule. pytest suite verifies the same with mocked LLM. Live
[PROMOTE] flow remains untested until used in the UI — the marker shape
is covered by parser tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
82 lines
2.9 KiB
Python
82 lines
2.9 KiB
Python
"""Pydantic schemas for the FlowPilot "What we know" session facts.
|
|
|
|
See FLOWPILOT-MIGRATION.md Section 4.2 for the data model rationale.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Literal
|
|
from uuid import UUID
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
# AI-emittable source types are a subset (`user_note` is engineer-only).
|
|
AIEmittableSourceType = Literal["question", "diagnostic_check", "ai_synthesis"]
|
|
SourceType = Literal["question", "diagnostic_check", "user_note", "ai_synthesis"]
|
|
|
|
|
|
class SessionFactResponse(BaseModel):
|
|
"""A single fact card in the What-we-know panel."""
|
|
id: UUID
|
|
session_id: UUID
|
|
text: str
|
|
source_type: SourceType
|
|
source_ref: UUID | None
|
|
source_summary: str | None
|
|
created_by: UUID
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
# `editable` is computed server-side so the client doesn't have to
|
|
# re-encode the editability rule. It mirrors the PATCH endpoint's
|
|
# 403 policy: only user_note and ai_synthesis facts are editable.
|
|
editable: bool
|
|
|
|
model_config = {"from_attributes": False}
|
|
|
|
|
|
class SessionFactListResponse(BaseModel):
|
|
facts: list[SessionFactResponse]
|
|
|
|
|
|
class SessionFactCreateRequest(BaseModel):
|
|
"""Engineer-created manual fact (the "+ Add a note" affordance).
|
|
|
|
The endpoint hard-codes source_type="user_note" — manual creation cannot
|
|
spoof a question/check origin. Source-type-bound creation goes through
|
|
`/promote` instead.
|
|
"""
|
|
text: str = Field(..., min_length=1, max_length=2000)
|
|
summary: str | None = Field(None, max_length=200)
|
|
|
|
|
|
class SessionFactUpdateRequest(BaseModel):
|
|
"""Edit an existing fact's text or summary.
|
|
|
|
The endpoint returns 403 when the fact's source_type is `question` or
|
|
`diagnostic_check` — those facts must be edited at the source item.
|
|
"""
|
|
text: str | None = Field(None, min_length=1, max_length=2000)
|
|
summary: str | None = Field(None, max_length=200)
|
|
|
|
|
|
class SessionFactPromoteRequest(BaseModel):
|
|
"""Promote a question answer / check result into a fact.
|
|
|
|
Two modes:
|
|
- **Direct**: caller provides `proposed_text` (and optionally `proposed_summary`).
|
|
The fact is persisted as-is. Used by the AI [PROMOTE] marker path and by the
|
|
engineer's "edit then save" affordance.
|
|
- **Synthesize**: caller provides `raw_input` (the engineer's typed answer or
|
|
the check output) and the server drafts `text`/`summary` via the
|
|
FactSynthesisService. The draft is persisted immediately for now —
|
|
the supervisor-staging review is a future enhancement (out of scope per
|
|
Section 12).
|
|
|
|
Exactly one of `proposed_text` or `raw_input` must be set.
|
|
"""
|
|
source_type: AIEmittableSourceType
|
|
source_ref: UUID | None = None
|
|
proposed_text: str | None = Field(None, min_length=1, max_length=2000)
|
|
proposed_summary: str | None = Field(None, max_length=200)
|
|
raw_input: str | None = Field(None, min_length=1, max_length=10_000)
|