<?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[llamnuds]]></title><description><![CDATA[llamnuds]]></description><link>https://llamnuds.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Thu, 17 Sep 2026 06:33:11 GMT</lastBuildDate><atom:link href="https://llamnuds.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building an IT Support Assistant with Retrieval-Augmented Generation and Gemini-2.0-flash and FAISS]]></title><description><![CDATA[For my Kaggle/Google 5 day intensive AI course’s Capstone project, I set out to build something practical — not just a chatbot that spins stories about servers, but an IT assistant that could help users get accurate answers to queries they would norm...]]></description><link>https://llamnuds.hashnode.dev/building-an-it-support-assistant-with-retrieval-augmented-generation-and-gemini-20-flash-and-faiss</link><guid isPermaLink="true">https://llamnuds.hashnode.dev/building-an-it-support-assistant-with-retrieval-augmented-generation-and-gemini-20-flash-and-faiss</guid><category><![CDATA[gemini]]></category><category><![CDATA[AI]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[flash]]></category><category><![CDATA[support]]></category><category><![CDATA[faiss]]></category><category><![CDATA[capstone project]]></category><category><![CDATA[kaggle]]></category><category><![CDATA[Google]]></category><dc:creator><![CDATA[Shaun Dunmall]]></dc:creator><pubDate>Tue, 08 Apr 2025 23:51:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1744155372843/f31a97c3-abaa-4503-af6d-1b6b090a3238.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>For my Kaggle/Google 5 day intensive AI course’s Capstone project, I set out to build something practical — not just a chatbot that spins stories about servers, but an IT assistant that could help users get accurate answers to queries they would normally have to speak to their I.T. department about.</p>
<p>Working in IT support, you're constantly solving the same classes of problems. The terminology changes. The context shifts. But the core issue — and its resolution — is usually something we've dealt with before. And yet, when someone asks a question, you still need to give a clear, accurate, and human-readable response. That’s where the idea for this project started.</p>
<p>The goal? Create a <strong>Retrieval-Augmented Generation (RAG)</strong> system that could serve as a safe, accurate IT support assistant — one that knows what it knows and doesn’t pretend otherwise.</p>
<h2 id="heading-the-hallucination-problem">The Hallucination Problem</h2>
<p>My early experiments used Gemini-2.0-flash to directly answer support-style prompts. The results were occasionally brilliant — but also occasionally made-up. That’s a problem in IT. It’s not enough for something to sound plausible — it has to be correct.</p>
<p>During initial testing Gemini-2.0-flash told me some entirely made up facts, this is when I knew I had to change approach. I needed a way to anchor the LLM’s responses in <strong>trusted, internal knowledge</strong> — a dataset of real Q&amp;A we already knew was right.</p>
<p>That’s when I moved to a RAG architecture.</p>
<h2 id="heading-the-rag-based-approach">The RAG-Based Approach</h2>
<p>Instead of letting Gemini-2.0-flash generate answers freely, I decided to <strong>retrieve similar questions from a curated Q&amp;A dataset</strong>, and instruct Gemini-2.0-flash to use only those retrieved entries when composing its reply.</p>
<p>Here’s the high-level workflow:</p>
<ol>
<li><p>Load a CSV containing known Q&amp;A pairs.</p>
</li>
<li><p>Encode the questions using a sentence transformer model.</p>
</li>
<li><p>Store the vectors in a FAISS index for similarity search.</p>
</li>
<li><p>When a new user query comes in:</p>
<ul>
<li><p>Encode it.</p>
</li>
<li><p>Search the FAISS index for top matches.</p>
</li>
<li><p>If the best match is close enough, build a prompt with the top few matches.</p>
</li>
<li><p>Send the prompt to Gemini-2.0-flash via API.</p>
</li>
<li><p>Return the grounded response from Gemini-2.0-flash.</p>
</li>
<li><p>Log everything.</p>
</li>
</ul>
</li>
</ol>
<h2 id="heading-code-snippets">Code Snippets</h2>
<h3 id="heading-load-and-index-the-knowledge-base">Load and Index the Knowledge Base</h3>
<pre><code class="lang-python"><span class="hljs-comment"># =============================================</span>
<span class="hljs-comment"># ✅ Function: Load and Index Q&amp;A Knowledge Base</span>
<span class="hljs-comment"># =============================================</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">load_qa_knowledge_base</span>(<span class="hljs-params">csv_path</span>):</span>
    <span class="hljs-string">"""
    Loads a CSV file with 'question' and 'answer' columns,
    generates sentence embeddings, and stores them in a FAISS index.
    """</span>
    df = pd.read_csv(csv_path, quotechar=<span class="hljs-string">'"'</span>, encoding=<span class="hljs-string">'utf-8'</span>, on_bad_lines=<span class="hljs-string">'skip'</span>)
    model = SentenceTransformer(<span class="hljs-string">"all-MiniLM-L6-v2"</span>)
    df[<span class="hljs-string">"embedding"</span>] = model.encode(df[<span class="hljs-string">"question"</span>].tolist(), convert_to_numpy=<span class="hljs-literal">True</span>).tolist()

    <span class="hljs-comment"># Build FAISS index for fast nearest-neighbor search</span>
    embeddings = np.vstack(df[<span class="hljs-string">"embedding"</span>].values).astype(<span class="hljs-string">"float32"</span>)
    index = faiss.IndexFlatL2(embeddings.shape[<span class="hljs-number">1</span>])
    index.add(embeddings)

    <span class="hljs-keyword">return</span> df, index, model
