<?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: Gunjan</title>
    <description>The latest articles on DEV Community by Gunjan (@gunjan-redu).</description>
    <link>https://dev.to/gunjan-redu</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%2F4141360%2Fc71aec82-3729-44df-9d88-b692e26f83ba.png</url>
      <title>DEV Community: Gunjan</title>
      <link>https://dev.to/gunjan-redu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gunjan-redu"/>
    <language>en</language>
    <item>
      <title>I built an AI teaching assistant, and it found gaps in my own teaching material</title>
      <dc:creator>Gunjan</dc:creator>
      <pubDate>Thu, 05 Nov 2026 13:11:07 +0000</pubDate>
      <link>https://dev.to/gunjan-redu/i-built-an-ai-teaching-assistant-and-it-found-gaps-in-my-own-teaching-material-g19</link>
      <guid>https://dev.to/gunjan-redu/i-built-an-ai-teaching-assistant-and-it-found-gaps-in-my-own-teaching-material-g19</guid>
      <description>&lt;p&gt;I've been teaching coding for quite a few years now. Since I'm also trying to transition into a career in IT, I've been working on learning new skills and building projects along the way.&lt;/p&gt;

&lt;p&gt;One of those projects was a RAG-based teaching assistant that could answer questions about Python concepts like variables, loops and functions.&lt;/p&gt;

&lt;p&gt;I wrote all the documents myself. They weren't scraped from the internet. I wanted the assistant to work with the same beginner-friendly explanations I use when teaching students.&lt;/p&gt;

&lt;p&gt;The first question I asked it was: "What is a function?"&lt;/p&gt;

&lt;p&gt;It gave me a correct explanation of how functions work and the different ways of using them in Python. But it also pointed out something interesting: the provided context didn't actually contain a definition of a function.&lt;/p&gt;

&lt;p&gt;And it was right. I hadn't written one anywhere in my documents.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built, in four steps
&lt;/h2&gt;

&lt;p&gt;The basic flow looks like this:&lt;/p&gt;

&lt;p&gt;Corpus → Chunks and embeddings → Retrieval and threshold check → Claude generates an answer with citations.&lt;/p&gt;

&lt;p&gt;The process started with building the corpus. I had to write all the documents about functions, variables and loops myself.&lt;/p&gt;

&lt;p&gt;I tried to keep the explanations simple and beginner-friendly. From my experience teaching students, I've found that this works best, especially when someone is learning Python as their first programming language.&lt;/p&gt;

&lt;p&gt;Once the documents were ready, I had to split them into smaller chunks.&lt;/p&gt;

&lt;p&gt;I decided to use a heading + paragraph approach, along with a merge pass that reattaches code blocks to their explanations. I didn't want the retrieval system returning a piece of code without the explanation that goes with it.&lt;/p&gt;

&lt;p&gt;I then converted these chunks into 1024-dimensional vectors using Voyage AI and stored them in my PostgreSQL database along with their document names and headings.&lt;/p&gt;

&lt;p&gt;When a user asks a question, I send it through Voyage AI as well to generate its embedding. I then compare it against the stored vectors and retrieve the three closest chunks.&lt;/p&gt;

&lt;p&gt;I also added a threshold check. If even the closest chunk has a distance greater than 0.93, the system stops there, without making a call to Claude.&lt;/p&gt;

&lt;p&gt;Otherwise, the text from those three chunks (not the vectors themselves) is passed to Claude, which uses that context to generate an answer.&lt;/p&gt;

&lt;p&gt;Finally, the answer is displayed on the frontend along with its sources and citations.&lt;/p&gt;

&lt;h2&gt;
  
  
  The audit story
&lt;/h2&gt;

&lt;p&gt;As I mentioned earlier, my first question to the assistant was "What is a function?"&lt;/p&gt;

&lt;p&gt;The answer did two things. It explained functions correctly using the available context, but it also pointed out that the context didn't contain an actual definition of a function.&lt;/p&gt;

&lt;p&gt;I found this particularly interesting.&lt;/p&gt;

&lt;p&gt;I wouldn't call myself an expert, but I've been teaching these concepts for years. And I think this is something that can happen when you've been teaching the same subject for a long time.&lt;/p&gt;

&lt;p&gt;Sometimes, we assume that beginners already know certain things because they seem too obvious to us.&lt;/p&gt;

&lt;p&gt;That's exactly what I had done.&lt;/p&gt;

&lt;p&gt;I had explained the different ways of using functions, how they work and how to write them. Somewhere along the way, I assumed that explaining all these things would automatically help a student understand what a function actually is.&lt;/p&gt;

&lt;p&gt;But I had never explicitly defined it in my document.&lt;/p&gt;

&lt;p&gt;The reason the assistant caught this was quite simple: I had instructed it to answer ONLY using the provided context.&lt;/p&gt;

&lt;p&gt;As a teacher, I automatically fill in gaps when explaining something. The definition of a function is something I probably say at the whiteboard without even thinking about it. So I never noticed that it was missing from my written material.&lt;/p&gt;

