<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Practical AI Engineering]]></title><description><![CDATA[Practical AI engineering tutorials — chatbots, RAG pipelines, LLM integration, and workflow automation. By a published AI researcher with production experience.]]></description><link>https://sammishra.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6ad99c302f0da22ce9049266/1794cc02-36bf-4160-9f84-42cf73a7dfdd.png</url><title>Practical AI Engineering</title><link>https://sammishra.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 27 Oct 2026 11:29:52 GMT</lastBuildDate><atom:link href="https://sammishra.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Automate 5 Business Workflows with AI and Python (Free Stack, Full Code)]]></title><description><![CDATA[Build AI automations in Python: email triage with drafted replies, document summaries, invoice extraction, and more — on a free API. Full code.]]></description><link>https://sammishra.hashnode.dev/automate-5-business-workflows-with-ai-and-python-free-stack-full-code</link><guid isPermaLink="true">https://sammishra.hashnode.dev/automate-5-business-workflows-with-ai-and-python-free-stack-full-code</guid><category><![CDATA[Python]]></category><category><![CDATA[AI]]></category><category><![CDATA[automation]]></category><category><![CDATA[Tutorial]]></category><category><![CDATA[Productivity]]></category><dc:creator><![CDATA[Sammishra Kh]]></dc:creator><pubDate>Sat, 24 Oct 2026 07:17:44 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad99c302f0da22ce9049266/36fcac37-f4a5-4a28-b2de-18f640e7bca8.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every small business runs on the same busywork: sorting email, summarizing documents, rewriting content for different channels, typing invoice numbers into spreadsheets, and turning meeting chaos into action items. None of it needs a human anymore.</p>
<p>In this guide I'll show you the pattern behind AI workflow automation, build one complete tool with you — an <strong>email classifier that triages your inbox and drafts replies</strong> — and then show you how the same pattern handles four more workflows. Everything runs on a free API. Total cost: <strong>₹0 / $0</strong>.</p>
<h2>The pattern behind every AI automation</h2>
<p>Every tool in this post is the same three moves:</p>
<ol>
<li><strong>Read</strong> — load files (emails, PDFs, notes) with plain Python</li>
<li><strong>Ask</strong> — send the content to an LLM with a carefully written instruction</li>
<li><strong>Write</strong> — save the structured answer somewhere useful (CSV, Markdown, text files)</li>
</ol>
<p>No frameworks, no servers, no "agent platforms." A script, a prompt, and a loop. Once you've built one, you can build a dozen.</p>
<h2>Prerequisites</h2>
<ul>
<li>Python 3.10+</li>
<li>A free API key from <a href="https://console.groq.com">console.groq.com</a> (their free tier runs fast open models — perfect for this)</li>
</ul>
<pre><code class="language-bash">pip install openai python-dotenv
</code></pre>
<p>We use the <code>openai</code> library pointed at Groq — same API, zero cost.</p>
<h2>Build it: the email triage tool</h2>
<p>Point this script at a folder of emails (as <code>.txt</code> files). For each one it decides the category, judges urgency, writes a one-line summary, and drafts a reply. You get a CSV you can open in Excel, plus a ready-to-edit reply for every email.</p>
<p>Create <code>email_classifier.py</code>:</p>
<pre><code class="language-python">"""Email Classifier &amp; Reply Drafter.

Usage:  python email_classifier.py emails_folder --out results
"""

import argparse
import csv
import json
import os
from pathlib import Path

from dotenv import load_dotenv
from openai import OpenAI

load_dotenv()

client = OpenAI(
    api_key=os.getenv("LLM_API_KEY"),
    base_url=os.getenv("LLM_BASE_URL") or None,
)
MODEL = os.getenv("LLM_MODEL", "openai/gpt-oss-20b")

CATEGORIES = [
    "sales_inquiry", "support_request", "billing_question",
    "complaint", "partnership", "spam_or_irrelevant", "other",
]

SYSTEM = f"""You are an email triage assistant for a small business.
Classify the email, judge its urgency, and draft a short, professional,
friendly reply. Categories: {', '.join(CATEGORIES)}.
Return JSON: {{"category": "...", "urgency": "high|medium|low",
"summary": "one line", "draft_reply": "the reply text"}}
Respond with valid JSON only."""


