Thursday, July 23, 2026

Capstone exercise

 #Capstone Exercise: 

A capstone project is a comprehensive, culminating academic assignment that learners complete at the end of a course such as GenAI training. It requires you to apply the skills and knowledge you've acquired throughout your studies to investigate, design a solution for, or evaluate a specific, real-world problem or research question

This Capstone project demonstrates:

✅ Chroma vector store

✅ text-embedding-3-small embeddings

✅ GPT generation

✅ Semantic retrieval

✅ Semantic + threshold retrieval

✅ Hybrid retrieval (BM25 + semantic)

✅ Hallucination-resistant prompt

✅ No-answer fallback

✅ Top-K ≤ 3

✅ Outputs submission.csv


#!/usr/bin/python


import os

import json

import numpy as np

import pandas as pd


from dotenv import load_dotenv


from tenacity import (

    retry,

    stop_after_attempt,

    wait_random_exponential

)


from langchain_community.document_loaders import CSVLoader

from langchain_community.vectorstores import Chroma


from langchain_openai import (

    AzureOpenAIEmbeddings,

    AzureChatOpenAI

)


from rank_bm25 import BM25Okapi



# ============================================================

# CONFIGURATION

# ============================================================


load_dotenv("./Data/vars.env")


DATASET_FILE = "./Data/capstone1_rag_dataset.csv"

TEST_FILE = "./Data/capstone1_rag_test_questions.csv"


VECTOR_DB_DIR = "./chroma_capstone_db"


AZURE_OPENAI_ENDPOINT = os.environ["MODEL_ENDPOINT"]

OPENAI_API_VERSION = os.environ["API_VERSION"]

CHAT_DEPLOYMENT_NAME = os.environ["MODEL_NAME"]

PROJECT_ID = os.environ["PROJECT_ID"]


EMBEDDINGS_DEPLOYMENT_NAME = os.environ["EMBEDDINGS_DEPLOYMENT_NAME "]


# Required by Chroma in many enterprise environments

os.environ["ANONYMIZED_TELEMETRY"] = "False"



# ============================================================

# AUTHENTICATION

# ============================================================


def get_access_token():

    auth = "https://<your-provider-endpoint>/oauth2/token"

    scope = "https:// <your-provider-endpoint>/.default"

    grant_type = "client_credentials"



    with httpx.Client() as client:

        body = {

            "grant_type": grant_type,

            "scope": scope,

            "client_id": dbutils.secrets.get(scope="AIML_Training", key="client_id"),

            "client_secret": dbutils.secrets.get(scope="AIML_Training", key="client_secret"),

        }

        headers = {"Content-Type": "application/x-www-form-urlencoded"}

        resp = client.post(auth, headers=headers, data=body, timeout=60)

        access_token = resp.json()["access_token"]

        return access_token


# ============================================================

# MODELS

# ============================================================


embeddings = AzureOpenAIEmbeddings(

    azure_deployment=EMBEDDINGS_DEPLOYMENT_NAME,

    azure_endpoint=AZURE_OPENAI_ENDPOINT,

    api_version=OPENAI_API_VERSION,

    azure_ad_token_provider=get_access_token,

    default_headers={

        "projectId": PROJECT_ID,

        "model-usage-type": "prod"

    }

)


llm = AzureChatOpenAI(

    azure_deployment=CHAT_DEPLOYMENT_NAME,

    azure_endpoint=AZURE_OPENAI_ENDPOINT,

    api_version=OPENAI_API_VERSION,

    azure_ad_token_provider=get_access_token,

    default_headers={

        "projectId": PROJECT_ID,

        "model-usage-type": "prod"

    },

    temperature=0.1

)



# ============================================================

# DATA LOADING

# ============================================================


def load_dataset():


    loader = CSVLoader(

        file_path=DATASET_FILE,

        encoding="utf-8"

    )


    return loader.load()



# ============================================================

# VECTOR STORE

# ============================================================


@retry(

    wait=wait_random_exponential(min=2, max=30),

    stop=stop_after_attempt(5),

    reraise=True

)

def build_vector_store(documents):


    return Chroma.from_documents(

        documents=documents,

        embedding=embeddings,

        persist_directory=VECTOR_DB_DIR

    )



# ============================================================

# BM25 INDEX

# ============================================================


def build_bm25_index(documents):


    corpus = [

        doc.page_content

        for doc in documents

    ]


    tokenized = [

        text.lower().split()

        for text in corpus

    ]


    bm25 = BM25Okapi(tokenized)


    return bm25, corpus



# ============================================================

# RETRIEVAL STRATEGY #1

# Semantic Search

# ============================================================


def semantic_retrieval(

    query,

    vectorstore,

    top_k=3

):


    results = vectorstore.similarity_search(

        query,

        k=top_k

    )


    docs = [

        doc.page_content

        for doc in results

    ]


    return docs



# ============================================================

# RETRIEVAL STRATEGY #2

# Semantic + Threshold Filtering

# ============================================================


def threshold_retrieval(

    query,

    vectorstore,

    threshold=0.70,

    top_k=3

):


    try:


        results = vectorstore.similarity_search_with_relevance_scores(

            query,

            k=10

        )


        filtered_docs = []


        for doc, score in results:


            if score >= threshold:

                filtered_docs.append(

                    doc.page_content

                )


        return filtered_docs[:top_k]


    except Exception:


        return semantic_retrieval(

            query,

            vectorstore,

            top_k

        )



