Stock Advisor Copilot — Cited Filing Research Behind an Approval Gate
A multi-agent LangGraph backend that pairs a live quote with a cited summary of a company's latest SEC 10-K — screened by an input guardrail up front and gated by a compliance agent that pauses for advisor sign-off before anything is treated as final.
The problem & requirements
- Get an advisor up to speed on a ticker fast: a current quote plus a plain-English summary of what the company's latest 10-K says about its risks and MD&A
- Route each question to only the specialists it needs — quote-only, filings-only, or both — from a single free-text message
- Pause every filing summary for advisor sign-off (approve, edit, or reject) before it is treated as final
- Grounding, not assertion: every summary carries citations back to the filing chunks it was built from
- Scoped by construction: off-topic requests, prompt injection, and insider-trading solicitations are blocked before any agent or tool runs
- Compliance-aware output: definitive-advice language forces the approval gate, and every note carries a not-investment-advice disclaimer
Scale & constraints
A two-hour prototype with deliberately narrow scope. Mocked quotes and pre-seeded filings keep the demo independent of flaky external APIs, and Pinecone integrated embeddings keep Anthropic as the only paid vendor.
API design
Data model
| Entity | Key fields |
|---|---|
| TickerQuote | symbol, company_name, price, change, change_percent, day_high/low, volume, market_cap, as_of. |
| FilingChunk | ticker, cik, filing_type, filing_date, section (e.g. Item 1A – Risk Factors), source_url, chunk_index, text. |
| FilingSummary | key_points, risks, citations, generated_at, and `approval_status: pending|approved|rejected|edited`. |
| AdvisorProfile | advisor_id, watchlist, recent_queries — a flat JSON file, not a database. |
| AgentState (LangGraph) | TypedDict carrying messages, ticker, quote, retrieved chunks, summary, guardrail flags, `blocked` + `block_category`, and the approval decision. |
Thread state lives in LangGraph's in-process MemorySaver, so a paused approval survives across the two HTTP calls but not a backend restart — an accepted MVP cut.
Architecture
Every request is screened before it can cost anything. Survivors are routed to one or both specialists, which converge on a compliance agent that can pause the whole graph for a human.
Fig. 1a — A blocked request short-circuits at the guardrail: no supervisor call, no retrieval, no quote lookup.
The backend is the only component that talks to Anthropic, Pinecone, and LangSmith. The Next.js frontend never holds a third-party credential.
Fig. 1b — The frontend runs on Vercel; the backend ships without deployment config, so the live demo depends on where it is hosted.
Key decisions & trade-offs
done event reports whether the run finished, was blocked, or is waiting on the advisor.Guardrails
Regex fast-path plus a temperature-0 Claude Haiku classifier blocks off-topic requests, prompt injection, and insider-trading solicitations.
The compliance agent calls interrupt(); the advisor's approve / edit / reject decision is recorded as the summary's approval_status.
Patterns like "you should buy" and "guaranteed return" are flagged and force the approval gate regardless of the summary path.
The insider-trading refusal says the assistant works only from market data and SEC filings, and cannot be used to source or act on material non-public information.
Each final output carries an informational-purposes-only, not-investment-advice disclaimer.
Lessons learned
- Putting the guardrail node ahead of the supervisor meant blocked requests cost nothing downstream and the supervisor's routing prompt never sees an injection attempt.
- Using `interrupt()` for approval rather than a UI-only confirm made the human gate a property of the graph itself — the summary cannot become final without a resume call.
- Mirroring a real quote API's shape in the mock kept the demo reliable without making the eventual provider swap a rewrite.
- Compliance checks are regex patterns, so advice phrased outside those patterns slips past the extra flag. The always-on summary sign-off is the real backstop; a classifier would be the better long-term check.
- State is in-memory and the watchlist is a JSON file — fine for a prototype, but an approval pending at restart is lost. A Postgres-backed LangGraph checkpointer is the obvious next step.
- The eval is a manual harness, not a CI gate, and covers only 8 cases across 3 tickers. It shows the shape of the loop but would not yet catch a subtle regression on its own.