def ask_json(email_text: str) -&gt; dict:
    resp = client.chat.completions.create(
        model=MODEL,
        messages=[
            {"role": "system", "content": SYSTEM},
            {"role": "user", "content": f"Email:\n\n{email_text}"},
        ],
        response_format={"type": "json_object"},
        max_tokens=800,
        temperature=0.2,
    )
    return json.loads(resp.choices[0].message.content)


def process(folder: Path, out_dir: Path) -&gt; None:
    out_dir.mkdir(parents=True, exist_ok=True)
    rows = []
    for path in sorted(folder.glob("*.txt")):
        print(f"Processing {path.name} ...")
        result = ask_json(path.read_text(encoding="utf-8")[:6000])
        rows.append({
            "file": path.name,
            "category": result.get("category", "other"),
            "urgency": result.get("urgency", "medium"),
            "summary": result.get("summary", ""),
        })
        reply = result.get("draft_reply", "")
        (out_dir / f"reply_{path.stem}.txt").write_text(reply, encoding="utf-8")

    with open(out_dir / "email_triage.csv", "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=["file", "category", "urgency", "summary"])
        writer.writeheader()
        writer.writerows(rows)
    print(f"Done: {out_dir}/email_triage.csv + draft replies")


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("folder", type=Path)
    ap.add_argument("--out", type=Path, default=Path("results"))
    args = ap.parse_args()
    process(args.folder, args.out)
</code></pre>
<p>Create a <code>.env</code> file next to it:</p>
<pre><code class="language-plaintext">LLM_API_KEY=your-groq-key
LLM_BASE_URL=https://api.groq.com/openai/v1
LLM_MODEL=openai/gpt-oss-20b
</code></pre>
<p>Run it:</p>
<pre><code class="language-bash">python email_classifier.py my_emails --out results
</code></pre>
<p>When I tested this on three sample emails — a bulk-order inquiry, an angry damaged-order complaint, and a confused double-charge question — it categorized all three correctly, flagged only the complaint as high-urgency, and drafted a reply to the angry customer that apologized, asked for a photo, and offered a replacement or refund. A reply I'd actually send.</p>
<h3>The two details that make it reliable</h3>
<p><strong>JSON mode.</strong> <code>response_format={"type": "json_object"}</code> forces the model to return parseable JSON instead of chatty prose. Structured output is what separates automation from a toy.</p>
<p><strong>A strict system prompt.</strong> Notice the prompt lists the exact categories and the exact JSON shape. Vague prompts produce vague data. When you want different categories — "wholesale," "press," "refund" — you edit one line of English, not code.</p>
<h2>The other four workflows (same pattern)</h2>
<p><strong>2. Document summarizer.</strong> Read a PDF with <code>pypdf</code>, send the text, ask for: executive summary, key points, action items. For long documents, split into ~9,000-character chunks, summarize each, then summarize the summaries. Output: a one-page Markdown brief.</p>
<p><strong>3. Content repurposer.</strong> One blog post in, four outputs out — a LinkedIn post, an X thread, an Instagram caption, a newsletter blurb. The whole tool is one prompt describing each format's rules ("first tweet is a hook, each under 260 characters…").</p>
<p><strong>4. Invoice → spreadsheet.</strong> The highest-ROI one for most businesses. Same JSON-mode trick: ask for <code>vendor, invoice_number, date, subtotal, tax, total</code>, append each result to a CSV. A folder of invoices becomes accounting-ready data in one command. One rule matters in the prompt: <em>"Never guess amounts — copy them exactly from the document."</em></p>
<p><strong>5. Meeting notes → action items.</strong> Paste your messy notes in, get decisions, an action-item table with owners and deadlines, and open questions out. The prompt's key rule: <em>"Only include what is actually in the notes — never invent owners or dates."</em> An AI that admits what it doesn't know is the difference between useful and dangerous.</p>
<h2>What this costs to run</h2>
<p>On Groq's free tier: nothing. On paid APIs: each email or invoice costs a fraction of a cent, because these scripts send only what's needed and cap the output length. Automating a hundred emails costs less than the electricity to read them.</p>
<h2>Common issues</h2>
<p><code>model_not_found</code> — providers retire model names; check your provider's current list and update <code>.env</code>.</p>
<p><strong>Invalid JSON errors</strong> — some models occasionally wrap JSON in code fences. Strip backticks before parsing, or use a model that supports strict JSON mode.</p>
<p><strong>Garbage from scanned PDFs</strong> — <code>pypdf</code> extracts <em>digital</em> text. Scanned-image PDFs need OCR first (or skip them — the script should warn, not crash).</p>
<h2>Want all five, ready to run?</h2>
<p>I packaged all five tools — tested, documented, with sample inputs so each produces results in your first minute, and every behavior customizable by editing plain-English prompts:</p>
<p><a href="https://sammishrakh.gumroad.com/l/ppwnx"><strong>AI Workflow Automation Pack — 5 Ready-to-Run Python Tools ($19)</strong></a></p>
<hr />
<p><em>I'm Sammishra — an AI engineer and published researcher. I build chatbots, RAG pipelines, and custom automations for businesses. See my work at</em> <a href="https://sammishra.hashnode.dev/work-with-me"><em><strong>sammishra.hashnode.dev/work-with-me</strong></em></a><em>, or email me at</em> <a href="mailto:sammishrakh.freelancer@gmail.com"><em>sammishrakh.freelancer@gmail.com</em></a><em>.</em></p>
]]></content:encoded></item><item><title><![CDATA[Build a Free AI Support Chatbot with FastAPI, Groq, and Vercel (Complete Guide)]]></title><description><![CDATA[Build an AI support chatbot that answers only from your docs — FastAPI, RAG, Groq’s free tier, deployed free on Vercel. Full working code.]]></description><link>https://sammishra.hashnode.dev/build-a-free-ai-support-chatbot-with-fastapi-groq-and-vercel-complete-guide</link><guid isPermaLink="true">https://sammishra.hashnode.dev/build-a-free-ai-support-chatbot-with-fastapi-groq-and-vercel-complete-guide</guid><category><![CDATA[AI]]></category><category><![CDATA[Python 3]]></category><category><![CDATA[FastAPI]]></category><category><![CDATA[#chatbots]]></category><category><![CDATA[Tutorial]]></category><dc:creator><![CDATA[Sammishra Kh]]></dc:creator><pubDate>Fri, 23 Oct 2026 07:06:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6ad99c302f0da22ce9049266/3cb373a4-9a82-4ac3-9b77-73edced44334.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>If you've wanted to add an AI chatbot to a website but assumed it needs a paid OpenAI account, a vector database cluster, and a weekend of DevOps — this guide is for you. We'll build a customer support chatbot that answers questions <strong>only from your own documents</strong>, refuses to make things up, and deploys to a public URL. Total cost: <strong>₹0 / $0</strong>.</p>
<p>Here's the finished thing, live: <a href="https://ai-support-chatbot-orcin.vercel.app"><strong>try the demo</strong></a> — ask it about shipping, then ask it for a pizza menu and watch it politely decline to hallucinate.</p>
<p>What we'll use:</p>
<ul>
<li><strong>FastAPI</strong> — the backend (one file)</li>
<li><strong>RAG (Retrieval-Augmented Generation)</strong> — a lightweight version with no vector database</li>
<li><strong>Groq</strong> — free-tier LLM API, extremely fast</li>
<li><strong>Vercel</strong> — free serverless hosting, deployed from your terminal</li>
</ul>
<p>Time to complete: about 30 minutes.</p>
<h2>Why "answers only from your documents" is the whole product</h2>
<p>A raw LLM will cheerfully invent your refund policy. That's not a chatbot a business can use. The fix is RAG: before answering, we <em>retrieve</em> the most relevant sections of the company's real documentation and tell the model: <strong>answer from these excerpts only — if it's not here, say so.</strong></p>
<p>That one constraint is the difference between a toy and a product.</p>
<h2>Prerequisites</h2>
<ul>
<li>Python 3.10+</li>
<li>Node.js (only for the Vercel CLI at deploy time)</li>
<li>A free account at <a href="https://console.groq.com">console.groq.com</a></li>
</ul>
<h2>Step 1 — Project structure</h2>
<pre><code class="language-plaintext">ai-support-chatbot/
├── api/
│   ├── index.py      # FastAPI app: retrieval + LLM call
│   └── kb.py         # your knowledge base, as a Python string
├── public/
│   └── index.html    # the chat widget
└── requirements.txt
</code></pre>
<p><code>requirements.txt</code> is two lines:</p>
<pre><code class="language-plaintext">fastapi&gt;=0.110
openai&gt;=1.30
</code></pre>
<p>(We use the <code>openai</code> client library, but pointed at Groq — it speaks the same API.)</p>
<h2>Step 2 — The knowledge base</h2>
<p>Create <code>api/kb.py</code>. This is your company's help docs as one markdown string, where every <code>## Heading</code> becomes a retrievable section:</p>
<pre><code class="language-python">"""Knowledge base content, bundled with the serverless function."""

