Spaces:
Sleeping
Sleeping
made several changes in all scripts
Browse files- Dockerfile +0 -16
- main.py +8 -21
- rag_chatbot.py +3 -16
- rank.py +1 -54
- recommender.py +2 -12
Dockerfile
CHANGED
|
@@ -1,36 +1,20 @@
|
|
| 1 |
-
# Use an official Python runtime as a parent image
|
| 2 |
FROM python:3.10-slim
|
| 3 |
|
| 4 |
-
# Set the working directory in the container
|
| 5 |
WORKDIR /app
|
| 6 |
|
| 7 |
-
# --- THIS IS THE CRITICAL NEW SECTION ---
|
| 8 |
-
# Install system dependencies required for building llama-cpp-python
|
| 9 |
-
# - apt-get update: Refreshes the package list
|
| 10 |
-
# - build-essential: Installs C/C++ compilers (gcc, g++) and make
|
| 11 |
-
# - cmake: The build system generator used by llama-cpp-python
|
| 12 |
-
# - --no-install-recommends: Reduces image size by not installing optional packages
|
| 13 |
-
# - rm -rf /var/lib/apt/lists/*: Cleans up the apt cache to keep the image small
|
| 14 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 15 |
build-essential \
|
| 16 |
cmake \
|
| 17 |
&& rm -rf /var/lib/apt/lists/*
|
| 18 |
-
# --- END OF NEW SECTION ---
|
| 19 |
|
| 20 |
-
# Copy the requirements file into the container at /app
|
| 21 |
COPY requirements.txt .
|
| 22 |
|
| 23 |
-
# Install any needed packages specified in requirements.txt
|
| 24 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 25 |
|
| 26 |
-
# Copy the local 'data' directory into the container's /app/data directory
|
| 27 |
COPY ./data ./data
|
| 28 |
|
| 29 |
-
# Copy the rest of the application source code
|
| 30 |
COPY . .
|
| 31 |
|
| 32 |
-
# Expose the port the app runs on
|
| 33 |
EXPOSE 8000
|
| 34 |
|
| 35 |
-
# Command to run the application
|
| 36 |
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
|
|
|
|
| 1 |
FROM python:3.10-slim
|
| 2 |
|
|
|
|
| 3 |
WORKDIR /app
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 6 |
build-essential \
|
| 7 |
cmake \
|
| 8 |
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
| 9 |
|
|
|
|
| 10 |
COPY requirements.txt .
|
| 11 |
|
|
|
|
| 12 |
RUN pip install --no-cache-dir -r requirements.txt
|
| 13 |
|
|
|
|
| 14 |
COPY ./data ./data
|
| 15 |
|
|
|
|
| 16 |
COPY . .
|
| 17 |
|
|
|
|
| 18 |
EXPOSE 8000
|
| 19 |
|
|
|
|
| 20 |
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
main.py
CHANGED
|
@@ -1,10 +1,9 @@
|
|
| 1 |
-
# main.py
|
| 2 |
from fastapi import FastAPI, HTTPException
|
| 3 |
from fastapi.middleware.cors import CORSMiddleware
|
| 4 |
from pydantic import BaseModel
|
| 5 |
from typing import List
|
| 6 |
import uvicorn
|
| 7 |
-
import traceback
|
| 8 |
from deep_translator import GoogleTranslator
|
| 9 |
from rag_chatbot import RAGChatBot
|
| 10 |
from recommender import QuestionRecommender
|
|
@@ -63,39 +62,33 @@ async def startup_event():
|
|
| 63 |
"""
|
| 64 |
global bot, recommender
|
| 65 |
try:
|
| 66 |
-
# 1. Create the embedding model - THE SINGLE SOURCE OF TRUTH
|
| 67 |
print(f"Loading embedding model: {EMBEDDING_MODEL_NAME}")
|
| 68 |
embeddings = HuggingFaceEmbeddings(
|
| 69 |
model_name=EMBEDDING_MODEL_NAME,
|
| 70 |
model_kwargs={'device': 'cpu'}
|
| 71 |
)
|
| 72 |
-
print("
|
| 73 |
|
| 74 |
-
# 2. Initialize the RAG ChatBot (pass embeddings)
|
| 75 |
print("Loading RAG ChatBot...")
|
| 76 |
-
bot = RAGChatBot(embeddings)
|
| 77 |
|
| 78 |
-
faiss_path = os.path.join("data", "faiss.index")
|
| 79 |
questions_path = os.path.join("data", "questions.npy")
|
| 80 |
|
| 81 |
-
print(f"Attempting to load Recommender index from: '{faiss_path}'")
|
| 82 |
|
| 83 |
-
# Initialize the Question Recommender with the corrected paths
|
| 84 |
recommender = QuestionRecommender(
|
| 85 |
faiss_index_path=faiss_path,
|
| 86 |
questions_path=questions_path,
|
| 87 |
embedding_model=embeddings
|
| 88 |
)
|
| 89 |
-
print("
|
| 90 |
|
| 91 |
except Exception as e:
|
| 92 |
-
print("
|
| 93 |
traceback.print_exc()
|
| 94 |
raise e
|
| 95 |
|
| 96 |
-
|
| 97 |
-
# --- Helper Functions ---
|
| 98 |
-
|
| 99 |
def translate_text(text: str, target_lang: str, source_lang: str = "auto") -> str:
|
| 100 |
if not text or source_lang == target_lang or (target_lang == "en" and source_lang == "auto"):
|
| 101 |
return text
|
|
@@ -119,8 +112,6 @@ def get_bot_response(query_en: str) -> tuple[str, List[str]]:
|
|
| 119 |
|
| 120 |
return answer_en, new_recommendations
|
| 121 |
|
| 122 |
-
# --- API Endpoints ---
|
| 123 |
-
|
| 124 |
@app.get("/")
|
| 125 |
async def root():
|
| 126 |
return {"message": "Sat2Farm AI Assistant API is running!"}
|
|
@@ -155,9 +146,7 @@ async def get_initial_recommendations():
|
|
| 155 |
async def chat(request: ChatRequest):
|
| 156 |
"""Main endpoint to process a user's chat message."""
|
| 157 |
if not bot or not recommender:
|
| 158 |
-
raise HTTPException(status_code=503, detail="Chat service is not available. Models are not loaded.")
|
| 159 |
-
|
| 160 |
-
# ========================== THE FIX IS HERE ==========================
|
| 161 |
try:
|
| 162 |
query_en = translate_text(request.message, target_lang="en", source_lang=request.user_language)
|
| 163 |
|
|
@@ -170,11 +159,9 @@ async def chat(request: ChatRequest):
|
|
| 170 |
recommendations=new_recommendations_en
|
| 171 |
)
|
| 172 |
except Exception as e:
|
| 173 |
-
# This will now print the FULL error traceback to your logs
|
| 174 |
print(f"Error during /chat processing. Full traceback below:")
|
| 175 |
traceback.print_exc()
|
| 176 |
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
|
| 177 |
-
# =====================================================================
|
| 178 |
|
| 179 |
@app.post("/recommendations/action")
|
| 180 |
async def handle_recommendation_action(request: RecommendationRequest):
|
|
|
|
|
|
|
| 1 |
from fastapi import FastAPI, HTTPException
|
| 2 |
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
from pydantic import BaseModel
|
| 4 |
from typing import List
|
| 5 |
import uvicorn
|
| 6 |
+
import traceback
|
| 7 |
from deep_translator import GoogleTranslator
|
| 8 |
from rag_chatbot import RAGChatBot
|
| 9 |
from recommender import QuestionRecommender
|
|
|
|
| 62 |
"""
|
| 63 |
global bot, recommender
|
| 64 |
try:
|
|
|
|
| 65 |
print(f"Loading embedding model: {EMBEDDING_MODEL_NAME}")
|
| 66 |
embeddings = HuggingFaceEmbeddings(
|
| 67 |
model_name=EMBEDDING_MODEL_NAME,
|
| 68 |
model_kwargs={'device': 'cpu'}
|
| 69 |
)
|
| 70 |
+
print("Embedding model loaded.")
|
| 71 |
|
|
|
|
| 72 |
print("Loading RAG ChatBot...")
|
| 73 |
+
bot = RAGChatBot(embeddings)
|
| 74 |
|
| 75 |
+
faiss_path = os.path.join("data", "faiss.index")
|
| 76 |
questions_path = os.path.join("data", "questions.npy")
|
| 77 |
|
| 78 |
+
print(f"Attempting to load Recommender index from: '{faiss_path}'")
|
| 79 |
|
|
|
|
| 80 |
recommender = QuestionRecommender(
|
| 81 |
faiss_index_path=faiss_path,
|
| 82 |
questions_path=questions_path,
|
| 83 |
embedding_model=embeddings
|
| 84 |
)
|
| 85 |
+
print("All models loaded successfully! API is ready.")
|
| 86 |
|
| 87 |
except Exception as e:
|
| 88 |
+
print("Critical Error: Failed to load models during startup:")
|
| 89 |
traceback.print_exc()
|
| 90 |
raise e
|
| 91 |
|
|
|
|
|
|
|
|
|
|
| 92 |
def translate_text(text: str, target_lang: str, source_lang: str = "auto") -> str:
|
| 93 |
if not text or source_lang == target_lang or (target_lang == "en" and source_lang == "auto"):
|
| 94 |
return text
|
|
|
|
| 112 |
|
| 113 |
return answer_en, new_recommendations
|
| 114 |
|
|
|
|
|
|
|
| 115 |
@app.get("/")
|
| 116 |
async def root():
|
| 117 |
return {"message": "Sat2Farm AI Assistant API is running!"}
|
|
|
|
| 146 |
async def chat(request: ChatRequest):
|
| 147 |
"""Main endpoint to process a user's chat message."""
|
| 148 |
if not bot or not recommender:
|
| 149 |
+
raise HTTPException(status_code=503, detail="Chat service is not available. Models are not loaded.")
|
|
|
|
|
|
|
| 150 |
try:
|
| 151 |
query_en = translate_text(request.message, target_lang="en", source_lang=request.user_language)
|
| 152 |
|
|
|
|
| 159 |
recommendations=new_recommendations_en
|
| 160 |
)
|
| 161 |
except Exception as e:
|
|
|
|
| 162 |
print(f"Error during /chat processing. Full traceback below:")
|
| 163 |
traceback.print_exc()
|
| 164 |
raise HTTPException(status_code=500, detail=f"An unexpected error occurred: {str(e)}")
|
|
|
|
| 165 |
|
| 166 |
@app.post("/recommendations/action")
|
| 167 |
async def handle_recommendation_action(request: RecommendationRequest):
|
rag_chatbot.py
CHANGED
|
@@ -1,10 +1,7 @@
|
|
| 1 |
-
# rag_chatbot.py
|
| 2 |
-
|
| 3 |
import os
|
| 4 |
from huggingface_hub import hf_hub_download
|
| 5 |
from langchain_community.document_loaders import PyPDFLoader
|
| 6 |
from langchain_community.vectorstores import FAISS
|
| 7 |
-
# <<< CHANGED: Import from the new, correct package
|
| 8 |
from langchain_huggingface import HuggingFaceEmbeddings
|
| 9 |
from langchain_community.retrievers import BM25Retriever
|
| 10 |
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
|
@@ -16,13 +13,8 @@ from langchain.retrievers.document_compressors import FlashrankRerank
|
|
| 16 |
from flashrank import Ranker
|
| 17 |
from typing import Dict, Any, List
|
| 18 |
|
| 19 |
-
# This is a one-time operation for flashrank, it's fine to keep it here.
|
| 20 |
-
# FlashrankRerank.model_rebuild()
|
| 21 |
-
|
| 22 |
-
# --- Configuration ---
|
| 23 |
PDF_PATH = "data/sat2farm_doc.pdf"
|
| 24 |
-
|
| 25 |
-
# EMBEDDING_MODEL = "BAAI/bge-large-en-v1.5"
|
| 26 |
MODEL_NAME = "TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF"
|
| 27 |
MODEL_FILE = "tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf"
|
| 28 |
|
|
@@ -37,7 +29,6 @@ class RAGChatBot:
|
|
| 37 |
print(f"Downloading GGUF model: {MODEL_NAME}/{MODEL_FILE}")
|
| 38 |
model_path = hf_hub_download(repo_id=MODEL_NAME, filename=MODEL_FILE)
|
| 39 |
|
| 40 |
-
# Use the provided embedding model to build the chain.
|
| 41 |
self.chain = self._create_rag_chain(model_path, embeddings)
|
| 42 |
print("\n✅ RAG ChatBot initialized successfully!")
|
| 43 |
|
|
@@ -54,12 +45,10 @@ class RAGChatBot:
|
|
| 54 |
chunks = text_splitter.split_documents(documents)
|
| 55 |
print(f"Created {len(chunks)} document chunks.")
|
| 56 |
|
| 57 |
-
# STAGE 1: RECALL (Ensemble Retriever)
|
| 58 |
print("Initializing Stage 1 Retriever (Ensemble)...")
|
| 59 |
bm25_retriever = BM25Retriever.from_documents(chunks)
|
| 60 |
bm25_retriever.k = 10
|
| 61 |
|
| 62 |
-
# This line is key: it uses the centrally-managed `embeddings` object.
|
| 63 |
vectorstore = FAISS.from_documents(chunks, embeddings)
|
| 64 |
faiss_retriever = vectorstore.as_retriever(search_kwargs={'k': 10})
|
| 65 |
|
|
@@ -67,7 +56,6 @@ class RAGChatBot:
|
|
| 67 |
retrievers=[bm25_retriever, faiss_retriever], weights=[0.5, 0.5], search_type="rrf"
|
| 68 |
)
|
| 69 |
|
| 70 |
-
# STAGE 2: RE-RANK (Flashrank)
|
| 71 |
print("Initializing Stage 2 Re-ranker (Flashrank)...")
|
| 72 |
compressor = FlashrankRerank(top_n=3)
|
| 73 |
final_retriever = ContextualCompressionRetriever(
|
|
@@ -76,7 +64,7 @@ class RAGChatBot:
|
|
| 76 |
|
| 77 |
print(f"Initializing GGUF LLM from: {model_path}")
|
| 78 |
llm = LlamaCpp(
|
| 79 |
-
model_path=model_path, n_ctx=2048, n_gpu_layers=-1,
|
| 80 |
temperature=0.0, top_k=1, verbose=False, max_tokens=512
|
| 81 |
)
|
| 82 |
|
|
@@ -85,6 +73,7 @@ You are a factual question-answering assistant. Your task is to answer the user'
|
|
| 85 |
Follow these rules strictly:
|
| 86 |
1. Provide only the direct answer to the question and nothing else. DO NOT add any summary, conclusion, or other extra information.
|
| 87 |
2. If the answer is not in the context, state that you do not have that information.
|
|
|
|
| 88 |
|
| 89 |
Context: {context}
|
| 90 |
Question: {question}
|
|
@@ -108,12 +97,10 @@ Helpful Answer:"""
|
|
| 108 |
print(f"Invoking RAG chain with query: '{query}'")
|
| 109 |
result = self.chain.invoke(query)
|
| 110 |
|
| 111 |
-
# Clean up the answer
|
| 112 |
answer = result.get("result", "").strip()
|
| 113 |
if "Helpful Answer:" in answer:
|
| 114 |
answer = answer.split("Helpful Answer:")[1].strip()
|
| 115 |
|
| 116 |
-
# Format sources
|
| 117 |
sources = []
|
| 118 |
if result.get("source_documents"):
|
| 119 |
for doc in result["source_documents"]:
|
|
|
|
|
|
|
|
|
|
| 1 |
import os
|
| 2 |
from huggingface_hub import hf_hub_download
|
| 3 |
from langchain_community.document_loaders import PyPDFLoader
|
| 4 |
from langchain_community.vectorstores import FAISS
|
|
|
|
| 5 |
from langchain_huggingface import HuggingFaceEmbeddings
|
| 6 |
from langchain_community.retrievers import BM25Retriever
|
| 7 |
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
|
|
|
| 13 |
from flashrank import Ranker
|
| 14 |
from typing import Dict, Any, List
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
PDF_PATH = "data/sat2farm_doc.pdf"
|
| 17 |
+
|
|
|
|
| 18 |
MODEL_NAME = "TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF"
|
| 19 |
MODEL_FILE = "tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf"
|
| 20 |
|
|
|
|
| 29 |
print(f"Downloading GGUF model: {MODEL_NAME}/{MODEL_FILE}")
|
| 30 |
model_path = hf_hub_download(repo_id=MODEL_NAME, filename=MODEL_FILE)
|
| 31 |
|
|
|
|
| 32 |
self.chain = self._create_rag_chain(model_path, embeddings)
|
| 33 |
print("\n✅ RAG ChatBot initialized successfully!")
|
| 34 |
|
|
|
|
| 45 |
chunks = text_splitter.split_documents(documents)
|
| 46 |
print(f"Created {len(chunks)} document chunks.")
|
| 47 |
|
|
|
|
| 48 |
print("Initializing Stage 1 Retriever (Ensemble)...")
|
| 49 |
bm25_retriever = BM25Retriever.from_documents(chunks)
|
| 50 |
bm25_retriever.k = 10
|
| 51 |
|
|
|
|
| 52 |
vectorstore = FAISS.from_documents(chunks, embeddings)
|
| 53 |
faiss_retriever = vectorstore.as_retriever(search_kwargs={'k': 10})
|
| 54 |
|
|
|
|
| 56 |
retrievers=[bm25_retriever, faiss_retriever], weights=[0.5, 0.5], search_type="rrf"
|
| 57 |
)
|
| 58 |
|
|
|
|
| 59 |
print("Initializing Stage 2 Re-ranker (Flashrank)...")
|
| 60 |
compressor = FlashrankRerank(top_n=3)
|
| 61 |
final_retriever = ContextualCompressionRetriever(
|
|
|
|
| 64 |
|
| 65 |
print(f"Initializing GGUF LLM from: {model_path}")
|
| 66 |
llm = LlamaCpp(
|
| 67 |
+
model_path=model_path, n_ctx=2048, n_gpu_layers=-1,
|
| 68 |
temperature=0.0, top_k=1, verbose=False, max_tokens=512
|
| 69 |
)
|
| 70 |
|
|
|
|
| 73 |
Follow these rules strictly:
|
| 74 |
1. Provide only the direct answer to the question and nothing else. DO NOT add any summary, conclusion, or other extra information.
|
| 75 |
2. If the answer is not in the context, state that you do not have that information.
|
| 76 |
+
3. The CEO of Satyukt Analytics is Dr. Satkumar Tomar, if user asks question about who is ceo, tell this answer.
|
| 77 |
|
| 78 |
Context: {context}
|
| 79 |
Question: {question}
|
|
|
|
| 97 |
print(f"Invoking RAG chain with query: '{query}'")
|
| 98 |
result = self.chain.invoke(query)
|
| 99 |
|
|
|
|
| 100 |
answer = result.get("result", "").strip()
|
| 101 |
if "Helpful Answer:" in answer:
|
| 102 |
answer = answer.split("Helpful Answer:")[1].strip()
|
| 103 |
|
|
|
|
| 104 |
sources = []
|
| 105 |
if result.get("source_documents"):
|
| 106 |
for doc in result["source_documents"]:
|
rank.py
CHANGED
|
@@ -1,67 +1,19 @@
|
|
| 1 |
-
|
| 2 |
import numpy as np
|
| 3 |
import faiss
|
| 4 |
from sentence_transformers import SentenceTransformer
|
| 5 |
import importlib, subprocess, sys
|
| 6 |
from rank_bm25 import BM25Okapi
|
| 7 |
-
# class HybridChatBot:
|
| 8 |
-
# def __init__(self, model_name="all-MiniLM-L6-v2", index_file="Chatbot/data/faiss.index"):
|
| 9 |
-
# # Load embeddings index
|
| 10 |
-
# self.model = SentenceTransformer(model_name)
|
| 11 |
-
# self.index = faiss.read_index(index_file)
|
| 12 |
-
# self.questions = np.load("Chatbot/data/questions.npy", allow_pickle=True)
|
| 13 |
-
# self.answers = np.load("Chatbot/data/answers.npy", allow_pickle=True)
|
| 14 |
-
|
| 15 |
-
# # Prepare BM25
|
| 16 |
-
# tokenized_corpus = [q.lower().split() for q in self.questions]
|
| 17 |
-
# self.bm25 = BM25Okapi(tokenized_corpus)
|
| 18 |
-
|
| 19 |
-
# def search(self, query, top_k, alpha):
|
| 20 |
-
# """
|
| 21 |
-
# Hybrid search:
|
| 22 |
-
# alpha = weight for BM25 vs embeddings (0.5 = equal weight)
|
| 23 |
-
# """
|
| 24 |
-
# # --- Embedding Search ---
|
| 25 |
-
# query_embedding = self.model.encode([query], convert_to_numpy=True)
|
| 26 |
-
# distances, indices = self.index.search(query_embedding, top_k)
|
| 27 |
-
# embedding_scores = {idx: 1/(1+dist) for idx, dist in zip(indices[0], distances[0])}
|
| 28 |
-
|
| 29 |
-
# # --- BM25 Search ---
|
| 30 |
-
# bm25_scores = self.bm25.get_scores(query.lower().split())
|
| 31 |
-
# bm25_top = np.argsort(bm25_scores)[::-1][:top_k]
|
| 32 |
-
# bm25_scores = {idx: bm25_scores[idx] for idx in bm25_top}
|
| 33 |
-
|
| 34 |
-
# # --- Combine Scores ---
|
| 35 |
-
# combined_scores = {}
|
| 36 |
-
# for idx in set(list(embedding_scores.keys()) + list(bm25_scores.keys())):
|
| 37 |
-
# emb_score = embedding_scores.get(idx, 0)
|
| 38 |
-
# bm_score = bm25_scores.get(idx, 0)
|
| 39 |
-
# combined_scores[idx] = alpha * bm_score + (1 - alpha) * emb_score
|
| 40 |
-
|
| 41 |
-
# # --- Sort and Return ---
|
| 42 |
-
# best = sorted(combined_scores.items(), key=lambda x: x[1], reverse=True)
|
| 43 |
|
| 44 |
-
# results = []
|
| 45 |
-
# for idx, score in best[:top_k]:
|
| 46 |
-
# results.append({
|
| 47 |
-
# "matched_question": self.questions[idx],
|
| 48 |
-
# "answer": self.answers[idx],
|
| 49 |
-
# "score": float(score)
|
| 50 |
-
# })
|
| 51 |
-
# return results
|
| 52 |
class HybridChatBot:
|
| 53 |
def __init__(self, model_name="all-MiniLM-L6-v2", index_file="data/faiss.index", fallback_threshold=0.05):
|
| 54 |
-
# Load embeddings index
|
| 55 |
self.model = SentenceTransformer(model_name)
|
| 56 |
self.index = faiss.read_index(index_file)
|
| 57 |
self.questions = np.load("data/questions.npy", allow_pickle=True)
|
| 58 |
self.answers = np.load("data/answers.npy", allow_pickle=True)
|
| 59 |
|
| 60 |
-
# Prepare BM25
|
| 61 |
tokenized_corpus = [q.lower().split() for q in self.questions]
|
| 62 |
self.bm25 = BM25Okapi(tokenized_corpus)
|
| 63 |
|
| 64 |
-
# Threshold for fallback
|
| 65 |
self.fallback_threshold = fallback_threshold
|
| 66 |
|
| 67 |
def search(self, query, top_k=5, alpha=0.5):
|
|
@@ -69,29 +21,24 @@ class HybridChatBot:
|
|
| 69 |
Hybrid search:
|
| 70 |
alpha = weight for BM25 vs embeddings (0.5 = equal weight)
|
| 71 |
"""
|
| 72 |
-
# --- Embedding Search ---
|
| 73 |
query_embedding = self.model.encode([query], convert_to_numpy=True)
|
| 74 |
distances, indices = self.index.search(query_embedding, top_k)
|
| 75 |
embedding_scores = {idx: 1/(1+dist) for idx, dist in zip(indices[0], distances[0])}
|
| 76 |
|
| 77 |
-
# --- BM25 Search ---
|
| 78 |
bm25_scores = self.bm25.get_scores(query.lower().split())
|
| 79 |
bm25_top = np.argsort(bm25_scores)[::-1][:top_k]
|
| 80 |
bm25_scores = {idx: bm25_scores[idx] for idx in bm25_top}
|
| 81 |
|
| 82 |
-
# --- Combine Scores ---
|
| 83 |
combined_scores = {}
|
| 84 |
for idx in set(list(embedding_scores.keys()) + list(bm25_scores.keys())):
|
| 85 |
emb_score = embedding_scores.get(idx, 0)
|
| 86 |
bm_score = bm25_scores.get(idx, 0)
|
| 87 |
combined_scores[idx] = alpha * bm_score + (1 - alpha) * emb_score
|
| 88 |
|
| 89 |
-
# --- Sort and Return ---
|
| 90 |
best = sorted(combined_scores.items(), key=lambda x: x[1], reverse=True)
|
| 91 |
|
| 92 |
results = []
|
| 93 |
if not best or best[0][1] < self.fallback_threshold:
|
| 94 |
-
# Low confidence → fallback message
|
| 95 |
results.append({
|
| 96 |
"matched_question": None,
|
| 97 |
"answer": "Sorry, I couldn't find a reliable answer. Please contact our support team.",
|
|
@@ -105,4 +52,4 @@ class HybridChatBot:
|
|
| 105 |
"score": float(score)
|
| 106 |
})
|
| 107 |
|
| 108 |
-
return results
|
|
|
|
|
|
|
| 1 |
import numpy as np
|
| 2 |
import faiss
|
| 3 |
from sentence_transformers import SentenceTransformer
|
| 4 |
import importlib, subprocess, sys
|
| 5 |
from rank_bm25 import BM25Okapi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
class HybridChatBot:
|
| 8 |
def __init__(self, model_name="all-MiniLM-L6-v2", index_file="data/faiss.index", fallback_threshold=0.05):
|
|
|
|
| 9 |
self.model = SentenceTransformer(model_name)
|
| 10 |
self.index = faiss.read_index(index_file)
|
| 11 |
self.questions = np.load("data/questions.npy", allow_pickle=True)
|
| 12 |
self.answers = np.load("data/answers.npy", allow_pickle=True)
|
| 13 |
|
|
|
|
| 14 |
tokenized_corpus = [q.lower().split() for q in self.questions]
|
| 15 |
self.bm25 = BM25Okapi(tokenized_corpus)
|
| 16 |
|
|
|
|
| 17 |
self.fallback_threshold = fallback_threshold
|
| 18 |
|
| 19 |
def search(self, query, top_k=5, alpha=0.5):
|
|
|
|
| 21 |
Hybrid search:
|
| 22 |
alpha = weight for BM25 vs embeddings (0.5 = equal weight)
|
| 23 |
"""
|
|
|
|
| 24 |
query_embedding = self.model.encode([query], convert_to_numpy=True)
|
| 25 |
distances, indices = self.index.search(query_embedding, top_k)
|
| 26 |
embedding_scores = {idx: 1/(1+dist) for idx, dist in zip(indices[0], distances[0])}
|
| 27 |
|
|
|
|
| 28 |
bm25_scores = self.bm25.get_scores(query.lower().split())
|
| 29 |
bm25_top = np.argsort(bm25_scores)[::-1][:top_k]
|
| 30 |
bm25_scores = {idx: bm25_scores[idx] for idx in bm25_top}
|
| 31 |
|
|
|
|
| 32 |
combined_scores = {}
|
| 33 |
for idx in set(list(embedding_scores.keys()) + list(bm25_scores.keys())):
|
| 34 |
emb_score = embedding_scores.get(idx, 0)
|
| 35 |
bm_score = bm25_scores.get(idx, 0)
|
| 36 |
combined_scores[idx] = alpha * bm_score + (1 - alpha) * emb_score
|
| 37 |
|
|
|
|
| 38 |
best = sorted(combined_scores.items(), key=lambda x: x[1], reverse=True)
|
| 39 |
|
| 40 |
results = []
|
| 41 |
if not best or best[0][1] < self.fallback_threshold:
|
|
|
|
| 42 |
results.append({
|
| 43 |
"matched_question": None,
|
| 44 |
"answer": "Sorry, I couldn't find a reliable answer. Please contact our support team.",
|
|
|
|
| 52 |
"score": float(score)
|
| 53 |
})
|
| 54 |
|
| 55 |
+
return results
|
recommender.py
CHANGED
|
@@ -1,8 +1,6 @@
|
|
| 1 |
-
# recommend.py (Upgraded Version)
|
| 2 |
-
|
| 3 |
import faiss
|
| 4 |
import numpy as np
|
| 5 |
-
from langchain_community.embeddings import HuggingFaceEmbeddings
|
| 6 |
|
| 7 |
class QuestionRecommender:
|
| 8 |
def __init__(self, faiss_index_path, questions_path, embedding_model: HuggingFaceEmbeddings, top_k=5):
|
|
@@ -12,7 +10,7 @@ class QuestionRecommender:
|
|
| 12 |
print("Initializing Question Recommender...")
|
| 13 |
self.index = faiss.read_index(faiss_index_path)
|
| 14 |
self.questions = np.load(questions_path, allow_pickle=True)
|
| 15 |
-
self.embedding_model = embedding_model
|
| 16 |
self.top_k = top_k
|
| 17 |
self.start_questions = [
|
| 18 |
"What is Sat2Farm?",
|
|
@@ -39,23 +37,17 @@ class QuestionRecommender:
|
|
| 39 |
self.history.append(self.current_recommendations)
|
| 40 |
|
| 41 |
embedding = None
|
| 42 |
-
# First, try the fast path: see if the query is a known question
|
| 43 |
try:
|
| 44 |
q_idx = np.where(self.questions == query)[0][0]
|
| 45 |
-
# If found, reconstruct its embedding directly from the index (very fast)
|
| 46 |
embedding = self.index.reconstruct(int(q_idx)).reshape(1, -1)
|
| 47 |
print(f"Recommending based on known question: '{query}'")
|
| 48 |
except IndexError:
|
| 49 |
-
# This is the new, powerful part!
|
| 50 |
-
# If the query is not in our list, embed it on the fly.
|
| 51 |
print(f"Recommending based on new user query: '{query}'")
|
| 52 |
embedding = np.array(self.embedding_model.embed_query(query)).reshape(1, -1)
|
| 53 |
|
| 54 |
if embedding is not None:
|
| 55 |
-
# Search for similar question embeddings in our FAISS index
|
| 56 |
distances, indices = self.index.search(embedding, self.top_k + 1)
|
| 57 |
|
| 58 |
-
# Create the list of recommended questions from the search results
|
| 59 |
recommended = [
|
| 60 |
self.questions[i] for i in indices[0]
|
| 61 |
if i < len(self.questions) and self.questions[i] != query
|
|
@@ -64,10 +56,8 @@ class QuestionRecommender:
|
|
| 64 |
self.current_recommendations = recommended[:self.top_k]
|
| 65 |
return self.current_recommendations
|
| 66 |
|
| 67 |
-
# Fallback if something went wrong
|
| 68 |
return self.start_questions
|
| 69 |
|
| 70 |
-
|
| 71 |
def go_back(self):
|
| 72 |
"""Returns the previous set of recommended questions from history."""
|
| 73 |
if self.history:
|
|
|
|
|
|
|
|
|
|
| 1 |
import faiss
|
| 2 |
import numpy as np
|
| 3 |
+
from langchain_community.embeddings import HuggingFaceEmbeddings
|
| 4 |
|
| 5 |
class QuestionRecommender:
|
| 6 |
def __init__(self, faiss_index_path, questions_path, embedding_model: HuggingFaceEmbeddings, top_k=5):
|
|
|
|
| 10 |
print("Initializing Question Recommender...")
|
| 11 |
self.index = faiss.read_index(faiss_index_path)
|
| 12 |
self.questions = np.load(questions_path, allow_pickle=True)
|
| 13 |
+
self.embedding_model = embedding_model
|
| 14 |
self.top_k = top_k
|
| 15 |
self.start_questions = [
|
| 16 |
"What is Sat2Farm?",
|
|
|
|
| 37 |
self.history.append(self.current_recommendations)
|
| 38 |
|
| 39 |
embedding = None
|
|
|
|
| 40 |
try:
|
| 41 |
q_idx = np.where(self.questions == query)[0][0]
|
|
|
|
| 42 |
embedding = self.index.reconstruct(int(q_idx)).reshape(1, -1)
|
| 43 |
print(f"Recommending based on known question: '{query}'")
|
| 44 |
except IndexError:
|
|
|
|
|
|
|
| 45 |
print(f"Recommending based on new user query: '{query}'")
|
| 46 |
embedding = np.array(self.embedding_model.embed_query(query)).reshape(1, -1)
|
| 47 |
|
| 48 |
if embedding is not None:
|
|
|
|
| 49 |
distances, indices = self.index.search(embedding, self.top_k + 1)
|
| 50 |
|
|
|
|
| 51 |
recommended = [
|
| 52 |
self.questions[i] for i in indices[0]
|
| 53 |
if i < len(self.questions) and self.questions[i] != query
|
|
|
|
| 56 |
self.current_recommendations = recommended[:self.top_k]
|
| 57 |
return self.current_recommendations
|
| 58 |
|
|
|
|
| 59 |
return self.start_questions
|
| 60 |
|
|
|
|
| 61 |
def go_back(self):
|
| 62 |
"""Returns the previous set of recommended questions from history."""
|
| 63 |
if self.history:
|