Estimated time: 3–4 hours
You have learned chaining, parallelization, tool calling, evaluator-optimizer, and LLM routers. This project wires them all together into a single production-shaped pipeline with no framework — raw API calls and Pydantic only.
The pipeline accepts one or more raw text documents, runs them through a series of AI steps, and emits a structured intelligence package for each document.
Each document passes through these steps in order:
langdetect package (pure code — no LLM)output/<filename>.json containing the full intelligence packageSteps 1–5 run in sequence per document. Multiple documents are processed in parallel using asyncio.gather(), capped at 5 concurrent documents.
DocumentIntelligence is the pipeline state object — it is created in Step 1 (Ingest) and passed through every subsequent step, with each step filling in more fields. The final object is serialised to JSON in Step 5 (Emit).
Entities is a nested model inside DocumentIntelligence. Define it first, then reference it as the type of the entities field. The printed output at the end is just the single DocumentIntelligence JSON — not a separate Entities object.
from pydantic import BaseModel
from typing import Optional
class Entities(BaseModel): # nested — used as a field type below
people: list[str]
organisations: list[str]
dates: list[str]
monetary_amounts: list[str]
class DocumentIntelligence(BaseModel): # pipeline state — populated step by step
filename: str
word_count: int
language: str
document_type: str # filled by Step 3 (router)
entities: Entities # filled by Step 2 (extract) — Entities is nested here
summary: str # filled by Step 4 (summarise, evaluator-approved)
summary_score: int # evaluator's final score (1–10)
key_points: list[str]
risk_flags: list[str] # empty list if none detected
processing_time_s: float
# Process one document
uv run pipeline.py docs/contract.txt
# Process a whole folder in parallel
uv run pipeline.py docs/
# Show a summary table instead of JSON files
uv run pipeline.py docs/ --report
Report output example:
╔══════════════════════╦══════════════╦═══════╦═══════╦═══════════╗
║ File ║ Type ║ Words ║ Score ║ Time ║
╠══════════════════════╬══════════════╬═══════╬═══════╬═══════════╣
║ contract.txt ║ legal ║ 3,241 ║ 8/10 ║ 6.2s ║
║ q3_report.txt ║ financial ║ 1,847 ║ 9/10 ║ 4.8s ║
║ support_email.txt ║ general ║ 312 ║ 8/10 ║ 3.1s ║
╚══════════════════════╩══════════════╩═══════╩═══════╩═══════════╝
Total: 3 docs | 14.1s parallel (vs ~24s sequential)
asyncio.gather() and a semaphore of 5output/ directory (created automatically)output/<filename>.error.json rather than crashing the batchpipeline_costs.jsonlasync def process_document(path).asyncio.gather() batch runner.--report table output.uv run pipeline.py docs/contract.txt writes a valid JSON output fileDocumentIntelligence schema.error.json without stopping the restpipeline_costs.jsonl accumulates one entry per document--report prints a formatted tableFinished this project? Share your GitHub repo link in the discussion below. Browse what others built to get inspiration and give feedback.