LogiMind
Multi-agent RAG over public logistics operational documentation
Multi-agent RAG system that answers natural-language questions about DHL’s public operational documents — rate guides, customs rules, packing requirements, prohibited items, incoterms, and sustainability reporting — or checks a simulated shipment’s tracking status. Out-of-scope questions are refused rather than answered from general knowledge: the system only speaks from what it actually retrieved.
Live demo: ui-production-b575.up.railway.app (rate-limited to 10 requests/minute)
Architecture
A one-time batch ingestion pipeline turns 14 PDFs (874 pages) into 5,133 chunks, embeds them, and writes them to both a vector store and a keyword index:
flowchart LR
A[sources.py] --> B[downloader.py]
B --> C["loader.py (PyMuPDF)"]
C --> D[chunker.py]
D --> E["embedder.py (OpenAI)"]
E --> F[store.py]
F --> G[(Qdrant)]
F --> H[(SQLite)]
At query time, a fixed three-agent pipeline runs — no open-ended agent-to-agent conversation:
flowchart LR
U[User] --> UI[Streamlit]
UI --> API["FastAPI /query"]
API --> O[Orchestrator]
O --> P["PlannerAgent (Claude)"]
P --> R["RetrieverAgent (deterministic)"]
R -->|knowledge search| H[hybrid.py]
H --> G[(Qdrant)]
H --> S[(SQLite / BM25)]
R -->|tracking lookup| T[tracking tool]
R --> Resp["ResponseAgent (Claude)"]
Resp --> API
Key decisions:
- Hybrid retrieval, not just one method. Qdrant vector search and BM25 keyword search run independently, get deduplicated, then re-ranked with a cross-encoder (
ms-marco-MiniLM-L-6-v2) — the two methods surface genuinely different useful chunks on the same query. - RetrieverAgent is deliberately not LLM-backed. PlannerAgent and ResponseAgent are Claude-backed AutoGen agents; the retriever is plain deterministic Python, since the planner’s decision already fully determines what needs to run.
- Fixed orchestration over AutoGen group chat. The orchestrator calls all three agents in a fixed sequence rather than an open-ended conversation, since there’s no genuine agent-to-agent negotiation to manage.
- Own evaluation loop instead of the
ragaslibrary. RAGAS’s metric definitions (faithfulness, answer relevancy) are reimplemented directly against the OpenAI SDK in ~30 lines, becauseragasonly imports through an old LangChain chain that conflicts with the numpy version required by sentence-transformers/scipy elsewhere in the stack. - Two Docker images, not one. The API image carries the full ML stack (CPU-only torch build); the UI image is just an HTTP client (
httpx+streamlit) — deployed as two separate services on Railway.
Results
Measured on 13 real queries through the full pipeline (Claude/Qdrant/OpenAI backends, the same ones the deployment uses), spanning every document category, a tracking lookup, a combined tracking+knowledge query, and an out-of-scope refusal:
| Step | Avg latency | Avg cost |
|---|---|---|
| PlannerAgent (Claude) | 1.9s | $0.0024 |
| RetrieverAgent (deterministic) | 1.2s | — |
| ResponseAgent (Claude) | 6.3s | $0.0255 |
| Total per query | ~9.4s | ~$0.028 |
Answer quality, scored by the eval loop above: 0.98 average faithfulness (answer claims checked against retrieved context, across the 11 queries with checkable claims) and 0.68 average answer relevancy across all 13. n=13 is enough to sanity-check the pipeline’s real cost/latency/quality profile, not a statistically rigorous benchmark.
Stack
Python 3.12, AutoGen, LangChain, Qdrant, BM25s, sentence-transformers, LangSmith, FastAPI, Streamlit, Docker, GitHub Actions, deployed on Railway.
Status
Ingestion, hybrid retrieval, the agent pipeline, monitoring (latency/cost tracking, faithfulness/relevancy evaluation), API/UI, Docker, CI, and deployment are all done — 108 tests, all external calls mocked. Still open: a curated ground-truth test set for retrieval-only quality scoring (context precision/recall).