Skip to content
This repository was archived by the owner on Jul 22, 2026. It is now read-only.

Latest commit

 

History

History
204 lines (164 loc) · 10.2 KB

File metadata and controls

204 lines (164 loc) · 10.2 KB

Brief A MVP — analyst-workstation SPA + streaming ReAct agent

Design spec for the next milestone of hipo (HK IPO Verify). Focuses on Brief A only; Briefs B/C/D are explicitly deferred. Source of truth for requirements is ../../goal.md; product vision in ../../initial-plan.md.

Goal

Ship a demoable MVP in two pieces, on top of the already-live infrastructure (catalog Worker, R2 cache, parse/chunk/validate tools, Factsheet schema, LLM gateway client):

  1. Agent backend (AWS Lambda) — turn the currently-deterministic /factsheet/{id} into a bounded ReAct loop that calls one model via the gateway to extract cited claims, validates against the schema, and emits a replayable agent_trace. It streams reason→act→observe steps live over SSE from a Lambda Function URL, ending at a hard "never auto-act" interrupt.
  2. Analyst-workstation SPA (Cloudflare Pages) — React + Vite. A list of recent PHIP filings (one row per stock, an Analyze button per row) and an analysis view: four factsheet panels, a live-filling agent trace, page-level citations that deep-link to the source PDF, the three degradation states, and a persistent boundary banner.

The product signature is trust: an inspectable agent behind a polished UI, held inside hard boundaries — not autonomy.

