Agentic Portfolio Copilot

A LangGraph agent that routes to deeper risk analysis when it detects a concentration flag — running fully local, no API key

Demo

Portfolio Copilot — basic holdings lookup Portfolio Copilot — risk flag routed to deep analysis
What this shows: the same agent answering two different kinds of questions. A basic lookup ("What are my current holdings?") goes straight to an answer. A risk question ("Am I overconcentrated in any sector?") triggers a concentration flag, and the graph routes to a deeper reasoning step before answering — visible in the expandable "Agent reasoning trace" under every response, showing the exact tool called, its arguments, and its raw output.

Project information

  • Category: Data Science / Agentic AI
  • Data: Synthetic portfolio (14 US large-caps, 6 sectors)
  • Stack: Python, LangGraph, Ollama (Llama 3.1 8B), Streamlit
  • Link to github: GitHub repo →
  • Live app: Live demo →

An agent whose path changes, not just its answer

A single LLM call with tools bolted on can call a function and describe the result — that's a wrapper. What makes this agentic is that the graph's path changes based on what a tool returns, not just what the user typed: a risk question that trips a concentration threshold is routed to a second reasoning step before the agent answers, while a plain lookup goes straight through. Built on LangGraph as a genuine state machine, running on a local Llama 3.1 model via Ollama — reproducible by anyone who clones the repo, no API key required.

Project deep dive

Problem statement

Build an agent that reasons over a portfolio's holdings, sector concentration, VaR, and drawdowns — and instead of just reporting a number, automatically routes to a deeper explanation whenever a tool result crosses a risk threshold. The goal was to demonstrate genuine conditional planning (the graph's path depends on tool output, not just the question), full grounding (every number in an answer traceable to a tool call), and zero-cost reproducibility for anyone reviewing the code.

Architecture

A LangGraph state machine, not a single tool-calling LLM call:

START → router → tool_exec → risk_check ─┬─ (flag raised) → deep_analysis → END
                                            └─ (no flag)     → respond      → END

What the agent can do

  1. get_holdings — current positions, market values, and weights.
  2. sector_exposure — weight by sector, with concentration flags above a configurable threshold.
  3. compute_var — historical-simulation Value-at-Risk, parameterized by confidence level and horizon.
  4. compute_drawdown — max drawdown, portfolio-wide or per ticker.
  5. get_price_history — recent price series for a single ticker.
  6. risk_check + deep_analysis — the conditional edge: when a tool result crosses a threshold (e.g. sector weight > 25%), the graph routes to a second reasoning step that explains the risk and suggests a mitigation, instead of just stating the number.

Eval results

MetricScore
Tool selection accuracy0.917
Grounding rate1.0
Errors0
  • Grounding rate (1.0): every number in every answer, across 18 test questions, traces back to an actual tool result — checked by a custom harness, not eyeballed. Getting this proxy right (list-numbering artifacts, sign mismatches, legitimate rounding of summed figures) took several debugging passes, documented in eval/eval_harness.py.
  • Tool selection accuracy (0.917): the gap comes from two deliberately ambiguous questions ("How risky is my portfolio right now?" and "Has NVDA dropped a lot recently?"), where the local 8B router sometimes picks a plausible-but-incomplete tool rather than the full expected set — a documented, honest limitation of running the router on a free local model instead of a hosted frontier one.

Tech stack

Python LangGraph LangChain Ollama Llama 3.1 8B Streamlit pytest

Full eval harness, test suite, and reasoning-trace UI are in the GitHub repo → — clone it and run python eval/eval_harness.py to reproduce the table above with no API key.