Agentic AI

Build a FastAPI Dashboard for LangGraph Multi-Agent Workflows

August 22, 2026 · 17 min read

Learn how to connect LangGraph multi-agent workflows to a live FastAPI dashboard. Transform terminal AI scripts into a production web app UI.


 

In Part 1, our LangGraph multi-agent system successfully turned messy customer reviews into structured intelligence. It knows exactly which customers are angry, which bugs are critical, and who might cancel their subscription.

But we have a massive production bottleneck:

Your AI is doing brilliant work, but that intelligence is completely trapped in the backend.

The Solution: Building a FastAPI LangGraph Dashboard

In Part 2, we are bridging the gap between the AI backend and the end-user. We will deploy our multi-agent workflow using a FastAPI web server to build a live, interactive customer intelligence dashboard.

By the end of this tutorial, you will master FastAPI LangGraph integration. We will completely demystify the magic and show you step-by-step how a raw JSON output from a LangGraph agent becomes a beautiful UI widget on a web page.

(📦 Source Code Included: You do not need to piece together random code snippets. The complete project—including the FastAPI server, LangGraph backend, HTML, CSS, and JavaScript—is provided for you to download and run).

What Changed from Part 1?

The core LangGraph multi-agent system hasn’t changed. We didn’t rebuild our four AI agents, and we didn’t touch the graph logic. We simply wrapped a FastAPI application layer around the backend we already created.

The Architecture Shift:


The New Project Files

Our existing src/ backend remains.

Part 2 adds the web application pieces:

multi-perspective-customer-feedback-dashboard/
├── data/
│ └── customer_reviews.jsonl
├── outputs/
│ └── analyzed_reviews.jsonl
├── src/
│ ├── agents.py
│ ├── aggregator.py
│ ├── data_loader.py
│ ├── graph.py
│ ├── llm.py
│ ├── report.py
│ ├── schemas.py
│ ├── state.py
│ └── dashboard_service.py ← NEW
├── templates/
│ └── index.html ← NEW
├── static/
│ ├── styles.css ← NEW
│ └── app.js ← NEW
├── app.py ← NEW
├── run_dashboard.py ← NEW
├── run_single.py
├── run_batch.py
└── requirements.txt

Each new piece has one clear responsibility:

dashboard_service.py
Transforms analyzed results into dashboard-friendly data
app.py
Connects browser requests to data and LangGraph
index.html
Defines what appears on the page
styles.css
Makes it look like an actual product
app.js
Fetches FastAPI data and fills charts/widgets
run_dashboard.py
Starts the FastAPI application

That separation matters. FastAPI should not contain 400 lines of chart rendering code, and JavaScript should not know how our LangGraph state works. Each layer gets its own job.

Install the Web Dependencies

Part 1 already gave us LangGraph, Groq and Pydantic.

Now requirements.txt additionally contains:

fastapi>=0.116.0
uvicorn[standard]>=0.35.0
jinja2>=3.1.6

Install everything:

pip install r requirements.txt

 

What are these three packages?

FastAPI builds our web API.

Uvicorn is the server that actually runs the FastAPI application.

Jinja2 lets FastAPI serve our HTML template.

A simple mental model is:

Uvicorn
runs
FastAPI
serves
HTML + APIs

Transforming Raw AI JSON Data into Dashboard KPIs

Before we build our FastAPI endpoints, we need to prepare our data.

In Part 1, our LangGraph agents processed 100 reviews and saved them to outputs/analyzed_reviews.jsonl. Every line in this file is a perfect, machine-readable JSON object representing one customer’s FinalAnalysis:

{
  "customer_id": "CUST-047",
  "feedback": "PDF uploads crash...",
  "sentiment": { "sentiment": "negative", "frustration_score": 9 },
  "routing": { "department": "Engineering" },
  "priority_score": 92.5,
  "priority_level": "CRITICAL"
}

The Data Problem: This raw JSON is great for backend storage, but frontend charts cannot read individual records.

A visual dashboard needs aggregated KPI data. A chart doesn’t want to read 100 individual JSON lines; it just wants the final math:

The Solution: We need a data transformation layer to bridge the gap between our raw AI output and our web UI. This is exactly what our new dashboard_service.py file does—it reads the JSONL data, aggregates the metrics, and serves ready-to-use numbers to our FastAPI charts!

Loading AI Data safely with Pydantic Validation

Before we build charts, we need to load the 100 AI responses we saved in Part 1 (analyzed_reviews.jsonl).