</code></pre>
<h3 id="heading-define-the-distance-threshold">Define the Distance Threshold</h3>
<p>FAISS calculates the L2 distance between embeddings. The smaller the value, the more semantically similar the text. If no good match is found (above our threshold), we don’t generate a response at all and we drop to our fallback message.</p>
<pre><code class="lang-python">MAX_DISTANCE = <span class="hljs-number">0.9</span>  <span class="hljs-comment"># Don't trust anything beyond this distance</span>
</code></pre>
<h3 id="heading-query-generation-pipeline">Query + Generation Pipeline</h3>
<pre><code class="lang-python"><span class="hljs-comment"># ====================================================</span>
<span class="hljs-comment"># ✅ Function: Answer Questions Using Gemini + Grounding</span>
<span class="hljs-comment"># ====================================================</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">grounded_gemini_answer_verbose</span>(<span class="hljs-params">user_query, df, index, model, api_key, log_path=None, top_k=<span class="hljs-number">3</span>, max_distance=<span class="hljs-number">0.9</span></span>):</span>
    <span class="hljs-string">"""
    Retrieves similar Q&amp;A entries based on the user query and uses Gemini Pro
    (via REST API) to generate a response grounded only in those matches.
    """</span>
    <span class="hljs-comment"># Convert query into vector</span>
    query_vec = model.encode([user_query], convert_to_numpy=<span class="hljs-literal">True</span>).astype(<span class="hljs-string">"float32"</span>)
    distances, match_indices = index.search(query_vec, top_k)

    <span class="hljs-comment"># Pick the best match</span>
    best_idx = match_indices[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>]
    best_dist = distances[<span class="hljs-number">0</span>][<span class="hljs-number">0</span>]
    best_q = df.iloc[best_idx][<span class="hljs-string">"question"</span>]

    <span class="hljs-comment"># Fallback if nothing similar enough</span>
    <span class="hljs-keyword">if</span> best_dist &gt; max_distance:
        fallback_msg = <span class="hljs-string">"⚠️ Sorry, I couldn't find a relevant answer to that question in the knowledge base."</span>
        <span class="hljs-keyword">if</span> log_path:
            log_qa_interaction(log_path, user_query, best_q, best_dist, fallback_msg, status=<span class="hljs-string">"Fallback"</span>)
        <span class="hljs-keyword">return</span> {
            <span class="hljs-string">"match_quality"</span>: <span class="hljs-string">f"🔍 Closest match distance: <span class="hljs-subst">{best_dist:<span class="hljs-number">.4</span>f}</span> (above threshold <span class="hljs-subst">{max_distance}</span>)"</span>,
            <span class="hljs-string">"top_match"</span>: best_q,
            <span class="hljs-string">"answer"</span>: fallback_msg
        }


    <span class="hljs-comment"># Build context prompt from top matches</span>
    context = <span class="hljs-string">"\n\n"</span>.join([
        <span class="hljs-string">f"Q: <span class="hljs-subst">{df.iloc[idx][<span class="hljs-string">'question'</span>]}</span>\nA: <span class="hljs-subst">{df.iloc[idx][<span class="hljs-string">'answer'</span>]}</span>"</span>
        <span class="hljs-keyword">for</span> idx <span class="hljs-keyword">in</span> match_indices[<span class="hljs-number">0</span>]
    ])

    prompt = <span class="hljs-string">f"""
You are an experienced IT support assistant.

Your task is to answer the user's question using only the provided Q&amp;A Context section.
You may paraphrase and adapt information from similar questions — you are allowed to interpret if the meaning is clearly close.
However, do not guess or invent new information. If the context clearly does not answer the user's question, say: "I'm sorry, I don't have enough information to answer that."

Respond in a friendly, helpful, and professional tone, using full sentences like you would in a support ticket response.

Q&amp;A Context:
<span class="hljs-subst">{context}</span>

User's question:
"<span class="hljs-subst">{user_query}</span>"
"""</span>


    <span class="hljs-comment"># Call Gemini Pro (v1beta) using REST API</span>
    url = <span class="hljs-string">"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent"</span>
    headers = {<span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>}
    params = {<span class="hljs-string">"key"</span>: api_key}
    payload = {
        <span class="hljs-string">"contents"</span>: [
            {
                <span class="hljs-string">"role"</span>: <span class="hljs-string">"user"</span>,
                <span class="hljs-string">"parts"</span>: [{<span class="hljs-string">"text"</span>: prompt}]
            }
        ]
    }

    response = requests.post(url, headers=headers, params=params, data=json.dumps(payload))

    <span class="hljs-keyword">if</span> response.status_code == <span class="hljs-number">200</span>:
        answer = response.json()[<span class="hljs-string">"candidates"</span>][<span class="hljs-number">0</span>][<span class="hljs-string">"content"</span>][<span class="hljs-string">"parts"</span>][<span class="hljs-number">0</span>][<span class="hljs-string">"text"</span>]
    <span class="hljs-keyword">else</span>:
        answer = <span class="hljs-string">f"❌ Gemini API error <span class="hljs-subst">{response.status_code}</span>: <span class="hljs-subst">{response.text}</span>"</span>

    <span class="hljs-comment"># Log the successful generation</span>
    <span class="hljs-keyword">if</span> log_path:
        log_qa_interaction(log_path, user_query, best_q, best_dist, answer, status=<span class="hljs-string">"Generated"</span>)


    <span class="hljs-keyword">return</span> {
        <span class="hljs-string">"match_quality"</span>: <span class="hljs-string">f"🔍 Closest match distance: <span class="hljs-subst">{best_dist:<span class="hljs-number">.4</span>f}</span>"</span>,
        <span class="hljs-string">"top_match"</span>: best_q,
        <span class="hljs-string">"answer"</span>: answer.strip()
    }