FAQ_MD = '''
# Verdant Coffee Co. — Help Center

## Shipping times and costs
Orders within India arrive in 2–4 business days and shipping is free over
₹999. International orders arrive in 7–14 business days; rates are
calculated at checkout. All orders ship with tracking.

## International shipping
Yes — we ship to over 40 countries, including the US, UK, EU, UAE,
Singapore, and Australia. Customs duties, where applicable, are the
recipient's responsibility.

## Returns and refunds
Coffee is perishable, so we can't accept returns of opened bags. If your
order arrived damaged or incorrect, email us a photo within 14 days and
we'll replace it or refund you in full.

## Contacting human support
Our human support team is available Monday–Saturday, 9:00–18:00 IST at
support@verdantcoffee.example.
'''
</code></pre>
<p>I'm using a fictional coffee company — swap in your own docs. More sections = smarter bot.</p>
<h2>Step 3 — Retrieval without a vector database</h2>
<p>For a knowledge base this size, you don't need embeddings or a vector store. Keyword overlap with one clever trick does the job. Create <code>api/index.py</code> and start with the retrieval engine:</p>
<pre><code class="language-python">import os
import re
from collections import defaultdict

from fastapi import FastAPI, HTTPException, Request
from openai import OpenAI
from pydantic import BaseModel

