Skip to content

SportsAgent: Autonomous NFL Stats Visualization with LangGraph

SportsAgent is an advanced autonomous agent designed to analyze NFL player statistics, perform comparative evaluations, and generate interactive visualizations on demand. Built with LangGraph, it demonstrates a robust Human-in-the-Loop (HITL) architecture where the agent autonomously plans analysis steps but defers to the user for critical decisions like data fetching and visualization generation.

High-Level Overview

The system operates as a cyclic graph that alternates between Retrieval (fetching data from nflreadpy) and Analysis (interpreting data with an LLM). It features:

  • Autonomous Reasoning: The AnalyzerAgent determines if the retrieved data is sufficient or if more is needed.
  • Team & Player Stats: Supports querying for individual players, entire positions, or specific teams (including "ALL" teams).
  • Interactive Visualization: Users can request charts, which are generated on-the-fly by the agent writing and executing Plotly code.
  • Stateful Conversations: Maintains context across multiple turns, allowing for iterative refinement of queries (e.g., "Compare Mahomes and Allen" -> "Now add Burrow").

Architecture & Design

The core of SportsAgent is a LangGraph workflow that orchestrates the interaction between the user, the LLM, and the data tools.

LangGraph Workflow

The workflow is defined in src/sportsagent/workflow.py and consists of the following key nodes:

  1. Query Parser: Structured intent extraction to understand which players, positions, and stats are requested.
  2. Retriever: Fetches raw play-by-play or season-level data using nflreadpy.
  3. Analyzer: A ReAct agent that computes statistics, generates insights, and decides the next step.
  4. Approval Node (HITL): A safeguard that pauses execution to ask for user permission before fetching large datasets or performing expensive operations.
  5. Visualization Node (HITL): A dedicated step where the agent proposes a visualization, and upon user approval, generates the rendering code.

Human-in-the-Loop (HITL) Patterns

This project showcases two distinct HITL patterns:

  • Active Approval: The workflow explicitly pauses at the approval interrupt when the agent signals it needs more data. Approval resumes to parsing, then retrieval, so the updated request is treated like any other user query.
  • Opt-In Visualization: Visualization is conditional and runs only after the user confirms in the UI.
  • Chart-only Follow-ups: If a follow-up is purely a chart presentation change, parsing can route directly to visualization without calling the retriever.

State Management

Managing state in a complex agentic workflow is critical. We use a custom Pydantic model ChatbotState that tracks:

  • retrieved_data: Stores base datasets (players, teams) plus optional enrichment datasets (e.g., rosters, snap_counts) attached under an extra mapping.
  • visualization: Stores the generated Plotly figure as a JSON-serialized dictionary, allowing it to be passed between nodes and rendered by the frontend without pickling issues.

Key Components

Analyzer Agent

ReAct agent which analyzes the data for statistical relevance, performs comparisons, and decides when to request more data or generate visualizations.

Visualization Engine

Translates data, preloaded assets, and user intent into high-quality Plotly charts. It receives a schema of the data and the user's query, then writes Python code to generate the figure, which is executed in a sandboxed environment.

Streamlit UI

The application frontend is built with Streamlit to provide a rich, interactive experience. It features:

  • Workflow Tracing: A sidebar that visualizes the active path through the LangGraph nodes.
  • Interactive Charts: Renders the Plotly JSON artifacts directly.
  • Data Inspection: Allows users to view the raw data tables used by the agent.

Tech Stack

  • Orchestration: LangGraph
  • Data Source: nflreadpy
  • Frontend: Streamlit
  • Visualization: Plotly

Diagram

```mermaid

config: flowchart: curve: stepAfter fontFamily: Arial look: handDrawn theme: neutral


graph TD; start([

start

]):::first entry(entry) exit(exit) query_parser(query_parser) retriever(retriever) AnalyzerReactAgent(AnalyzerReactAgent) generate_visualization(generate_visualization
interrupt = before):::hitl execute_visualization(execute_visualization
__interrupt = before):::hitl save_report(save_report
__interrupt = before):::hitl approval(approval
__interrupt = before):::hitl __end
([

end

]):::last AnalyzerReactAgent -.-> approval; AnalyzerReactAgent -.-> exit; AnalyzerReactAgent -.-> generate_visualization; AnalyzerReactAgent -.-> save_report; start --> entry; approval -.-> exit; approval -.-> query_parser; entry -.-> exit; entry -.-> query_parser; execute_visualization --> save_report; generate_visualization --> execute_visualization; query_parser -.-> exit; query_parser -.-> generate_visualization; query_parser -.-> retriever; retriever -.-> AnalyzerReactAgent; retriever -.-> exit; save_report --> exit; exit --> end; classDef default fill:#E1F5FE,stroke:#01579B,stroke-width:2px,color:#000000,line-height:1.2 classDef first fill:#D1C4E9,stroke-dasharray: 5 5,color:#000000 classDef last fill:#D1C4E9,stroke:#4A148C,stroke-width:2px,color:#000000

classDef hitl fill:#FFF3E0,stroke:#E65100,stroke-width:2px,color:#000000

subgraph Legend
    direction LR
    L1(System Step):::default
    L2(Start / End):::first
    L3(Human Action):::hitl
end

```

alt text

Generated Reports