<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: manasviboineypally</title>
    <description>The latest articles on DEV Community by manasviboineypally (@manasviboineypally).</description>
    <link>https://dev.to/manasviboineypally</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4076537%2F57500b0d-2bdc-4dc8-975d-327e26ee9a31.png</url>
      <title>DEV Community: manasviboineypally</title>
      <link>https://dev.to/manasviboineypally</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/manasviboineypally"/>
    <language>en</language>
    <item>
      <title>Building a Multi-Agent AI Pipeline That Ships: LangGraph, RAG, and Evals That Matter</title>
      <dc:creator>manasviboineypally</dc:creator>
      <pubDate>Thu, 13 Aug 2026 17:14:25 +0000</pubDate>
      <link>https://dev.to/manasviboineypally/building-a-multi-agent-ai-pipeline-that-ships-langgraph-rag-and-evals-that-matter-32db</link>
      <guid>https://dev.to/manasviboineypally/building-a-multi-agent-ai-pipeline-that-ships-langgraph-rag-and-evals-that-matter-32db</guid>
      <description>&lt;p&gt;I spent 18 days building an AI product that converts research papers into audience-tailored PowerPoint presentations. Not a toy — a real deployed thing at &lt;a href="https://web-production-6eded.up.railway.app" rel="noopener noreferrer"&gt;doc2slides on Railway&lt;/a&gt; that anyone can use.&lt;/p&gt;

&lt;p&gt;The interesting parts weren't the "make it work" moments. They were the tradeoffs I had to make honestly, and the times I resisted the temptation to add a "clever" fix that would have made things worse.&lt;/p&gt;

&lt;p&gt;This post is about those decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Doc2Slides&lt;/strong&gt; takes a PDF and produces a &lt;code&gt;.pptx&lt;/code&gt; file tailored to four audiences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Kid&lt;/strong&gt; — fun analogies, simple words&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Student&lt;/strong&gt; — educational, terms defined&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineer&lt;/strong&gt; — technical depth, assumes domain knowledge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Executive&lt;/strong&gt; — business focus, impact-oriented&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The magic is that &lt;strong&gt;the same paper produces radically different output&lt;/strong&gt; based on the audience. A compiler theory paper for a kid becomes "compilers are like magic helpers." The same paper for an executive becomes "advancing compiler technology with formal frameworks."&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/manasviboineypally/doc2slides" rel="noopener noreferrer"&gt;github.com/manasviboineypally/doc2slides&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture: 5 agents in LangGraph
&lt;/h2&gt;

&lt;p&gt;I built this as a multi-agent pipeline instead of one giant LLM prompt. Here's the flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PDF Upload
    ↓
Parser        → extracts sections + metadata
    ↓
Summarizer    → RAG-based section summarization
    ↓
Planner       → designs slide structure for audience
    ↓
Writer        → generates audience-adaptive slide content
    ↓
Builder       → produces editable .pptx file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each agent is an independent node in a LangGraph state machine. They share a &lt;code&gt;TypedDict&lt;/code&gt; state and read/write specific fields.&lt;/p&gt;

&lt;p&gt;Here's what the graph definition actually looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;langgraph.graph&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.agents.state&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AgentState&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.agents.parser&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;parser_agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.agents.summarizer&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;summarizer_agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.agents.planner&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;planner_agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.agents.writer&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;writer_agent&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app.agents.builder&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;builder_agent&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_pipeline&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;StateGraph&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AgentState&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parser_agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summarizer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;summarizer_agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;planner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;planner_agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;writer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;writer_agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;builder_agent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_entry_point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summarizer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;summarizer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;planner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;planner&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;writer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;writer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_edge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;graph&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why LangGraph over a sequential chain?&lt;/strong&gt; Adding a new agent is a 2-line change to the graph. In a sequential chain, adding a new step often means refactoring the previous ones. State-based multi-agent design scales better.&lt;/p&gt;




&lt;h2&gt;
  
  
  The interesting tradeoff #1: My RAG top-1 precision is 42%
&lt;/h2&gt;