But we don’t just load them as messy, random dictionaries. We use Pydantic to strictly validate every single line of text back into a perfect Python object (FinalAnalysis).

results = []
with path.open("r", encoding="utf-8") as f:
    for line in f:
        if line.strip():
            # Validate raw JSON straight into a strict Python object
            results.append(FinalAnalysis.model_validate_json(line))

The Data Pipeline: Raw JSONL TextPydantic ValidationList of 100 FinalAnalysis Objects

This guarantees that by the time our FastAPI dashboard touches the data, it is 100% bug-free and structured.

Transforming AI Results into Dashboard KPIs

Frontend UI libraries (like Chart.js) cannot read 100 individual customer reviews. They need aggregated metrics—like total counts and averages.

Inside our dashboard_service.py, we use Python’s built-in Counter to instantly group our data.

# Instantly count how many reviews are Negative, Positive, etc.
sentiment = Counter(x.sentiment.sentiment for x in results)
priority = Counter(x.priority_level for x in results)

# Group them into a simple KPI dictionary for the frontend
"summary": {
    "total_reviews": len(results),
    "critical_count": priority["CRITICAL"],
    "negative_count": sentiment["negative"],
}

How it works in plain English: Instead of sending 100 separate records to the browser, Counter looks at three negative reviews and simply outputs: {"negative": 3}.

The Final Dashboard Payload Flow: 100 Pydantic ObjectsPython Counter()Clean Dashboard KPIs (Summary, Sentiment, Priority)

This perfectly structured JSON payload is exactly what our FastAPI endpoint will send to the frontend!

FastAPI dashboard data pipeline from LangGraph JSONL output to analytics widgets

FastAPI: The Bridge Between Python and the Browser

Now open app.py. The most important imports are:

from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates

 

And from our own project:

from src.dashboard_service import (
build_dashboard_payload,
load_analyzed_reviews,
)
from src.graph import feedback_graph

That last line is the key connection:

from src.graph import feedback_graph

FastAPI is importing the same compiled LangGraph application used by our command-line project.

Architecture:

src/graph.py
│ feedback_graph
app.py
FastAPI endpoint

We are not calling LangGraph through another server. FastAPI and LangGraph live inside the same Python application. That makes the integration quite simple.

Setting Up the FastAPI App

First, we need to initialize the core FastAPI engine. We define our app with all its details so it acts as a proper, documented backend.

# 1. Initialize the FastAPI App
app = FastAPI(
    title="Customer Feedback Intelligence",
    version="1.0.0",
    description=(
        "Parallel multi-agent customer feedback "
        "analytics dashboard."
    ),
)

What this does:

Next, we must tell FastAPI where our web design and logic files live. By default, FastAPI is a backend API and doesn’t know what CSS or JavaScript is.

# 2. Tell FastAPI where static frontend files live
app.mount(
    "/static",
    StaticFiles(directory=BASE_DIR / "static"),
    name="static",
)

What this does:

Finally, we need to serve the actual webpage (HTML) to the user.

# 3. Connect the HTML folder using Jinja2
templates = Jinja2Templates(
    directory=BASE_DIR / "templates"
)

What this does:

The Final Result: By writing these few lines, FastAPI now knows exactly where to find all three critical frontend pieces to build your UI:

  1. templates/index.html (The structure)

  2. static/styles.css (The paint and styling)

  3. static/app.js (The brain of the frontend)

Serve the Dashboard Page (HTML vs. Data)

Our very first FastAPI endpoint is beautifully simple. Its only job is to load the blank webpage layout.

@app.get("/", response_class=HTMLResponse)
def dashboard(request: Request):
    return templates.TemplateResponse(
        request=request,
        name="index.html",
        context={},
    )

What is happening here? 

When the browser loads this page, it does not have the 100-review analytics yet. The charts and tables will be completely empty.

Why? Because in modern web architecture, loading the screen and loading the data happen in two separate steps. This is a crucial concept to understand:

By keeping the UI and the data completely separate, your dashboard loads instantly for the user, and the heavy AI data fetches quietly in the background!

The Endpoint That Feeds Every Dashboard Widget

Here is the historical analytics endpoint:

@app.get(“/api/dashboard”)
def dashboard_data():
results = load_analyzed_reviews(RESULTS_FILE)
return build_dashboard_payload(results)

 

Only two lines. But those two lines connect almost the entire project.

Follow the journey carefully.

Step 1 — Browser requests data

