Case Study

How We Built a Multi-Agent RFP Analysis Pipeline with LangGraph

By Expedey Engineering · Updated · 3 min read

Short answer

We built a pipeline for a government-contracting software platform that turns a solicitation package into a checklist: every required upload and written answer, with deadlines, compared against what the bidder already has. LangGraph runs the workflow; small, focused agents built with the OpenAI Agents SDK do the reading, merging and gap analysis inside it.

The problem: solicitation packages are long and scattered

A single grant or contract opportunity can come with a solicitation document, several attachments, standard forms, spreadsheets and sometimes a ZIP of everything, including scanned PDFs. Somewhere in that pile are the things a bid team must not miss: each file to upload, each certification, each narrative question and its deadline.

Our client, a government-contracting software platform, wanted that list produced automatically, then compared with each bidder's existing profile so the platform could ask only for what was missing. Client and data details are withheld under NDA.

The architecture at a glance

  1. Jobs arrive on queues. Each job type (new opportunity, requirements gathering, gap analysis, proposal generation, document upload) has its own AWS SQS queue, with retries and a dead-letter queue. A router sends each message to the right processor.
  2. Download and extract. Linked documents are downloaded and converted to text: PDFs with PyMuPDF, plus Word, Excel, HTML and ZIP archives. When a PDF has no text layer, Tesseract OCR takes over.
  3. Chunk. Long documents are split on sentence boundaries into chunks of up to about 50,000 characters.
  4. Extract (one agent per chunk). An extraction agent pulls out two kinds of requirement: uploadable (files, forms, certifications, every "Attachment N" or "Form X") and text (narrative answers), each with a deadline and notes.
  5. Compose. A composer agent merges the per-chunk lists, removes duplicates and splits the result into upload requirements and writing guidelines.
  6. Find gaps. A gap-analysis agent compares the requirements with the bidder's profile (answers, uploaded files, certifications) and lists what's missing, with the kind of input needed: a file, text, a checkbox or a certification.
  7. Draft. A proposal agent drafts sections from the profile and the requirements.

Results are stored in MongoDB and files in S3. The service is Python with FastAPI.

The workflow graph

LangGraph holds the steps together. Each node is a function that reads the shared state and returns the fields it changed; edges fix the order. A simplified version of the requirements-gathering graph:

from typing import TypedDict
from langgraph.graph import StateGraph, START, END

class RFPState(TypedDict):
    resource_links: list[str]
    chunks: list[str]
    extracted: list[list[dict]]   # one list of requirements per chunk
    requirements: list[dict]      # merged and de-duplicated
    errors: list[str]

graph = StateGraph(RFPState)
graph.add_node("download_and_extract", download_and_extract)
graph.add_node("extract_requirements", extract_requirements)
graph.add_node("compose", compose)
graph.add_edge(START, "download_and_extract")
graph.add_edge("download_and_extract", "extract_requirements")
graph.add_edge("extract_requirements", "compose")
graph.add_edge("compose", END)
app = graph.compile()
Simplified for illustration; the production graph also loads the opportunity and organisation data.

Seven design decisions that made it reliable

  1. Map, then reduce. Extract from each chunk separately, then compose once. Every extraction call sees a manageable amount of text, and the composer sees only structured requirements, not raw documents.
  2. Hand the model its IDs. We pre-generate requirement IDs and pass them in instead of letting the model invent them. Later steps, such as gap analysis, refer to requirements by ID, so the IDs must be stable.
  3. A schema at every boundary. Each agent's output is validated against a Pydantic model, so a malformed result is caught at the step that produced it, not three steps later.
  4. Errors are state, not exceptions. Each node appends problems to an errors list and carries on. One unreadable attachment doesn't lose the other twenty.
  5. A small model for narrow work. Extraction is repetitive and well defined, so it runs on a small, inexpensive model (gpt-4o-mini).
  6. Queues for long-running jobs. Processing a package can take minutes. Queues decouple it from the web request, retry transient failures and park repeated failures in a dead-letter queue for inspection.
  7. OCR only as a fallback. OCR is slow and less accurate, so it runs only when a PDF has no text layer.

What we'd change in the next version

  • Structured outputs from the SDK. Declaring an output_type on each agent lets the SDK return a validated object, instead of parsing JSON out of free text.
  • Concurrent chunk processing. Chunks are independent, so they can run in parallel with a concurrency limit.
  • A persistent checkpointer. In-memory checkpoints don't survive a restart; a database-backed one lets a crashed run resume where it stopped.
  • An evaluation set. A handful of real solicitation packages with hand-labelled requirements, scored on every prompt or model change, turns "seems to work" into a recall number.

Frequently Asked Questions

Why not send the whole RFP to the model in one prompt?

Packages are often too long for one call, cost grows with every token, and recall tends to drop on very long inputs. Extracting per chunk and composing afterwards keeps each call focused and lets each step's output be validated.

How do you handle scanned PDFs?

Text is extracted directly when the PDF has a text layer. When it doesn't, the page images go through Tesseract OCR. Running OCR only as a fallback keeps most documents fast and accurate.

Why use LangGraph and the OpenAI Agents SDK together?

LangGraph is good at the workflow: fixed steps, shared state and checkpoints. The Agents SDK is a simple way to define each focused agent. Each tool does the part it's best at.

Drowning in Documents? Let's Talk

Expedey is a team of forward-thinking individuals working and servicing clientele across the globe for their software development requirements.

Our Offices

Contact Us

© 2024 Expedey. All Rights Reserved.