&lt;p&gt;The assistant couldn't do that. It wasn't allowed to make assumptions, and that made the missing information obvious.&lt;/p&gt;

&lt;p&gt;I went back and added a definition:&lt;/p&gt;

&lt;p&gt;"In Python, a function is a reusable block of code that executes only when called."&lt;/p&gt;

&lt;p&gt;But that wasn't the end of it. While reviewing the documents, I found a few more things that needed attention:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What happens when a function doesn't return anything? (The whole &lt;code&gt;None&lt;/code&gt; surprise.)&lt;/li&gt;
&lt;li&gt;What's the difference between parameters and arguments?&lt;/li&gt;
&lt;li&gt;Some examples had code but didn't show what the actual output would look like.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I fixed these as well.&lt;/p&gt;

&lt;p&gt;Then I just had to run &lt;code&gt;python store.py&lt;/code&gt; to chunk the documents again, generate new embeddings and store everything in the database.&lt;/p&gt;

&lt;p&gt;When I asked "What is a function?" again, the assistant now started with an actual definition.&lt;/p&gt;

&lt;p&gt;What I liked most about this was that the project became more than just a tool for answering questions about my course material. It also helped me improve the material itself.&lt;/p&gt;

&lt;p&gt;And I can repeat this process whenever I want: ask questions, see where the assistant struggles or points out missing information, update the documents and run the ingestion process again.&lt;/p&gt;

&lt;p&gt;In a way, the assistant ended up auditing the very material it was built to teach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The threshold story
&lt;/h2&gt;

&lt;p&gt;One thing I struggled with while building this project was deciding on the right threshold for rejecting questions that were outside the scope of my documents.&lt;/p&gt;

&lt;p&gt;Initially, I assumed that questions my documents could answer would have a smaller distance from the stored chunks than questions they couldn't answer.&lt;/p&gt;

&lt;p&gt;But when I started testing, the results weren't what I expected.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"What is a float?" (a topic covered in my documents) scored 0.853.&lt;/li&gt;
&lt;li&gt;"What is a dictionary?" (a topic not covered in my documents) scored 0.852.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The distances were almost identical.&lt;/p&gt;

&lt;p&gt;However, when I tried something completely unrelated, like "Who is the PM of India?", the distance was 0.957.&lt;/p&gt;

&lt;p&gt;This made me realise something about how embeddings work in this particular setup.&lt;/p&gt;

&lt;p&gt;Questions about floats and dictionaries are both Python-related questions. Even though my documents didn't cover dictionaries, the embedding model still placed that question close to my Python-related content.&lt;/p&gt;

&lt;p&gt;In other words, being semantically close doesn't necessarily mean that my documents contain the answer.&lt;/p&gt;

&lt;p&gt;I had seen tutorials suggesting thresholds around 0.78, but using that value would have rejected a question about floats that my documents could actually answer.&lt;/p&gt;

&lt;p&gt;After experimenting with my own data, I settled on 0.93.&lt;/p&gt;

&lt;p&gt;I deliberately kept it quite high because I didn't want to reject questions just because their nearest chunks weren't particularly close.&lt;/p&gt;

&lt;p&gt;This also changed how I thought about the threshold.&lt;/p&gt;

&lt;p&gt;Its main purpose isn't to decide whether a question is relevant or not. It's more of a first-level filter to catch obviously unrelated questions before making an API call to Claude.&lt;/p&gt;

&lt;p&gt;Anything that sounds like a Python-related question gets a chance to go through. Claude then reads the retrieved chunks and decides whether there's enough information in the context to answer.&lt;/p&gt;

&lt;p&gt;So, the actual decision about whether the answer exists in my documents happens at the LLM stage, not through the distance threshold.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment day
&lt;/h2&gt;

&lt;p&gt;For deployment, I initially planned to use Render's free tier for both the application and the database.&lt;/p&gt;

&lt;p&gt;But while going through their pricing page, I noticed something important: free-tier databases are deleted after 30 days.&lt;/p&gt;

&lt;p&gt;I didn't want to risk losing all the documents and embeddings I'd worked on, so I decided to split the deployment.&lt;/p&gt;

&lt;p&gt;The application is hosted on Render, while the PostgreSQL database (with pgvector) is hosted on Supabase.&lt;/p&gt;

&lt;p&gt;Another interesting thing I learned during deployment was how differently Windows and Linux handle environment variables.&lt;/p&gt;

&lt;p&gt;My first deployment on Render failed because I was using an environment variable called &lt;code&gt;DB_Port&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;It had been working perfectly fine on my Windows machine for weeks. But Windows environment variables are case-insensitive, whereas Linux environment variables are case-sensitive.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;DB_Port&lt;/code&gt; and &lt;code&gt;DB_PORT&lt;/code&gt; weren't treated as the same thing.&lt;/p&gt;

&lt;p&gt;The fix was just one character, but it took down the entire production deployment.&lt;/p&gt;

&lt;p&gt;After fixing it, all I had to do was push the changes to GitHub. Render automatically picked them up and redeployed the application.&lt;/p&gt;

