Fade0510's picture
Fix logging and enable semantic cache, stock qoute cache
357c48c
|
Raw
History Blame Contribute Delete
17 kB
---
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.