Spaces:
Sleeping
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) | |
| ```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) | |
| ```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/<category>/*.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/<category>/*.md` | |
| - `src/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_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/<category>/`. | |
| - If cached retrieval seems stale, clear the local Chroma and SQLite cache files and rerun the app. | |