Rifqi Hafizuddin
[NOTICKET] fix(db): reconcile analyses schema + migrate chat to analyses_messages
283eb0e | """Deterministic routing gate β policy check over the router's intent. | |
| After the LLM router picks an intent, the gate checks it against the per-analysis | |
| Analysis State and returns the **effective** intent: allow as-is, or redirect. No | |
| LLM, no I/O in `gate()` itself. | |
| Only one rule has teeth in v1: an analytical request (`structured_flow`) requires a | |
| validated problem statement (`problem_validated is True`); otherwise it is | |
| redirected to `problem_statement` so the user defines the goal first. Everything | |
| else passes through. `generate_report` is not a router intent (button / report | |
| API), so it is not gated here. | |
| `AnalysisState` is the locked 8-field contract (mirrors the `analysis_states` DB | |
| table). `get_analysis_state` reads the real per-analysis row via `AnalysisStateStore` | |
| (#9, landed); it fails closed to a not-validated stub on a missing row or read error. | |
| See `ORCHESTRATOR_REWORK_PLAN.md` Β§4. | |
| """ | |
| from __future__ import annotations | |
| from datetime import UTC, datetime | |
| from pydantic import BaseModel, Field | |
| from src.agents.orchestration import Intent | |
| from src.middlewares.logging import get_logger | |
| logger = get_logger("gate") | |
| class AnalysisState(BaseModel): | |
| """Per-analysis state the Help skill + report layer read every turn. | |
| Field names mirror the dedorch `analyses` table so the DB read swaps in without | |
| touching readers. The goal is the user-entered `objective` + `business_questions` | |
| (set at onboarding by Go); the old `problem_statement`/`problem_validated` gate fields | |
| were dropped (dedorch #3 / KM-652). `report_id` is null until a report exists. | |
| """ | |
| id: str | |
| analysis_title: str | |
| objective: str = "" | |
| business_questions: list[str] = Field(default_factory=list) | |
| user_id: str | |
| report_id: str | None = None | |
| created_at: datetime | |
| updated_at: datetime | |
| def gate(intent: Intent, state: AnalysisState) -> Intent: | |
| """Return the effective intent (NEUTERED 2026-06-24 β passes everything through). | |
| The `problem_validated` gate was removed: analysis is no longer gated on a validated | |
| problem statement (the goal is now two user-entered fields, `objective` + | |
| `business_questions`, captured at onboarding with no agent validation). Kept as a | |
| no-op seam so gating can be restored without re-threading call sites. | |
| """ | |
| # Pre-2026-06-24 policy: redirect analytical requests until the goal was validated. | |
| # if intent == "structured_flow" and not state.problem_validated: | |
| # logger.info( | |
| # "gate redirect", | |
| # requested=intent, | |
| # effective="problem_statement", | |
| # reason="problem_not_validated", | |
| # ) | |
| # return "problem_statement" | |
| return intent | |
| def stub_analysis_state() -> AnalysisState: | |
| """Hardcoded Analysis State for never-throw fallbacks / tests. | |
| Shared fixture so the gate seam, the Help skill, and tests all exercise the same | |
| shape when a real row is missing or a read fails. | |
| """ | |
| now = datetime.now(UTC) | |
| return AnalysisState( | |
| id="stub-analysis", | |
| analysis_title="Stub analysis", | |
| objective="", | |
| business_questions=[], | |
| user_id="stub-user", | |
| report_id=None, | |
| created_at=now, | |
| updated_at=now, | |
| ) | |
| async def get_analysis_state(analysis_id: str) -> AnalysisState: | |
| """Load the Analysis State for an analysis (shared id with the chat room). | |
| Reads the `analysis_states` row via `AnalysisStateStore`. Never-throw seam: a | |
| missing row (e.g. a legacy room created before this table) or a read failure | |
| degrades to a **not-validated** stub, so the gate fails closed (β steer to | |
| `problem_statement`) rather than running ungated analysis. The store import is | |
| lazy so this module stays import-safe without a DB. | |
| """ | |
| try: | |
| from src.agents.state_store import AnalysisStateStore | |
| state = await AnalysisStateStore().get(analysis_id) | |
| except Exception as exc: # noqa: BLE001 β never-throw; fail closed to not-validated | |
| logger.warning( | |
| "get_analysis_state read failed β default not-validated", | |
| analysis_id=analysis_id, | |
| error=str(exc), | |
| ) | |
| return stub_analysis_state() | |
| if state is None: | |
| logger.debug("analysis_state missing β default not-validated", analysis_id=analysis_id) | |
| return stub_analysis_state() | |
| return state | |