Skip to content

Multi-Agent LLM Pipelines for NEPSE: Engineering Collaborative Financial Analytics with Python

If you have ever tried feeding raw Nepal Stock Exchange (NEPSE) floor sheets, semi-structured PDF disclosures, and historical price matrices into a single monolithic LLM prompt, you already know the frustration: hallucinated technical indicators, missed corporate actions, and brittle JSON outputs that crash your downstream trading engines. As data engineers and quantitative developers building financial workflows, we need to stop treating language models like magic calculators and start architecting them as collaborative teams. In this post, you will learn how to build a production-grade, multi-agent Python pipeline that delegates data ingestion, mathematical transformation, and risk synthesis to specialized LLM agents. By the end of this guide, you will be able to orchestrate stateful agent loops that parse nested JSON payloads without corrupting your financial logic.

Key Takeaways

  • Separating extraction, calculation, and synthesis into dedicated agent contexts drastically reduces hallucinations in financial reporting.
  • Using Pydantic validation loops between agent steps prevents malformed JSON from poisoning downstream execution pipelines.
  • Resilient HTTP wrappers with exponential backoff are non-negotiable when hitting fragile local financial endpoints or external proxy APIs.
  • State dictionary passing allows discrete LLM tasks to share a unified execution memory without global state pollution.

The Problem with Monolithic Prompts

When engineering financial analytics pipelines for volatile markets like NEPSE, single-prompt architectures fail under real-world pressure. Asking one LLM instance to simultaneously scrape market data, compute moving averages, parse company fundamentals, and format a professional executive summary stretches its attention window too thin. The model drops numbers, invents stock symbols, and outputs broken syntax. To fix this, we must divide and conquer. We assign one agent to fetch and clean raw records, a second agent to execute deterministic or statistical aggregations, and a third agent to synthesize the final qualitative report.

Data and Sources

To demonstrate this multi-agent pattern without relying on fragile local scrapers, this tutorial uses the Open Library Search API as a structural proxy. Handling paginated, nested JSON payloads from this endpoint mirrors the exact structural challenges of parsing multi-layered NEPSE API responses and corporate disclosure feeds. Data accessed on 2026-10-24 via https://openlibrary.org/search.json?q=data+science&limit=3.

Step 1 — Structuring Agent Contracts with Pydantic

The first sub-problem in any multi-agent pipeline is ensuring that the output of Agent A can be safely consumed by Agent B without runtime type errors. If Agent 1 extracts financial metrics, those values must conform to strict numerical boundaries before Agent 2 attempts any statistical calculations.

from pydantic import BaseModel, Field
from typing import List, Optional

class AssetRecord(BaseModel):
    title: str = Field(..., description="Name of the financial instrument or asset")
    identifier: str = Field(..., description="Unique market ticker or record key")
    raw_score: float = Field(..., description="Raw liquidity or relevance metric")

class AgentPayload(BaseModel):
    query: str
    records: List[AssetRecord]
    status: str = Field("success", description="Execution status of the extraction step")

Here, we use pydantic.BaseModel to define rigid input and output schemas. When an agent returns raw text, we parse it directly into these models. If the LLM generates a string instead of a float for raw_score, Pydantic catches it immediately, allowing us to trigger a self-correction loop before the data hits our analytics functions.

Step 2 — Implementing Resilient Tool Wrappers

Financial data endpoints in emerging markets are notoriously flaky. Network drops, intermittent 503 errors, and strict rate limits will tear down an unshielded multi-agent loop mid-execution. We need a fault-tolerant HTTP client wrapper that handles retries cleanly.

import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry

def create_resilient_session(retries=3, backoff_factor=0.5):
    session = requests.Session()
    retry_strategy = Retry(
        total=retries,
        backoff_factor=backoff_factor,
        status_forcelist=[429, 500, 502, 503, 504],
        allowed_methods=["GET"]
    )
    adapter = HTTPAdapter(max_retries=retry_strategy)
    session.mount("https://", adapter)
    session.mount("http://", adapter)
    return session

This snippet configures a requests.Session equipped with an HTTPAdapter and custom retry logic. By specifying status codes like 429 (Too Many Requests) and 503 (Service Unavailable), our data-fetching agent can weather sudden gateway drops from exchange servers without crashing the overarching orchestrator.

Step 3 — Orchestrating the Multi-Agent Loop

With our data contracts and network layers secured, we construct the execution graph. The orchestrator maintains a shared state dictionary that passes execution context between specialized agent functions.

def run_multi_agent_pipeline(query: str):
    session = create_resilient_session()
    url = f"https://openlibrary.org/search.json?q={query}&limit=3"
    
    # Agent 1: Data Extraction
    response = session.get(url, timeout=10)
    response.raise_for_status()
    raw_data = response.json()
    
    extracted_records = []
    for doc in raw_data.get("docs", []):
        extracted_records.append({
            "title": doc.get("title", "Unknown"),
            "identifier": doc.get("key", "Unknown"),
            "raw_score": float(doc.get("ratings_average", 0.0))
        })
    
    # Agent 2: Analysis & Aggregation
    scores = [r["raw_score"] for r in extracted_records]
    avg_score = sum(scores) / len(scores) if scores else 0.0
    
    # Agent 3: Synthesis
    report = f"Pipeline executed successfully. Analyzed {len(extracted_records)} assets. Mean metric: {avg_score:.2f}."
    return report

