<?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" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ streamlit - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ streamlit - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 28 Jul 2026 03:51:20 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/streamlit/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Serve a Multi-User AI Agent with FastAPI and Streamlit ]]>
                </title>
                <description>
                    <![CDATA[ In this tutorial, I’ll show you how to serve a multi-user local AI agent as a REST API using FastAPI, then add a lightweight Streamlit UI on top. Instead of interacting with the agent through a termin ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-serve-a-multi-user-ai-agent-with-fastapi-and-streamlit/</link>
                <guid isPermaLink="false">6a5e9c35892c69a16fdf27df</guid>
                
                    <category>
                        <![CDATA[ AI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ai-agent ]]>
                    </category>
                
                    <category>
                        <![CDATA[ ollama ]]>
                    </category>
                
                    <category>
                        <![CDATA[ #qwen ]]>
                    </category>
                
                    <category>
                        <![CDATA[ FastAPI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ api ]]>
                    </category>
                
                    <category>
                        <![CDATA[ streamlit ]]>
                    </category>
                
                    <category>
                        <![CDATA[ UI ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ llm ]]>
                    </category>
                
                    <category>
                        <![CDATA[ streaming ]]>
                    </category>
                
                    <category>
                        <![CDATA[ chatgpt ]]>
                    </category>
                
                    <category>
                        <![CDATA[ agentic ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Streaming API ]]>
                    </category>
                
                    <category>
                        <![CDATA[ langgraph ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Darsh Shah ]]>
                </dc:creator>
                <pubDate>Mon, 20 Jul 2026 22:07:49 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/e5bf4093-e618-4388-954c-f1a49bc87cfe.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this tutorial, I’ll show you how to serve a multi-user local AI agent as a REST API using FastAPI, then add a lightweight Streamlit UI on top.</p>
<p>Instead of interacting with the agent through a terminal, we’ll expose it over HTTP so multiple users can access it through a chat-style frontend interface. Each session will maintain its own conversation history and streamed responses.</p>
<p>The local AI agent will be built with LangChain v1, Ollama, Qwen, and Python, running on your own machine and ready to plug into larger applications without any per-call model API charges.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-background">Background</a></p>
</li>
<li><p><a href="#heading-what-is-fastapi">What is FastAPI</a>?</p>
</li>
<li><p><a href="#heading-what-is-streamlit">What is Streamlit</a>?</p>
</li>
<li><p><a href="#heading-what-is-multi-user-support">What Is Multi-User Support</a>?</p>
</li>
<li><p><a href="#heading-motivation-and-architecture">Motivation and Architecture</a></p>
</li>
<li><p><a href="#heading-step-1-install-ollama-and-pull-the-model">Step 1: Install Ollama and Pull the Model</a></p>
</li>
<li><p><a href="#heading-step-2-install-python-dependencies">Step 2: Install Python Dependencies</a></p>
</li>
<li><p><a href="#heading-step-3-build-the-agent-and-api-layer-with-fastapi">Step 3: Build the agent and API layer with FastAPI</a></p>
</li>
<li><p><a href="#heading-step-4-build-streamlit-ui">Step 4: Build Streamlit UI</a></p>
</li>
<li><p><a href="#heading-step-5-run-the-backend-app">Step 5: Run the backend app</a></p>
</li>
<li><p><a href="#heading-step-6-run-the-frontend-app">Step 6: Run the frontend app</a></p>
</li>
<li><p><a href="#heading-sample-output">Sample Output</a></p>
</li>
<li><p><a href="#heading-what-to-improve-before-production">What to Improve Before Production</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-background">Background</h2>
<p>Many AI agents start out as simple Python scripts that run in a command-line terminal. You type a message, the agent responds, and everything happens in a single local session.</p>
<p>That setup is great for development and testing, but it becomes limiting when you want other people or applications to interact with the agent.</p>
<p>To make an AI agent truly useful, we need to expose it through an interface that other users can access. A REST API is a practical way to do that.</p>
<p>To follow this tutorial, you'll need Ollama installed on your machine. The tutorial works on macOS, Windows, and Linux. I'm using a MacBook Pro with 32 GB of RAM, but you can run this on a lower-memory machine by choosing a smaller Qwen model from Ollama.</p>
<h2 id="heading-what-is-fastapi"><strong>What is FastAPI?</strong></h2>
<p><a href="https://github.com/fastapi/fastapi">FastAPI</a> is a Python web framework for building APIs. In this tutorial, it gives us a simple way to expose the agent over HTTP so other apps, scripts, or services can call it.</p>
<p>FastAPI is a good fit for AI apps because it gives us a clean boundary around the system. We define the request and response models in Python, FastAPI validates them automatically, and it turns HTTP requests into Python objects and Python objects back into JSON. It also generates interactive API docs for free and supports async endpoints, which is useful for AI workloads that may take longer to respond.</p>
<h2 id="heading-what-is-streamlit"><strong>What is Streamlit?</strong></h2>
<p><a href="https://streamlit.io">Streamlit</a> is a Python framework for building lightweight web interfaces with minimal frontend work. It lets us create interactive browser-based apps using normal Python code instead of HTML, CSS, and JavaScript.</p>
<p>In this tutorial, Streamlit sits on top of the FastAPI backend as a thin client. FastAPI exposes the AI agent over HTTP, and Streamlit gives us a simple UI for calling that API and displaying the results. That separation keeps the backend reusable while still making the agent easy to use in the browser.</p>
<h2 id="heading-what-is-multi-user-support"><strong>What Is Multi-User Support?</strong></h2>
<p>Multi-user support means the AI agent can handle requests from more than one user while keeping each user’s session separate.</p>
<p>For example, User 1&nbsp;asks the agent one question and User 2&nbsp;asks a different question. The agent should remember the correct context for each user independently. Without multi-user support, all users may end up sharing the same conversation state, which can lead to mixed responses, incorrect memory, or overwritten context.</p>
<h2 id="heading-motivation-and-architecture"><strong>Motivation and Architecture</strong></h2>
<p>Turning an AI agent into an API is the natural next step after building it locally. A Python script is great for experimenting, but an API makes the agent reusable. And adding multi-user support makes the agent extensible to be used by others.</p>
<p>To keep things simple, we’ll use a small local agent powered by Ollama and Qwen. The agent has two tools: one for checking the current time and another for counting words.</p>
<p>FastAPI provides the HTTP layer by exposing one endpoint called <code>/chat/stream</code>. When the request comes in with a user message, Pydantic validates the request, LangChain handles the agent loop and tool calling, and the final answer is returned as stream. Streamlit sits on top of that API and acts as a frontend that sends requests to the API and displays the results.</p>
<img src="https://cdn.hashnode.com/uploads/covers/684c95e159698b4bf6a0e4be/21a2b03d-b4c3-4211-82b1-aa265ac6fb1e.png" alt="image showing the sequence diagram of user calling the streamlit UI. The it goes to FastAPI layer, then to AI agent and finally Qwen and tool calls" style="display:block;margin:0 auto" width="1478" height="1000" loading="lazy">

<p>Example request:</p>
<pre><code class="language-json">{ 
    "message": "How many words are in: LangChain makes tool calling easier",
    "user_id":"123e4567-e89b-12d3-a456-426614174000"
 }
</code></pre>
<p>Example response:</p>
<pre><code class="language-json">{
  "answer": "There are **5** words in LangChain makes tool calling easier."
}
</code></pre>
<p>The model runs locally through Ollama, so there are no per-call model API charges.</p>
<h2 id="heading-step-1-install-ollama-and-pull-the-model"><strong>Step 1: Install Ollama and Pull the Model</strong></h2>
<p>To get started, install the Ollama application for your platform.</p>
<p>We’ll use Qwen as the chat model. I’m using <code>qwen3.5:4b</code>. If your machine has less RAM, you can use <code>qwen3.5:0.8b</code> instead.</p>
<pre><code class="language-plaintext">ollama pull qwen3.5:4b
</code></pre>
<h2 id="heading-step-2-install-python-dependencies"><strong>Step 2: Install Python Dependencies</strong></h2>
<p>Create a virtual environment and install the required packages:</p>
<pre><code class="language-plaintext">python3 -m venv venv
source venv/bin/activate

pip install fastapi uvicorn streamlit requests langchain langchain-core langchain-ollama langgraph
</code></pre>
<p>If tutorial requires LangChain &gt;= 1.0.0.</p>
<h2 id="heading-step-3-build-the-agent-and-api-layer-with-fastapi">Step 3: <strong>Build the Agent and API Layer with FastAPI</strong></h2>
<p>This application has three main responsibilities. FastAPI exposes the HTTP endpoint, Pydantic validates the incoming request data, and LangChain runs the agent, including tool calling and short-term memory.</p>
<p>The <code>user_id</code> sent with each request is used as the thread identifier, allowing the checkpointer to keep each user’s conversation history separate. This memory is per session. So every new session will have its own memory.</p>
<p>Another important detail is that the agent is created only once at startup with <code>agent = build_agent()</code>. Reusing the same agent instance avoids rebuilding the model and tool list for every request, which reduces overhead and improves response times while still supporting multiple users.</p>
<p>Inside the <code>/chat/stream</code> endpoint, the backend uses <a href="https://docs.langchain.com/oss/python/langchain/event-streaming">LangChain’s</a> <code>stream_events(..., version="v3")</code> to generate the response as a stream instead of waiting for the full answer all at once. FastAPI then wraps that stream in a <code>StreamingResponse</code>, so the frontend can receive the output gradually as it's produced. This makes the app feel much more interactive, because users can start reading the answer immediately while the rest is still being generated.</p>
<p>Put together, this gives you a lightweight backend that validates input, preserves separate memory for each user, and streams responses to the UI in real time.</p>
<p>Save the following code as <code>app.py</code>:</p>
<pre><code class="language-python">from datetime import datetime
from uuid import UUID

from fastapi import FastAPI, HTTPException
from fastapi.responses import StreamingResponse

from pydantic import BaseModel

from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_ollama import ChatOllama
from langgraph.checkpoint.memory import InMemorySaver

CHAT_MODEL = "qwen3.5:4b"

SYSTEM_PROMPT = (
    "You are a helpful assistant with access to tools for getting the current time "
    "and counting words in text. "
    "Use tools when needed. If the question does not need a tool, answer directly."
)

# -----------------------------
# Request model
# -----------------------------

class ChatRequest(BaseModel):
    user_id: UUID
    message: str

# -----------------------------
# Tools
# -----------------------------

@tool
def current_time() -&gt; str:
    """Return the current local date and time."""
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")


@tool
def word_count(text: str) -&gt; int:
    """Count the number of words in a piece of text."""
    return len(text.split())


# -----------------------------
# Agent + checkpoint memory
# -----------------------------

# Store conversation history in short term memory
checkpointer = InMemorySaver()

def build_agent():
    model = ChatOllama(model=CHAT_MODEL, temperature=0)
    return create_agent(
        model=model,
        tools=[current_time, word_count],
        system_prompt=SYSTEM_PROMPT,
        checkpointer=checkpointer,
    )


agent = build_agent()

# -----------------------------
# Streaming endpoint
# -----------------------------

app = FastAPI()

@app.post("/chat/stream")
def chat_stream(req: ChatRequest):
    def generate():
        run = agent.stream_events(
            {
                "messages": [{"role": "user", "content": req.message}],
            },
            config={
                "configurable": {
                    # Keep each user's short-term memory isolated
                    # by using their user_id as the thread ID.
                    "thread_id": str(req.user_id),
                }
            },
            version="v3",
        )

        for message in run.messages:
            for token in message.text:
                yield token

    return StreamingResponse(generate(), media_type="text/plain")
</code></pre>
<h2 id="heading-step-4-build-streamlit-ui">Step 4: Build Streamlit UI</h2>
<p>The Streamlit code creates a simple chat interface for the AI agent and keeps each browser session tied to a unique user_id.</p>
<p>When the app first loads, it generates and stores a UUID in st.session_state, which is later sent to the backend so the agent can keep that user’s conversation history separate from other users. It also creates a chat_history list in session state so previous messages remain visible every time Streamlit reruns the script. The app then loops through that saved history and displays each message in a chat-style format using st.chat_message().</p>
<p>When the user enters a new message through st.chat_input(), the app immediately saves and displays it, then sends it to the backend API with a POST request to <code>http://127.0.0.1:8001/chat/stream</code> along with the session’s user_id.</p>
<p>The request is made with stream=True, which allows the response to arrive gradually instead of all at once. As each chunk of text is received from the backend, the code appends it to full_answer and updates a placeholder on the page, creating a live streaming effect. Once the response is complete, the final assistant message is stored in chat_history so it remains part of the conversation on the page</p>
<p>Save the below as <code>streamlit_app.py</code></p>
<pre><code class="language-python">import uuid
import requests
import streamlit as st

API_URL = "http://127.0.0.1:8001/chat/stream"

st.title("Local AI Agent")

if "user_id" not in st.session_state:
    st.session_state.user_id = str(uuid.uuid4())

if "chat_history" not in st.session_state:
    st.session_state.chat_history = []

# Show previous messages
for item in st.session_state.chat_history:
    with st.chat_message(item["role"]):
        st.markdown(item["content"])

message = st.chat_input("Enter a message")

if message:
    # Save and show user message
    st.session_state.chat_history.append({"role": "user", "content": message})
    with st.chat_message("user"):
        st.markdown(message)

    # Stream assistant response
    full_answer = ""
    with st.chat_message("assistant"):
        placeholder = st.empty()

        # Send the reqeust to backend API via POST request
        with requests.post(
            API_URL,
            json={
                "message": message,
                "user_id": st.session_state.user_id,
            },
            stream=True,
        ) as response:
            response.raise_for_status()

            for chunk in response.iter_content(chunk_size=None, decode_unicode=True):
                if chunk:
                    full_answer += chunk
                    placeholder.markdown(full_answer)

    # Save final assistant response
    st.session_state.chat_history.append(
        {"role": "assistant", "content": full_answer}
    )
</code></pre>
<h2 id="heading-step-5-run-the-backend-app">Step 5: Run the Backend App</h2>
<p>Start the server with Uvicorn:</p>
<pre><code class="language-bash">uvicorn app:app --reload --port 8001
</code></pre>
<p>Once the application starts, open:</p>
<ul>
<li><p><code>http://127.0.0.1:8001/</code></p>
</li>
<li><p><code>http://127.0.0.1:8001/docs</code></p>
</li>
</ul>
<p>The <code>/docs</code> endpoint is automatically generated by FastAPI using your Pydantic models. It provides an interactive interface where you can test the API without writing any client code.</p>
<img src="https://cdn.hashnode.com/uploads/covers/684c95e159698b4bf6a0e4be/5cf32ff0-273c-47cd-80be-ebf807e4443d.png" alt="Api docs that was generated by FastAPI. It includes /chat/stream  endpoint and schema" style="display:block;margin:0 auto" width="2712" height="1034" loading="lazy">

<p>You can send requests directly from <code>curl</code>. In your terminal, run these commands to invoke the API for the AI agent and check the output:</p>
<pre><code class="language-bash">$ curl -X POST http://127.0.0.1:8001/chat/stream \
  -H "Content-Type: application/json" \
  -d '{"message":"What time is it?","user_id":"123e4567-e89b-12d3-a456-426614174000"}'

$ curl -X POST http://127.0.0.1:8001/chat/stream \
  -H "Content-Type: application/json" \
  -d '{"message":"How many words are in: LangChain makes tool calling easier","user_id":"123e4567-e89b-12d3-a456-426614174000"}'

$ curl -X POST "http://127.0.0.1:8001/chat/stream" \
-H "Content-Type: application/json" \
-d '{"message":"What is the capital of France?","user_id":"123e4567-e89b-12d3-a456-426614174000"}'
</code></pre>
<p>To stop the server, press Ctrl+C in the terminal.</p>
<h2 id="heading-step-6-run-the-frontend-app"><strong>Step 6: Run the Frontend App</strong></h2>
<p>In another terminal, go to the project directory:</p>
<pre><code class="language-plaintext">source venv/bin/activate
streamlit run streamlit_app.py
</code></pre>
<p>That opens the frontend in your browser at <code>http://localhost:8501/</code>. Try the example prompts like "What is the capital of France". You should see the answer in a chat style interface.</p>
<img src="https://cdn.hashnode.com/uploads/covers/684c95e159698b4bf6a0e4be/1030735a-49ed-43e1-995d-07b122c2c965.png" alt="Streamlit UI provides a simple chat frontend for the local AI agent" style="display:block;margin:0 auto" width="1848" height="1710" loading="lazy">

<p>The UI is calling the FastAPI endpoint and invoking the AI agent. You now have a working end to end application for your local AI agent that you can play with.</p>
<p>To stop the server, press Ctrl+C in the terminal.</p>
<h2 id="heading-sample-output">Sample Output</h2>
<p>The image below show two browser sessions of the app running side by side on the same endpoint. Each session is assigned a unique id, which allows the backend to maintain a separate conversation history for each user.</p>
<p>Even though both users ask the same question, “Who am I?”, the responses are different because each session’s answer is based on its own prior messages.</p>
<img src="https://cdn.hashnode.com/uploads/covers/684c95e159698b4bf6a0e4be/b97b8efa-6fca-4e80-9c0a-d0d2601fc2b6.png" alt="Image showing two sessions with the agent and it gives different answers based on the the conversation history" style="display:block;margin:0 auto" width="2914" height="1906" loading="lazy">

<h2 id="heading-what-to-improve-before-production">What to Improve Before Production</h2>
<p>Although this application is fully functional, it's still intentionally minimal. It already supports a reusable FastAPI backend, a Streamlit chat interface, per-user conversation history, and streaming responses.</p>
<p>If you wanted to take it further, the next steps would be adding authentication, persistent storage, structured logging, monitoring, and more robust deployment setup.</p>
<p>It's also worth noting that if your goal is simply to get a polished self-hosted chat UI up and running quickly, you may not need to build the frontend yourself. Projects like <a href="https://www.librechat.ai/">LibreChat</a> and <a href="https://docs.openwebui.com/">Open WebUI</a> already provide richer interfaces and broader features out of the box.</p>
<p>This tutorial takes a different approach: instead of adopting a full platform, it shows how to build a lightweight custom stack yourself so you can better understand the architecture and have more control over how the agent is exposed.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, we took a local AI agent, wrapped it in a FastAPI app, and used Streamlit UI on top of it.</p>
<p>This transforms the AI agent from a standalone script into a reusable service. Instead of only working in a terminal, it can now be accessed through a simple HTTP endpoint by other apps, scripts, or internal tools.</p>
<p>By assigning each session a unique id, the service can also maintain separate conversation history for multiple users, making it possible to support a chat-style interface with isolated memory per session.</p>
<p>From here, you can continue extending the same service by adding authentication or production-ready features. Happy tinkering!</p>
<p>If you enjoyed this tutorial, you can find more of my writing on my&nbsp;<a href="https://darshshah.org/blog/">blog</a>&nbsp;(recent posts include system design paper series), my work on my&nbsp;<a href="https://darshshah.org/">personal website</a>, and updates on&nbsp;<a href="https://www.linkedin.com/in/darshs">LinkedIn</a>.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Résumé Screening System Using Python and Multiprocessing ]]>
                </title>
                <description>
                    <![CDATA[ Hiring the right candidate starts with one time-consuming task: screening résumés. If you’ve ever posted a job opening, you know the pain of hundreds of applications in your inbox, leaving you to spend hours reviewing each résumé manually. In this ar... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/python-resume-screening-system/</link>
                <guid isPermaLink="false">698614754058fffacf721cc8</guid>
                
                    <category>
                        <![CDATA[ Python ]]>
                    </category>
                
                    <category>
                        <![CDATA[ streamlit ]]>
                    </category>
                
                    <category>
                        <![CDATA[ multiprocessing ]]>
                    </category>
                
                    <category>
                        <![CDATA[ automation ]]>
                    </category>
                
                    <category>
                        <![CDATA[ pdf ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Abdul Talha ]]>
                </dc:creator>
                <pubDate>Fri, 06 Feb 2026 16:19:01 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770331777028/1ac80e66-cf22-4160-8812-ea917384cd3f.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Hiring the right candidate starts with one time-consuming task: screening résumés. If you’ve ever posted a job opening, you know the pain of hundreds of applications in your inbox, leaving you to spend hours reviewing each résumé manually.</p>
<p>In this article, you’ll build a résumé screening system using pure Python, focusing on core programming concepts and the power of multiprocessing. You’ll create a custom system that automates the evaluation process by transforming unstructured résumé documents into a ranked leaderboard.</p>
<p>By the end of this guide, you will:</p>
<ul>
<li><p>Parse documents by extracting text from PDF and DOCX résumés using Python</p>
</li>
<li><p>Extract information by identifying skills and keywords from résumé content</p>
</li>
<li><p>Design a scoring algorithm using weighted logic to rank candidates objectively</p>
</li>
<li><p>Build a web interface using Streamlit</p>
</li>
<li><p>Deploy the application on Streamlit Cloud for public access</p>
</li>
</ul>
<p>By following this tutorial, you’ll build a tool capable of processing hundreds of résumés in seconds.</p>
<p>Here’s the source code: <a target="_blank" href="https://github.com/abdultalha0862/Resume_Parser_Project">GitHub Repository</a></p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a class="post-section-overview" href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-overview">Project Overview</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-how-the-system-works">How the System Works</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-system-architecture">System Architecture</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-project-structure">Project Structure</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-1-set-up-the-project">Step 1: Set Up the Project</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-2-build-the-resume-parser">Step 2: Build the Résumé Parser</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-3-build-the-keyword-extractor">Step 3: Build the Keyword Extractor</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-4-implement-the-scoring-engine">Step 4: Implement the Scoring Engine</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-5-build-the-web-interface">Step 5: Build the Web Interface</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-6-test-the-system">Step 6: Test the System</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-step-7-deploy-the-application">Step 7: Deploy the Application</a></p>
</li>
<li><p><a class="post-section-overview" href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To follow along with this tutorial, you should have:</p>
<ul>
<li><p>Basic knowledge of Python (functions, loops, dictionaries)</p>
</li>
<li><p>Python 3.8 or higher installed</p>
</li>
<li><p>Familiarity with installing packages using <code>pip</code></p>
</li>
<li><p>A code editor such as VS Code, PyCharm, or any editor you prefer</p>
</li>
</ul>
<h2 id="heading-project-overview">Project Overview</h2>
<p>In this guide, you’ll develop a system that takes a folder of résumés and a Job Description (JD) as input. The system processes each résumé, extracts relevant information, and calculates a score based on how well the candidate matches the job requirements.</p>
<h2 id="heading-how-the-system-works">How the System Works</h2>
<p>The project consists of four core components:</p>
<ul>
<li><p><strong>Résumé Parser</strong>: Reads PDF and DOCX files and extracts text</p>
</li>
<li><p><strong>JD Parser</strong>: Analyses the job description to identify required skills</p>
</li>
<li><p><strong>Keyword Extractor</strong>: Matches résumé content against a skills taxonomy</p>
</li>
<li><p><strong>Scoring Engine</strong>: Ranks candidates using a weighted algorithm</p>
</li>
</ul>
<h3 id="heading-scoring-formula">Scoring Formula</h3>
<p>Here’s the scoring formula we’ll use:</p>
<pre><code class="lang-plaintext">Total Score =
(Required Skills × 50%) +
(Preferred Skills × 25%) +
(Experience × 15%) +
(Keywords × 10%)
</code></pre>
<p>This approach ensures that essential skills carry more weight than secondary keywords.</p>
<h3 id="heading-how-this-approach-helps-reduce-bias">How This Approach Helps Reduce Bias</h3>
<p>This system evaluates résumés using predefined criteria instead of subjective judgment. Each résumé is scored based on the same set of required skills, preferred skills, experience indicators, and keywords.</p>
<p>Because all candidates are evaluated using the same weighted formula, personal factors such as writing style, formatting, or unconscious preferences don’t influence the ranking. The scoring logic focuses only on how closely a résumé matches the job requirements.</p>
<p>By normalising the evaluation process, the system promotes more consistent and objective screening, which helps reduce bias during the initial résumé review stage.</p>
<h2 id="heading-system-architecture">System Architecture</h2>
<pre><code class="lang-plaintext">Input                    Processing                     Output
─────                    ──────────                     ──────

Résumés ──► Résumé Parser ──► Keyword Extractor ──┐
(PDF/DOCX)                                        │
                                                  ├──► Scoring Engine ──► Ranked Results
Job Description ──► JD Parser ────────────────────┘
(TXT/PDF)
</code></pre>
<p>The system follows a simple input–process–output flow.</p>
<p>Résumés and the job description are provided as inputs. The Résumé Parser extracts text from each résumé, while the JD Parser identifies required and preferred skills from the job description.</p>
<p>The extracted résumé text is then passed to the Keyword Extractor, which matches skills and keywords using a predefined taxonomy.</p>
<p>Finally, the Scoring Engine applies a weighted formula to calculate a score for each candidate and outputs a ranked list of résumés.</p>
<h2 id="heading-project-structure">Project Structure</h2>
<pre><code class="lang-plaintext">resume_screening_system/
├── app.py                    # Streamlit web interface
├── main.py                   # Command-line interface
├── parsers/
│   ├── resume_parser.py      # PDF/DOCX text extraction
│   └── jd_parser.py          # Job description parsing
├── extractors/
│   └── keyword_extractor.py  # Skills and experience extraction
├── matcher/
│   └── scorer.py             # Scoring algorithm
├── data/
│   ├── config.json           # Scoring weights
│   └── skills_taxonomy.json  # Skills database
└── requirements.txt          # Dependencies
</code></pre>
<p>The project is organised into clear, modular directories. Parsing logic, keyword extraction, and scoring are separated into their own folders, while configuration files and data are kept isolated. This structure keeps the codebase easy to navigate, maintain, and extend.</p>
<h2 id="heading-step-1-set-up-the-project">Step 1: Set Up the Project</h2>
<p>Create the folder structure and set up a virtual environment:</p>
<pre><code class="lang-bash">mkdir resume_screening_system
<span class="hljs-built_in">cd</span> resume_screening_system
mkdir parsers extractors matcher data input output
python -m venv venv
</code></pre>
<p>Then go ahead and activate the virtual environment:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># Windows</span>
<span class="hljs-built_in">source</span> venv/Scripts/activate

<span class="hljs-comment"># macOS / Linux</span>
<span class="hljs-built_in">source</span> venv/bin/activate
</code></pre>
<p>Install the required dependencies like this:</p>
<pre><code class="lang-bash">pip install PyPDF2 python-docx streamlit pandas
</code></pre>
<h2 id="heading-step-2-build-the-resume-parser">Step 2: Build the Résumé Parser</h2>
<p>The résumé parser handles different file formats by using a separate extraction method for each type.</p>
<p>For PDF files, the parser opens the document page by page and extracts text from each page using a PDF reader. The extracted text is combined into a single string for further processing.</p>
<p>For DOCX files, the parser reads each paragraph in the document and joins the paragraph text into one block. This ensures consistent text output regardless of the résumé format.</p>
<p>By combining all résumés into plain text, the parser allows components such as keyword extraction and scoring to work efficiently.</p>
<p><strong>File:</strong> <code>parsers/resume_parser.py</code></p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_extract_pdf</span>(<span class="hljs-params">self, file_path: Path</span>) -&gt; str:</span>
    text = <span class="hljs-string">""</span>
    <span class="hljs-keyword">with</span> open(file_path, <span class="hljs-string">"rb"</span>) <span class="hljs-keyword">as</span> file:
        pdf_reader = PyPDF2.PdfReader(file)
        <span class="hljs-keyword">for</span> page <span class="hljs-keyword">in</span> pdf_reader.pages:
            page_text = page.extract_text()
            <span class="hljs-keyword">if</span> page_text:
                text += page_text + <span class="hljs-string">"\\n"</span>
    <span class="hljs-keyword">return</span> text.strip()

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">_extract_docx</span>(<span class="hljs-params">self, file_path: Path</span>) -&gt; str:</span>
    <span class="hljs-keyword">from</span> docx <span class="hljs-keyword">import</span> Document
    doc = Document(file_path)
    <span class="hljs-keyword">return</span> <span class="hljs-string">"\\n"</span>.join(
        para.text <span class="hljs-keyword">for</span> para <span class="hljs-keyword">in</span> doc.paragraphs
    ).strip()
</code></pre>
<h2 id="heading-step-3-build-the-keyword-extractor">Step 3: Build the Keyword Extractor</h2>
<p>This project uses a résumé dataset from <a target="_blank" href="https://www.kaggle.com/datasets/snehaanbhawal/resume-dataset">Kaggle</a> to ensure the logic works with real-world professional data. The keyword extractor identifies skills by scanning the résumé text.</p>
<p>The résumé text is first converted to lowercase so that matching is case-insensitive. A predefined skills taxonomy stores each skill along with its possible variations. The extractor checks the résumé text against these variations to find matches.</p>
<p>Word boundaries are used during matching to avoid partial matches, such as matching “Java” inside “JavaScript”. Matched skills are stored in a set to prevent duplicates.</p>
<p>This approach ensures consistent and controlled skill detection across all résumés.</p>
<p><strong>File:</strong> <code>extractors/keyword_extractor.py</code></p>
<pre><code class="lang-python"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">extract_skills</span>(<span class="hljs-params">self, text: str</span>) -&gt; Set[str]:</span>
    text_lower = text.lower()
    found_skills = set()

    <span class="hljs-keyword">for</span> category, skills_dict <span class="hljs-keyword">in</span> self.skills_taxonomy.items():
        <span class="hljs-keyword">for</span> skill_name, variations <span class="hljs-keyword">in</span> skills_dict.items():
            <span class="hljs-keyword">for</span> variation <span class="hljs-keyword">in</span> variations:
                <span class="hljs-comment"># Prevent "Java" matching "JavaScript"</span>
                pattern = <span class="hljs-string">r"\\b"</span> + re.escape(variation) + <span class="hljs-string">r"\\b"</span>
                <span class="hljs-keyword">if</span> re.search(pattern, text_lower):
                    found_skills.add(skill_name)
                    <span class="hljs-keyword">break</span>

    <span class="hljs-keyword">return</span> found_skills
</code></pre>
<h2 id="heading-step-4-implement-the-scoring-engine">Step 4: Implement the Scoring Engine</h2>
<p>To produce objective rankings, the system uses a weighted scoring formula.</p>
<div class="hn-table">
<table>
<thead>
<tr>
<td>Component</td><td>Weight</td><td>Rationale</td></tr>
</thead>
<tbody>
<tr>
<td>Required Skills</td><td>50%</td><td>Essential technical needs</td></tr>
<tr>
<td>Preferred Skills</td><td>25%</td><td>Competitive differentiators</td></tr>
<tr>
<td>Experience</td><td>15%</td><td>Professional depth</td></tr>
<tr>
<td>Keywords</td><td>10%</td><td>Domain familiarity</td></tr>
</tbody>
</table>
</div><pre><code class="lang-plaintext">Total Score =
(S_req × 0.50) +
(S_pref × 0.25) +
(E_exp × 0.15) +
(K_key × 0.10)
</code></pre>
<p>The scoring engine calculates a final score for each résumé using weighted values.</p>
<p>It counts how many required skills, preferred skills, experience indicators, and keywords appear in a résumé. Each count is multiplied by its assigned weight, with required skills contributing the most.</p>
<p>The weighted values are summed to produce a single score. Résumés are then sorted by this score to generate a ranked list of candidates.</p>
<h2 id="heading-step-5-build-the-web-interface">Step 5: Build the Web Interface</h2>
<p>Streamlit provides a simple web interface for interacting with the résumé screening system.</p>
<p>The text area allows users to input a job description, while the file uploader lets them upload multiple résumé files. When the button is clicked, Streamlit triggers the backend logic to parse résumés, extract data, and calculate scores.</p>
<p>The results are then displayed in the browser, allowing users to run the screening process without using the command line.</p>
<p><strong>File:</strong> <code>app.py</code></p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> streamlit <span class="hljs-keyword">as</span> st

jd_text = st.text_area(
    <span class="hljs-string">"Paste the job description here:"</span>,
    height=<span class="hljs-number">300</span>
)

uploaded_files = st.file_uploader(
    <span class="hljs-string">"Upload resume files:"</span>,
    type=[<span class="hljs-string">"pdf"</span>, <span class="hljs-string">"docx"</span>, <span class="hljs-string">"txt"</span>],
    accept_multiple_files=<span class="hljs-literal">True</span>
)

<span class="hljs-keyword">if</span> st.button(<span class="hljs-string">"Screen Resumes"</span>, type=<span class="hljs-string">"primary"</span>):
    st.success(<span class="hljs-string">"Processing resumes..."</span>)
</code></pre>
<p>Run the application:</p>
<pre><code class="lang-bash">streamlit run app.py
</code></pre>
<p>The app will be available at <a target="_blank" href="http://localhost:8501/"><code>http://localhost:8500</code></a>.</p>
<h2 id="heading-step-6-test-the-system">Step 6: Test the System</h2>
<h3 id="heading-sample-job-description-input">Sample Job Description Input</h3>
<p>Below is an example of a job description you can use to test the system:</p>
<pre><code class="lang-plaintext">We are looking for a Senior Python Developer with strong experience in backend development.

Required Skills:
- Python
- Django
- REST APIs
- SQL

Preferred Skills:
- PostgreSQL
- Docker
- AWS

Experience:
- 3+ years of professional Python development
- Experience building web applications
</code></pre>
<p>This input helps the system identify required skills, preferred skills, and experience keywords, which are then used by the scoring engine to rank résumés.</p>
<pre><code class="lang-bash">python main.py
</code></pre>
<h3 id="heading-sample-output">Sample Output</h3>
<pre><code class="lang-plaintext">============================================================
SCREENING RESULTS
============================================================
Rank #1: Alice Johnson | Score: 85.42/100 | Matched: python, django, postgresql
Rank #2: Carol Davis   | Score: 72.50/100 | Matched: python, django
</code></pre>
<h2 id="heading-step-7-deploy-the-application">Step 7: Deploy the Application</h2>
<p>To make the system publicly accessible:</p>
<ol>
<li><p>Push the code to GitHub</p>
</li>
<li><p>Go to <a target="_blank" href="http://share.streamlit.io/"><code>share.streamlit.io</code></a></p>
</li>
<li><p>Select your <a target="_blank" href="http://app.py/"><code>app.py</code></a> file</p>
</li>
<li><p>Deploy the application</p>
</li>
</ol>
<p>Your app will be live at:</p>
<pre><code class="lang-plaintext">&lt;https://your-app-name.streamlit.app&gt;
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>In this tutorial, you’ve built a complete résumé screening system from scratch using Python. By combining text processing, structured scoring, and automation, this project demonstrates how manual résumé screening can be transformed into an efficient and objective workflow.</p>
<p>This system helps reduce bias, save time, and evaluate candidates more consistently. Happy coding!</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