try:  # Vercel may load this as a top-level module or as a package
    from .kb import FAQ_MD
except ImportError:
    from kb import FAQ_MD

LLM_API_KEY = os.getenv("LLM_API_KEY", "")
LLM_BASE_URL = os.getenv("LLM_BASE_URL") or None
LLM_MODEL = os.getenv("LLM_MODEL", "openai/gpt-oss-20b")
MAX_MESSAGES_PER_VISITOR = int(os.getenv("MAX_MESSAGES_PER_VISITOR", "10"))
BOT_NAME = os.getenv("BOT_NAME", "Support Assistant")

SYSTEM_PROMPT = """You are {bot_name}, a friendly customer support assistant.
Answer the user's question using ONLY the knowledge-base excerpts provided.
If the answer is not in the excerpts, say you don't have that information
and suggest contacting human support. Keep answers short, warm, and concrete.

Knowledge-base excerpts:
{context}
"""

_STOPWORDS = set(
    "a an the is are am do does did you your yours i we our of to in on for "
    "and or but how what when where which who why my it its with can could "
    "would should this that these those be been have has had will there".split()
)


def _tokens(text: str) -&gt; set[str]:
    return {w for w in re.findall(r"[a-z']+", text.lower()) if w not in _STOPWORDS}


def _overlap(query_tokens: set[str], chunk_tokens: set[str]) -&gt; int:
    """Count query tokens present in the chunk, with prefix matching so
    'ship' matches 'shipping' and 'internationally' matches 'international'."""
    count = 0
    for q in query_tokens:
        for c in chunk_tokens:
            if q == c or (len(q) &gt;= 4 and c.startswith(q)) or (len(c) &gt;= 4 and q.startswith(c)):
                count += 1
                break
    return count


def _build_chunks() -&gt; list[dict]:
    chunks, title, lines = [], "General", []

    def flush():
        body = "\n".join(lines).strip()
        if body:
            chunks.append(
                {"title": title, "text": body, "tokens": _tokens(body), "title_tokens": _tokens(title)}
            )

    for line in FAQ_MD.splitlines():
        if line.startswith("## "):
            flush()
            title, lines = line[3:].strip(), []
        else:
            lines.append(line)
    flush()
    return chunks


_CHUNKS = _build_chunks()


def retrieve(query: str, k: int = 3) -&gt; list[dict]:
    q = _tokens(query)
    if not q:
        return _CHUNKS[:k]
    scored = []
    for c in _CHUNKS:
        score = _overlap(q, c["tokens"]) + 2 * _overlap(q, c["title_tokens"])
        scored.append((score, c))
    scored.sort(key=lambda s: s[0], reverse=True)
    top = [c for score, c in scored[:k] if score &gt; 0]
    return top or _CHUNKS[:1]