&lt;p&gt;I built an evaluation harness because I wanted to measure quality, not just claim it. Three eval types:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Parser evals&lt;/strong&gt; — deterministic ground-truth assertions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RAG evals&lt;/strong&gt; — top-K precision on hand-labeled query→section pairs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Summarizer evals&lt;/strong&gt; — LLM-as-judge scoring faithfulness, completeness, clarity&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The parser evals scored 100% (34/34 checks). The summarizer evals averaged 4.4/5.&lt;/p&gt;

&lt;p&gt;But the RAG top-1 precision came in at &lt;strong&gt;42%&lt;/strong&gt;. Only 3 of 7 queries returned the correct section as the top result.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My first instinct:&lt;/strong&gt; hide the number. Report top-3 (57%) instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I did instead:&lt;/strong&gt; publish both numbers and explain why.&lt;/p&gt;

&lt;p&gt;Looking at the failures revealed a real limitation of RAG:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Query: "how does the genetic algorithm work?"&lt;/li&gt;
&lt;li&gt;Expected section: &lt;code&gt;Methodology&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Actual top result: &lt;code&gt;3.6 Stopping Criteria&lt;/code&gt; (a subsection of methodology)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Genetic algorithms are discussed in 6 subsections (3.1 through 3.6). Vector search returns the highest-scoring &lt;em&gt;chunk&lt;/em&gt;, not the highest-scoring &lt;em&gt;section&lt;/em&gt;. For queries about broad topics, subsections often outrank the parent section because they mention the specific term more densely.&lt;/p&gt;

&lt;p&gt;This is a known problem in RAG. Solutions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hierarchical retrieval (search subsections, bubble to parent)&lt;/li&gt;
&lt;li&gt;Query rewriting to be more specific&lt;/li&gt;
&lt;li&gt;Retrieve top-K and let an LLM pick the right section&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are fixed today. But I know exactly what's broken and why — which is more useful than pretending it works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; deterministic metrics beat vibes. Vibes let you convince yourself the AI is smart. Metrics tell you where it's dumb.&lt;/p&gt;




&lt;h2&gt;
  
  
  The interesting tradeoff #2: I refused to use word count as a proxy for content density
&lt;/h2&gt;

&lt;p&gt;Users can request any number of slides between 3 and 50. When the paper's actual content density doesn't match the requested slide count, the LLM either pads shallow sections or compresses dense ones. This creates mild redundancy at high slide counts.&lt;/p&gt;

&lt;p&gt;The obvious fix: allocate slides based on section word count. Long section = more slides. Short section = fewer slides.&lt;/p&gt;

&lt;p&gt;I almost built this. Then I realized: &lt;strong&gt;word count is not content density&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A 100-word section with 3 distinct concepts should get multiple slides&lt;/li&gt;
&lt;li&gt;A 2000-word section rambling around one idea should get one slide&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Word count would systematically reward verbose sections and penalize concise ones. That's not a fix — it's a bug with math.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I did instead:&lt;/strong&gt; documented the tradeoff and shipped without the heuristic. From the project's &lt;code&gt;testing_notes.md&lt;/code&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rejected quick fix:&lt;/strong&gt; using section word count as a proxy for content density. Word count is not density — a short section may contain multiple distinct ideas while a long section may ramble around one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Proper solution deferred:&lt;/strong&gt; content-aware slide allocation with LLM judgment, verified by an evaluation harness that measures output quality against ground truth. Requires infrastructure work not appropriate for the initial version.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; the right answer to "should I add this heuristic?" is often "no." Heuristics feel like progress. Sometimes they're anti-progress dressed up as pragmatism.&lt;/p&gt;




&lt;h2&gt;
  
  
  The interesting tradeoff #3: SQLite dev → PostgreSQL prod is one variable
&lt;/h2&gt;

&lt;p&gt;I built with local SQLite during development but deployed to Railway with PostgreSQL. The migration was one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/db/session.py
&lt;/span&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_engine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;echo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For local dev, &lt;code&gt;.env&lt;/code&gt; has:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;sqlite:///./doc2slides.db&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Railway, the environment variable is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;postgresql+psycopg2://postgres:xxx@host:5432/railway&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing else changes. SQLAlchemy models are backend-agnostic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is boring engineering.&lt;/strong&gt; But boring engineering is what lets you sleep at night. When someone asks "how do you handle database migrations?" the answer isn't a clever hack — it's "environment-driven configuration and a repository pattern."&lt;/p&gt;




