Spaces:
Sleeping
title: FinanceEducationAssistant
emoji: 🚀
colorFrom: red
colorTo: red
sdk: docker
app_port: 8501
tags:
- streamlit
pinned: false
short_description: Finance education assistant
license: apache-2.0
Finance Education Assistant
An interactive Streamlit app for finance education, market lookup, portfolio analysis, crypto pricing, tax education, and goal planning.
Architecture Overview
The application is built around a LangGraph routing engine with specialist agents and a layered retrieval stack.
Technical Design Doc
This section is the project’s technical design document. It covers:
- Agent contracts (inputs/outputs and safety constraints)
- LangGraph state model and transitions
- Retrieval + caching strategy
- Performance considerations
System diagram (Mermaid)
flowchart TD
U[User] --> UI[Streamlit UI]
UI --> SAN[Sanitize input<br/>bad-words + OpenAI moderation]
SAN -->|blocked| BLK[Blocked response]
SAN -->|allowed| ST[Build FinanceState<br/>user_profile + history + portfolio]
ST --> ENG[FinAgentEngine.invoke()]
ENG --> G[LangGraph app.invoke(state)]
G --> R[Router node<br/>LLM returns JSON route + entities]
R -->|one agent| A1[Single agent node]
R -->|multiple agents| AM[Multi-agent node<br/>run sequentially]
R -->|none| END[END]
A1 --> OUT[Response in state["response"]]
AM --> COMB[Combine outputs (LLM)<br/>or stable concat fallback]
COMB --> OUT
OUT --> FMT[Format response (LLM HTML formatter)]
FMT --> UI
subgraph Agents
EDU[EducationAgent]
TAX[TaxAgent]
MKT[MarketAgent]
PORT[PortfolioAgent]
CRYPTO[CryptoAgent]
GOAL[GoalPlanningAgent]
NEWS[NewsSynthesizerAgent]
end
A1 --> EDU
A1 --> TAX
A1 --> MKT
A1 --> PORT
A1 --> CRYPTO
A1 --> GOAL
A1 --> NEWS
Execution flow (ASCII)
[User]
|
v
[Streamlit UI]
|
v
[Sanitize input] --(blocked)--> [Blocked response]
|
(allowed)
|
v
[Build FinanceState]
|
v
[FinAgentEngine.invoke]
|
v
[LangGraph app.invoke]
|
v
[Router node: intent + agents + entities]
|
+--(1 agent)--> [Agent node] -> state["response"]
|
+--(>1 agents)--> [Multi-agent] -> [Combine] -> state["response"]
|
+--(none)--> state["response"]="Not supported"
|
v
[Format response HTML] -> [Render]
Agent contracts
All agents follow a shared contract:
- Interface:
src/core/AgentCommand.py__init__(state: FinanceState)process() -> FinanceState(mutates and returnsstate)
- Shared state type:
src/core/FinanceState.py(TypedDict) - Safety rules (project convention):
- Education-oriented responses (no trade instructions)
- A short disclaimer line (agents enforce this in prompts and/or template responses)
- Best-effort citations when using KB or web sources
Inputs each agent may read
state["user_query"](required)state["user_profile"](optional)state["conversation_history"](optional, may be trimmed inside the agent)state["symbol"]/state["crypto_symbol"]/state["portfolio"](optional, entity-specific)
Outputs each agent should write
state["response"](required)state["retrieved_sources"](optional; list of source metadata when grounded)state["errors"](optional; append normalized error entries viasrc/core/errors.py)
LangGraph state model (“graph states”)
The graph passes a single mutable FinanceState object between nodes.
Core fields
user_query: str— the active user questionresponse: str— final text responseintent: str— router intent label (education|portfolio|market|tax|crypto|goal_planning|news|multi|none)agents: list[str]— selected agent node names
Entity fields (router output)
symbol: str | None— stock ticker (e.g.,AAPL)crypto_symbol: str | None— crypto ticker (e.g.,BTC)portfolio: list[{symbol, quantity}] | None— holdings extracted from user text/UI
Context fields (from UI)
conversation_history: list[{role, content}]user_profile: dict
Telemetry
retrieved_sources: list[dict]errors: list[dict]— normalized error entries:{ts, code, message, agent?, detail?}
Router contract (LLM JSON schema)
FinAgentEngine.llm_router() prompts the LLM to return only JSON with:
{
"intent": "education|portfolio|market|tax|crypto|goal_planning|news|multi|none",
"agents": ["education_agent|portfolio_agent|market_agent|tax_agent|crypto_agent|goal_planning_agent|news_synthesizer_agent|none"],
"symbol": "AAPL",
"crypto_symbol": "BTC",
"portfolio": [{"symbol": "AAPL", "quantity": 10}],
"query": "original user query (possibly normalized)"
}
The router node copies these into FinanceState and the conditional edge function chooses the next node based on state["agents"].
Retrieval and caching strategy
Retrieval primitives
- Curated KB (local, Chroma-backed):
src/rag/KnowledgeBase.py- Reads from
src/data/knowledge_base/<category>/*.md|*.txt(when present) - Used by
EducationAgent/TaxAgent(KB-first)
- Reads from
- Web search:
src/rag/TavilySearchRag.py- Used as a fallback by
EducationAgent,TaxAgent,MarketAgent, andNewsSynthesizerAgent
- Used as a fallback by
- Market data:
src/rag/StockMarketRag.py- Quotes via Alpha Vantage
GLOBAL_QUOTE, with a fallback to Finnhub for price + OHLC snapshots - Best-effort enrichment via Alpha Vantage
OVERVIEWfor sector/industry/52-week range
- Quotes via Alpha Vantage
- Crypto data:
src/rag/CcxtRag.py(CCXT ticker)
Caching (current)
- Streamlit process caching:
st.cache_resourcefor long-lived objects (KB ingestion bootstrap, router creation)st.cache_datafor small data loads (e.g., CSV read patterns)
- Chroma persistence:
- KB vectors persist in
src/data/.chromaonce ingested
- KB vectors persist in
Caching (available but not currently wired end-to-end)
- Web cache class exists:
src/rag/TavilyWebCache.py - Semantic cache class exists:
src/data/SemanticCache.py
If you wire these in, the intended policy is:
- KB retrieve (grounded) → answer with citations
- Web cache retrieve → answer with citations
- Fresh Tavily search → save to web cache → answer with citations
- Semantic cache can short-circuit repeated queries before LangGraph runs
Performance considerations
- Deterministic routing: router uses
temperature=0to stabilize agent selection. - Context growth control: some agents only pass the last few messages to the LLM.
- “Fast-fail” telemetry: agent failures append structured errors to
state["errors"]and Streamlit surfaces a banner so users understand partial results. - Provider robustness:
- Alpha Vantage rate limits are handled as “best-effort”; the UI will show partial data when enrichment fails.
- Multi-agent combine has a stable concatenation fallback if synthesis fails.
Model configuration
Models are centralized in src/core/settings.py and can be overridden by:
FIN_ASSISTANT_ROUTER_MODELFIN_ASSISTANT_AGENT_MODELFIN_ASSISTANT_FORMATTER_MODELFIN_ASSISTANT_EMBEDDING_MODEL
High-level flow
- User submits a question in Streamlit.
- Input is screened with lightweight bad-word filtering and OpenAI moderation.
FinAgentEngineroutes the query to one or more agents.- Each agent either:
- answers from a local knowledge base,
- calls a web search tool,
- looks up market or crypto data,
- or combines multiple specialist outputs.
- The final answer is formatted and shown in the UI.
Execution flow
[User]
|
v
[Streamlit UI]
|
v
[Sanitize input] --(blocked)--> [Blocked response]
| |
(allowed) v
| [Render assistant message]
v ^
[Build graph_state] |
| |
v |
[FinAgentEngine.invoke] |
| |
v |
[LangGraph app.invoke] |
| |
v |
[LLM Router Node] |
| |
+--(none/not supported)--> [Set response: Not supported]
| |
+--(1 agent)--> [Single Agent Node]--+
| | |
| +--> EDU |
| +--> TAX |
| +--> MKT |
| +--> PORT |
| +--> CRYPTO |
| +--> GOAL |
| +--> NEWS |
| |
+--(>1 agents)--> [Multi-Agent Node] |
| |
v |
[Run Sequentially] |
| |
v |
[LLM Combine/Concat]---+
Main components
src/streamlit_app.py- Streamlit UI, chat history, input sanitation, and session state.
src/core/FinAgentEngine.py- LangGraph routing and multi-agent orchestration.
src/agents/- Specialist behavior for education, market data, portfolio analysis, taxes, crypto, goal planning, and news summarization.
src/rag/- Retrieval adapters for knowledge base search, Tavily search, stock data, and crypto data.
src/data/- Persistent caches, prompt files, guardrails, and CSV-based filters.
Setup Instructions
1. Create a Python environment
Use Python 3.12 if possible, matching the Docker image used by the repo.
python -m venv .venv
source .venv/bin/activate
2. Install dependencies
pip install -r requirements.txt
3. Configure environment variables
Create your local environment file and set the API keys used by the app.
Required or commonly used variables:
OPENAI_API_KEYTAVILY_API_KEYALPHA_VANTAGE_KEYorALPHAVANTAGE_API_KEYorALPHA_VANTAGE_API_KEYFINNHUB_API_KEY
Optional app settings:
FIN_ASSISTANT_TITLEFIN_ASSISTANT_PAGE_TITLEFIN_ASSISTANT_TABSFIN_ASSISTANT_MARKET_WATCHLISTFIN_ASSISTANT_PORTFOLIO_DEFAULT_INPUTFIN_ASSISTANT_PORTFOLIO_EXAMPLESFIN_ASSISTANT_RESPONSE_CACHE_TTL_DAYSFIN_ASSISTANT_DEFAULT_RISKFIN_ASSISTANT_DEFAULT_EXPERIENCEFIN_ASSISTANT_ROUTER_MODELFIN_ASSISTANT_AGENT_MODELFIN_ASSISTANT_FORMATTER_MODELFIN_ASSISTANT_EMBEDDING_MODEL
Optional local configuration:
config.yamlat the repo root can override app title, tabs, watchlist, portfolio examples, cache TTL, and default user profile values.
4. Run the app locally
streamlit run src/streamlit_app.py
5. Run with Docker
docker build -t finance-education-assistant .
docker run --rm -p 8501:8501 --env-file .env finance-education-assistant
RAG Implementation Details
The repo uses a layered retrieval strategy instead of a single retrieval path.
1. Curated knowledge base
src/rag/KnowledgeBase.py loads documents from src/data/knowledge_base when that directory is present:
src/data/knowledge_base/<category>/*.mdsrc/data/knowledge_base/<category>/*.txt
Documents are embedded with text-embedding-3-large and stored in a single local Chroma collection at src/data/.chroma.
This is the primary path for:
- education queries
- tax education queries
2. Cached web retrieval
src/rag/TavilyWebCache.py stores Tavily search results in the same single Chroma collection at src/data/.chroma.
Note: the cache helper exists, but the current agents call Tavily directly on fallback. Wiring in the web cache is a recommended next step (see next_steps.md).
3. Market data retrieval
src/rag/StockMarketRag.py gets stock details using a cache-first approach:
Current behavior:
- Alpha Vantage
GLOBAL_QUOTEfor price + basic day metrics - fallback to Finnhub when Alpha Vantage fails/unusable (price + OHLC/previous close)
- optional Alpha Vantage
OVERVIEWenrichment for company metadata (best-effort)
The market and portfolio agents use this adapter.
4. Crypto retrieval
src/rag/CcxtRag.py queries CCXT exchange data for crypto tickers.
The crypto agent uses this for symbol-level pricing.
5. Semantic response cache
src/data/SemanticCache.py stores prior prompt/response pairs in the same single Chroma collection and can return a cached answer when a new query is semantically similar enough.
Note: the semantic cache helper exists, but FinAgentEngine.invoke() does not currently short-circuit on it. Wiring it in is a recommended next step (see next_steps.md).
Performance Considerations
st.cache_resourcekeeps the router and moderation agent alive across reruns.st.cache_datacaches the bad-words CSV load.- Chroma collections persist locally, so repeated queries do not re-ingest or re-embed every run.
- The router uses
temperature=0to keep routing deterministic. Conversation historyis trimmed before being passed into some agents to reduce context growth.- Structured errors are accumulated in
state["errors"]and shown in the UI to make partial failures explicit. - Cache directories can be cleared if you need a clean rebuild:
src/data/.chromasrc/data/.persistent_cache.sqlite3
API Documentation
This project does not expose a separate HTTP API. The main public surfaces are Python classes and methods.
Core engine
src/core/FinAgentEngine.py
FinAgentEngine()- Builds the LangGraph router and wires in the specialist agents.
invoke(query: str, initial_state: dict | None = None) -> dict- Runs the full routing graph and returns the final state.
routeAgent(query: str) -> dict- Convenience wrapper around
invoke().
- Convenience wrapper around
State model
src/core/FinanceState.py
FinanceState is a typed dictionary used across agents.
Key fields:
user_queryresponseintentagentssymbolcrypto_symbolportfolioconversation_historyuser_profileretrieved_sourceserrors
Retrieval and cache helpers
src/rag/KnowledgeBase.py
KnowledgeBase(docs_root="src/data/knowledge_base", persist_directory="src/data/.chroma")ensure_ingested() -> Noneretrieve(query: str, k: int = 4, categories: Sequence[str] | None = None) -> list[RetrievedSource]
src/rag/TavilySearchRag.py
TavilySearchRag(max_results: int = 3)search(query: str)
src/rag/TavilyWebCache.py
TavilyWebCache(...)save_results(query: str, results: list[dict], intent: str = "education") -> intretrieve(query: str, k: int = 4, intent: str = "education", threshold: float = 0.65) -> list[CachedWebSource]
src/rag/StockMarketRag.py
StockMarketRag(cache: PersistentTTLCache | None = None)get_stock_details(symbol: str) -> dict
src/rag/CcxtRag.py
CcxtRag(exchange_name: str = "kraken")get_crypto_details(symbol: str) -> dict
src/data/PersistentTTLCache.py
PersistentTTLCache(db_path: str = "src/data/.persistent_cache.sqlite3")get(namespace: str, key: str) -> dict | Noneset(namespace: str, key: str, value: dict, ttl_seconds: int) -> None
src/data/SemanticCache.py
SemanticCache(persist_directory: str = "src/data/.chroma")check_cache(query: str, threshold: float = 0.75)save_to_cache(query: str, response: str) -> None
src/agents/ModerationAgent.py
ModerationAgent()classify(text: str) -> dictis_flagged(text: str) -> bool
Response formatting
src/core/ResponseGenerator.py
ResponseGenerator(model: str | None = None, include_guardrails: bool = True, guardrails_path: str | None = None)generate(...) -> str
Usage Examples
General education
Explain ETFs in simple terms.
Tax education
What is the difference between capital gains and ordinary income?
Market lookup
What is happening with NVDA today?
Portfolio analysis
My portfolio is 10 AAPL, 5 MSFT, 2 VTI. What stands out?
Crypto pricing
Show me the latest price for BTC.
Goal planning
Help me plan for a house down payment in 5 years.
News synthesis
Summarize the latest headlines about Apple.
Programmatic use
from src.core.FinAgentEngine import FinAgentEngine
engine = FinAgentEngine()
result = engine.invoke("Explain ETFs in simple terms.")
print(result["response"])
Notes
- The app is designed for education, not personalized financial, tax, or investment advice.
- If you add new knowledge base documents, keep them under
src/data/knowledge_base/<category>/. - If cached retrieval seems stale, clear the local Chroma and SQLite cache files and rerun the app.