Case Study

AI OS Finance

Deterministic valuation engine with AI-augmented research workflows, scenario modelling, and n8n-orchestrated financial data pipelines — LLMs for narrative, Python for the math.

FastAPI OpenAI n8n Pydantic Docker

LLMs for narrative.
Python for the math.

2025 AI ENGINEERING · FINTECH OPENAI · FASTAPI · N8N · PYDANTIC

Deterministic DCF engine with AI-augmented research — LLM never touches the math, only the narrative

A finance-grade system where the separation between AI and computation is an architectural constraint enforced by agent boundaries — not a guardrail bolted on afterwards. The LLM has no access to the computation layer, only to its validated output.

4
specialised agents with typed boundaries
2
execution modes — MOCK and PROD
0
LLM calls in the valuation computation path
100%
agent responses as typed Pydantic envelopes

Replace the spreadsheet without introducing the opposite failure mode: numbers that look plausible but can't be audited.

Financial analysts run valuation models — DCF, comparables, sensitivity tables — manually in spreadsheets. The workflow is slow to iterate on, impossible to version, and entirely disconnected from the research context that informs the assumptions. Replacing it with an LLM introduces the opposite failure mode: models that produce numbers that look plausible but aren't deterministically reproducible and can't be audited back to their inputs.

A finance-grade system needs both: AI-assisted research and interpretation, with deterministic computation underneath it that an analyst can verify line by line. The hard part is enforcing that separation architecturally — not as a comment in the code, but as a boundary the LLM physically cannot cross.

Four specialised agents, typed Pydantic boundaries, zero LLM involvement in the computation path.

  • The pipeline runs four agents behind FastAPI endpoints: a Research Agent (interprets intent, can call web search), a Data Engineering Agent (normalises incoming dataset context), and a two-stage finance pair — finance_v1 builds the valuation model scaffold and finance_v2 interprets it. Every agent returns a typed Pydantic AgentResponse envelope (status · agent · data · errors · metadata), and every endpoint declares a Pydantic request model, so malformed requests are rejected at the API boundary before an agent runs.
  • The deterministic computation layer — calculate_dcf and the scenario analyzer — runs pure Python with no LLM involvement at any point. It enforces its own explicit preconditions rather than trusting upstream data: it refuses to run if WACC ≤ terminal growth, requires at least one of shares-outstanding or net-debt, and normalises a scalar growth rate into a validated per-year vector. Outputs (projection table, valuation summary, named-scenario comparison) are exported as CSV for Excel/BI compatibility.
  • The finance_v2 interpreter then receives the model scaffold and generates the written investment narrative — its system prompt hard-constrains it to the provided scaffold: it must not invent numbers, must not modify inputs, and must state missing values explicitly. It reasons over the numbers, not around them.
  • n8n acts as the external control plane, orchestrating the agent sequence, triggering data fetches, handling retry logic and error routing, and assembling the final report. FastAPI exposes the computation layer as a service that n8n calls — orchestration and computation stay decoupled.

Why these choices — each enforces the AI/computation separation as a hard constraint, not a soft guideline.

  • LLMs for narrative only — deterministic Python for all math. Financial calculations (DCF and scenario analysis) are deterministic Python. LLMs are invoked only to interpret results and generate written analysis — never to produce numbers. This isn't a guardrail; it's an architectural constraint: the math lives in calculate_dcf, which no agent's LLM call can reach — the interpreter only ever sees the computed scaffold.
  • n8n for workflow orchestration over custom DAG code. The data fetch, model trigger, and report assembly steps change frequently as the analytical workflows evolve. A visual DAG in n8n is faster to iterate on than Python scheduler code, makes the workflow auditable without reading source, and handles retry logic and error routing without custom implementation.
  • Typed boundaries via Pydantic. Every agent returns a Pydantic AgentResponse envelope, and every FastAPI endpoint declares a Pydantic request model — so a malformed request is rejected at the API boundary before any agent runs. The deterministic layer then adds its own explicit numeric guards on top (WACC > terminal growth; a required equity input), because a well-typed request can still carry economically invalid numbers.
  • MOCK mode as a first-class execution mode. All LLM calls route through a single call_llm entrypoint; in MOCK mode every one of them is swapped for a deterministic response, making the pipeline fully testable without API keys or spend. The pure-Python computation path is identical in both modes. CI runs against MOCK exclusively.

Full Docker Compose stack · n8n + FastAPI + OpenAI SDK · MOCK and PROD modes.

4
specialised agents with typed I/O
0
LLM calls in computation path
2
execution modes (MOCK / PROD)
100%
agent responses typed (Pydantic envelope)

What broke during build — and what I changed to fix it.

  • The interpreter drifted from the model output when its prompt was too open-ended. An early prompt asked the interpreter to "summarise the DCF analysis" and it paraphrased — occasionally contradicting — the actual numbers. The fix hardened the finance_v2 system prompt into a contract: use only the provided scaffold as the source of truth, never invent numbers or assumptions, never modify inputs, and state anything missing explicitly. The LLM interprets; it cannot invent.
  • Deterministic math still needs explicit failure modes — silence is the dangerous default. An early DCF run with WACC below the terminal growth rate produced a plausible-looking but mathematically nonsensical enterprise value (the Gordon-growth denominator goes negative). The fix: calculate_dcf raises on WACC ≤ terminal growth, on a missing revenue input, and when neither shares-outstanding nor net-debt is supplied — the computation fails loudly at the input boundary rather than emitting a confident wrong number.
  • An agent could loop on tool calls with no ceiling. The router originally let an agent request a tool, get a result, and request again — an unbounded loop that could run up latency and spend. The fix: the orchestrator enforces a single tool-call maximum — one tool request, one follow-up call with the result, then a final answer. No unbounded loops.
  • The MOCK response matcher was order-sensitive in a way that silently broke tests. MOCK responses are selected by substring-matching the prompt, and the finance_v2 "interpret" branch had to be checked before the finance_v1 branch — the strings "dcf"/"valuation" in a v1 request would otherwise swallow a v2 interpret request and return the wrong shape. The fix pinned the interpreter branch first (marked "MUST BE FIRST" in the code) so MOCK mode stays faithful to what PROD would return.

How a DCF analysis
actually runs.

ai-os-finance · v1.0 hover any node →
validate DCF · scenario validated out narrative orchestrate input guard CSV artifact retry
User Request
n8n trigger
n8n orchestrator
DAG · retry · error routing
Report Artifacts
CSV export
Data Eng Agent
typed AgentResponse
Finance compute · DCF
deterministic Python · 0 LLM calls
Pydantic boundaries
typed envelope + request models
Interpreter · finance_v2
grounded narrative only
FastAPI endpoint
auto-generated OpenAPI
MOCK / PROD mode
env-var toggle · CI-safe
ingest
store
serve
— hot path animated · cold path dashed

Run a DCF analysis.
Watch the agents.

ai-os-finance / data → valuation → narrative

A valuation request is validated at the FastAPI boundary, runs through deterministic DCF computation, then finance_v2 writes the narrative. Malformed requests are rejected before compute runs; economically invalid inputs (e.g. WACC ≤ terminal growth) are rejected by the DCF calculator itself.

01 COMPUTATION · finance agent
02 NARRATIVE · finance_v2 interpreter
runs completed0
validation errors0
avg DCF time
LLM calls in math path0
How it works: n8n triggers the workflow → Data Eng Agent normalises context → finance_v1 builds the model scaffold → deterministic DCF runs in pure Python (0 LLM calls) → the scaffold is passed to finance_v2, whose OpenAI call writes the narrative constrained to the provided fields.
CONNECT