--- 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) ```mermaid flowchart TD U[User] --> UI[Streamlit UI] UI --> SAN[Sanitize input
bad-words + OpenAI moderation] SAN -->|blocked| BLK[Blocked response] SAN -->|allowed| ST[Build FinanceState
user_profile + history + portfolio] ST --> ENG[FinAgentEngine.invoke()] ENG --> G[LangGraph app.invoke(state)] G --> R[Router node
LLM returns JSON route + entities] R -->|one agent| A1[Single agent node] R -->|multiple agents| AM[Multi-agent node
run sequentially] R -->|none| END[END] A1 --> OUT[Response in state["response"]] AM --> COMB[Combine outputs (LLM)
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) ```text [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 returns `state`) - 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 via `src/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 question - `response: str` — final text response - `intent: 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: ```json { "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//*.md|*.txt` (when present) - Used by `EducationAgent` / `TaxAgent` (KB-first) - Web search: `src/rag/TavilySearchRag.py` - Used as a fallback by `EducationAgent`, `TaxAgent`, `MarketAgent`, and `NewsSynthesizerAgent` - 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 `OVERVIEW` for sector/industry/52-week range - Crypto data: `src/rag/CcxtRag.py` (CCXT ticker) **Caching (current)** - Streamlit process caching: - `st.cache_resource` for long-lived objects (KB ingestion bootstrap, router creation) - `st.cache_data` for small data loads (e.g., CSV read patterns) - Chroma persistence: - KB vectors persist in `src/data/.chroma` once ingested **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: 1) KB retrieve (grounded) → answer with citations 2) Web cache retrieve → answer with citations 3) Fresh Tavily search → save to web cache → answer with citations 4) Semantic cache can short-circuit repeated queries before LangGraph runs ### Performance considerations - Deterministic routing: router uses `temperature=0` to 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_MODEL` - `FIN_ASSISTANT_AGENT_MODEL` - `FIN_ASSISTANT_FORMATTER_MODEL` - `FIN_ASSISTANT_EMBEDDING_MODEL` ### High-level flow 1. User submits a question in Streamlit. 2. Input is screened with lightweight bad-word filtering and OpenAI moderation. 3. `FinAgentEngine` routes the query to one or more agents. 4. 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. 5. The final answer is formatted and shown in the UI. ### Execution flow ```text [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. ```bash python -m venv .venv source .venv/bin/activate ``` ### 2. Install dependencies ```bash 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_KEY` - `TAVILY_API_KEY` - `ALPHA_VANTAGE_KEY` or `ALPHAVANTAGE_API_KEY` or `ALPHA_VANTAGE_API_KEY` - `FINNHUB_API_KEY` Optional app settings: - `FIN_ASSISTANT_TITLE` - `FIN_ASSISTANT_PAGE_TITLE` - `FIN_ASSISTANT_TABS` - `FIN_ASSISTANT_MARKET_WATCHLIST` - `FIN_ASSISTANT_PORTFOLIO_DEFAULT_INPUT` - `FIN_ASSISTANT_PORTFOLIO_EXAMPLES` - `FIN_ASSISTANT_RESPONSE_CACHE_TTL_DAYS` - `FIN_ASSISTANT_DEFAULT_RISK` - `FIN_ASSISTANT_DEFAULT_EXPERIENCE` - `FIN_ASSISTANT_ROUTER_MODEL` - `FIN_ASSISTANT_AGENT_MODEL` - `FIN_ASSISTANT_FORMATTER_MODEL` - `FIN_ASSISTANT_EMBEDDING_MODEL` Optional local configuration: - `config.yaml` at the repo root can override app title, tabs, watchlist, portfolio examples, cache TTL, and default user profile values. ### 4. Run the app locally ```bash streamlit run src/streamlit_app.py ``` ### 5. Run with Docker ```bash 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//*.md` - `src/data/knowledge_base//*.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_QUOTE` for price + basic day metrics - fallback to Finnhub when Alpha Vantage fails/unusable (price + OHLC/previous close) - optional Alpha Vantage `OVERVIEW` enrichment 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_resource` keeps the router and moderation agent alive across reruns. - `st.cache_data` caches 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=0` to keep routing deterministic. - `Conversation history` is 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/.chroma` - `src/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()`. ### State model #### `src/core/FinanceState.py` `FinanceState` is a typed dictionary used across agents. Key fields: - `user_query` - `response` - `intent` - `agents` - `symbol` - `crypto_symbol` - `portfolio` - `conversation_history` - `user_profile` - `retrieved_sources` - `errors` ### Retrieval and cache helpers #### `src/rag/KnowledgeBase.py` - `KnowledgeBase(docs_root="src/data/knowledge_base", persist_directory="src/data/.chroma")` - `ensure_ingested() -> None` - `retrieve(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") -> int` - `retrieve(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 | None` - `set(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) -> dict` - `is_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 ```text Explain ETFs in simple terms. ``` ### Tax education ```text What is the difference between capital gains and ordinary income? ``` ### Market lookup ```text What is happening with NVDA today? ``` ### Portfolio analysis ```text My portfolio is 10 AAPL, 5 MSFT, 2 VTI. What stands out? ``` ### Crypto pricing ```text Show me the latest price for BTC. ``` ### Goal planning ```text Help me plan for a house down payment in 5 years. ``` ### News synthesis ```text Summarize the latest headlines about Apple. ``` ### Programmatic use ```python 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//`. - If cached retrieval seems stale, clear the local Chroma and SQLite cache files and rerun the app.