Agentic RAG System

A document question-answering pipeline built from scratch in Python — raw file in, grounded cited answer out, with charts when the data supports one.

python openai flask rag graph agents embeddings fastapi

Most RAG demos do three things: embed some text, run a similarity search, and paste the top chunks into a prompt. That works fine until the answer requires reasoning across multiple documents, the relevant content is spread across different source types, or the question is quantitative and a chart would actually be more useful than prose.

This system adds a graph reasoning layer between retrieval and the LLM. Retrieved passages become typed nodes — facts, data points, definitions, procedures — connected by weighted edges based on keyword overlap, source proximity, and relevance score. A union-find algorithm clusters them into topic threads, which become the labeled sections in the final prompt. The model only sees its cluster's context and is explicitly blocked from going off-document.

The result is an answer that's traceable to source, structured around the actual topic shape of the documents, and accompanied by a chart whenever the retrieved data supports one.

Grounding as a hard constraint. The prompt explicitly tells the model to use only the retrieved context and to say so when it can't answer. This isn't a guardrail bolted on after the fact — it's the core discipline the whole pipeline is built around.

Graph over straight retrieval. A flat top-k retrieval collapses everything into one blob. The graph layer preserves the topic structure of the source documents, so the LLM gets organized sections rather than a random pile of passages.

One API key, two stages. The same OpenAI key covers embedding and completion, but they're kept separate in code so swapping providers for either stage stays a small, isolated change.

Zero heavy dependencies where possible. The cosine similarity math is written by hand. The knowledge base is JSON. The server is plain Flask. The goal was a system you can read, understand, and extend without a library archaeology expedition.

Agentic RAG — Multimodal

Extends the agentic RAG pipeline from Project 01 to ingest video, audio, images, and web pages — everything normalized to text and routed through the same graph reasoning core, with reranking, sentiment rollups, and SSRF-hardened URL fetching added on top.

python openai whisper multimodal rag graph agents sentiment analysis

The original system only accepted PDFs and CSVs. This version drops that restriction: a URL, a video file, a podcast, or an image can all go into the same ingest call as a document, and every source gets normalized down to plain text before it ever reaches the chunker. Video and audio are transcribed with per-segment timestamps, so an answer can point back to the exact moment in a recording it came from. Images are captioned and OCR'd through a vision model. Web pages are scraped through a layered set of extraction backends, guarded against SSRF so a malicious URL can't be used to reach internal infrastructure.

Retrieval quality also moved up a level. Instead of taking the top-k cosine matches at face value, the system over-fetches a wider candidate pool and reranks it with a cross-encoder, which scores query and chunk jointly rather than comparing two independently embedded vectors. A graph-walk expansion step then pulls in any lower-ranked candidate that's strongly connected to the top-k set, so relevant context that didn't rank directly on its own can still make it into the answer.

Answers are checked claim-by-claim against the retrieved context, with unsupported claims flagged on the response rather than silently passed through. An optional sentiment pass scores chunks in batched LLM calls at ingest time and rolls up into a summary alongside the answer — useful for questions like "what's the overall sentiment toward the new policy" across a mixed set of articles, transcripts, and comments.

One text path for every source. Video, audio, images, and web pages are all normalized to plain text before chunking, so retrieval, clustering, and synthesis are completely unchanged from Project 01 — the graph reasoning layer doesn't know or care whether a chunk originated from a PDF or a YouTube transcript.

Lazy imports, graceful degradation. Heavy dependencies (yt-dlp, whisper, Pillow) are imported inside the methods that need them, not at module load. An environment missing one can still route documents and web pages; the affected ingester raises a specific named error only when it's actually called.

Rerank, then walk the graph. A flat top-k cutoff after cosine search throws away context that's relevant but didn't rank in the first pass. Reranking improves the ordering; graph-walk expansion recovers connected context the ranking alone would have dropped.

SSRF isn't a one-time check. The initial URL validation resolves and checks the hostname, but every backend re-resolves it again when it actually connects. Treating that as a real gap — not a theoretical one — is what the second check at connect time is for.

Coming soon
Next project in progress.