&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Choice&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python 3.13&lt;/td&gt;
&lt;td&gt;AI ecosystem&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;FastAPI&lt;/td&gt;
&lt;td&gt;Async support, auto Swagger docs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orchestration&lt;/td&gt;
&lt;td&gt;LangGraph&lt;/td&gt;
&lt;td&gt;State-based multi-agent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;td&gt;OpenAI GPT-4o-mini&lt;/td&gt;
&lt;td&gt;Cheap enough for iteration, smart enough for structured output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vector DB&lt;/td&gt;
&lt;td&gt;ChromaDB&lt;/td&gt;
&lt;td&gt;Local, no cloud dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured output&lt;/td&gt;
&lt;td&gt;JSON mode + Pydantic&lt;/td&gt;
&lt;td&gt;Two-layer validation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database&lt;/td&gt;
&lt;td&gt;SQLAlchemy + PostgreSQL&lt;/td&gt;
&lt;td&gt;Env-driven, portable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Vanilla HTML/CSS/JS&lt;/td&gt;
&lt;td&gt;No build step, portable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;Railway&lt;/td&gt;
&lt;td&gt;GitHub CI/CD, managed Postgres&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The frontend is worth calling out. I used no framework — just HTML, CSS, and vanilla JavaScript in ~500 lines. Zero build step. Anyone can clone the repo, open the file, and understand it in 5 minutes.&lt;/p&gt;

&lt;p&gt;For an MVP, that's a feature, not a limitation.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I didn't build (and why that's OK)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Skipped:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User authentication&lt;/li&gt;
&lt;li&gt;Multi-tenant workspaces&lt;/li&gt;
&lt;li&gt;Custom presentation templates&lt;/li&gt;
&lt;li&gt;Streaming responses&lt;/li&gt;
&lt;li&gt;Job queue with Celery/Redis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why:&lt;/strong&gt; MVP. Every feature has a cost. Shipping the core value (PDF → audience-tailored slides) matters more than shipping every possible feature.&lt;/p&gt;

&lt;p&gt;For a portfolio project, "I could have added X but chose not to for these reasons" is a stronger answer than "I added X poorly."&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons I'd tell my past self
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Build evals before optimizing.&lt;/strong&gt; I built the pipeline first, then evals. If I had built evals first, I would have known earlier that my RAG had issues. Now I have to make eval-driven improvements Week 3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Resist heuristics.&lt;/strong&gt; Every time I thought "this is a quick fix," it was actually a technical debt I was about to bake in. Word count as density. Silent AI slide count overrides. Boolean status flags instead of proper enums.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Deploy early.&lt;/strong&gt; I deployed on Day 16 of 18. I should have deployed on Day 8. Deployment reveals real bugs — environment variable typos, missing dependencies, hardcoded localhost URLs. The sooner you find them, the cheaper they are.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Document tradeoffs, not features.&lt;/strong&gt; Anyone can read code to know what it does. Almost no one leaves notes on &lt;strong&gt;why&lt;/strong&gt; a design choice was made. My &lt;code&gt;testing_notes.md&lt;/code&gt; file is where most of the actual engineering thinking lives.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;The project is live, but not "done." Future work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Content-aware slide count (with an eval harness measuring output quality)&lt;/li&gt;
&lt;li&gt;Multi-language support for input PDFs&lt;/li&gt;
&lt;li&gt;Custom presentation templates&lt;/li&gt;
&lt;li&gt;Fix RAG for hierarchical sections (subsection → parent bubbling)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to try Doc2Slides yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://web-production-6eded.up.railway.app" rel="noopener noreferrer"&gt;web-production-6eded.up.railway.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/manasviboineypally/doc2slides" rel="noopener noreferrer"&gt;github.com/manasviboineypally/doc2slides&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;60-second video demo:&lt;/strong&gt; &lt;a href="https://www.loom.com/share/693e5b567f284af99dc86286b33a4b66" rel="noopener noreferrer"&gt;Loom link&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Upload any PDF, pick your audience, get back a deck. Same paper, radically different output depending on who you say you're presenting to.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Author:&lt;/strong&gt; Manasvi Boineypally — &lt;a href="https://github.com/manasviboineypally" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; · &lt;a href="https://www.linkedin.com/in/manasviboineypally" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>langchain</category>
      <category>rag</category>
    </item>
  </channel>
</rss>
