Update config to load .env, add HF token support, and clean up settings
Browse files- app/config.py +22 -18
- app/rag/chat_history.py +80 -0
- app/rag/embeddings.py +4 -0
- app/rag/routes.py +24 -21
- requirements.txt +3 -0
app/config.py
CHANGED
|
@@ -1,38 +1,38 @@
|
|
| 1 |
-
import os
|
| 2 |
-
from dotenv import load_dotenv
|
| 3 |
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 4 |
|
| 5 |
-
# Load environment variables from .env
|
| 6 |
-
load_dotenv()
|
| 7 |
-
|
| 8 |
class Settings(BaseSettings):
|
| 9 |
"""Application settings loaded from environment variables."""
|
| 10 |
|
| 11 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 12 |
# Google API Keys
|
| 13 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 14 |
-
pagespeed_api_key: str
|
| 15 |
-
gemini_api_key: str
|
| 16 |
-
|
| 17 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 18 |
# Chat & RAG Configuration
|
| 19 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 20 |
-
groq_api_key: str
|
| 21 |
-
vectorstore_base_path: str =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
|
| 23 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 24 |
# MongoDB Configuration (Local)
|
| 25 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 26 |
-
mongo_uri: str =
|
| 27 |
-
mongo_chat_db: str =
|
| 28 |
-
mongo_chat_collection: str =
|
| 29 |
|
| 30 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 31 |
# FastAPI Server Configuration
|
| 32 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 33 |
-
host: str =
|
| 34 |
-
port: int =
|
| 35 |
-
debug: bool =
|
| 36 |
|
| 37 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 38 |
# App Metadata (unchanged)
|
|
@@ -44,10 +44,14 @@ class Settings(BaseSettings):
|
|
| 44 |
"using Google's APIs and Gemini AI"
|
| 45 |
)
|
| 46 |
|
|
|
|
|
|
|
|
|
|
| 47 |
model_config = SettingsConfigDict(
|
| 48 |
env_file=".env",
|
| 49 |
-
env_file_encoding="utf-8"
|
|
|
|
| 50 |
)
|
| 51 |
|
| 52 |
-
#
|
| 53 |
settings = Settings()
|
|
|
|
|
|
|
|
|
|
| 1 |
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 2 |
|
|
|
|
|
|
|
|
|
|
| 3 |
class Settings(BaseSettings):
|
| 4 |
"""Application settings loaded from environment variables."""
|
| 5 |
|
| 6 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 7 |
# Google API Keys
|
| 8 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 9 |
+
pagespeed_api_key: str
|
| 10 |
+
gemini_api_key: str
|
| 11 |
+
|
| 12 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 13 |
# Chat & RAG Configuration
|
| 14 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 15 |
+
groq_api_key: str
|
| 16 |
+
vectorstore_base_path: str = "./vectorstores"
|
| 17 |
+
|
| 18 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 19 |
+
# Hugging Face Hub
|
| 20 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 21 |
+
huggingfacehub_api_token: str
|
| 22 |
|
| 23 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 24 |
# MongoDB Configuration (Local)
|
| 25 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 26 |
+
mongo_uri: str = "mongodb://localhost:27017"
|
| 27 |
+
mongo_chat_db: str = "Education_chatbot"
|
| 28 |
+
mongo_chat_collection: str = "chat_histories"
|
| 29 |
|
| 30 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 31 |
# FastAPI Server Configuration
|
| 32 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 33 |
+
host: str = "0.0.0.0"
|
| 34 |
+
port: int = 8000
|
| 35 |
+
debug: bool = False
|
| 36 |
|
| 37 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 38 |
# App Metadata (unchanged)
|
|
|
|
| 44 |
"using Google's APIs and Gemini AI"
|
| 45 |
)
|
| 46 |
|
| 47 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 48 |
+
# Tell Pydantic to load from .env and ignore extras
|
| 49 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 50 |
model_config = SettingsConfigDict(
|
| 51 |
env_file=".env",
|
| 52 |
+
env_file_encoding="utf-8",
|
| 53 |
+
extra="ignore",
|
| 54 |
)
|
| 55 |
|
| 56 |
+
# Single shared Settings instance
|
| 57 |
settings = Settings()
|
app/rag/chat_history.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
from typing import List, Dict, Any
|
| 3 |
+
from pymongo import ReturnDocument
|
| 4 |
+
|
| 5 |
+
from app.config import settings
|
| 6 |
+
from .db import mongo_client, chat_collection_name
|
| 7 |
+
from .embeddings import get_llm
|
| 8 |
+
from langchain.prompts import ChatPromptTemplate
|
| 9 |
+
from .logging_config import logger
|
| 10 |
+
|
| 11 |
+
# Get the actual collection object
|
| 12 |
+
db = mongo_client[settings.mongo_chat_db]
|
| 13 |
+
coll = db[chat_collection_name]
|
| 14 |
+
|
| 15 |
+
# LLM & summarization prompt
|
| 16 |
+
llm = get_llm()
|
| 17 |
+
summarization_prompt = ChatPromptTemplate.from_messages([
|
| 18 |
+
("system", "Summarize the following conversation into a concise summary:"),
|
| 19 |
+
("human", "{chat_history}")
|
| 20 |
+
])
|
| 21 |
+
|
| 22 |
+
class ChatHistoryManager:
|
| 23 |
+
@staticmethod
|
| 24 |
+
def create_session(chat_id: str) -> None:
|
| 25 |
+
"""Ensure a document exists for this chat_id with empty messages."""
|
| 26 |
+
coll.update_one(
|
| 27 |
+
{"session_id": chat_id},
|
| 28 |
+
{"$setOnInsert": {"session_id": chat_id, "messages": []}},
|
| 29 |
+
upsert=True
|
| 30 |
+
)
|
| 31 |
+
logger.info("Initialized chat session %s", chat_id)
|
| 32 |
+
|
| 33 |
+
@staticmethod
|
| 34 |
+
def get_messages(chat_id: str) -> List[Dict[str, Any]]:
|
| 35 |
+
"""Return the messages array for this session (or empty if none)."""
|
| 36 |
+
doc = coll.find_one({"session_id": chat_id}, {"_id": 0, "messages": 1})
|
| 37 |
+
return doc.get("messages", []) if doc else []
|
| 38 |
+
|
| 39 |
+
@staticmethod
|
| 40 |
+
def add_message(chat_id: str, role: str, content: str) -> None:
|
| 41 |
+
"""Append a new {role,content,timestamp} entry to the messages array."""
|
| 42 |
+
entry = {
|
| 43 |
+
"type": role,
|
| 44 |
+
"content": content,
|
| 45 |
+
"timestamp": time.time()
|
| 46 |
+
}
|
| 47 |
+
coll.update_one(
|
| 48 |
+
{"session_id": chat_id},
|
| 49 |
+
{"$push": {"messages": entry}}
|
| 50 |
+
)
|
| 51 |
+
logger.debug("Appended %s message to %s", role, chat_id)
|
| 52 |
+
|
| 53 |
+
@staticmethod
|
| 54 |
+
def summarize_if_needed(chat_id: str, threshold: int = 10) -> bool:
|
| 55 |
+
"""
|
| 56 |
+
If message count > threshold, summarize and replace all messages
|
| 57 |
+
with a single "ai" summary entry.
|
| 58 |
+
"""
|
| 59 |
+
messages = ChatHistoryManager.get_messages(chat_id)
|
| 60 |
+
if len(messages) <= threshold:
|
| 61 |
+
return False
|
| 62 |
+
|
| 63 |
+
# Flatten for summarization
|
| 64 |
+
chat_text = "\n".join(f"{m['type'].upper()}: {m['content']}" for m in messages)
|
| 65 |
+
|
| 66 |
+
# Run summarization
|
| 67 |
+
summary_chain = summarization_prompt | llm
|
| 68 |
+
result = summary_chain.invoke({"chat_history": chat_text})
|
| 69 |
+
summary = getattr(result, "content", result)
|
| 70 |
+
|
| 71 |
+
# Replace entire messages array with the summary
|
| 72 |
+
coll.find_one_and_update(
|
| 73 |
+
{"session_id": chat_id},
|
| 74 |
+
{"$set": {"messages": [
|
| 75 |
+
{"type": "ai", "content": summary, "timestamp": time.time()}
|
| 76 |
+
]}},
|
| 77 |
+
return_document=ReturnDocument.AFTER
|
| 78 |
+
)
|
| 79 |
+
logger.info("Summarized chat %s down to one message", chat_id)
|
| 80 |
+
return True
|
app/rag/embeddings.py
CHANGED
|
@@ -2,6 +2,10 @@ import os
|
|
| 2 |
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
|
| 3 |
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
| 4 |
from langchain.prompts import ChatPromptTemplate
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
def get_llm():
|
| 7 |
"""
|
|
|
|
| 2 |
from langchain_community.embeddings import HuggingFaceBgeEmbeddings
|
| 3 |
from langchain.text_splitter import RecursiveCharacterTextSplitter
|
| 4 |
from langchain.prompts import ChatPromptTemplate
|
| 5 |
+
from dotenv import load_dotenv
|
| 6 |
+
|
| 7 |
+
load_dotenv() # now os.getenv(...) will pick up values from your .env file
|
| 8 |
+
|
| 9 |
|
| 10 |
def get_llm():
|
| 11 |
"""
|
app/rag/routes.py
CHANGED
|
@@ -22,6 +22,9 @@ from .utils import (
|
|
| 22 |
)
|
| 23 |
from .logging_config import logger
|
| 24 |
|
|
|
|
|
|
|
|
|
|
| 25 |
router = APIRouter(prefix="/rag", tags=["rag"])
|
| 26 |
|
| 27 |
@router.post("/ingest/{user_id}", response_model=IngestResponse)
|
|
@@ -92,32 +95,32 @@ async def create_chat_session(user_id: str):
|
|
| 92 |
logger.error("Error creating chat for user_id=%s: %s", user_id, e, exc_info=True)
|
| 93 |
raise HTTPException(status_code=500, detail=f"Failed to create chat session: {e}")
|
| 94 |
|
|
|
|
| 95 |
@router.post("/chat/{user_id}/{chat_id}", response_model=ChatResponse)
|
| 96 |
async def chat_with_user(user_id: str, chat_id: str, body: ChatRequest):
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
- Loads the FAISS index for user_id (404 if not found).
|
| 100 |
-
- Retrieves (or initializes) the MongoDBChatMessageHistory for chat_id.
|
| 101 |
-
- Runs the ConversationalRetrievalChain to get an answer.
|
| 102 |
-
- Returns the answer, plus reβstores chat history in Mongo automatically.
|
| 103 |
-
"""
|
| 104 |
-
question = body.question
|
| 105 |
-
logger.info("Received chat request: user_id=%s, chat_id=%s, question='%s'", user_id, chat_id, question)
|
| 106 |
|
| 107 |
try:
|
| 108 |
-
# 1
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
-
#
|
| 112 |
-
|
| 113 |
-
# Some chains use "answer", some use "output_text"
|
| 114 |
-
answer = result.get("answer") or result.get("output_text") or None
|
| 115 |
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
-
|
|
|
|
| 121 |
|
| 122 |
return ChatResponse(
|
| 123 |
success=True,
|
|
@@ -126,11 +129,11 @@ async def chat_with_user(user_id: str, chat_id: str, body: ChatRequest):
|
|
| 126 |
chat_id=chat_id,
|
| 127 |
user_id=user_id
|
| 128 |
)
|
|
|
|
| 129 |
except HTTPException:
|
| 130 |
-
# Reβraise known HTTPExceptions (e.g. 404 from build_rag_chain)
|
| 131 |
raise
|
| 132 |
except Exception as e:
|
| 133 |
-
logger.error("Error
|
| 134 |
return ChatResponse(
|
| 135 |
success=False,
|
| 136 |
answer=None,
|
|
|
|
| 22 |
)
|
| 23 |
from .logging_config import logger
|
| 24 |
|
| 25 |
+
from .chat_history import ChatHistoryManager
|
| 26 |
+
from .logging_config import logger
|
| 27 |
+
|
| 28 |
router = APIRouter(prefix="/rag", tags=["rag"])
|
| 29 |
|
| 30 |
@router.post("/ingest/{user_id}", response_model=IngestResponse)
|
|
|
|
| 95 |
logger.error("Error creating chat for user_id=%s: %s", user_id, e, exc_info=True)
|
| 96 |
raise HTTPException(status_code=500, detail=f"Failed to create chat session: {e}")
|
| 97 |
|
| 98 |
+
|
| 99 |
@router.post("/chat/{user_id}/{chat_id}", response_model=ChatResponse)
|
| 100 |
async def chat_with_user(user_id: str, chat_id: str, body: ChatRequest):
|
| 101 |
+
question = body.question.strip()
|
| 102 |
+
logger.info("Chat request user=%s chat=%s question=%s", user_id, chat_id, question)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
try:
|
| 105 |
+
# 1) Ensure session exists
|
| 106 |
+
ChatHistoryManager.create_session(chat_id)
|
| 107 |
+
|
| 108 |
+
# 2) Summarize long histories
|
| 109 |
+
ChatHistoryManager.summarize_if_needed(chat_id, threshold=10)
|
| 110 |
|
| 111 |
+
# 3) Record the user message
|
| 112 |
+
ChatHistoryManager.add_message(chat_id, role="human", content=question)
|
|
|
|
|
|
|
| 113 |
|
| 114 |
+
# 4) Build and invoke the RAG chain
|
| 115 |
+
chain = build_rag_chain(user_id, chat_id)
|
| 116 |
+
history = ChatHistoryManager.get_messages(chat_id)
|
| 117 |
+
result = chain.invoke({"question": question, "chat_history": history})
|
| 118 |
+
answer = result.get("answer") or result.get("output_text")
|
| 119 |
+
if not answer:
|
| 120 |
+
raise Exception("No answer returned from chain")
|
| 121 |
|
| 122 |
+
# 5) Record the AI response
|
| 123 |
+
ChatHistoryManager.add_message(chat_id, role="ai", content=answer)
|
| 124 |
|
| 125 |
return ChatResponse(
|
| 126 |
success=True,
|
|
|
|
| 129 |
chat_id=chat_id,
|
| 130 |
user_id=user_id
|
| 131 |
)
|
| 132 |
+
|
| 133 |
except HTTPException:
|
|
|
|
| 134 |
raise
|
| 135 |
except Exception as e:
|
| 136 |
+
logger.error("Error chatting user=%s chat=%s: %s", user_id, chat_id, e, exc_info=True)
|
| 137 |
return ChatResponse(
|
| 138 |
success=False,
|
| 139 |
answer=None,
|
requirements.txt
CHANGED
|
@@ -10,3 +10,6 @@ langchain_community
|
|
| 10 |
faiss-cpu
|
| 11 |
pymongo
|
| 12 |
langchain-mongodb
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
faiss-cpu
|
| 11 |
pymongo
|
| 12 |
langchain-mongodb
|
| 13 |
+
huggingface_hub
|
| 14 |
+
python_dotenv
|
| 15 |
+
sentence_transformers
|