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

{
  "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

 [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_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

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>/*.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

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.