Locked scope decisions

  • Single model, not multi-model. One cheap extractor per claim. The multi-model agreement panel (Brief B) is deferred; the schema and trace are built so it drops in later without rework.
  • SSE streaming from a Lambda Function URL in RESPONSE_STREAM mode — the agent visibly "thinks", and it sidesteps the ~29s API Gateway ceiling. The existing buffered /factsheet/{id} endpoint stays for tests and non-streaming callers.
  • React 19 + Vite + TypeScript on Cloudflare Pages (the literal match for Brief A's "React summary" wording).
  • PHIP only in the list (richest factsheet; fixture app-108691-1 on hand). The catalog Worker already indexes all three doc types; the list filters to docType === "phip" client-side.
  • Visual direction: calm-institutional light theme (dense table, restrained indigo accent, white surfaces) as the default, with a dark-mode toggle seeded from prefers-color-scheme and persisted. Analysis view uses the right-rail agent trace layout (reasoning always in view).

Deferred out of this MVP: multi-model agreement (Brief B), CI eval gate (Brief C), trust-degradation alarm (Brief D).

Piece 1 — Agent backend (AWS Lambda)

New apps/api/hipo_api/agent/ package, separate from the existing deterministic tools so each unit stays small and testable.

The ReAct loop

  • agent/tools.py — the whitelisted action space, each a plain function with a deterministic schema: parse, chunk, extract (the only model-calling tool, via the gateway client), validate. The registry is built to accept compare/arbitrate later but they are not in the MVP whitelist.
  • agent/loop.py — the bounded controller: reason → act → observe, a hard step cap (8), a tool whitelist it cannot escape, and a single terminal action interrupt that surfaces the draft and stops. It never calls anything outside the registry. Each iteration appends a TraceStep.
  • agent/trace.pyAgentTrace / TraceStep Pydantic models, added to packages/contracts so the frontend gets matching types.
  • agent/stream.py — renders each TraceStep as an SSE event: step and the final payload as event: result (or event: rejected).

How "reasoning" works with one model, cheaply

The loop is code-driven, not a model deciding each step. The model is called only inside extract, to pull claims + citations from candidate chunks. The "thoughts" are real controller decisions ("parsed 214 pages", "extracting from 6 candidate chunks", "validation failed on 1 field, degrading to Partial"). Cost is essentially one extract call per filing, and the trace is honest and inspectable.

Re-planning is genuine: if extract returns a claim whose citation fails validate, the loop drops that claim, records it in the trace, and ends Partial rather than fabricating.

Transport

lambda_handler.py gains a streaming entry (Lambda RESPONSE_STREAM) alongside the existing Mangum handler. New route POST /analyze/{id} streams SSE; the buffered POST /factsheet/{id} is unchanged.

Degradation states

  • Full — all extracted fields validated (schema + citations).
  • Partial — one or more claims dropped on citation/schema failure; the surviving validated output renders with a degraded badge.
  • Rejected — the schema/citation gate fails wholesale; a Rejection is emitted, no unsupported claims rendered.

Extract tool contract

extract receives candidate chunks (each tagged with its source page) and asks the model for claims in the Factsheet shape, each carrying page citations. Code — not the model — normalizes the model output into the Pydantic schema before validate runs. The model never sees or controls the loop; it only returns candidate claims.

Testing (TDD)

Loop tests with a fake gateway client assert: never exceeds the step cap; never calls outside the whitelist; always terminates at interrupt; and each of the three degradation states produces the correct payload. Plus an SSE-framing test. Existing parse/chunk/validate/retrieval tests stay green.

Piece 2 — Frontend (Cloudflare Pages)

apps/web/ — React 19 + Vite + TS, deployed to Pages. Small, well-bounded units:

  • api/client.ts — the only network module. Two calls: GET {CATALOG_BASE_URL}/catalog (filtered to docType === "phip"), and the streaming POST {API_BASE_URL}/analyze/{id} read via fetch + ReadableStream reader parsing SSE frames.
  • theme.ts + a toggle — CSS custom properties, prefers-color-scheme default, persisted to localStorage, switched via <html data-theme>.
  • Routes: FilingList (entry screen) and Analysis/:id (workstation).
  • Analysis composes: Overview, FinancialHighlights, KeyRisks, TraceRail (right rail — streams live then stays for replay), BoundaryBanner (persistent), DegradationBadge (Full/Partial/Rejected), CitationLink (deep-links to the cached PDF at the cited page).
  • Types come from packages/contracts — the Factsheet + AgentTrace shapes mirrored/generated as TS so front and back share one contract.

Data flow

list → click Analyze → navigate to /analysis/:id → open SSE stream → each event: step appends to the trace rail live → terminal event: result renders the four panels (or event: rejected renders the Rejected state with its reason, no unsupported claims). Citations render as buttons; clicking opens the cached PDF at the cited page ({CATALOG_BASE_URL}/filing/{id}#page=N) in a new tab.

Layout & visual

  • Entry screen: header + "recent PHIP filings, refreshed daily", then a dense table — one row per stock (name, doc type, date, sector/page count) and an Analyze button per row.
  • Analysis view: boundary banner pinned at top; factsheet panels on the left (~2/3), agent-trace + trust rail on the right (~1/3, always visible); citations as inline [p.N] buttons.
  • Theme: calm-institutional light default, indigo accent, excellent typography; dark-mode toggle seeded from prefers-color-scheme, persisted.

Error handling & degradation (Brief A reliability guards)

  • Stream error / model timeout mid-analysis → the trace shows the failed step and the UI settles into Partial (if validated claims survived) or Rejected (if not) — never a hanging spinner.
  • Catalog fetch failure → a plain "couldn't load filings, retry" state.
  • Every response carries the correlation ID; the frontend surfaces it in the trust rail.

Testing (TDD)

Vitest + React Testing Library: list renders rows; Analyze navigates; the SSE reducer appends steps in order; each degradation state renders correctly; the citation link builds the right #page=N URL. Accessibility: axe check in tests plus keyboard-navigable citations and visible focus states.

Integration & non-goals

  • mise run check stays the single green gate; apps/web lint/test wire into it as the catalog Worker already does.
  • Stack conventions (mirroring ~/dev/basis-vol-lab/apps/web): Vite + @vitejs/plugin-react, Tailwind v4 (@tailwindcss/vite), Biome, Bun; a check script of biome check src/ && tsc -b. React 19 (upgrade from the reference's 18) + React Router.
  • Hosting: Cloudflare Pages, custom domain hipo.vsh852.com. Following the catalog split, Terraform owns the Pages project + custom domain + DNS record (vsh852.com zone), and CI deploys the built dist/ via wrangler pages deploy on green. basis-vol-lab deploys its web app via Docker/nginx, not Pages, so the Pages wiring is net-new here.
  • CORS: the Lambda Function URL must allow the https://hipo.vsh852.com origin (and http://localhost:5173 for dev).
  • Auth: the existing API authorizer stays; the SPA calls the API with the configured key. (This is a public-data demo; no user accounts.) The SPA reads CATALOG_BASE_URL, API_BASE_URL, and the API key from Vite build-time env (VITE_*) injected by the Pages build; nothing secret is committed. Since the key ships to the browser in this demo, it is a low-privilege demo key scoped to these read/analyze routes only — noted as a demo limitation, not production auth.
  • No new paid infra — Pages, Lambda Function URL, and the existing R2/KV/gateway all sit in free tiers.

Non-goals (explicitly out)

Multi-model comparison, arbiter, eval gate/CI regression test, the observability alarm, user upload, doc types other than PHIP, and any agent action outside the tool whitelist.

Success criteria

  • Deployed Pages link (or screenshots) showing: the PHIP list, a live-streaming analysis, and each of Full / Partial / Rejected.
  • Every rendered claim deep-links to its cited source page.
  • The persistent "never auto-act" boundary banner is always visible on the analysis view.
  • The agent trace replays every reason→act→observe step and always ends at the interrupt.
  • mise run check green (backend + frontend + infra).