GET /api/dashboard

Step 2 — FastAPI receives it

dashboard_data()

Step 3 — Read Part 1 output

load_analyzed_reviews(RESULTS_FILE)

Step 4 — Get validated objects

list[FinalAnalysis]

Step 5 — Transform them

build_dashboard_payload(results)

Step 6 — FastAPI serializes the returned dictionary as JSON

Conceptually:

{
“summary”: {
“total_reviews”: 100,
“critical_count”: 22,
“negative_count”: 59
},
“sentiment”: {
“negative”: 59,
“neutral”: 23,
“positive”: 18
},
“departments”: {
“Engineering”: 34,
“Product”: 27
},
“reviews”: […]
}

Now our Python data has crossed into the browser world.

How JSON Actually Becomes a Dashboard Widget

This is the missing link most tutorials skip: How does Python AI data actually appear on a web page?

Open your static/app.js file. The browser pulls data from our backend using the JavaScript Fetch API:

// 1. Browser asks FastAPI for the data
const response = await fetch("/api/dashboard");

// 2. The JSON response becomes a usable JavaScript object
const dashboardData = await response.json();

The Journey of a Single Data Point (Total Reviews) :

Let’s watch exactly how a single metric travels from our backend AI analysis directly to your screen.

1. The Python Backend calculates the number:

"total_reviews": len(results)

2. The FastAPI Endpoint sends it over the web as structured JSON:

{
  "summary": {
    "total_reviews": 100
  }
}

3. The Frontend JavaScript targets the HTML by its ID and injects the data:

document.getElementById("totalReviews").textContent = dashboardData.summary.total_reviews;

4. The HTML DOM updates live for the user to see:

<div class="metric-value" id="totalReviews">100</div>

The Complete Data Flow :

100 FinalAnalysis Records (Python)FastAPI JSON ResponseJavaScript ObjectHTML Dashboard Widget

Takeaway: There is no AI magic here. Building a production AI dashboard is simply passing well-structured data cleanly across architectural layers.

How Python AI Data Becomes a Web Chart

How do we turn raw LangGraph agent outputs into a beautiful dashboard widget? The secret is simple data mapping.

Remember our backend Python object? We use Counter() to extract and aggregate the sentiment from all 100 results:

# 1. Python Backend Aggregation
sentiment = Counter(
    x.sentiment.sentiment for x in results
)

# This creates a simple dictionary:
# { "negative": 59, "neutral": 23, "positive": 18 }

FastAPI serves this exact dictionary as JSON. Next, our frontend JavaScript receives it:

// 2. JavaScript receives the FastAPI JSON
const data = dashboardData.sentiment;

Then, we feed this directly into Chart.js, which expects arrays for both labels and data:

// 3. Chart.js Data Mapping
labels: [
    "Negative",
    "Neutral",
    "Positive"
],
data: [
    data.negative || 0, 
    data.neutral || 0, 
    data.positive || 0
]

The Complete Data Pipeline:

Priority and department charts use exactly the same principle. Once you master this single widget data flow, you understand almost the entire analytics frontend.

The Review Explorer Uses the Same Data Differently

Charts need aggregated counts. The table needs individual records.

That is why dashboard_service.py also creates:

reviews = []

 

For each analysis:

reviews.append({
“customer_id”: item.customer_id,
“feedback”: item.feedback,
“sentiment”: item.sentiment.sentiment,
“issue”: item.issue.issue,
“urgency_score”: item.risk.urgency_score,
“churn_risk_score”: item.risk.churn_risk_score,
“department”: item.routing.department,
“priority_score”: item.priority_score,
“priority_level”: item.priority_level,
})

Before that, results are ranked:

ranked = sorted(
results,
key=lambda x: x.priority_score,
reverse=True
)

So critical customers naturally appear first.

JavaScript receives:

allReviews = dashboardData.reviews || [];

 

and creates table rows. That gives us:

Customer | Issue | Sentiment | Department | Urgency | Churn | Priority

 

The search and dropdown filters happen in the browser, so filtering does not require another LLM call.

Again:

Don’t use AI for work normal software can perform perfectly.

The AI Pipeline: Browser ➔ FastAPI ➔ LangGraph

Everything we have visualized so far was historical data. Now, let’s analyze brand-new feedback in real-time.

When a user types a new complaint (e.g., “The app crashes whenever I upload a PDF…”) and clicks “Run 4-Agent Analysis”, a completely new pipeline begins.

