added vectordb
Browse files- .gitignore +1 -1
- chroma/__init__.py +0 -0
- chroma/chatbot_chroma_db.py +186 -0
- chroma/chatbot_retriever.py +259 -0
.gitignore
CHANGED
|
@@ -3,5 +3,5 @@ __pycache__/
|
|
| 3 |
.venv/
|
| 4 |
|
| 5 |
models/
|
| 6 |
-
|
| 7 |
*.sqlite3
|
|
|
|
| 3 |
.venv/
|
| 4 |
|
| 5 |
models/
|
| 6 |
+
|
| 7 |
*.sqlite3
|
chroma/__init__.py
ADDED
|
File without changes
|
chroma/chatbot_chroma_db.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import chromadb
|
| 2 |
+
from sklearn.cluster import KMeans
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import json
|
| 5 |
+
import ast
|
| 6 |
+
from itertools import groupby
|
| 7 |
+
import uuid
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
root_dir = Path(__file__).parent.parent
|
| 12 |
+
data_dir = root_dir / "datasets"
|
| 13 |
+
|
| 14 |
+
embed_faq_df = pd.read_csv(data_dir / "processed" / "bank_faq" / "embed_faq.csv")
|
| 15 |
+
|
| 16 |
+
# chroma db
|
| 17 |
+
client = chromadb.PersistentClient(path="./chroma_db")
|
| 18 |
+
|
| 19 |
+
# client.delete_collection("bank_faq")
|
| 20 |
+
# client.delete_collection("chat_history")
|
| 21 |
+
|
| 22 |
+
# create collection
|
| 23 |
+
collection = client.get_or_create_collection(
|
| 24 |
+
name="bank_faq",
|
| 25 |
+
metadata={"hnsw:space": "cosine"} # cosine similarity for sentence transformers
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
embed_faq_df["embedding"] = embed_faq_df["embedding"].apply(ast.literal_eval)
|
| 29 |
+
# prepare data
|
| 30 |
+
# ids = [str(i) for i in range(len(embed_faq_df))]
|
| 31 |
+
ids = [
|
| 32 |
+
f"doc_{row['source_id']}_chunk_{row['chunk_num']}"
|
| 33 |
+
for _, row in embed_faq_df.iterrows()
|
| 34 |
+
]
|
| 35 |
+
embeddings = embed_faq_df["embedding"].tolist()
|
| 36 |
+
documents = embed_faq_df["text"].tolist()
|
| 37 |
+
metadatas = [
|
| 38 |
+
{
|
| 39 |
+
"doc_id": f"doc_{row['source_id']}_chunk_{row['chunk_num']}",
|
| 40 |
+
"domain": row["domain"],
|
| 41 |
+
"source_id": row["source_id"],
|
| 42 |
+
"chunk_num": row["chunk_num"],
|
| 43 |
+
"question": row["question"]
|
| 44 |
+
}
|
| 45 |
+
for _, row in embed_faq_df.iterrows()
|
| 46 |
+
]
|
| 47 |
+
|
| 48 |
+
# print(ids)
|
| 49 |
+
# print(type(embeddings[0]))
|
| 50 |
+
# print(documents)
|
| 51 |
+
# print(metadatas)
|
| 52 |
+
|
| 53 |
+
# store
|
| 54 |
+
collection.add(
|
| 55 |
+
ids=ids,
|
| 56 |
+
embeddings=embeddings,
|
| 57 |
+
documents=documents,
|
| 58 |
+
metadatas=metadatas
|
| 59 |
+
)
|
| 60 |
+
|
| 61 |
+
print(f"Stored {collection.count()} documents")
|
| 62 |
+
|
| 63 |
+
faq_collection = client.get_collection(name="bank_faq")
|
| 64 |
+
results = faq_collection.get(
|
| 65 |
+
include=["documents", "metadatas"]
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
source_id_to_doc_ids = {}
|
| 69 |
+
for doc_id, meta in zip(ids, metadatas):
|
| 70 |
+
source_id = meta["source_id"]
|
| 71 |
+
if source_id not in source_id_to_doc_ids:
|
| 72 |
+
source_id_to_doc_ids[source_id] = []
|
| 73 |
+
source_id_to_doc_ids[source_id].append(doc_id)
|
| 74 |
+
|
| 75 |
+
# build a dict grouped by source_id
|
| 76 |
+
groups = {}
|
| 77 |
+
for doc_id, meta, text in zip(results["ids"], results["metadatas"], results["documents"]):
|
| 78 |
+
source_id = meta["source_id"]
|
| 79 |
+
if source_id not in groups:
|
| 80 |
+
groups[source_id] = []
|
| 81 |
+
groups[source_id].append({
|
| 82 |
+
"doc_id": doc_id,
|
| 83 |
+
"chunk_num": int(meta["chunk_num"]),
|
| 84 |
+
"domain": meta["domain"],
|
| 85 |
+
"question": meta["question"],
|
| 86 |
+
"text": text.split("A:", 1)[1].strip() if "A:" in text else text,
|
| 87 |
+
})
|
| 88 |
+
|
| 89 |
+
# build viewable
|
| 90 |
+
viewable = []
|
| 91 |
+
for source_id, chunks in groups.items():
|
| 92 |
+
chunks = sorted(chunks, key=lambda x: x["chunk_num"]) # sort by chunk order
|
| 93 |
+
|
| 94 |
+
entry = {
|
| 95 |
+
"id": source_id,
|
| 96 |
+
"domain": chunks[0]["domain"],
|
| 97 |
+
"question": chunks[0]["question"],
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
if len(chunks) == 1:
|
| 101 |
+
entry["answer"] = chunks[0]["text"]
|
| 102 |
+
else:
|
| 103 |
+
entry["answer"] = {"chunks": [{"chunk": c["chunk_num"] + 1, "doc_id": c["doc_id"], "text": c["text"]} for c in chunks]}
|
| 104 |
+
|
| 105 |
+
viewable.append(entry)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
with open("chroma_preview.json", "w") as f:
|
| 109 |
+
json.dump(viewable, f, indent=2)
|
| 110 |
+
|
| 111 |
+
print("Saved to chroma_preview.json")
|
| 112 |
+
|
| 113 |
+
with open("chroma_preview.json", "r") as f: # or test.json / val.json
|
| 114 |
+
faq_db = json.load(f)
|
| 115 |
+
|
| 116 |
+
for item in faq_db:
|
| 117 |
+
source_id = item["id"]
|
| 118 |
+
item["relevant_doc_ids"] = source_id_to_doc_ids.get(source_id, [])
|
| 119 |
+
|
| 120 |
+
with open("chroma_preview.json", "w") as f:
|
| 121 |
+
json.dump(faq_db, f, indent=2)
|
| 122 |
+
|
| 123 |
+
# save chats in db
|
| 124 |
+
chat_collection = client.get_or_create_collection(name="chat_history")
|
| 125 |
+
|
| 126 |
+
def create_new_chat():
|
| 127 |
+
# creates a new chat session and returns its ID
|
| 128 |
+
chat_id = str(uuid.uuid4())
|
| 129 |
+
print(f"New chat created: {chat_id}")
|
| 130 |
+
return chat_id
|
| 131 |
+
|
| 132 |
+
def get_chat_history(chat_id):
|
| 133 |
+
"""Retrieve full history of a specific chat."""
|
| 134 |
+
results = chat_collection.get(
|
| 135 |
+
where={"chat_id": chat_id}
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
# Pair up queries and answers
|
| 139 |
+
history = []
|
| 140 |
+
for doc, meta in zip(results["documents"], results["metadatas"]):
|
| 141 |
+
history.append({
|
| 142 |
+
"query": doc,
|
| 143 |
+
"answer": meta["answer"],
|
| 144 |
+
"timestamp": meta["timestamp"]
|
| 145 |
+
})
|
| 146 |
+
|
| 147 |
+
# Sort by timestamp
|
| 148 |
+
history.sort(key=lambda x: x["timestamp"])
|
| 149 |
+
return history
|
| 150 |
+
|
| 151 |
+
def load_chats_from_db():
|
| 152 |
+
# rebuild the chats dict from ChromaDB on page load
|
| 153 |
+
try:
|
| 154 |
+
results = chat_collection.get(include=["metadatas", "documents"])
|
| 155 |
+
except Exception:
|
| 156 |
+
return {}
|
| 157 |
+
|
| 158 |
+
chats = {}
|
| 159 |
+
for doc, meta in zip(results["documents"], results["metadatas"]):
|
| 160 |
+
chat_id = meta.get("chat_id")
|
| 161 |
+
chat_name = meta.get("chat_name", "Chat 1")
|
| 162 |
+
timestamp = meta.get("timestamp", "")
|
| 163 |
+
query = doc
|
| 164 |
+
answer = meta.get("answer", "")
|
| 165 |
+
|
| 166 |
+
if chat_name not in chats:
|
| 167 |
+
chats[chat_name] = {"messages": [], "chat_id": chat_id}
|
| 168 |
+
|
| 169 |
+
chats[chat_name]["messages"].append({"role": "user", "content": query, "timestamp": timestamp})
|
| 170 |
+
chats[chat_name]["messages"].append({"role": "assistant", "content": answer, "timestamp": timestamp})
|
| 171 |
+
|
| 172 |
+
# sort messages within each chat by timestamp
|
| 173 |
+
for chat_name in chats:
|
| 174 |
+
chats[chat_name]["messages"].sort(key=lambda x: x.get("timestamp", ""))
|
| 175 |
+
|
| 176 |
+
return chats
|
| 177 |
+
|
| 178 |
+
def delete_chat(chat_id: str):
|
| 179 |
+
"""Remove a chat from ChromaDB and the JSON log."""
|
| 180 |
+
# Remove from ChromaDB
|
| 181 |
+
results = chat_collection.get(where={"chat_id": chat_id})
|
| 182 |
+
if results["ids"]:
|
| 183 |
+
chat_collection.delete(ids=results["ids"])
|
| 184 |
+
|
| 185 |
+
print(f"Chat deleted: {chat_id}")
|
| 186 |
+
|
chroma/chatbot_retriever.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import chromadb
|
| 2 |
+
from langchain_ollama import ChatOllama
|
| 3 |
+
from langchain_classic.chains import create_history_aware_retriever, create_retrieval_chain
|
| 4 |
+
from langchain_classic.chains.combine_documents import create_stuff_documents_chain
|
| 5 |
+
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
|
| 6 |
+
from langchain_chroma import Chroma
|
| 7 |
+
from langchain_huggingface import HuggingFaceEmbeddings
|
| 8 |
+
from langchain_core.messages import HumanMessage, AIMessage
|
| 9 |
+
from chroma.chatbot_chroma_db import create_new_chat, get_chat_history
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
import uuid
|
| 12 |
+
from presidio_analyzer import AnalyzerEngine, PatternRecognizer, Pattern
|
| 13 |
+
from presidio_anonymizer import AnonymizerEngine
|
| 14 |
+
from presidio_analyzer import PatternRecognizer, Pattern
|
| 15 |
+
from presidio_anonymizer.entities import OperatorConfig
|
| 16 |
+
from presidio_analyzer.nlp_engine import NlpEngineProvider
|
| 17 |
+
from sentence_transformers import CrossEncoder
|
| 18 |
+
from langchain_classic.retrievers import ContextualCompressionRetriever
|
| 19 |
+
from langchain_classic.retrievers.document_compressors import CrossEncoderReranker
|
| 20 |
+
from langchain_community.cross_encoders import HuggingFaceCrossEncoder
|
| 21 |
+
from sentence_transformers import CrossEncoder
|
| 22 |
+
from sentence_transformers import SentenceTransformer
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# load model and collection
|
| 27 |
+
# reranking_model = CrossEncoder("cross-encoder/ms-marco-MiniLM-L-6-v2")
|
| 28 |
+
# reranking_model.save("../models/reranker")
|
| 29 |
+
# model = SentenceTransformer("multi-qa-mpnet-base-dot-v1")
|
| 30 |
+
# model.save("../models/embedder")
|
| 31 |
+
|
| 32 |
+
rerank_model = HuggingFaceCrossEncoder(model_name="./models/reranker")
|
| 33 |
+
compressor = CrossEncoderReranker(model=rerank_model, top_n=3)
|
| 34 |
+
model = HuggingFaceEmbeddings(model_name="./models/embedder")
|
| 35 |
+
client = chromadb.PersistentClient(path="./chroma_db")
|
| 36 |
+
chat_collection = client.get_collection("chat_history", embedding_function=None)
|
| 37 |
+
|
| 38 |
+
print(client.list_collections())
|
| 39 |
+
|
| 40 |
+
vectorstore = Chroma(
|
| 41 |
+
client = client,
|
| 42 |
+
collection_name = "bank_faq",
|
| 43 |
+
embedding_function = model
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
retriever = vectorstore.as_retriever(
|
| 47 |
+
search_type = "similarity",
|
| 48 |
+
search_kwargs = {"k": 3}
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
reranking_retriever = ContextualCompressionRetriever(
|
| 52 |
+
base_compressor=compressor,
|
| 53 |
+
base_retriever=vectorstore.as_retriever(search_kwargs={"k": 20})
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
llm = ChatOllama(model="llama3.2", temperature=0)
|
| 57 |
+
|
| 58 |
+
# contextualize qn
|
| 59 |
+
def contextualize_question():
|
| 60 |
+
question_reformulation_prompt = """
|
| 61 |
+
Given a chat history and the latest user question
|
| 62 |
+
which might reference context in the chat history,
|
| 63 |
+
formulate a standalone question that can be understood
|
| 64 |
+
without the chat history. Do NOT answer the question,
|
| 65 |
+
just reformulate it if needed and otherwise return it as is."""
|
| 66 |
+
|
| 67 |
+
question_reformulation_template = ChatPromptTemplate.from_messages([
|
| 68 |
+
("system", question_reformulation_prompt),
|
| 69 |
+
MessagesPlaceholder("chat_history"),
|
| 70 |
+
("human", "{input}"),
|
| 71 |
+
])
|
| 72 |
+
|
| 73 |
+
return create_history_aware_retriever(llm, reranking_retriever, question_reformulation_template)
|
| 74 |
+
|
| 75 |
+
# answer qn
|
| 76 |
+
def answer_question():
|
| 77 |
+
# If the answer is not in the context, say "I don't have information on that.
|
| 78 |
+
answer_prompt = """You are a helpful banking assistant.
|
| 79 |
+
Answer the user's question using ONLY the context below.
|
| 80 |
+
|
| 81 |
+
Before answering, check whether the user's message:
|
| 82 |
+
1. Contains sensitive information (account numbers, passwords, PINs, OTPs, card numbers, etc.), OR
|
| 83 |
+
2. Requests a banking transaction or account-specific action (transfers, payments, account changes, balance checks, etc.).
|
| 84 |
+
|
| 85 |
+
If either condition is true:
|
| 86 |
+
- Do NOT process, verify, store, or repeat the information.
|
| 87 |
+
- Inform the user not to share sensitive information.
|
| 88 |
+
- Explain that account-specific actions cannot be performed through this chatbot.
|
| 89 |
+
- Do not ask for additional sensitive information.
|
| 90 |
+
- Do not continue the transaction.
|
| 91 |
+
|
| 92 |
+
Only after passing the security check should you use the context to answer the question.
|
| 93 |
+
|
| 94 |
+
If the answer is clearly in the context, answer it directly and MUST provide it.
|
| 95 |
+
|
| 96 |
+
If the answer is NOT in the context, first consider whether the question
|
| 97 |
+
might be vague or incomplete. If so, ask a single clarifying question to
|
| 98 |
+
help the user elaborate (e.g. "Could you tell me more about what you're
|
| 99 |
+
looking for?", "Are you referring to a specific account type or product?").
|
| 100 |
+
|
| 101 |
+
Do not generate information on your own that is not within the context.
|
| 102 |
+
|
| 103 |
+
Do not say 'I don't have information' if the context contains relevant details.
|
| 104 |
+
|
| 105 |
+
Only say "I don't have information on that" if:
|
| 106 |
+
- The question is already specific and clear, AND
|
| 107 |
+
- The answer is genuinely not in the context."
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
Context: {context}"""
|
| 111 |
+
|
| 112 |
+
answer_template = ChatPromptTemplate.from_messages([
|
| 113 |
+
("system", answer_prompt),
|
| 114 |
+
MessagesPlaceholder("chat_history"),
|
| 115 |
+
("human", "{input}"),
|
| 116 |
+
])
|
| 117 |
+
|
| 118 |
+
return create_stuff_documents_chain(llm, answer_template)
|
| 119 |
+
|
| 120 |
+
history_aware_retriever = contextualize_question()
|
| 121 |
+
answer_chain = answer_question()
|
| 122 |
+
|
| 123 |
+
rag_chain = create_retrieval_chain(history_aware_retriever, answer_chain)
|
| 124 |
+
|
| 125 |
+
chat_history = []
|
| 126 |
+
|
| 127 |
+
# analyzer = AnalyzerEngine()
|
| 128 |
+
anonymizer = AnonymizerEngine()
|
| 129 |
+
|
| 130 |
+
provider = NlpEngineProvider(nlp_configuration={
|
| 131 |
+
"nlp_engine_name": "spacy",
|
| 132 |
+
"models": [{"lang_code": "en", "model_name": "en_core_web_lg"}]
|
| 133 |
+
})
|
| 134 |
+
|
| 135 |
+
analyzer = AnalyzerEngine(nlp_engine=provider.create_engine())
|
| 136 |
+
|
| 137 |
+
account_recognizer = PatternRecognizer(
|
| 138 |
+
supported_entity="ACCOUNT_NUMBER",
|
| 139 |
+
patterns=[Pattern(name="account_number", regex=r"\d{3}-\d{3}-\d{3}", score=0.8)],
|
| 140 |
+
context=["account", "acct", "num", "number", "no.", "#"]
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
analyzer.registry.add_recognizer(account_recognizer)
|
| 144 |
+
|
| 145 |
+
def _analyze(text: str):
|
| 146 |
+
return analyzer.analyze(text=text, language="en", entities=["ACCOUNT_NUMBER", "PERSON", "PHONE_NUMBER", "EMAIL_ADDRESS", "CREDIT_CARD"])
|
| 147 |
+
|
| 148 |
+
def scrub_pii(text: str) -> str:
|
| 149 |
+
results = _analyze(text)
|
| 150 |
+
scrubbed = anonymizer.anonymize(text=text, analyzer_results=results)
|
| 151 |
+
return scrubbed.text
|
| 152 |
+
|
| 153 |
+
SECURITY_MESSAGE = (
|
| 154 |
+
"""
|
| 155 |
+
For your security and privacy, please do not share sensitive
|
| 156 |
+
information such as account numbers, passwords, PINs, or OTPs
|
| 157 |
+
in this chat. If you require account-related assistance, please
|
| 158 |
+
use the bank's secure authenticated channels.
|
| 159 |
+
"""
|
| 160 |
+
)
|
| 161 |
+
|
| 162 |
+
def contains_pii(text: str) -> bool:
|
| 163 |
+
results = _analyze(text)
|
| 164 |
+
return len(results) > 0
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
def ask(query, chat_id=None, chat_name = None, persist=True):
|
| 168 |
+
|
| 169 |
+
if chat_id is None:
|
| 170 |
+
lc_history = []
|
| 171 |
+
past = []
|
| 172 |
+
else:
|
| 173 |
+
MAX_HISTORY_TURNS = 10
|
| 174 |
+
past = get_chat_history(chat_id)[-MAX_HISTORY_TURNS:]
|
| 175 |
+
# past = get_chat_history(chat_id)
|
| 176 |
+
lc_history = []
|
| 177 |
+
for turn in past:
|
| 178 |
+
lc_history.append(HumanMessage(content=turn["query"]))
|
| 179 |
+
lc_history.append(AIMessage(content=turn["answer"]))
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
# load long term history from db
|
| 183 |
+
clarification_phrases = ["could you", "can you elaborate", "what do you mean", "are you referring"]
|
| 184 |
+
clarification_count = sum(
|
| 185 |
+
1 for turn in past
|
| 186 |
+
if any(phrase in turn["answer"].lower() for phrase in clarification_phrases)
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
clean_query = scrub_pii(query)
|
| 190 |
+
# print(clean_query)
|
| 191 |
+
|
| 192 |
+
# docs = retriever.invoke(clean_query)
|
| 193 |
+
# print("Retrieved docs:")
|
| 194 |
+
# for i, doc in enumerate(docs):
|
| 195 |
+
# print(f"\n--- Doc {i+1} ---")
|
| 196 |
+
# print(doc.page_content[:500])
|
| 197 |
+
|
| 198 |
+
# run rag chain with full history
|
| 199 |
+
response = rag_chain.invoke({
|
| 200 |
+
"input": clean_query,
|
| 201 |
+
"chat_history": lc_history
|
| 202 |
+
})
|
| 203 |
+
answer = response["answer"]
|
| 204 |
+
|
| 205 |
+
is_clarifying = any(phrase in answer.lower() for phrase in clarification_phrases)
|
| 206 |
+
if is_clarifying and clarification_count >= 1:
|
| 207 |
+
answer = "I'm sorry, I don't have information on that."
|
| 208 |
+
|
| 209 |
+
# persist new turn back to db
|
| 210 |
+
if persist and chat_id is not None:
|
| 211 |
+
message_id = str(uuid.uuid4())
|
| 212 |
+
chat_collection.add(
|
| 213 |
+
ids=[message_id],
|
| 214 |
+
documents=[clean_query],
|
| 215 |
+
metadatas=[{
|
| 216 |
+
"chat_id": chat_id,
|
| 217 |
+
"chat_name": chat_name,
|
| 218 |
+
"answer": answer,
|
| 219 |
+
"timestamp": datetime.now().isoformat()
|
| 220 |
+
}]
|
| 221 |
+
)
|
| 222 |
+
|
| 223 |
+
return answer
|
| 224 |
+
|
| 225 |
+
# Test questions
|
| 226 |
+
# print(ask("What are the minimum and maximum age limits for investing in M&N Life Uday"))
|
| 227 |
+
|
| 228 |
+
# print(ask("what documents do I need to open a bank account?"))
|
| 229 |
+
# print(ask("what about for a savings account?"))
|
| 230 |
+
# print(ask("do I need KYC for that too?"))
|
| 231 |
+
|
| 232 |
+
def retrieve_with_rerank(query, top_k=20, top_n=5):
|
| 233 |
+
# Step 1: Retrieve
|
| 234 |
+
raw_chunks = vectorstore.similarity_search(query, k=top_k)
|
| 235 |
+
|
| 236 |
+
# Step 2: Score each chunk against the query
|
| 237 |
+
pairs = [(query, chunk.page_content) for chunk in raw_chunks]
|
| 238 |
+
scores = rerank_model.predict(pairs)
|
| 239 |
+
|
| 240 |
+
# Step 3: Sort by score and take top_n
|
| 241 |
+
ranked = sorted(zip(scores, raw_chunks), key=lambda x: x[0], reverse=True)
|
| 242 |
+
return [chunk for _, chunk in ranked[:top_n]]
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
|
| 246 |
+
def answer_query(query):
|
| 247 |
+
# Before: chunks = vector_store.similarity_search(query, k=5)
|
| 248 |
+
# After:
|
| 249 |
+
chunks = retrieve_with_rerank(query, top_k=20, top_n=5)
|
| 250 |
+
|
| 251 |
+
context = "\n\n".join([c.page_content for c in chunks])
|
| 252 |
+
|
| 253 |
+
response = llm.invoke(f"""
|
| 254 |
+
Context: {context}
|
| 255 |
+
Question: {query}
|
| 256 |
+
Answer only from the context above.
|
| 257 |
+
""")
|
| 258 |
+
return response
|
| 259 |
+
|