# ============================================================

# RETRIEVAL STRATEGY #3

# Hybrid BM25 + Semantic

# ============================================================


def hybrid_retrieval(

    query,

    vectorstore,

    bm25,

    corpus,

    top_k=3

):


    semantic_results = vectorstore.similarity_search(

        query,

        k=10

    )


    semantic_texts = {

        doc.page_content

        for doc in semantic_results

    }


    bm25_scores = bm25.get_scores(

        query.lower().split()

    )


    ranked_idx = np.argsort(

        bm25_scores

    )[::-1][:10]


    bm25_texts = {

        corpus[idx]

        for idx in ranked_idx

    }


    combined_docs = list(

        semantic_texts.union(

            bm25_texts

        )

    )


    scored_docs = []


    query_embedding = embeddings.embed_query(

        query

    )


    for doc_text in combined_docs:


        try:


            doc_embedding = embeddings.embed_query(

                doc_text[:8000]

            )


            cosine = np.dot(

                query_embedding,

                doc_embedding

            ) / (

                np.linalg.norm(query_embedding)

                * np.linalg.norm(doc_embedding)

            )


            scored_docs.append(

                (

                    doc_text,

                    float(cosine)

                )

            )


        except Exception:

            pass


    scored_docs.sort(

        key=lambda x: x[1],

        reverse=True

    )


    return [

        doc

        for doc, _

        in scored_docs[:top_k]

    ]



# ============================================================

# GENERATION

# ============================================================


@retry(

    wait=wait_random_exponential(min=2, max=30),

    stop=stop_after_attempt(5),

    reraise=True

)

def generate_answer(

    query,

    retrieved_docs

):


    if len(retrieved_docs) == 0:


        return (

            "The question cannot be answered "

            "using the available documents."

        )


    context = "\n\n".join(

        retrieved_docs

    )


    prompt = f"""

You are a clinical intelligence assistant.


IMPORTANT RULES:


1. Use ONLY the provided context.

2. Do NOT use prior medical knowledge.

3. Do NOT hallucinate.

4. If the answer is not present in the context,

   say:

   "The question cannot be answered using the available documents."

5. Cite information only from context.

6. Keep responses concise and factual.


CONTEXT:


{context}


QUESTION:


{query}


ANSWER:

"""


    response = llm.invoke(

        prompt

    )


    return response.content



# ============================================================

# MAIN RAG PIPELINE

# ============================================================


def rag_pipeline(

    query,

    vectorstore,

    bm25,

    corpus,

    retrieval_strategy="hybrid"

):


    if retrieval_strategy == "semantic":


        docs = semantic_retrieval(

            query,

            vectorstore

        )


    elif retrieval_strategy == "threshold":


        docs = threshold_retrieval(

            query,

            vectorstore

        )


    else:


        docs = hybrid_retrieval(

            query,

            vectorstore,

            bm25,

            corpus

        )


    answer = generate_answer(

        query,

        docs

    )


    return {

        "retrieved_documents": docs,

        "generated_answer": answer

    }



# ============================================================

# MAIN

# ============================================================


if __name__ == "__main__":


    print("Loading dataset...")


    documents = load_dataset()


    print(

        f"Documents Loaded: {len(documents)}"

    )


    print("Building vector store...")


    vectorstore = build_vector_store(

        documents

    )


    print("Building BM25 index...")


    bm25, corpus = build_bm25_index(

        documents

    )


    print("Loading questions...")


    questions_df = pd.read_csv(

        TEST_FILE,

        dtype=str

    ).fillna("")


    questions_df[

        "retrieved_documents"

    ] = ""


    questions_df[

        "generated_answer"

    ] = ""


    for idx, row in questions_df.iterrows():


        question = row["question"]


        print("\n" + "=" * 80)

        print(

            f"QUESTION {idx + 1}:"

        )

        print(question)


        result = rag_pipeline(

            query=question,

            vectorstore=vectorstore,

            bm25=bm25,

            corpus=corpus,

            retrieval_strategy="hybrid"

        )


        print("\nANSWER:")

        print(

            result["generated_answer"]

        )


        questions_df.loc[

            idx,

            "retrieved_documents"

        ] = json.dumps(

            result[

                "retrieved_documents"

            ]

        )


        questions_df.loc[

            idx,

            "generated_answer"

        ] = result[

            "generated_answer"

        ]


    submission = questions_df[

        [

            "question",

            "retrieved_documents",

            "generated_answer"

        ]

    ]


    submission.to_csv(

        "submission.csv",

        index=False

    )


    print("\nsubmission.csv created.")

    print(

        f"Rows: {len(submission)}"

    )

 # ============================================================

# SAMPLE OUTPUT

# ============================================================

Loading dataset...

Creating vector store...

Failed to send telemetry event ClientStartEvent: capture() takes 1 positional argument but 3 were given

Failed to send telemetry event ClientCreateCollectionEvent: capture() takes 1 positional argument but 3 were given

Loading questions...

Processing: What are the key features of …

Failed to send telemetry event CollectionQueryEvent: capture() takes 1 positional argument but 3 were given

document_id: 94

document_url: https://...

context: Auto…

---

document_id: 769

document_url: https://...

context: Palm…

---

document_id: 784

document_url: https://...

context: La...

  questions_df.loc[idx, "retrieved_documents"]


  questions_df.loc[idx, "generated_answer"]



No comments:

Post a Comment