Because the browser is sending data to the backend, our JavaScript fires a POST request:

// Send the live feedback to the FastAPI backend
const response = await fetch("/api/analyze", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ feedback: text }),
});

The Request Payload: { "feedback": "The app crashes whenever..." }

Validating Live Inputs with FastAPI and Pydantic

How does FastAPI safely receive this incoming JSON? We use a Pydantic model to strictly validate the browser’s input before it ever touches our AI:

# 1. Enforce strict character limits to prevent empty inputs or spam
class LiveFeedbackRequest(BaseModel):
    feedback: str = Field(min_length=5, max_length=5000)

# 2. Define the POST endpoint
@app.post("/api/analyze")
def analyze_feedback(payload: LiveFeedbackRequest):

FastAPI automatically validates the incoming request against LiveFeedbackRequest. So even before LangGraph runs, the data flow is strictly controlled:

Next, we generate a unique tracking ID for this live session (e.g., LIVE-A7C91F):

# Generate a random 6-character UUID
customer_id = f"LIVE-{uuid.uuid4().hex[:6].upper()}"

And now comes the single most important line of code in Part 2…

FastAPI Calls the LangGraph Workflow

Inside the endpoint:

result = feedback_graph.invoke(
{
“customer_id”: customer_id,
“feedback”: payload.feedback,
},
config={“max_concurrency”: 4},
)

Stop here. This is where the web application enters the AI graph.

Input:

{
“customer_id”: “LIVE-A7C91F”,
“feedback”: “The app crashes…”
}

 

becomes the same FeedbackState we learned in Part 1.

STATE — INITIAL
customer_id LIVE-A7C91F
feedback The app crashes…
sentiment_result EMPTY
issue_result EMPTY
risk_result EMPTY
routing_result EMPTY
final_analysis EMPTY

LangGraph then follows our existing graph:

Sentiment Agent
FastAPI → START → Issue Agent
Risk Agent
Routing Agent

 

All four inspect the same feedback.

They update:

sentiment_result
issue_result
risk_result
routing_result

Then Fan-In waits for the required branches and calls the aggregator.

4 Structured Results
Fan-In Aggregator
Priority Score
FinalAnalysis

FastAPI did not become an AI agent. Its job is orchestration at the application boundary:

HTTP request
Python input
LangGraph
Python result
HTTP response

 

That is the clean relationship between FastAPI and LangGraph in this project.

FastAPI and LangGraph integration for live parallel multi-agent customer feedback analysis

How FinalAnalysis Returns to the Browser

After LangGraph finishes:

final = result[“final_analysis”]

Remember where that came from?

The aggregator created it in Part 1.

So FastAPI now has our Pydantic object:

FinalAnalysis
├── customer_id
├── feedback
├── sentiment
├── issue
├── risk
├── routing
├── priority_score
└── priority_level

We return:

return final.model_dump()

Pydantic converts the model into a normal Python dictionary.

FastAPI serializes that dictionary into JSON.

The browser receives something like:

{
“customer_id”: “LIVE-A7C91F”,
“sentiment”: {
“sentiment”: “negative”,
“frustration_score”: 9
},
“issue”: {
“issue_type”: “bug”,
“issue”: “PDF upload crashes”
},
“risk”: {
“urgency_score”: 10,
“churn_risk_score”: 9
},
“routing”: {
“department”: “Engineering”
},
“priority_score”: 96.5,
“priority_level”: “CRITICAL”
}

Now JavaScript calls:

renderLiveResult(data);

And accesses nested values such as:

data.sentiment.sentiment
data.sentiment.frustration_score
data.issue.issue_type
data.issue.issue
data.risk.urgency_score
data.risk.churn_risk_score
data.routing.department
data.priority_score
data.priority_level

Each value goes into its corresponding result box.

So the complete transformation is:

Customer sentence
HTTP JSON
LiveFeedbackRequest
FeedbackState
4 structured agent outputs
FinalAnalysis
Python dictionary
FastAPI JSON
JavaScript object
HTML result widgets

That is the entire application.


Why We Have Two API Endpoints

At this point the architecture becomes very clean.

Historical analytics

GET /api/dashboard
analyzed_reviews.jsonl
dashboard_service.py
Aggregated JSON
Charts + KPIs + Table

No Groq calls.

No agents.

The expensive AI work already happened during batch processing.

Live intelligence:

POST /api/analyze
New customer feedback
LangGraph
4 Groq-powered agents
Aggregator
FinalAnalysis
Live result

