Out-of-Core Streaming with Multi-Agent LangGraph for Massive Enterprise CSV Processing
In enterprise AI, we frequently encounter the "Massive File Problem." A logistics analyst asks: "Summarize the critical bottlenecks in today's 15GB global shipping manifest and cross-reference them with our vendor risk database." A standard RAG system fails here. You cannot chunk a 15GB CSV into a vector database; indexing 50 million raw shipping rows is computationally wasteful and semantically meaningless. Furthermore, passing 15GB to an LLM's context window is impossible.
The solution requires a paradigm shift: Out-of-Core Streaming. We must build a "Data Ingestion Agent" that uses a Python generator to stream the massive CSV in fixed-size chunks, process it in memory, and extract only the high-level metadata. This metadata is then passed via strict LangGraph state to a RAG agent for contextual synthesis.
In this article, we will implement a memory-efficient CSV generator and integrate it into an end-to-end Multi-Agent LangGraph System with persistent memory and strict state management.
Part 1: The Memory-Efficient Generator
To process a multi-gigabyte CSV without triggering an Out-Of-Memory (OOM) error, we cannot use pd.read_csv() without arguments. Instead, we use a generator function that yields fixed-size chunks.
Crucially, we do not yield the raw rows. We process each chunk in RAM, aggregate the findings, and yield only the aggregated insights. This ensures that even if the file is 100GB, our memory footprint remains flat and tiny.
import pandas as pd
from typing import Generator, Dict, Any, List
def stream_and_aggregate_massive_csv(file_path: str, chunk_size: int = 250_000) -> Generator[List[Dict[str, Any]], None, None]:
"""
Streams a multi-GB CSV in fixed-size chunks, processes it, and yields
aggregated insights without loading the whole file into memory.
"""
# pandas read_csv with chunksize returns a TextFileReader (an iterator/generator)
# We use usecols and dtype to further minimize the memory footprint of each chunk
chunk_iterator = pd.read_csv(
file_path,
chunksize=chunk_size,
usecols=['shipment_id', 'origin_port', 'delay_hours', 'vendor_id'],
dtype={'delay_hours': 'float32', 'vendor_id': 'category'}
)
for chunk in chunk_iterator:
# 1. Filter the chunk in memory (e.g., find severe delays > 72 hours)
severe_delays = chunk[chunk['delay_hours'] > 72.0]
if not severe_delays.empty:
# 2. Aggregate the chunk
aggregated = severe_delays.groupby('origin_port').agg(
total_delayed_shipments=('shipment_id', 'count'),
avg_delay_hours=('delay_hours', 'mean')
).reset_index()
# 3. Yield ONLY the processed, aggregated metadata
yield aggregated.to_dict(orient='records')
Why This Matters
If the CSV has 50 million rows, the generator processes it in 200 chunks of 250,000 rows. At any given millisecond, only 250,000 rows are in RAM. The raw data is immediately garbage-collected, and only the tiny dictionary of port bottlenecks is retained.
Part 2: The Enterprise Use Case
The Scenario: A Global Supply Chain Command Center
Every night, a 15GB CSV dump of global shipping manifests is deposited in the enterprise data lake.
The Workflow
User: "What are the worst port bottlenecks in today's manifest, and are the vendors associated with them high-risk?"
Agent 1 (Data Ingestion): Consumes the generator to stream the 15GB file, identifying the top 3 most delayed ports.
Agent 2 (RAG Risk Analyst): Takes those 3 port names, queries the internal Vector DB for vendor risk profiles, and retrieves relevant compliance documents.
Agent 3 (Synthesizer): Combines the data insights and RAG context into an executive briefing.
Part 3: The Multi-Agent LangGraph Implementation
We will use LangGraph to orchestrate this flow. We will use Pydantic models for strict state management, ensuring data passed between agents is strongly typed.
1. Defining the State and Memory
import os
import getpass
from typing import TypedDict, Annotated, Sequence, List
from pydantic import BaseModel, Field
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage, ToolMessage
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
# --- 1. Strict State Definitions ---
class PortBottleneck(BaseModel):
port: str
severe_delays: int
avg_delay_hours: float
class AgentState(TypedDict):
# Conversational Memory (Messages)
messages: Annotated[Sequence[BaseMessage], "add_messages"]
# Data Processing State (Passed from Ingestion to RAG)
bottlenecks: List[PortBottleneck]
# RAG State
vendor_risk_context: str
2. Defining the Tools (The "Hands" of the Agents)
We wrap our generator in a LangChain tool. The tool consumes the generator, accumulates the tiny metadata payloads, and returns the final top bottlenecks to the LLM.
# --- 2. Define the Tools ---
@tool
def analyze_global_shipping_manifest(file_path: str) -> List[dict]:
"""
Streams the multi-GB daily shipping manifest in fixed-size chunks.
Returns a list of critical port bottlenecks (delays > 72 hours).
"""
print(" [INGESTION AGENT] Streaming 15GB manifest via out-of-core generator...")
port_delays = {}
# Consume the generator chunk by chunk
for chunk_data in stream_and_aggregate_massive_csv(file_path, chunk_size=250_000):
for record in chunk_data:
port = record['origin_port']
if port not in port_delays:
port_delays[port] = {'count': 0, 'total_hours': 0.0}
port_delays[port]['count'] += record['total_delayed_shipments']
port_delays[port]['total_hours'] += record['avg_delay_hours'] * record['total_delayed_shipments']
# Calculate final averages across all chunks
final_bottlenecks = []
for port, stats in port_delays.items():
avg_delay = stats['total_hours'] / stats['count'] if stats['count'] > 0 else 0
final_bottlenecks.append({
"port": port,
"severe_delays": stats['count'],
"avg_delay_hours": round(avg_delay, 2)
})
final_bottlenecks.sort(key=lambda x: x['severe_delays'], reverse=True)
print(f" [INGESTION AGENT] Streaming complete. Identified {len(final_bottlenecks)} bottlenecked ports.")
return final_bottlenecks[:3] # Return top 3
@tool
def search_vendor_risk_rag(port_names: List[str]) -> str:
"""
Searches the Vector Database for vendor risk profiles and compliance docs
related to specific origin ports.
"""
print(f" [RAG AGENT] Querying Vector DB for vendor risks at ports: {port_names}...")
# Simulated Vector DB retrieval
return f"""
Port 'Shanghai': Vendor 'Oceanic Freight Ltd' has a Tier-2 Risk rating due to recent customs compliance audits.
Port 'Rotterdam': Vendor 'EuroLogistics' is currently under review for labor strike contingencies (Tier-1 Risk).
Port 'Los Angeles': Vendor 'Pacific Transit' has a clean compliance record but faces infrastructure bottlenecks.
"""
tools = [analyze_global_shipping_manifest, search_vendor_risk_rag]


Join the conversation! Your thoughts help the community grow.