# ================================ # Import Libraries # ================================ import os import torch from pathlib import Path from dotenv import load_dotenv from huggingface_hub import login from langchain_core.documents import Document from langchain_core.messages import HumanMessage, SystemMessage from langchain_chroma import Chroma from langchain_huggingface import HuggingFaceEmbeddings, ChatHuggingFace, HuggingFacePipeline from langchain_core.prompts import PromptTemplate from langchain_core.output_parsers import StrOutputParser from transformers import ( AutoTokenizer, AutoModelForCausalLM, pipeline, BitsAndBytesConfig, GenerationConfig, ) from sentence_transformers import CrossEncoder # ================================ # Environment Setup # ================================ load_dotenv(override=True) HF_TOKEN = os.getenv("HF_TOKEN") MODEL = os.getenv("QWEN_MODELS") login(token=HF_TOKEN, add_to_git_credential=True) device = "cuda" if torch.cuda.is_available() else "cpu" print(f"Device Using: {device}") # ================================ # Embedding Model # ================================ EMBEDDING_MODELS = HuggingFaceEmbeddings( model_name=os.getenv("EMBEDDING_MODELS") ) # ================================ # Vector Database # ================================ DB_NAME = str(Path(__file__).parent.parent/"vector_db") RETRIEVAL_K = 200 RERANK_K = 30 vectorstore = Chroma( persist_directory=DB_NAME, embedding_function=EMBEDDING_MODELS ) retriever = vectorstore.as_retriever( search_kwargs={ "k": RETRIEVAL_K } ) # ================================ # Cross Encoder Reranker # ================================ reranker = CrossEncoder( "cross-encoder/ms-marco-MiniLM-L-12-v2", device=device, max_length=512 ) # ================================ # LLM Setup (Qwen) # ================================ bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_use_double_quant=True, bnb_4bit_compute_dtype=torch.float16, bnb_4bit_quant_type="nf4", llm_int8_enable_fp32_cpu_offload=True ) tokenizer = AutoTokenizer.from_pretrained( MODEL, trust_remote_code=True ) tokenizer.pad_token = tokenizer.eos_token model = AutoModelForCausalLM.from_pretrained( MODEL, device_map="auto", quantization_config=bnb_config, ) generation_config = GenerationConfig( max_new_tokens=512, temperature=0.0, top_p=0.9, pad_token_id=tokenizer.pad_token_id, eos_token_id=tokenizer.eos_token_id ) model.generation_config = generation_config text_pipeline = pipeline( "text-generation", model=model, tokenizer=tokenizer, return_full_text=False ) hf_llm = HuggingFacePipeline(pipeline=text_pipeline) llm = ChatHuggingFace(llm=hf_llm) # ================================ # System Prompt # ================================ SYSTEM_PROMPT = """ You are a Python documentation assistant. Answer the question using ONLY the provided context from the official Python documentation. Rules: - Use exact Python terms from the context (modules, functions, classes, exceptions). - Do not add information not present in the context. - If the answer is not in the context, respond exactly: I do not know. Context: {context} """ # ================================ # Query Rewrite Prompt # ================================ QUERY_REWRITE_PROMPT = PromptTemplate.from_template( """ Rewrite the query for Python documentation retrieval. Put the core concept first, then add related Python identifiers and keywords. For comparisons, include both terms. Keep output under 15 words, no punctuation. Examples: input: What is a lambda function? output: lambda anonymous function expression syntax callable input: What is the difference between a list and a tuple? output: list tuple mutable immutable sequence difference input: What is the purpose of __init__? output: __init__ constructor initialization instance object class Return only the rewritten query. Query: {question} """ ) # ================================ # Query Rewrite Chain # ================================ query_rewrite_chain = QUERY_REWRITE_PROMPT | llm | StrOutputParser() # ================================ # Cross Encoder Reranking # ================================ def rerank_documents(query: str, docs: list[Document], top_k: int = RERANK_K): if not docs: return [] pairs = [ (query, doc.page_content) for doc in docs ] scores = reranker.predict( pairs, batch_size=4 ) ranked_docs = sorted( zip(docs, scores), key=lambda x: x[1], reverse=True ) return [doc for doc, _ in ranked_docs[:top_k]] def rewrite_query(question: str) -> str: # Step 1 — Rewrite Query rewritten_query = query_rewrite_chain.invoke({ "question": question }) return rewritten_query # ================================ # Retrieval Pipeline # ================================ def fetch_context(question: str) -> list[Document]: # Step 1 — Vector Retrieval docs = retriever.invoke(question) # Step 2 — CrossEncoder Rerank docs = rerank_documents(question, docs) return docs # ================================ # Answer Question # ================================ def answer_question( question: str, history: list[dict] = [], use_rewrite: bool = False, eval_mode: bool = False ) -> tuple[str, list[Document]]: history = history or [] query = rewrite_query(question=question) if use_rewrite else question docs = fetch_context(question=query) if eval_mode: docs = docs[:10] context = "\n\n".join(doc.page_content for doc in docs) system_prompt = SYSTEM_PROMPT.format( context=context ) messages = [ SystemMessage(content=system_prompt), HumanMessage(content=question) ] answer = llm.invoke(messages).content return answer, docs