- Python 97.1%
- Dockerfile 2.9%
| .bg-shell | ||
| .gsd | ||
| retrieval | ||
| tests | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .python-version | ||
| agent.py | ||
| app.py | ||
| devui_server.py | ||
| docker-compose.yml | ||
| Dockerfile | ||
| loader.py | ||
| mise.toml | ||
| pyproject.toml | ||
| README.md | ||
| renovate.json | ||
| uv.lock | ||
spending-agent-ai
A personal Streamlit chat app for querying bank statements in natural language. Drop your PDFs in files/, run the app, and ask questions about your spending. Answers stream token-by-token, follow-up questions work, and a sidebar button reloads statements without restarting the server.
Powered by MS Agent Framework + GitHub Copilot (no OpenAI API key required).
Prerequisites
- Python 3.14 — managed via mise (
mise install) - uv — for the virtualenv and deps (
mise installorpip install uv) - GitHub Copilot — active subscription + CLI auth (
gh auth loginwith Copilot access)
Setup
# 1. Clone and enter the repo
git clone <repo-url> spending-agent-ai
cd spending-agent-ai
# 2. Create virtualenv and install dependencies
uv sync
# 3. Authenticate GitHub Copilot (if not already done)
gh auth login
# 4. Add your bank statement PDFs
mkdir -p files
cp ~/Downloads/*.pdf files/
Run
streamlit run app.py
Open http://localhost:8501 in your browser. The sidebar shows how many statements were loaded and the date range. Start typing in the chat input at the bottom.
To stop the server: Ctrl+C.
DevUI (Development server)
A lightweight HTTP API server for querying the agent directly — no browser required. Useful for scripted testing, debugging context injection, or driving the agent from other tools.
Prerequisites: files/ must be populated with PDF bank statements (same as the Streamlit app).
Start:
uv run python devui_server.py
Open http://127.0.0.1:9898 in your browser for the DevUI interface, or query the API directly (see below).
Restarting during development:
The --reload flag is accepted by the devui CLI entry-point but is not forwarded to uvicorn.run() — it is a no-op. In-memory entities (the BM25 index and loaded chunks) have no source files to watch, so file-watch-based hot-reload is not supported. The correct dev loop is to restart the process after each edit:
uv run python devui_server.py
The server starts in ~3 seconds (BM25 index: ~1 s, uvicorn bind: ~2 s), making a manual restart the practical approach.
Direct API querying (two-step pattern):
# Step 1: get the entity ID
ENTITY_ID=$(curl -sf http://127.0.0.1:9898/v1/entities \
| python3 -c "import sys,json; d=json.load(sys.stdin); print(d['entities'][0]['id'])")
# Step 2: send a spending query
curl -sf http://127.0.0.1:9898/v1/responses \
-H "Content-Type: application/json" \
-d "{\"model\":\"spending-analyst\",\"input\":\"What did I spend the most on in November?\",\"metadata\":{\"entity_id\":\"$ENTITY_ID\"}}" \
| python3 -c "import sys,json; r=json.load(sys.stdin); print(r['output'][0]['content'][0]['text'])"
Note on response shape: The DevUI API nests the answer text at
output[0].content[0].text, not a top-leveloutput_textkey.
Diagnostic check (at import time, no server needed):
# Confirm BM25CopilotAgent is wired and index is populated
PYTHONPATH=. python -c "from devui_server import agent, bm25_index, chunks; print('agent:', type(agent).__name__, '| index:', type(bm25_index).__name__, '| chunks:', len(chunks))"
If index is NoneType or chunks is 0, BM25 context injection is silently skipped on every query — check that PDFs are present in files/.
Example queries
Top spending
What did I spend the most on?
Returns a ranked table of payees with totals across all loaded statements.
Monthly breakdown
Give me a full breakdown of my spending for each month.
Returns a month-by-month summary with totals and notable payees.
Follow-up (conversation memory)
What did I spend the most on?
→ [agent answers with ranked list]
What about just November?
→ [agent scopes previous answer to November only]
And how does that compare to December?
→ [agent compares the two months using prior context]
Specific vendor or category
How much did I spend at grocery stores total?
How many times did I pay rent and what was the total?
What recurring subscriptions do I have?
Date-range queries
What did I spend in Q4 2025?
Show me my spending for October and November combined.
Which month was most expensive overall?
Anomaly detection
Are there any unusually large transactions I should know about?
What's the biggest single purchase in each month?
Are there any charges that look like duplicates?
Reload statements
Click 🔄 Reload statements in the sidebar to re-parse the files/ directory and reset the conversation — useful after adding new monthly PDFs without restarting the server.
Project layout
app.py — Streamlit chat UI
agent.py — GitHub Copilot agent wrapper (build, session, stream)
devui_server.py — DevUI entry point (dev server — BM25 context injection wired)
loader.py — PDF text extraction (pdfplumber)
retrieval/ — BM25 chunker + index (used by both app.py and devui_server.py)
files/ — Bank statement PDFs (gitignored — add your own)
pyproject.toml — Dependencies
Diagnostics
# Is the server healthy?
curl http://localhost:8501/_stcore/health # → "ok"
# Test PDF extraction independently
python loader.py files/
# Test the agent independently (no Streamlit needed)
python agent.py
If the sidebar shows "No statements loaded", check that PDFs are present in files/ and that streamlit run app.py was launched from the project root directory.
If the chat input is disabled with "Agent not ready", check gh auth status — the Copilot CLI session may have expired.
Docker
Build the image (required once, or after code changes):
mise run docker:build
Copy .env.example to .env and set your token:
cp .env.example .env
# Edit .env and set GITHUB_TOKEN=<your_token>
Start the container (detached):
mise run docker:up
The Streamlit UI is available at http://localhost:8501. Bank statement PDFs in the files/ directory are bind-mounted into the container — the sidebar will show parsed statements automatically.
Tail logs:
mise run docker:logs
Stop the container:
mise run docker:down