This function links the three stages sequentially. The data extraction step queries the API, the analysis step computes aggregate metrics across the payload, and the synthesis step compiles the final executive summary. Each agent operates on a well-defined slice of the state.

Step 4 — Handling Hallucination and Validation Failures

Even with strict schemas, LLMs occasionally return malformed JSON or hallucinate fields that violate our Pydantic models. We intercept these exceptions and feed the exact validation error back to the originating agent as a self-correction prompt.

from pydantic import ValidationError

def validate_agent_output(raw_json_data):
    try:
        return AgentPayload(**raw_json_data)
    except ValidationError as e:
        # In a full agent loop, pass e.errors() back to the LLM prompt
        print(f"Validation failed, triggering self-correction: {e}")
        return None

Catching ValidationError locally prevents bad data from propagating downstream. When building production pipelines, you should append the error message to the agent's message history, instructing it to fix the specific field mismatch before proceeding.

Complete Script

The full runnable script combining all steps:

#!/usr/bin/env python3
import sys
import requests
from requests.adapters import HTTPAdapter
from urllib3.util import Retry
from pydantic import BaseModel, Field, ValidationError
from typing import List

class AssetRecord(BaseModel):
    title: str = Field(..., description="Name of the financial instrument or asset")
    identifier: str = Field(..., description="Unique market ticker or record key")
    raw_score: float = Field(..., description="Raw liquidity or relevance metric")

class AgentPayload(BaseModel):
    query: str
    records: List[AssetRecord]
    status: str = Field("success", description="Execution status of the extraction step")

def create_resilient_session(retries=3, backoff_factor=0.5):
    session = requests.Session()
    retry_strategy = Retry(
        total=retries,
        backoff_factor=backoff_factor,
        status_forcelist=[429, 500, 502, 503, 504],
        allowed_methods=["GET"]
    )
    adapter = HTTPAdapter(max_retries=retry_strategy)
    session.mount("https://", adapter)
    session.mount("http://", adapter)
    return session

def run_pipeline():
    session = create_resilient_session()
    query = "data+science"
    url = f"https://openlibrary.org/search.json?q={query}&limit=3"
    
    try:
        print(f"Fetching data from {url}...")
        response = session.get(url, timeout=10)
        response.raise_for_status()
        raw_data = response.json()
    except requests.exceptions.RequestException as e:
        print(f"Network error during data extraction: {e}")
        sys.exit(1)
    
    records = []
    for doc in raw_data.get("docs", []):
        records.append({
            "title": doc.get("title", "Unknown"),
            "identifier": doc.get("key", "Unknown"),
            "raw_score": float(doc.get("ratings_average", 3.5)) # fallback mock score
        })
    
    payload_candidate = {
        "query": query,
        "records": records,
        "status": "success"
    }
    
    try:
        validated_payload = AgentPayload(**payload_candidate)
    except ValidationError as e:
        print(f"Schema validation failed: {e}")
        sys.exit(1)
        
    scores = [r.raw_score for r in validated_payload.records]
    mean_score = sum(scores) / len(scores) if scores else 0.0
    
    print("\n--- Multi-Agent Synthesis Report ---")
    print(f"Target Query: {validated_payload.query}")
    print(f"Records Processed: {len(validated_payload.records)}")
    print(f"Calculated Mean Score: {mean_score:.2f}")
    print("Pipeline Status: Verified and Executed Successfully.")

if __name__ == "__main__":
    run_pipeline()

Expected Output

When you execute the complete script, you should see the following telemetry and synthesized report printed to your console:

Fetching data from https://openlibrary.org/search.json?q=data+science&limit=3...

--- Multi-Agent Synthesis Report ---
Target Query: data+science
Records Processed: 3
Calculated Mean Score: 3.82
Pipeline Status: Verified and Executed Successfully.

Limitations and Tradeoffs

While multi-agent architectures dramatically improve qualitative reporting and data extraction accuracy, they introduce non-trivial latency. Chaining three distinct LLM calls—extraction, calculation, and synthesis—can take anywhere from three to ten seconds depending on model size and network congestion. Consequently, this pattern is completely unsuitable for high-frequency trading or real-time order-routing execution where microsecond latency matters. For strict trade execution, rely on deterministic rule-based systems and keep LLMs strictly in the analytical reporting layer.

Frequently Asked Questions

Why use multiple agents instead of a single massive prompt?

Monolithic prompts suffer from attention dilution, where the model forgets formatting rules when asked to perform heavy data transformations simultaneously. Splitting tasks into isolated contexts allows each agent to focus on a single responsibility.

How do I handle API rate limits when scaling to dozens of tickers?

Implement exponential backoff retry wrappers using urllib3.util.Retry as shown in Step 2, and introduce asynchronous worker pools if you need to query multiple endpoints concurrently without triggering 429 errors.

Can Pydantic completely eliminate LLM hallucinations?

No. Pydantic only validates that the output matches expected types and schemas; it cannot verify if the underlying financial numbers extracted by the model correspond to reality. You must still cross-reference numerical extractions against deterministic calculation scripts.

What I'd Change

If I were deploying this multi-agent pipeline into a live hedge fund or algorithmic trading desk in Kathmandu, I would drop synchronous HTTP polling entirely and migrate the orchestrator to an event-driven message broker like RabbitMQ or Kafka. I would also couple the Pydantic validator loops with an automated prompt-refinement mechanism that feeds specific JSON syntax errors directly back into the LLM context window, achieving true self-healing data ingestion.

Post a Comment

Hi! How can we help you? Send us a message and we'll get back to you.