From 81260e4447f308c846448455e5b1708ccab91411 Mon Sep 17 00:00:00 2001
From: Fatima367 <170196704+Fatima367@users.noreply.github.com>
Date: Wed, 11 Feb 2026 00:07:37 +0500
Subject: [PATCH 1/3] feat: update the embedding model (previous one is now
deprecated)
---
.github/workflows/deploy.yml | 19 ++--
backend/Dockerfile | 2 +-
backend/pyproject.toml | 22 ++--
backend/src/api/auth.py | 1 -
backend/src/api/chat.py | 22 ++--
backend/src/api/query.py | 2 +-
backend/src/main.py | 35 +++---
backend/src/services/embedding_service.py | 4 +-
.../gemini_personalization_service.py | 104 ++++++++++--------
9 files changed, 114 insertions(+), 97 deletions(-)
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index fb927a5..990e9a6 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -4,6 +4,8 @@ on:
push:
branches:
- master # Trigger on pushes to the main branch
+ - 005-personalization-urdu
+
jobs:
deploy:
@@ -24,26 +26,23 @@ jobs:
pip install huggingface_hub
pip install -r backend/requirements.txt # Install your backend dependencies
- - name: Login to Hugging Face
+ - name: Deploy to Hugging Face Space
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
+ HF_USERNAME: ${{ secrets.HF_USERNAME }}
+ HF_SPACE_NAME: ${{ secrets.HF_SPACE_NAME }}
run: |
- python -m huggingface_hub.commands.user_access login --token $HF_TOKEN
+ git config --global user.email "bot@github.com"
+ git config --global user.name "Bot"
- - name: Deploy to Hugging Face Space
- run: |
- # Copy backend files to a temporary directory for deployment
- mkdir -p huggingface_space
+ git clone https://huggingface.co/spaces/$HF_USERNAME/$HF_SPACE_NAME huggingface_space
cp -r backend/* huggingface_space/
# Initialize a git repository in the temporary directory
cd huggingface_space
- git init
- git config user.name "github-actions[bot]"
- git config user.email "github-actions[bot]@users.noreply.github.com"
git add .
git commit -m "Deploy backend to Hugging Face Space" || true # '|| true' to avoid failing if no changes
# Push the changes to your Hugging Face Space
# Replace 'YOUR_HF_USERNAME/YOUR_SPACE_NAME' with your actual Space ID
- git push https://HF_TOKEN:${{ secrets.HF_TOKEN }}@huggingface.co/spaces/YOUR_HF_USERNAME/YOUR_SPACE_NAME main -f
+ git push https://$HF_USERNAME:$HF_TOKEN@huggingface.co/spaces/$HF_USERNAME/$HF_SPACE_NAME main
\ No newline at end of file
diff --git a/backend/Dockerfile b/backend/Dockerfile
index 64904d9..1168641 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -1,5 +1,5 @@
# Use an official Python runtime as a parent image
-FROM python:3.12-slim-buster
+FROM python:3.12-slim
# Set the working directory in the container
WORKDIR /app
diff --git a/backend/pyproject.toml b/backend/pyproject.toml
index e0721f0..dfa87e4 100644
--- a/backend/pyproject.toml
+++ b/backend/pyproject.toml
@@ -5,18 +5,26 @@ description = "Add your description here"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
- "aiosqlite>=0.21.0",
+ "aiohttp>=3.13.3",
+ "aiosqlite>=0.22.1",
"asyncpg>=0.31.0",
- "fastapi>=0.124.0",
+ "bcrypt>=5.0.0",
+ "fastapi>=0.128.6",
+ "fastapi-limiter>=0.2.0",
"fastembed>=0.7.4",
+ "google-generativeai>=0.8.6",
+ "groq>=1.0.0",
"httpx>=0.28.1",
- "mangum>=0.19.0",
- "openai>=2.9.0",
+ "openai>=2.18.0",
"passlib[bcrypt]>=1.7.4",
+ "psycopg2>=2.9.11",
"pydantic>=2.12.5",
"pytest>=9.0.2",
"python-dotenv>=1.2.1",
- "qdrant-client>=1.16.1",
- "sqlmodel>=0.0.27",
- "uvicorn>=0.38.0",
+ "python-jose[cryptography]>=3.5.0",
+ "qdrant-client>=1.16.2",
+ "redis>=7.1.1",
+ "sqlalchemy>=2.0.46",
+ "sqlmodel>=0.0.32",
+ "uvicorn>=0.40.0",
]
diff --git a/backend/src/api/auth.py b/backend/src/api/auth.py
index ce67c15..593874b 100644
--- a/backend/src/api/auth.py
+++ b/backend/src/api/auth.py
@@ -13,7 +13,6 @@
ErrorResponse
)
from ..services.auth_service import auth_service
-from fastapi_limiter.depends import RateLimiter # Import RateLimiter
auth_router = APIRouter(prefix="/auth", tags=["auth"])
diff --git a/backend/src/api/chat.py b/backend/src/api/chat.py
index f854f08..e7a3c1d 100644
--- a/backend/src/api/chat.py
+++ b/backend/src/api/chat.py
@@ -2,8 +2,10 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List, Optional
+from groq import Groq # Import Groq client
+import os # Ensure os is imported for environment variables
-from ..services.embedding_service import get_embeddings, get_gemini_embedding_client
+from ..services.embedding_service import get_embeddings # Keep Gemini for embeddings
from ..services.qdrant_service import get_qdrant_client
from .ingest import DocumentChunk # Reuse DocumentChunk model
from dotenv import load_dotenv
@@ -12,6 +14,10 @@
router = APIRouter()
+# Initialize Groq client globally or per request as needed
+# For now, let's initialize it here. It will get the API key from environment.
+groq_client = Groq(api_key=os.getenv("GROQ_API_KEY"))
+
class Citation(BaseModel):
doc_id: str
chunk_id: str
@@ -79,23 +85,19 @@ async def chat_with_bot(request: ChatRequest):
{"role": "user", "content": f"Context from the book:\n{context_text}\n\nUser Question: {request.query}"}
]
- # Get Gemini LLM client configured via OpenAI client
- gemini_llm_client = get_gemini_embedding_client() # Reusing the same client setup, but will call chat.completions
-
- # For chat completion, Gemini uses models like "gemini-pro" or "gemini-1.5-pro"
- # I'll use "gemini-1.5-pro" as it's a capable model.
- # This might need adjustment based on available models via the OpenAI-compatible endpoint.
+ print("Sending request to Groq with prompt_messages:")
+ print(prompt_messages)
try:
- chat_response = gemini_llm_client.chat.completions.create(
- model="gemini-2.5-flash-lite", # Use an appropriate Gemini chat model
+ chat_response = groq_client.chat.completions.create(
+ model="llama-3.1-8b-instant", # Use an appropriate Groq chat model
messages=prompt_messages,
temperature=0.7,
max_tokens=500
)
answer = chat_response.choices[0].message.content
except Exception as llm_error:
- # Fallback for polite refusal if LLM call fails
print(f"LLM call failed: {llm_error}")
+ traceback.print_exc() # Print the full traceback for LLM errors
return ChatResponse(answer="I apologize, but I couldn't process your request at the moment. Please try again later.", citations=[])
return ChatResponse(answer=answer, citations=citations)
diff --git a/backend/src/api/query.py b/backend/src/api/query.py
index 634ea7f..f1858c6 100644
--- a/backend/src/api/query.py
+++ b/backend/src/api/query.py
@@ -31,7 +31,7 @@ async def query_docs(request: QueryRequest):
if not qdrant_client.collection_exists(collection_name=collection_name):
raise HTTPException(status_code=404, detail=f"Qdrant collection '{collection_name}' not found. Please ingest documents first.")
- query_embedding = get_embeddings(request.query)
+ query_embedding = get_embeddings([request.query])
search_result = qdrant_client.query(
collection_name=collection_name,
diff --git a/backend/src/main.py b/backend/src/main.py
index 10d0fe2..ac855e1 100644
--- a/backend/src/main.py
+++ b/backend/src/main.py
@@ -5,21 +5,25 @@
from .api.auth import auth_router
from dotenv import load_dotenv
import os
-from fastapi_limiter import FastAPILimiter
-import redis.asyncio as redis # Using async Redis client
-from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
-from starlette.middleware.base import BaseHTTPMiddleware
-from starlette.responses import Response
-from starlette.types import ASGIApp
-
-load_dotenv()
-
+from slowapi import Limiter, _rate_limit_exceeded_handler
+from slowapi.util import get_remote_address
+from slowapi.errors import RateLimitExceeded
+limiter = Limiter(key_func=get_remote_address)
app = FastAPI(
title="RAG Chatbot API",
description="API for the RAG chatbot that answers questions based on book content",
version="1.0.0"
)
+app.state.limiter = limiter
+app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
+
+from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
+from starlette.middleware.base import BaseHTTPMiddleware
+from starlette.responses import Response
+from starlette.types import ASGIApp
+
+load_dotenv()
origins = [
"https://physical-ai-n-humanoid-robotics.vercel.app", # Without trailing slash
@@ -56,19 +60,6 @@ async def dispatch(self, request: Request, call_next):
app.add_middleware(HTTPSRedirectMiddleware)
app.add_middleware(SecurityHeadersMiddleware)
-# Add a startup event handler for FastAPILimiter
-@app.on_event("startup")
-async def startup_event():
- try:
- # Initialize Redis client for rate limiting
- redis_url = os.getenv("REDIS_URL", "redis://localhost:6379/0")
- r = redis.from_url(redis_url, encoding="utf8", decode_responses=True)
- await FastAPILimiter.init(r)
- except Exception as e:
- print(f"Warning: Could not initialize Redis for rate limiting: {e}")
- print("Rate limiting will be disabled.")
-
-
# Include API routes
app.include_router(ingest.router, prefix="/api/v1", tags=["ingest"])
app.include_router(query.router, prefix="/api/v1", tags=["query"])
diff --git a/backend/src/services/embedding_service.py b/backend/src/services/embedding_service.py
index 9d1bea8..b7f3106 100644
--- a/backend/src/services/embedding_service.py
+++ b/backend/src/services/embedding_service.py
@@ -7,6 +7,8 @@
def get_gemini_embedding_client() -> OpenAI:
gemini_api_key = os.getenv("GEMINI_API_KEY")
+ if gemini_api_key:
+ gemini_api_key = gemini_api_key.strip() # Strip whitespace, including newlines
# Use the Google Generative Language API endpoint for OpenAI compatibility
gemini_base_url = os.getenv("GEMINI_BASE_URL", "https://generativelanguage.googleapis.com/v1beta/openai/")
@@ -23,7 +25,7 @@ def get_embeddings(texts: list[str]) -> list[list[float]]:
client = get_gemini_embedding_client()
# Using a common Gemini embedding model identifier
# If this fails, consult Google's latest documentation
- model = "text-embedding-004"
+ model = "gemini-embedding-001"
response = client.embeddings.create(
input=texts,
diff --git a/backend/src/services/gemini_personalization_service.py b/backend/src/services/gemini_personalization_service.py
index 65e761c..edb6807 100644
--- a/backend/src/services/gemini_personalization_service.py
+++ b/backend/src/services/gemini_personalization_service.py
@@ -1,20 +1,18 @@
"""
Gemini-based Personalization Service for adapting content based on user profile
-Uses Google's Gemini API to intelligently simplify or enhance content while maintaining syllabus compliance
+Uses Groq API to intelligently simplify or enhance content while maintaining syllabus compliance
"""
import os
-try:
- import google.generativeai as genai
- GENAI_AVAILABLE = True
-except ImportError:
- genai = None
- GENAI_AVAILABLE = False
+from groq import Groq
from typing import Dict, Any, Optional
from ..utils.logging_utils import log_info, log_warning, log_error
from dotenv import load_dotenv
load_dotenv()
+GROQ_API_KEY = os.getenv("GROQ_API_KEY")
+GROQ_MODEL_NAME = os.getenv("GROQ_MODEL_NAME", "llama-3.1-8b-instant") # Default Groq model
+
class SyllabusComplianceValidator:
"""
@@ -76,20 +74,20 @@ class GeminiPersonalizationService:
def __init__(self):
self.validator = SyllabusComplianceValidator()
self.available = False
- if GENAI_AVAILABLE and genai:
- GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
- if GEMINI_API_KEY:
- try:
- genai.configure(api_key=GEMINI_API_KEY)
- self.model = genai.GenerativeModel('gemini-2.5-flash-lite')
- self.available = True
- log_info("Google GenAI configured successfully")
- except Exception as e:
- log_warning(f"Failed to configure Google GenAI: {str(e)}", "Configuration")
- else:
- log_warning("GEMINI_API_KEY not found", "Configuration")
+ self.groq_client = None
+
+ if GROQ_API_KEY:
+ try:
+ self.groq_client = Groq(api_key=GROQ_API_KEY)
+ # Test a simple call to ensure the client is working
+ # This part is illustrative, and might need actual implementation depending on Groq API's init behavior
+ # For now, just setting self.available to True if client is initialized
+ self.available = True
+ log_info("Groq client configured successfully")
+ except Exception as e:
+ log_warning(f"Failed to configure Groq client: {str(e)}", "Configuration")
else:
- log_warning("google.generativeai library not found", "Configuration")
+ log_warning("GROQ_API_KEY not found", "Configuration")
# System instruction for maintaining syllabus compliance
self.syllabus_context = """
@@ -114,13 +112,13 @@ def simplify_for_beginners(self, content: str, tech_background: str = "general")
Simplify content for users with beginner experience level
Adds more explanations, breaks down complex concepts, uses simpler language
"""
- if not self.available:
- log_warning("GenAI not available, returning original content", "simplify_for_beginners")
+ if not self.available or not self.groq_client:
+ log_warning("Groq client not available, returning original content", "simplify_for_beginners")
return content
- prompt = f"""
-{self.syllabus_context}
-
+ messages = [
+ {"role": "system", "content": self.syllabus_context},
+ {"role": "user", "content": f"""
TASK: Adapt this Physical AI & Robotics textbook content for BEGINNER-level learners.
User's technical background: {tech_background}
@@ -140,11 +138,17 @@ def simplify_for_beginners(self, content: str, tech_background: str = "general")
{content}
ADAPTED CONTENT (maintain MDX format):
-"""
+"""}
+ ]
try:
- response = self.model.generate_content(prompt)
- personalized_content = response.text
+ chat_completion = self.groq_client.chat.completions.create(
+ messages=messages,
+ model=GROQ_MODEL_NAME,
+ temperature=0.7, # You can adjust temperature as needed
+ max_tokens=2000, # Adjust max_tokens as needed
+ )
+ personalized_content = chat_completion.choices[0].message.content
# Validate syllabus compliance
is_valid, message = self.validator.validate_content(content, personalized_content)
@@ -166,13 +170,13 @@ def enhance_for_advanced(self, content: str, tech_background: str = "advanced")
Enhance content for users with advanced experience level
Adds technical depth, advanced concepts, implementation details
"""
- if not self.available:
- log_warning("GenAI not available, returning original content", "enhance_for_advanced")
+ if not self.available or not self.groq_client:
+ log_warning("Groq client not available, returning original content", "enhance_for_advanced")
return content
- prompt = f"""
-{self.syllabus_context}
-
+ messages = [
+ {"role": "system", "content": self.syllabus_context},
+ {"role": "user", "content": f"""
TASK: Adapt this Physical AI & Robotics textbook content for ADVANCED-level learners.
User's technical background: {tech_background}
@@ -192,11 +196,17 @@ def enhance_for_advanced(self, content: str, tech_background: str = "advanced")
{content}
ENHANCED CONTENT (maintain MDX format):
-"""
+"""}
+ ]
try:
- response = self.model.generate_content(prompt)
- personalized_content = response.text
+ chat_completion = self.groq_client.chat.completions.create(
+ messages=messages,
+ model=GROQ_MODEL_NAME,
+ temperature=0.7, # You can adjust temperature as needed
+ max_tokens=2000, # Adjust max_tokens as needed
+ )
+ personalized_content = chat_completion.choices[0].message.content
# Validate syllabus compliance
is_valid, message = self.validator.validate_content(content, personalized_content)
@@ -221,8 +231,8 @@ def apply_user_preferences(
"""
Apply specific user preferences to content adaptation
"""
- if not self.available:
- log_warning("GenAI not available, returning original content", "apply_user_preferences")
+ if not self.available or not self.groq_client:
+ log_warning("Groq client not available, returning original content", "apply_user_preferences")
return content
preference_instructions = []
@@ -244,9 +254,9 @@ def apply_user_preferences(
if not preference_instructions:
return content
- prompt = f"""
-{self.syllabus_context}
-
+ messages = [
+ {"role": "system", "content": self.syllabus_context},
+ {"role": "user", "content": f"""
TASK: Adapt this content with the following user preferences:
{chr(10).join(f'- {instruction}' for instruction in preference_instructions)}
@@ -256,11 +266,17 @@ def apply_user_preferences(
{content}
ADAPTED CONTENT (maintain MDX format):
-"""
+"""}
+ ]
try:
- response = self.model.generate_content(prompt)
- personalized_content = response.text
+ chat_completion = self.groq_client.chat.completions.create(
+ messages=messages,
+ model=GROQ_MODEL_NAME,
+ temperature=0.7, # You can adjust temperature as needed
+ max_tokens=2000, # Adjust max_tokens as needed
+ )
+ personalized_content = chat_completion.choices[0].message.content
# Validate syllabus compliance
is_valid, message = self.validator.validate_content(content, personalized_content)
From 3aeccf51ec2fb866fd30fc43976523a82365981f Mon Sep 17 00:00:00 2001
From: Fatima367 <170196704+Fatima367@users.noreply.github.com>
Date: Thu, 12 Feb 2026 14:07:54 +0500
Subject: [PATCH 2/3] feat: add rate limit handling for ingestion and optimize
chat retrieval by reducing the chunk size
---
backend/delete_collection.py | 39 +++++++++
backend/src/api/chat.py | 160 +++++++++++++++++++++++++----------
backend/src/api/ingest.py | 38 +++++++--
trigger_ingestion.py | 37 ++++++++
4 files changed, 224 insertions(+), 50 deletions(-)
create mode 100644 backend/delete_collection.py
create mode 100644 trigger_ingestion.py
diff --git a/backend/delete_collection.py b/backend/delete_collection.py
new file mode 100644
index 0000000..d03ff8f
--- /dev/null
+++ b/backend/delete_collection.py
@@ -0,0 +1,39 @@
+"""
+Script to delete the Qdrant collection and start fresh.
+Run this before re-ingesting to avoid duplicates.
+"""
+import os
+from dotenv import load_dotenv
+from src.services.qdrant_service import get_qdrant_client
+
+load_dotenv()
+
+def delete_collection():
+ """Delete the book_content_chunks collection."""
+ try:
+ client = get_qdrant_client()
+ collection_name = "book_content_chunks"
+
+ if client.collection_exists(collection_name=collection_name):
+ print(f"šļø Deleting collection '{collection_name}'...")
+ client.delete_collection(collection_name=collection_name)
+ print(f"ā
Collection '{collection_name}' deleted successfully!")
+ print(f"š You can now run ingestion to create fresh data with language tags.")
+ else:
+ print(f"ā¹ļø Collection '{collection_name}' does not exist. Nothing to delete.")
+
+ except Exception as e:
+ print(f"ā Error: {e}")
+ import traceback
+ traceback.print_exc()
+
+if __name__ == "__main__":
+ print("š Qdrant Collection Cleanup")
+ print("=" * 80)
+
+ response = input("ā ļø This will delete ALL data in the collection. Continue? (yes/no): ")
+
+ if response.lower() in ['yes', 'y']:
+ delete_collection()
+ else:
+ print("ā Operation cancelled.")
diff --git a/backend/src/api/chat.py b/backend/src/api/chat.py
index e7a3c1d..0919d5f 100644
--- a/backend/src/api/chat.py
+++ b/backend/src/api/chat.py
@@ -1,23 +1,26 @@
import traceback
+import re
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List, Optional
-from groq import Groq # Import Groq client
-import os # Ensure os is imported for environment variables
+from groq import Groq
+import os
+from dotenv import load_dotenv
-from ..services.embedding_service import get_embeddings # Keep Gemini for embeddings
+from ..services.embedding_service import get_embeddings
from ..services.qdrant_service import get_qdrant_client
-from .ingest import DocumentChunk # Reuse DocumentChunk model
-from dotenv import load_dotenv
+from .ingest import DocumentChunk
load_dotenv()
router = APIRouter()
-# Initialize Groq client globally or per request as needed
-# For now, let's initialize it here. It will get the API key from environment.
groq_client = Groq(api_key=os.getenv("GROQ_API_KEY"))
+# -------------------------
+# Models
+# -------------------------
+
class Citation(BaseModel):
doc_id: str
chunk_id: str
@@ -32,28 +35,75 @@ class ChatResponse(BaseModel):
answer: str
citations: List[Citation] = []
-@router.post("/chat", response_model=ChatResponse)
-async def chat_with_bot(request: ChatRequest):
+
+# -------------------------
+# Utility: Clean Context
+# -------------------------
+
+def clean_context(text: str) -> str:
"""
- Interacts with the RAG chatbot, retrieves context, and provides answers with citations.
+ Remove markdown, frontmatter, JSX components,
+ and Docusaurus formatting before sending to LLM.
"""
+ # Remove frontmatter (--- title ---)
+ text = re.sub(r"---.*?---", "", text, flags=re.DOTALL)
+
+ # Remove import lines
+ text = re.sub(r"import .*", "", text)
+
+ # Remove JSX components like