Skip to main content
Glama
FixNPE

agentic-financial-advisor

by FixNPE
README.md
# Agentic Financial Advisor

A multi-agent financial advisor built with LangChain/LangGraph agents, exposed
to each other over the **A2A protocol**, pulling live market data through an
**MCP** (Alpha Vantage) server, and grounding answers in a local **RAG**
(Chroma) knowledge base. Usable via a chat CLI, a **Streamlit UI** with a
live view of the routing/agent-call flow (plus a RAG Explorer page), or
directly from Claude Code — the app's own operations are exposed as an
**MCP server** too.

Informational/educational only — nothing here is personalized financial advice.

## Architecture

- **Specialist agents** (`agents/`) — each is a standalone LangGraph
  `create_react_agent`, wrapped as an A2A server (agent card + JSON-RPC
  endpoint) via `a2a-sdk`:
  - `market_research_agent` — live quotes/fundamentals/technicals/macro data,
    via the Alpha Vantage **MCP** server (`mcp_integration/`).
  - `portfolio_analyst_agent` — allocation weighting and concentration risk,
    via local calculation tools.
  - `financial_planning_agent` — savings/retirement projections, via local
    calculation tools.
  - `document_research_agent` — **RAG** over `data/documents/` using Chroma +
    local HuggingFace embeddings (`rag/`).
- **Orchestrator** (`orchestrator/`) — a LangGraph supervisor graph that
  routes a user query to the relevant specialist agent(s), calls them in
  parallel over **A2A** (`orchestrator/a2a_client.py`), and synthesizes one
  final answer.
- **`main.py`** — interactive chat CLI that talks to the orchestrator.

```
User -> main.py -> supervisor graph (route -> fan-out -> synthesize)
                        |         |            |            |
                   market_research portfolio  planning   document_research
                     (MCP: Alpha Vantage)    (local tools)  (RAG: Chroma)
```

## Setup

```bash
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
cp .env.example .env   # fill in ANTHROPIC_API_KEY and ALPHAVANTAGE_API_KEY
```

If `python -m venv` fails with `ensurepip is not available` (this system had
no `python3-venv`/pip installed at all and no passwordless sudo), bootstrap
pip inside the venv directly instead of installing the apt package:
```bash
python3 -m venv --without-pip .venv
curl -sS https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py
.venv/bin/python /tmp/get-pip.py
.venv/bin/python -m pip install -r requirements.txt
```

Verify `ALPHAVANTAGE_MCP_URL` in `.env` against Alpha Vantage's current MCP
documentation — hosted MCP endpoints can change.

`a2a-sdk` is pinned to `==0.3.26` in `requirements.txt`: newer 1.x releases
restructured the SDK around gRPC/protobuf and dropped `a2a.server.apps`
entirely, which this project's Starlette-based agent servers depend on. This
pin was verified against the actual installed package, not assumed.

## Running

1. Start the four specialist agent servers:
   ```bash
   python scripts/run_all_agents.py
   ```
   (or run each individually in its own terminal, e.g.
   `python -m agents.market_research_agent`). The Document Research Agent
   auto-ingests `data/documents/` into Chroma on first startup if the index
   is empty — no manual step needed (`python -m rag.ingest` still works
   directly if you want to force a rebuild after changing the corpus).
2. Either the chat CLI, in another terminal:
   ```bash
   python main.py
   ```
   or the Streamlit UI, which also shows the routing decision and each
   agent's call live as it happens rather than only the final answer:
   ```bash
   streamlit run app/streamlit_app.py
   ```
   The Streamlit app has a second page, **RAG Explorer** (in its sidebar),
   for browsing the Chroma collection — chunks, metadata, a chunk's raw
   embedding vector, and a live semantic-search test against the same index
   the Document Research Agent retrieves from.

## Corpus

`data/documents/` holds the RAG source material: two markdown reference docs
(investment glossary, model risk-profile allocations) plus two public
government-published PDFs under `data/documents/pdfs/` — a FINRA guide to
spotting investment scams and a CFPB home-loan toolkit. Ingestion
(`rag/ingest.py`) handles `.md`, `.txt`, and `.pdf` (text-extracted
page-by-page via `pypdf`) uniformly. Drop more files of any of those types
into `data/documents/` (subdirectories are fine) and either restart the
Document Research Agent (auto-ingests if the index was empty) or run
`python -m rag.ingest` to force a full rebuild.

## MCP server (use these tools from Claude Code)

