Skip to content

Commit f017e54

Browse files
committed
Implemented vector index for chat history context and MCP tool for semantic search & summarization
1 parent ee8652c commit f017e54

File tree

4 files changed

+125
-3
lines changed

4 files changed

+125
-3
lines changed

services/chatbot/src/chatbot/chat_service.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from uuid import uuid4
2-
2+
import os
33
from langgraph.graph.message import Messages
4-
4+
from .vector_index import build_vector_index_from_chat_history, update_vector_index, retrieval_index_path
55
from .extensions import db
6-
from .langgraph_agent import build_langgraph_agent, execute_langgraph_agent
6+
from .langgraph_agent import execute_langgraph_agent
77

88

99
async def get_chat_history(session_id):
@@ -39,4 +39,8 @@ async def process_user_message(session_id, user_message, api_key, model_name, us
3939
# Limit chat history to last 20 messages
4040
history = history[-20:]
4141
await update_chat_history(session_id, history)
42+
if not os.path.exists(retrieval_index_path):
43+
await build_vector_index_from_chat_history(api_key)
44+
else:
45+
await update_vector_index(api_key, session_id, {"user": user_message, "assistant": reply.content})
4246
return reply.content, response_message_id
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from langchain_community.embeddings import OpenAIEmbeddings
2+
from langchain_community.vectorstores import FAISS
3+
from langchain_core.documents import Document
4+
from .extensions import db
5+
6+
retrieval_index_path = "/app/resources/chat_index"
7+
8+
async def build_vector_index_from_chat_history(api_key):
9+
docs = []
10+
async for chat in db.chat_sessions.find({}):
11+
session_id = chat.get("session_id", "unknown")
12+
messages = chat.get("messages", [])
13+
for msg in messages:
14+
role = msg.get("role")
15+
content = msg.get("content")
16+
if content:
17+
doc = Document(
18+
page_content=content,
19+
metadata={"session_id": session_id, "role": role}
20+
)
21+
docs.append(doc)
22+
23+
embeddings = OpenAIEmbeddings(api_key=api_key)
24+
vectorstore = FAISS.from_documents(docs, embeddings)
25+
vectorstore.save_local(retrieval_index_path)
26+
27+
async def update_vector_index(api_key, session_id, new_messages):
28+
docs = []
29+
for role, content in new_messages.items():
30+
if content:
31+
doc = Document(
32+
page_content=content,
33+
metadata={"session_id": session_id, "role": role}
34+
)
35+
docs.append(doc)
36+
37+
if docs:
38+
embeddings = OpenAIEmbeddings(api_key=api_key)
39+
vectorstore = FAISS.load_local(
40+
retrieval_index_path,
41+
embeddings,
42+
allow_dangerous_deserialization=True,
43+
)
44+
vectorstore.add_documents(docs)
45+
vectorstore.save_local(retrieval_index_path)

services/chatbot/src/mcpserver/server.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
import os
55
import logging
66
import time
7+
from .tool_helpers import (
8+
get_any_api_key,
9+
get_chat_history_retriever,
10+
)
711
# Configure logging
812
logging.basicConfig(
913
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
@@ -77,6 +81,27 @@ def get_http_client():
7781
name="My crAPI MCP Server"
7882
)
7983

84+
@mcp.tool(tags={"history", "search", "summary", "context"},)
85+
async def search_chat_history(question: str) -> str:
86+
"""Answer questions based on user chat history (summarized and semantically indexed).
87+
Use this when the user asks about prior chats, what they asked earlier, or wants a summary of past conversations.
88+
Answer questions based on the user's prior chat history.
89+
90+
Use this tool when the user refers to anything mentioned before, asks for a summary of previous messages or sessions,
91+
or references phrases like 'what I said earlier', 'things we discussed', 'my earlier question', 'until now', 'till date', 'all my conversations' or 'previously mentioned'.
92+
The chat history is semantically indexed and summarized using vector search."""
93+
94+
logger.info(f"search_chat_history called with: {question}")
95+
api_key=await get_any_api_key()
96+
if not api_key:
97+
logger.error("API key is not available. Cannot search chat history.")
98+
return "OpenAI API key is not available. Cannot search chat history."
99+
retriever = await get_chat_history_retriever(api_key=api_key)
100+
response = await retriever.ainvoke({"query": question})
101+
result = response["result"]
102+
logger.info(f"RESULT: {result}")
103+
return result
104+
80105
if __name__ == "__main__":
81106
mcp_server_port = int(os.environ.get("MCP_SERVER_PORT", 5500))
82107
mcp.run(transport="streamable-http", host="0.0.0.0", port=mcp_server_port,)
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import os
2+
from langchain_community.embeddings import OpenAIEmbeddings
3+
from langchain_community.vectorstores import FAISS
4+
from langchain.prompts import PromptTemplate
5+
from chatbot.extensions import db
6+
from langchain.chains import RetrievalQA
7+
from langchain_openai import ChatOpenAI
8+
9+
retrieval_index_path = "/app/resources/chat_index"
10+
11+
async def get_any_api_key():
12+
if os.environ.get("CHATBOT_OPENAI_API_KEY"):
13+
return os.environ.get("CHATBOT_OPENAI_API_KEY")
14+
doc = await db.sessions.find_one(
15+
{"openai_api_key": {"$exists": True, "$ne": None}},
16+
{"openai_api_key": 1}
17+
)
18+
if doc and "openai_api_key" in doc:
19+
return doc["openai_api_key"]
20+
return None
21+
22+
async def get_chat_history_retriever(api_key: str):
23+
prompt_template = PromptTemplate.from_template(
24+
"""You are an assistant that summarizes chat history across sessions.
25+
26+
Given the following chat excerpts:
27+
{context}
28+
Answer the user's question: {question}
29+
30+
If the user asks for a summary, provide a coherent, high-level summary of the conversations in natural language.
31+
If the user asks a specific question, extract and answer it from the chats.
32+
Be detailed, accurate, and neutral."""
33+
)
34+
embeddings = OpenAIEmbeddings(api_key=api_key)
35+
vectorstore = FAISS.load_local(
36+
retrieval_index_path,
37+
embeddings,
38+
allow_dangerous_deserialization=True
39+
)
40+
retriever = vectorstore.as_retriever(search_type="similarity", search_kwargs={"k": 5})
41+
qa_chain = RetrievalQA.from_chain_type(
42+
llm=ChatOpenAI(api_key=api_key, model="gpt-4o"),
43+
retriever=retriever,
44+
chain_type="stuff",
45+
chain_type_kwargs={"prompt": prompt_template, "document_variable_name": "context"},
46+
return_source_documents=False,
47+
)
48+
return qa_chain

0 commit comments

Comments
 (0)