</code></pre>
<p><strong>The clever trick — prefix matching.</strong> My first version used exact token matching, and when I tested "do you ship internationally?", the <em>wrong</em> section won. Why? "internationally" ≠ "international" and "ship" ≠ "shipping" as exact strings. The <code>_overlap</code> function fixes this: tokens match if one is a prefix of the other (minimum 4 characters). Simple, no stemming library, works.</p>
<p>Title matches get double weight — if your question's words appear in a section <em>heading</em>, that section is almost certainly the right one.</p>
<h2>Step 4 — The chat endpoint</h2>
<p>Append to <code>api/index.py</code>:</p>
<pre><code class="language-python">app = FastAPI(title="AI Customer Support Chatbot")
_message_counts: dict[str, int] = defaultdict(int)

_llm_client: OpenAI | None = None


def _llm() -&gt; OpenAI:
    """Create the LLM client on first use, with a clear error if unconfigured."""
    global _llm_client
    if _llm_client is None:
        if not LLM_API_KEY:
            raise HTTPException(
                status_code=500,
                detail="Server not configured: the LLM_API_KEY environment variable is missing.",
            )
        _llm_client = OpenAI(api_key=LLM_API_KEY, base_url=LLM_BASE_URL)
    return _llm_client


class ChatRequest(BaseModel):
    message: str
    history: list[dict] = []


@app.get("/api/health")
def health() -&gt; dict:
    return {"status": "ok", "model": LLM_MODEL, "chunks": len(_CHUNKS)}


@app.post("/api/chat")
def chat(req: ChatRequest, request: Request) -&gt; dict:
    visitor = (request.headers.get("x-forwarded-for") or "unknown").split(",")[0].strip()
    if _message_counts[visitor] &gt;= MAX_MESSAGES_PER_VISITOR:
        raise HTTPException(status_code=429, detail="Demo message limit reached. Thanks for trying it out!")
    _message_counts[visitor] += 1

    question = req.message.strip()
    if not question:
        raise HTTPException(status_code=400, detail="Empty message.")
    if len(question) &gt; 1000:
        raise HTTPException(status_code=400, detail="Message too long.")

    chunks = retrieve(question, k=3)
    context = "\n\n---\n\n".join(f"[{c['title']}]\n{c['text']}" for c in chunks)

    messages = [
        {"role": "system", "content": SYSTEM_PROMPT.format(bot_name=BOT_NAME, context=context)},
        *req.history[-6:],
        {"role": "user", "content": question},
    ]

    try:
        completion = _llm().chat.completions.create(
            model=LLM_MODEL, messages=messages, max_tokens=400, temperature=0.3
        )
    except Exception as exc:
        raise HTTPException(status_code=502, detail=f"LLM error: {exc}") from exc

    return {
        "answer": completion.choices[0].message.content,
        "sources": [{"title": c["title"], "source": "faq"} for c in chunks],
    }
</code></pre>
<p>Three production touches worth noticing:</p>
<ol>
<li><strong>Per-visitor message cap</strong> — a public demo calls <em>your</em> API key on strangers' behalf. Cap it.</li>
<li><strong>Rolling history window</strong> (<code>req.history[-6:]</code>) — context stays short, token costs stay tiny.</li>
<li><strong>Lazy LLM client</strong> — if the API key is missing, you get a clear error message instead of a mysterious crash at startup. (I learned this one the hard way.)</li>
</ol>
<h2>Step 5 — The chat widget</h2>
<p>Create <code>public/index.html</code>. The full file is ~190 lines of plain HTML/CSS/JS — no framework, no build step. The important part is the fetch call:</p>
<pre><code class="language-javascript">const res = await fetch("/api/chat", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ message: question, history: history.slice(0, -1) }),
});
const data = await res.json();
</code></pre>
<p>Everything else is styling: a message log, a typing indicator, suggestion chips, and a sources line under each bot reply. Style it however you like — or grab my complete widget from the live demo (right-click → View Source).</p>
<h2>Step 6 — Get a free LLM key</h2>
<ol>
<li>Sign up at <a href="https://console.groq.com">console.groq.com</a></li>
<li>API Keys → Create API Key → copy it somewhere safe</li>
</ol>
<p>Groq's free tier runs open models at absurd speed. We'll use <code>openai/gpt-oss-20b</code> — around 1,000 tokens/second, which makes the bot feel instant.</p>
<h2>Step 7 — Run it locally</h2>
<pre><code class="language-bash">python -m venv .venv
source .venv/bin/activate        # Windows: .venv\Scripts\activate
pip install -r requirements.txt fastapi uvicorn

