The problem that started it

Every quarter, finance teams spend days doing the same thing: pulling numbers out of PDFs and spreadsheets, recomputing ratios by hand, writing the same analysis in three different formats for three different audiences. The tools that exist either make you do the work yourself, or trust a chatbot that confidently makes up the numbers.

That second problem is what Finance Copilot is built to solve.


The core insight

Language models hallucinate arithmetic. Ask GPT to total a column of revenue figures and it gives you a confident, wrong answer. Every AI finance tool that stuffs a spreadsheet into a prompt has this problem. The model reads a sample, estimates the rest, and presents it as fact.

Finance Copilot never asks the model to add anything.

Instead, it uses a trust loop:

  1. GPT-5 plans the visible workflow
  2. Codex writes a self-contained Python script to compute exact figures
  3. A guarded sandbox runs that script over every row of the real file
  4. The printed output, verified feeds back as ground truth
  5. GPT-5 reasons from those figures and writes the answer

The model is never the calculator. It is the analyst who reads the calculator output and explains what it means.


How I built it

The agent loop

The backend is FastAPI streaming Server-Sent Events. Each message triggers a five-phase agent: plan → Codex writes code → sandbox executes → verified figures → GPT-5 answers. The frontend renders each phase live so you see the work, not a spinner.

# The three lines that define Finance Copilot
code = ai_service.generate_analysis_code(request, context)  # Codex
calc_output = run_analysis(code, context, data_files)        # Sandbox
answer = ai_service.stream_answer(request, context, calc_output)  # GPT-5

The sandbox

The generated Python runs in a subprocess with no network access, CPU and memory limits, and a wall-clock timeout. The real uploaded file is copied into the temp directory and exposed as FILES[0]. Generated code reads it with csv.DictReader, row by row. This is why exact math scales past 500,000 rows the sandbox reads the whole file, not a prompt excerpt.

Three model roles

  • GPT-5 (OPENAI_MODEL) plans, interprets, writes narrative, summarizes
  • Codex (OPENAI_CODE_MODEL) writes the analysis script, called via /v1/responses
  • Realtime (OPENAI_REALTIME_MODEL) powers voice over WebRTC with an ephemeral key

All three model names are environment variables. The app resolves whichever your key exposes.

Deliverables

One analysis, four renders: PDF (shareholder letter style, statement tables, chart), Word memo, Excel model, CSV. The expensive model calls happen once. Four deterministic renders cost nothing extra.

Voice that acts

The backend injects extracted financial data into the Realtime session instructions and declares a generate_document tool. Say "prepare a report for Germany" and the analyst filters the data, computes the exact subset, produces the file, and confirms out loud. The API key never reaches the browser.


What I learned

Codex needs the Responses endpoint. It returns a 404 on /v1/chat/completions pointing to /v1/responses. The SDK version most projects pin does not have .responses yet. I built a direct httpx fallback that detects the 404, switches endpoints, parses the response shape, and returns a shim.

Reasoning models truncate code. With max_output_tokens set too low, Codex spends the budget on reasoning and the code arrives cut off mid-line, producing a SyntaxError in the sandbox. Fix: set reasoning.effort to low, raise the cap to 16,000 tokens, and compile-check every script before it runs. If it fails, a robust fallback reads the full file and sums every numeric column.

Pre-computed digests beat prompt stuffing. I compute a complete analytical rollup at upload time every period, region, and segment, and place it at the front of every prompt. The model never misses a month on a multi-period file, even when the raw data exceeds the context window.

SSE streams can silently die. A 30-60 second generation step would hit the frontend idle timeout and wipe the conversation. Fix: emit a progress answer_delta before blocking work to keep the connection alive, and always reach answer_done regardless of what fails.


Challenges

  • Getting Codex to run through three different failure modes before the Responses API fallback worked
  • Voice transcript was empty for several runs, input transcription requires both a session-level config and a session.update over the data channel after connection
  • Parsing markdown pipe tables into real table structures consistently across PDF (ReportLab), DOCX (python-docx), and XLSX (openpyxl)
  • Keeping the SSE stream alive during blocking generation without the frontend timing out

What Finance Copilot can do that a prompted LLM cannot

Capability Prompted LLM Finance Copilot
Exact arithmetic on 500K+ rows Samples and guesses Codex reads the full file
Auditable figures No source Code block attached to every reply
Voice grounded in your documents Generic answers Ephemeral key, injected context
Four board-ready formats from one analysis Manual export One analysis, four renders
Scoped reports (e.g. Germany only) Estimated Filtered rows, exact subset figures

Built With

Share this project:

Updates