&lt;p&gt;This was also when I realised how useful it was to have all my database configuration handled through environment variables.&lt;/p&gt;

&lt;p&gt;For example, when I wanted to point the application to a database in Tokyo, I only had to make a small change in my configuration file. I didn't have to change the application code at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's live now
&lt;/h2&gt;

&lt;p&gt;The teaching assistant is now deployed, and I can ask it questions about the Python concepts covered in my documents.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ai-teaching-assistant-d2tw.onrender.com/" rel="noopener noreferrer"&gt;https://ai-teaching-assistant-d2tw.onrender.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm4e5f9hk813j765e8ft0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm4e5f9hk813j765e8ft0.png" alt="AI teaching assistant" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/Gunjan-redu/ai-teaching-assistant" rel="noopener noreferrer"&gt;Github repo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are still things I'd like to improve, especially around retrieval quality and testing how well the assistant handles questions that aren't covered in the documents.&lt;/p&gt;

&lt;p&gt;But this project taught me quite a bit, not just about RAG, embeddings, vector databases and deployment, but also about something I hadn't expected: how building a tool to teach something can sometimes reveal gaps in the way you teach it yourself.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>claude</category>
    </item>
    <item>
      <title>What building an API aggregator taught me about calling other people's APIs</title>
      <dc:creator>Gunjan</dc:creator>
      <pubDate>Fri, 25 Sep 2026 17:24:16 +0000</pubDate>
      <link>https://dev.to/gunjan-redu/what-building-an-api-aggregator-taught-me-about-calling-other-peoples-apis-47ll</link>
      <guid>https://dev.to/gunjan-redu/what-building-an-api-aggregator-taught-me-about-calling-other-peoples-apis-47ll</guid>
      <description>&lt;p&gt;I am a teacher, I teach coding to kids. I am trying to learn backend development.&lt;br&gt;
In my journey to learn FastAPI I got to make many APIs. But I'd never gotten a chance to use an external API. So I built a service that lives entirely on other people's APIs- weather, currency and news. Here's what it taught me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chaining API calls-
&lt;/h2&gt;

&lt;p&gt;I used Open-meteo's weather API, which requires the latitudes and longitude of the city whose weather we want to know. I had to use another API of open-meteo to get the latitudes and longitude of any city in the world by using its name. So, we get the latitudes and longitude of city whose weather is requested by the client, from the first API and give that as input to the weather API. The output of one API became the input of the next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Whose failure is it-
&lt;/h2&gt;

&lt;p&gt;I handled the failures of the system in a way that the client knows whose fault it is. If the request fails because of the timeout error then the client should be informed about it so they don't keep trying, if our own server fails, then client can inform us. A 500 error if our own server fails, 502 error if the external API provided an invalid response and 504 error if the external API service timed out.&lt;/p&gt;

&lt;p&gt;I tested this by setting my timeout to 0.001 seconds, forcing every call to fail. Before: an ugly 500 traceback. After: a clean 504 weather service timed out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Graceful degradation-
&lt;/h2&gt;

&lt;p&gt;I made an aggregate endpoint which gets the responses from all three APIs and gives it to the client. Client enters the city name and we get the weather of the city, currency conversion and news. If any of these 3 APIs fail, we only get the responses from the successful APIs and also the name of the API that's causing the error and give it to the user.&lt;/p&gt;

&lt;p&gt;In case user enters an unknown city name, the request would be killed and client would get a 404 error. I could have followed the same way of handling the unknown city failure just like the ones we mentioned before but it just didn't make sense to me to give the currency conversion and news if the city is not recognised.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching-
&lt;/h2&gt;

&lt;p&gt;I used a TTL cache for my project, every response is remembered for 5 minutes which saves a lot of resources. But, TTL cache has its own limits, one of which is that if the system is to be stopped and restarted for some reason we might get a lot of requests from the clients which is called cache stampede.&lt;/p&gt;

&lt;p&gt;Another big issue with TTL is that it lives in the process, if any other workers in the project want to use it, it wouldn't work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing with mocks-
&lt;/h2&gt;

&lt;p&gt;I set up tests with Pytest and tested all the endpoints, including the failure paths.. The project contains the code for what would happen if the external APIs timed out or gave an invalid response. That code was untestable because we can't actually wait for the API to be down to test that code.&lt;/p&gt;

&lt;p&gt;So I staged the disasters myself, during tests, I replaced httpx with a fake that returns whatever I script — a 500, a timeout. My real error-handling code runs against fake failures, and the tests verify it responds correctly, without any real service being down.&lt;/p&gt;

&lt;p&gt;I learned not only how to use external APIs but also pytest in this project. Here's the link for the GitHub repo:&lt;br&gt;
&lt;a href="https://github.com/Gunjan-redu/api-aggregator" rel="noopener noreferrer"&gt;https://github.com/Gunjan-redu/api-aggregator&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>beginners</category>
      <category>api</category>
    </item>
  </channel>
</rss>