set LLM_API_KEY=your-key-here            # Windows
export LLM_API_KEY=your-key-here         # Mac/Linux
set LLM_BASE_URL=https://api.groq.com/openai/v1
set LLM_MODEL=openai/gpt-oss-20b

uvicorn api.index:app --reload
</code></pre>
<p>Open <a href="http://localhost:8000">http://localhost:8000</a> — wait, one catch: locally, FastAPI doesn't serve <code>public/</code> automatically. Either open <code>public/index.html</code> directly and point the fetch at <code>http://localhost:8000/api/chat</code>, or add two lines to serve it. On Vercel (next step) this is handled for you — files in <code>public/</code> are served at the site root automatically.</p>
<p>Test with a question your docs cover, then one they don't. The refusal is the feature.</p>
<h2>Step 8 — Deploy to a public URL (free)</h2>
<pre><code class="language-bash">npm i -g vercel
vercel login
vercel
</code></pre>
<p>Accept the defaults — Vercel auto-detects FastAPI. Then add your environment variables in the Vercel dashboard (your project → Settings → Environment Variables):</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody><tr>
<td><code>LLM_API_KEY</code></td>
<td>your Groq key</td>
</tr>
<tr>
<td><code>LLM_BASE_URL</code></td>
<td><code>https://api.groq.com/openai/v1</code></td>
</tr>
<tr>
<td><code>LLM_MODEL</code></td>
<td><code>openai/gpt-oss-20b</code></td>
</tr>
<tr>
<td><code>MAX_MESSAGES_PER_VISITOR</code></td>
<td><code>10</code></td>
</tr>
</tbody></table>
<p>Finally:</p>
<pre><code class="language-bash">vercel --prod
</code></pre>
<p>That's it. Your chatbot is live at <code>https://your-project.vercel.app</code> — a real URL you can put on a portfolio, send to a client, or open on your phone.</p>
<h2>Common issues (I hit most of these myself)</h2>
<p><code>model_not_found</code> <strong>error.</strong> LLM providers retire model names. Open your provider's model list (Groq: the Playground dropdown) and use an exact current ID.</p>
<p><strong>Bot replies "Not Found".</strong> If you added a <code>vercel.json</code> with rewrites from an older tutorial — delete it. Vercel's FastAPI auto-detection routes everything through your app already; a rewrite renames the path before your routes see it.</p>
<p><strong>"LLM_API_KEY is missing" after deploy.</strong> Environment variables only apply to deployments made <em>after</em> you add them. Add the variables, then deploy once more.</p>
<p><strong>429 "Demo message limit reached."</strong> Working as intended — that's your cost cap. Raise <code>MAX_MESSAGES_PER_VISITOR</code> if you want longer conversations.</p>
<p><strong>Wrong section retrieved.</strong> Check your <code>## headings</code> — they carry double weight. A section titled "International shipping" gets found by "do you ship internationally?"; one titled "FAQ item 7" doesn't.</p>
<h2>Where to take it next</h2>
<p>This is deliberately the minimal production-shaped version. Natural upgrades:</p>
<ul>
<li><strong>Vector search</strong> — swap keyword retrieval for ChromaDB embeddings when your docs outgrow a few dozen sections</li>
<li><strong>Multiple documents</strong> — index a folder of markdown files instead of one string</li>
<li><strong>Streaming responses</strong> — token-by-token replies for the ChatGPT feel</li>
<li><strong>Analytics</strong> — log questions people ask; it's free product research<br /><br /><strong>Want the code without the copy-pasting?</strong><ul>
<li><a href="https://sammishrakh.gumroad.com/l/tgguty">Download this tutorial's complete project — free</a></li>
<li><a href="https://sammishrakh.gumroad.com/l/mijcup">AI Chatbot Starter Kit ($29)</a> — the production version: ChromaDB vector search, Docker deployment, one-command setup, full guides, and a commercial license</li>
</ul>
</li>
</ul>
<hr />
<p><em>I'm Sammishra — an AI engineer and published researcher. I build chatbots, RAG pipelines, and AI automations for businesses. See my work and get in touch at</em> <a href="https://sammishra.hashnode.dev/work-with-me"><em><strong>sammishra.hashnode.dev/work-with-me</strong></em></a> <em>— or just</em> <a href="https://ai-support-chatbot-orcin.vercel.app"><em>talk to the demo</em></a> <em>this guide builds.</em></p>
]]></content:encoded></item></channel></rss>