</code></pre>
<ul>
<li><p><strong>User Query</strong>: "Free disk space via CLI?"</p>
</li>
<li><p><strong>Closest Match</strong>: "How do I check available disk space using the Windows command line?"</p>
</li>
<li><p><strong>Distance</strong>: 0.2057</p>
</li>
<li><p><strong>Response</strong>: “Run the WMIC command: <strong>wmic logicaldisk get size,freespace,caption</strong> to display disk usage statistics.”</p>
</li>
</ul>
<h2 id="heading-lessons-from-iteration">Lessons from Iteration</h2>
<p>Like most GenAI projects, this one went through several rewrites:</p>
<ul>
<li><p>Initially there was no fallback logic — and Gemini-2.0-flash would answer confidently even with a poor match.</p>
</li>
<li><p>Early versions logged nothing, which made debugging difficult.</p>
</li>
<li><p>Tuning the match quality necessitated rewriting the questions and answers in a way that was easier for the system to semantically analyse, this took some time.</p>
</li>
</ul>
<p>Correcting each mistake made the system more robust. Logging, similarity thresholding, and prompt grounding turned it from a guesser into a dependable assistant.</p>
<h2 id="heading-capstone-requirements-met">Capstone Requirements Met</h2>
<p>This project checks all the boxes:</p>
<ul>
<li><p>✅ Uses multiple GenAI techniques: Embeddings, Vector Search, Prompt Engineering, RAG</p>
</li>
<li><p>✅ Addresses a real IT use case with practical value</p>
</li>
<li><p>✅ Produces safe, auditable responses</p>
</li>
<li><p>✅ Clean, functional end-to-end notebook</p>
</li>
</ul>
<h2 id="heading-potential-next-steps">Potential Next Steps</h2>
<ul>
<li><p>Add a simple Streamlit or Gradio UI.</p>
</li>
<li><p>Let users rate answers with 👍/👎.</p>
</li>
<li><p>Handle follow-up questions (multi-turn support).</p>
</li>
<li><p>Use function calling to create support tickets or link to internal systems.</p>
</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This project taught me that you don’t need to fine-tune a language model to get reliable results — not if you ground it properly. With FAISS, good embeddings, and a well-written prompt, you can get consistent, safe answers from a powerful model like Gemini-2.0-flash.</p>
<p>I built this to solve a real problem I face every day; and now that it works, I’m thinking about what else it could do.</p>
<hr />
<p><em>Project developed as part of the Kaggle &amp; Google Generative AI Capstone, 2025.</em></p>
]]></content:encoded></item></channel></rss>