`mcp_server.py` exposes this app's own operations — portfolio allocation,
concentration risk, retirement/savings projections, RAG document search, and
a full multi-agent advisor query — as MCP tools, the same way the Alpha
Vantage MCP server exposes its functions to the Market Research Agent. Every
tool is a thin wrapper reusing the actual app logic (the same
`@tool`-decorated calculators the agents use, the same Chroma retriever, the
same supervisor graph), not a reimplementation.

It's registered in this project's `.mcp.json` for Claude Code to pick up as a
local (stdio) MCP server. **Restart your Claude Code session** for a newly
added `.mcp.json` entry to take effect — like skills, MCP servers are loaded
at session start, not picked up mid-session.

To test it manually without Claude Code:
```bash
python mcp_server.py   # runs the stdio server; Ctrl+C to stop
```

`ask_financial_advisor` (the full-orchestrator tool) requires the four
specialist agent servers to already be running — it calls out to them over
A2A exactly like `main.py`/the Streamlit UI do. The other five tools
(calculators + RAG search) are self-contained and work with just this one
process.

## Workflow testing

`tests/scenarios.json` has ~19 test queries covering each specialist agent
individually, multi-agent combinations, and edge cases (ambiguous/gibberish
input, a zero-value portfolio). `tests/run_scenarios.py` runs them against the
live supervisor graph (requires the four agent servers running and real API
keys — these are real LLM/MCP calls, not mocked):

```bash
python -m tests.run_scenarios                            # all scenarios
python -m tests.run_scenarios --category market_research  # one category
python -m tests.run_scenarios --id multi-01 planning-02   # specific cases
```

Router agent-selection mismatches print as warnings (routing is LLM-based and
won't always pick the exact expected set); the runner only exits non-zero on
a genuine agent error or unhandled exception.

## LLM response caching

On by default: a persistent SQLite cache (`data/llm_cache.sqlite`, gitignored)
so re-running the same query/scenario doesn't re-spend on the Anthropic API —
useful since `tests/scenarios.json` gets run repeatedly during development.
Measured on a real re-run: ~2x faster, with a genuine (partial, not 100%)
reduction in API calls — LangGraph embeds a random `tool_call_id` into each
agent's own tool-calling turns, so those specific turns miss the cache even
on an identical repeat query, while the router/synthesizer calls and each
agent's first turn hit it reliably.

Set `LLM_CACHE_ENABLED=false` in `.env`, or delete `data/llm_cache.sqlite`,
whenever you need a guaranteed-fresh answer — this matters most for market
data queries, where an exact-wording repeat would otherwise replay a stale
quote instead of fetching a current one.

## Observability (LangSmith)

Set `LANGSMITH_API_KEY` in `.env` (get one at https://smith.langchain.com/ ->
Settings -> API Keys) to turn on full tracing across the whole system —
every LLM call, tool call (MCP/RAG/calculators), and LangGraph node in every
process shows up in the LangSmith UI under your `LANGSMITH_PROJECT`. This is
zero-code auto-instrumentation from LangChain/LangGraph once the env vars are
set (`agents/common/observability.py::enable_tracing()`, called at the top of
every entrypoint). Leave `LANGSMITH_API_KEY` blank to run with tracing off.

Each user query gets a `request_id`, generated in `main.py`/`tests/run_scenarios.py`
and propagated through the supervisor graph and over A2A (as message metadata)
to every specialist agent it calls. The orchestrator's own run (route -> fan-out
-> synthesize) appears as one nested trace; each specialist agent runs in its
own process so it appears as a separate trace, but every trace involved in one
user query is tagged with the same `request_id` — filter on `metadata.request_id`
in the LangSmith UI to reconstruct the full "360 view" of one query across all
four agents plus the orchestrator. (Traces aren't stitched into a single
literal parent-child tree across the A2A/process boundary — that would need
distributed trace-context propagation, which isn't implemented here.)

For a testing view in the LangSmith UI (rather than only console output):

```bash
python -m tests.upload_dataset   # push tests/scenarios.json as a LangSmith dataset (idempotent)
python -m tests.langsmith_eval   # run it as an experiment; prints a results URL
```

This runs the same scenarios as `tests/run_scenarios.py` but records them as a
LangSmith **experiment** against the **financial-advisor-workflow-scenarios**
dataset — viewable under Datasets & Testing as a results table (routing-match
and no-agent-errors scores per row) with a full trace attached to every row.

## Notes

- Embeddings are local (`sentence-transformers/all-MiniLM-L6-v2`) so RAG works
  without an extra API key.
- `create_react_agent` (from `langgraph.prebuilt`) is deprecated as of
  LangGraph 1.x in favor of `langchain.agents.create_agent`, but still works —
  every agent in this repo has been smoke-tested against the installed
  version. Migrate when LangGraph actually removes it (planned for 2.0).