This separation is important for both performance and cost.

Imagine refreshing the dashboard ten times.

Should that rerun 400 LLM calls every time?

Absolutely not.

Historical results are stored once and visualized repeatedly.

Only genuinely new feedback needs AI inference.


Start the Dashboard

run_dashboard.py is intentionally tiny:

import uvicorn
if __name__ == “__main__”:
uvicorn.run(
“app:app”,
host=“127.0.0.1”,
port=8000,
reload=True,
)

Run:

python run_dashboard.py

Open:

http://127.0.0.1:8000

FastAPI also automatically provides interactive API documentation:

http://127.0.0.1:8000/docs

 

You can test /api/analyze directly there without using the dashboard UI.

We also created:

@app.get(“/health”)
def health():
return {“status”: “ok”}

So:

GET /health

returns:

{
“status”: “ok”
}

 

A tiny endpoint, but useful for checking whether the application itself is alive.


One Important Gotcha: Why Your Dashboard May Show Zero

If the page loads but shows:

Total Reviews 0
Critical 0
Negative 0
Avg Priority 0

FastAPI is probably working perfectly.

Look back at:

if not path.exists():
return []

If:

outputs/analyzed_reviews.jsonl

does not exist in the dashboard project, load_analyzed_reviews() returns an empty list.

Therefore:

No analyzed_reviews.jsonl
[]
zero results
zero counters
dashboard full of zeros

The solution is not to unnecessarily spend another 400 Groq calls.

Use the analyzed_reviews.jsonl already generated in Part 1.

This also teaches an important architectural lesson:

The dashboard is a consumer of analyzed data, not the thing responsible for regenerating the entire dataset every time it starts.


What We Actually Built

Look at how far the same customer review now travels:

RAW WORLD
Customer Feedback
WEB LAYER
Browser / JS
POST /api/analyze
APPLICATION LAYER
FastAPI
AI WORKFLOW
LangGraph
┌─────────┼─────────┐
↓ ↓ ↓
Sentiment Issue Risk
↘ ↓
Routing
└─────────┬─────────┘
Fan-In Aggregator
Priority Score
FinalAnalysis
FastAPI
JSON Response
JavaScript
DASHBOARD UI

The most important lesson is not how to make a glowing chart.

It is learning how to maintain clean data contracts between layers.

LangGraph produces FinalAnalysis.

FastAPI exposes that result.

JavaScript consumes predictable JSON.

Widgets render specific fields.

Every layer knows exactly what it receives and what it should return.


From AI Demo to AI Application

In Part 1, we learned multi-agent orchestration:

Fan-Out
Parallel Agents
Shared State
Fan-In
Deterministic Aggregation

In Part 2, we learned how to turn that orchestration into an application:

Stored AI Results
+
Live LangGraph Execution
FastAPI
REST APIs
JavaScript
Interactive Dashboard

And that distinction matters.

A terminal demo proves that your AI logic works.

An application makes that intelligence usable by someone who has never heard of StateGraph, Pydantic, Groq, or Python.

Our final system can now do both.

It can process 100 customer reviews in batch, transform them into structured intelligence, rank critical customers, visualize sentiment and operational patterns—and accept a completely new customer complaint from the browser and send it through four parallel AI specialists in real time.

All using the same LangGraph backend we built in Part 1.

That is the architecture worth remembering:

Build the intelligence once. Then expose it through whatever interface your users actually need.

Today that interface is a FastAPI customer intelligence dashboard.

Tomorrow the same graph could sit behind a mobile app, Slack bot, CRM integration, internal support platform, or automated escalation system.

The UI can change.

The orchestration does not have to.

The complete source-code folder accompanying Part 2 contains the FastAPI application, dashboard service, HTML template, CSS, JavaScript, existing LangGraph backend, and the same project structure used throughout this tutorial.

Never miss what we build next.

New articles and interactive labs, straight to your inbox the moment they ship — no fixed schedule, no fluff.

Logic Lama
Neural Ninjas
// Continuing from this article

Your Neural Path

0 0 votes
Article Rating
Subscribe
Notify of
guest
1 Comment
Oldest
Newest Most Voted
trackback
8 hours ago

[…] Coming in Part 2: We will connect this multi-agent backend to a full-stack FastAPI Customer Intelligence Dashboard […]

// STILL BROWSING?
Build along, don't just read.
Get labs & articles matched to what you're into — free, takes 30 seconds.
Start building free