A multi-agent conversational financial analytics platform that combines company fundamentals analysis, SEC filing intelligence, and machine learning-based stock price prediction through an intuitive chat interface.
This project applies architectural principles from knowledge graphs and AI agents, adapting them from healthcare to finance and market analysis. The core idea demonstrates how a combination of graph databases, RAG (Retrieval-Augmented Generation), and predictive models can create powerful, domain-specific AI assistants. Data content was shrinked to comply with submission requirements.
This project builds on concepts from several technical articles of mine:
- Building Knowledge Graphs from Scratch Using Neo4j and Vertex AI - March 2024
- Use LLMs to Turn CSVs into Knowledge Graphs: A Case in Healthcare - June 2024
- Understanding Alzheimer's: Building Knowledge Graphs from Unstructured Data with Gemini - February 2025
The Financial AI Assistant empowers investors, analysts, and researchers to analyze market data through natural language queries. Instead of writing complex database queries or manually sifting through lengthy SEC filings, users can simply ask questions like:
- "What was the revenue for Apple in 2023?"
- "Summarize the key risks mentioned in NVIDIA's latest 10-K filing?"
This natural language interface activates tools in the backend agent system to provide comprehensive answers.
The system provides three main capabilities via its agent tools:
- Corporate Financial Analysis: Query company fundamentals, financials, and key metrics from structured data
- SEC Filing Intelligence: Perform semantic search and summarization on the full text of annual 10-K reports to extract qualitative insights
- Predictive Analytics: Generate next-day price estimates for individual stocks using per-ticker LightGBM models. See Prediction model quality below — the models are measurably worse than assuming no change, and are included to demonstrate ML integration, not to forecast markets.
The Financial AI Assistant uses a sophisticated multi-layered architecture:
- Frontend Layer: Clean web interface with HTML, CSS, and JavaScript chat interface
- API Layer: FastAPI server handling request validation and response formatting
- Agent Orchestration: Root/Sub-Agent architecture using Google's ADK (Agent Development Kit)
- Data Storage: Multi-modal Neo4j database serving as both graph and vector database
- External AI Services: Google Vertex AI for language understanding and embeddings
The system employs a Root/Sub-Agent pattern:
- Root Agent: Acts as the "lead analyst," analyzing questions and delegating to appropriate specialists
- Sub-Agents: Function as "specialist analysts" with specific tools:
- Graph QA Agent: Generates Cypher queries for statistical questions about company financials
- Document RAG Agent: Performs semantic search through SEC filings
- Stock Predictor Agent: Runs ML models for next-day price predictions
- Query Analysis: Root Agent analyzes user intent
- Tool Selection: Appropriate sub-agent selected based on query type
- Data Retrieval: Sub-agent executes its tool (Cypher query, embedding search, or ML prediction)
- Response Synthesis: LLM processes results into natural language
- Delivery: Final answer sent through API to web interface
- Historical data pulled using
yfinancelibrary - SEC 10-K reports fetched from EDGAR database in complex iXBRL format
- SEC filings contain extensive boilerplate legal jargon
- RAG pipeline uses
UnstructuredHTMLLoaderto extract core textual content
- Stock prediction demonstrates ML integration (not financial advice)
- Per-ticker LightGBM model over historical price/volume
- The target is the next-day return, not the next-day price level. A tree ensemble can only output values it saw in training, so a level target cannot extrapolate on a trending series — measured on held-out data, the original level-target version was worse than a naive "tomorrow equals today" baseline for all 22 tickers (median 2.1x the error, worst case 9.8x). Predicting a scale-free return narrows that to ~1.11x.
- It still does not beat the naive baseline. See Prediction model quality.
- Does not account for market sentiment, news, or macroeconomic factors
- Cloud deployment scales to zero to minimize idle costs
financial-assistant/
├── app/
│ ├── agents/
│ │ └── agents.py
│ ├── graph_db/
│ │ └── connection.py
│ ├── models/
│ │ ├── predict.py
│ │ ├── train_predictor.py
│ │ └── saved_models/ <- .joblib models + metrics.json
│ ├── templates/
│ │ └── index.html
│ └── main.py
├── data/
│ ├── structured/
│ │ ├── financials/
│ │ └── prices/
│ └── unstructured/
│ └── 10k/
├── fetch_data.py
├── populate_graph.py
├── companies.csv
├── .env (gitignored; create from .env.example)
├── .env.example
├── Dockerfile
├── README.md
└── requirements.txt
- Google Cloud Platform account with Vertex AI enabled
- Neo4j database instance (Neo4j AuraDB Free Tier recommended)
- Python 3.11 or higher
- Docker (for deployment)
- Create the virtual environment:
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt- Configure environment variables by creating a
.envfile from.env-example:
GOOGLE_PROJECT_ID="your-gcp-project-id"
GOOGLE_LOCATION="us-central1"
NEO4J_URI="neo4j+s://your-instance.databases.neo4j.io"
NEO4J_USERNAME="neo4j"
NEO4J_PASSWORD="your-password"
SEC_USER_AGENT="Your Name your.email@provider.com"- Run the complete data pipeline:
# Step 1: Create the list of target companies
python3 create_companies.py
# Step 2: Download all raw data from APIs (this will take a while)
python3 fetch_data.py
# Step 3: Populate the Neo4j database (Graph + Embeddings)
python3 app/graph_db/2_populate_graph.py
# Step 4: Train the predictive models (one for each stock)
python3 app/models/train_predictor.py- Start the development server:
uvicorn app.main:app --reload --port 8080- Open your browser to
http://127.0.0.1:8080and start asking questions.
- "How many companies are in the technology sector?"
- "What was the net income for MSFT in 2023?"
- "List the major events for TSLA in their 2024 filing."
- "What did Google say about their AI strategy in the last 10-K?"
- "Summarize Amazon's management outlook from their 2024 filing."
- "What are the common concerns about supply chain disruptions?"
- "Predict the next closing price for AAPL."
- "What is the stock price prediction for NVDA tomorrow?"
The system is containerized for easy deployment to Google Cloud Run.
gcloud auth login
gcloud config set project YOUR-PROJECT
gcloud artifacts repositories create financial-assistant-repo \
--repository-format=docker \
--location=us-central1 \
--description="Docker repository for financial assistant service"
gcloud builds submit --tag us-central1-docker.pkg.dev/YOUR-PROJECT/financial-assistant-repo/assistant-service:latest
gcloud run deploy financial-assistant-service \
--image=us-central1-docker.pkg.dev/YOUR-PROJECT/financial-assistant-repo/assistant-service:latest \
--platform=managed \
--region=us-central1 \
--allow-unauthenticated \
--set-env-vars-from-file=.env \
--min-instances 0 \
--max-instances 3 \
--cpu 4 \
--memory 8192Mi \
--concurrency 10The frontend calls /chat relatively and is served by the same app, so no
post-deployment edit is needed.
--allow-unauthenticatedmakes the service public./chathas no application-level authentication or rate limit, and each request costs Vertex AI tokens. For anything beyond a demo, deploy with--no-allow-unauthenticatedand grantroles/run.invokerto the identities that should reach it.
--set-env-vars-from-file=.envputs the Neo4j password in the service's configuration, where it shows up ingcloud run services describe. Prefer Secret Manager:--set-secrets=NEO4J_PASSWORD=neo4j-password:latest.
python -m app.models.train_predictor writes
app/models/saved_models/metrics.json, holding out the last 20% of each price
series in time order and comparing against the naive baseline "tomorrow's
close equals today's close":
| Metric | Meaning |
|---|---|
mae |
Mean absolute error of the model on held-out days |
naive_lag1_mae |
Same error for "assume no change" |
mae_vs_naive_ratio |
mae / naive_lag1_mae. Above 1.0 means the model is less accurate than assuming no change. |
beats_naive_baseline |
true only if the model is genuinely better |
direction_accuracy |
Fraction of days the sign of the predicted move was right |
Current result across the 22 tickers with price history: 0 of 22 beat the
baseline, median ratio ≈ 1.11x. This is the expected outcome — next-day price
movement is close to unpredictable from price and volume history alone — and it
is reported rather than hidden. Every prediction response carries a
model_quality string stating its own ratio, plus a disclaimer.
If you extend this, the honest bar to clear is mae_vs_naive_ratio < 1.0.
The README notes the dataset was reduced for submission. Concretely:
| Layer | Coverage |
|---|---|
Company profiles (companies.csv) |
402 tickers |
| Financials (JSON) | 401 tickers |
| Price history (CSV) | 22 tickers (AAL through AMGN) |
| SEC 10-K filings (HTML) | 2 companies — AAPL and ABBV, 5 years each |
The agent instructions used to advertise a hardcoded {NVDA, MSFT, AAPL, GOOGL, AMZN}. Of those, only AAPL has price history here and none had a trained model,
so every prediction request was accepted and then failed. The prediction tool now
derives its ticker list from what is actually on disk. Note that DocumentRAG can
only answer about AAPL and ABBV, since those are the only filings present.
pip install pytest pytest-asyncio
pytest -q73 tests, no network access and no GCP credentials required — tests/conftest.py
mocks the ADK, Vertex AI and Neo4j modules before importing the app.
tests/test_regressions.py covers the defects found in the code audit.
- Sessions are in-memory.
InMemorySessionServicemeans conversation history is lost on restart and is not shared across Cloud Run instances. A client that passes a stablesession_idwill still get a fresh session after a cold start. - Generated Cypher is constrained to reads.
assert_read_only()inapp/agents/agents.pyrejects any generated statement containing a write clause before it reaches the database. For defence in depth, also connect with a Neo4j user that has no write privileges. - Re-running
populate_graph.pywipes the graph. It begins with a batchedDETACH DELETEof every node, with no confirmation prompt. - Entity extraction reads only the first 20,000 characters of each filing, and embeds the first 80,000 — so questions about later sections of a 10-K (often including much of the risk discussion) have no supporting context.
Financial information is inherently interconnected. Companies operate in sectors, have competitors, file reports, and are affected by market events. A graph database makes exploring these complex relationships natural and efficient.
Using Google's ADK, the separation between a root agent (orchestrator) and specialist sub-agents creates a clean division of responsibilities, making the system modular and easier to extend with new tools.
Different financial questions require different approaches. The system automatically selects the right tool: structured graph queries for quantitative facts (e.g., revenue) and semantic vector search for qualitative insights (e.g., management sentiment).
SEC filings are dense and unstructured. Using an LLM to intelligently extract key entities like risks, events, and strategies is far more flexible and robust than building rigid parsers.

