Spaces:
Sleeping
Sleeping
Muhammad Saad commited on
Commit ·
7d2a2eb
1
Parent(s): 1fac55e
'code'
Browse files- .env +4 -0
- Dockerfile +18 -0
- app/__pycache__/config.cpython-311.pyc +0 -0
- app/__pycache__/database.cpython-311.pyc +0 -0
- app/__pycache__/main.cpython-311.pyc +0 -0
- app/__pycache__/qdrant_client.cpython-311.pyc +0 -0
- app/config.py +18 -0
- app/database.py +17 -0
- app/main.py +34 -0
- app/models/__pycache__/chat.cpython-311.pyc +0 -0
- app/models/__pycache__/user.cpython-311.pyc +0 -0
- app/models/chat.py +14 -0
- app/models/user.py +9 -0
- app/qdrant_client.py +38 -0
- app/routes/__pycache__/chat.cpython-311.pyc +0 -0
- app/routes/chat.py +60 -0
- app/schemas/__pycache__/chat.cpython-311.pyc +0 -0
- app/schemas/chat.py +23 -0
- app/services/__pycache__/embeddings_service.cpython-311.pyc +0 -0
- app/services/__pycache__/openai_service.cpython-311.pyc +0 -0
- app/services/__pycache__/rag_service.cpython-311.pyc +0 -0
- app/services/embeddings_service.py +21 -0
- app/services/openai_service.py +26 -0
- app/services/rag_service.py +37 -0
- requirements.txt +12 -0
- scripts/ingest_content.py +104 -0
.env
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
OPENAI_API_KEY=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
|
| 2 |
+
NEON_DATABASE_URL=postgresql://neondb_owner:npg_HUp8hzrBtK4M@ep-calm-bonus-a4wci2me-pooler.us-east-1.aws.neon.tech/neondb?sslmode=require&channel_binding=require
|
| 3 |
+
QDRANT_URL=https://2758614b-500a-4a16-acf1-11c62603db81.us-east4-0.gcp.cloud.qdrant.io
|
| 4 |
+
QDRANT_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3MiOiJtIn0.wQ2fPOoxaLGEl6ss0O4v6CqPcQoFwoFUUWJ7qwNvVQ4
|
Dockerfile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Base image
|
| 2 |
+
FROM python:3.11-slim
|
| 3 |
+
|
| 4 |
+
# Set work directory
|
| 5 |
+
WORKDIR /app
|
| 6 |
+
|
| 7 |
+
# Install dependencies
|
| 8 |
+
COPY requirements.txt .
|
| 9 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 10 |
+
|
| 11 |
+
# Copy project files
|
| 12 |
+
COPY . .
|
| 13 |
+
|
| 14 |
+
# Expose the port Hugging Face expects
|
| 15 |
+
EXPOSE 7860
|
| 16 |
+
|
| 17 |
+
# Command to run FastAPI with uvicorn
|
| 18 |
+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
app/__pycache__/config.cpython-311.pyc
ADDED
|
Binary file (1.4 kB). View file
|
|
|
app/__pycache__/database.cpython-311.pyc
ADDED
|
Binary file (1.07 kB). View file
|
|
|
app/__pycache__/main.cpython-311.pyc
ADDED
|
Binary file (1.8 kB). View file
|
|
|
app/__pycache__/qdrant_client.cpython-311.pyc
ADDED
|
Binary file (2.11 kB). View file
|
|
|
app/config.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import os
|
| 3 |
+
from pydantic_settings import BaseSettings
|
| 4 |
+
|
| 5 |
+
class Settings(BaseSettings):
|
| 6 |
+
OPENAI_API_KEY: str
|
| 7 |
+
DATABASE_URL: str = os.getenv("DATABASE_URL", "")
|
| 8 |
+
NEON_DATABASE_URL: str = os.getenv("NEON_DATABASE_URL", "")
|
| 9 |
+
QDRANT_URL: str = os.getenv("QDRANT_URL", "http://localhost:6333")
|
| 10 |
+
QDRANT_API_KEY: str = os.getenv("QDRANT_API_KEY", "")
|
| 11 |
+
OPENAI_MODEL_CHAT: str = "gpt-4o-mini"
|
| 12 |
+
OPENAI_MODEL_EMBEDDING: str = "text-embedding-3-small"
|
| 13 |
+
|
| 14 |
+
class Config:
|
| 15 |
+
env_file = ".env"
|
| 16 |
+
extra = "ignore"
|
| 17 |
+
|
| 18 |
+
settings = Settings()
|
app/database.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import create_engine
|
| 2 |
+
from sqlalchemy.orm import sessionmaker
|
| 3 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 4 |
+
from app.config import settings
|
| 5 |
+
|
| 6 |
+
# Use NEON_DATABASE_URL if available, otherwise fall back to DATABASE_URL
|
| 7 |
+
SQLALCHEMY_DATABASE_URL = settings.NEON_DATABASE_URL or settings.DATABASE_URL or "sqlite:///./test.db"
|
| 8 |
+
engine = create_engine(SQLALCHEMY_DATABASE_URL)
|
| 9 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 10 |
+
Base = declarative_base()
|
| 11 |
+
|
| 12 |
+
def get_db():
|
| 13 |
+
db = SessionLocal()
|
| 14 |
+
try:
|
| 15 |
+
yield db
|
| 16 |
+
finally:
|
| 17 |
+
db.close()
|
app/main.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
+
from app.routes import chat
|
| 4 |
+
from app.database import engine, Base
|
| 5 |
+
from app.qdrant_client import init_qdrant_collection
|
| 6 |
+
|
| 7 |
+
app = FastAPI(title="RAG Chatbot API")
|
| 8 |
+
|
| 9 |
+
# CORS Configuration - Allow frontend to connect
|
| 10 |
+
app.add_middleware(
|
| 11 |
+
CORSMiddleware,
|
| 12 |
+
allow_origins=["http://localhost:3000", "http://127.0.0.1:3000"],
|
| 13 |
+
allow_credentials=True,
|
| 14 |
+
allow_methods=["*"],
|
| 15 |
+
allow_headers=["*"],
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
# Include routers
|
| 19 |
+
app.include_router(chat.router)
|
| 20 |
+
|
| 21 |
+
@app.on_event("startup")
|
| 22 |
+
async def startup_event():
|
| 23 |
+
# Create database tables
|
| 24 |
+
Base.metadata.create_all(bind=engine)
|
| 25 |
+
# Initialize Qdrant collection
|
| 26 |
+
init_qdrant_collection()
|
| 27 |
+
|
| 28 |
+
@app.get("/")
|
| 29 |
+
async def root():
|
| 30 |
+
return {"message": "RAG Chatbot API"}
|
| 31 |
+
|
| 32 |
+
@app.get("/api/health")
|
| 33 |
+
async def health():
|
| 34 |
+
return {"status": "ok"}
|
app/models/__pycache__/chat.cpython-311.pyc
ADDED
|
Binary file (1.17 kB). View file
|
|
|
app/models/__pycache__/user.cpython-311.pyc
ADDED
|
Binary file (794 Bytes). View file
|
|
|
app/models/chat.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, Integer, String, ForeignKey, DateTime, func
|
| 2 |
+
from sqlalchemy.orm import relationship
|
| 3 |
+
from app.database import Base
|
| 4 |
+
|
| 5 |
+
class ChatHistory(Base):
|
| 6 |
+
__tablename__ = "chat_history"
|
| 7 |
+
|
| 8 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 9 |
+
user_id = Column(Integer, ForeignKey("users.id"))
|
| 10 |
+
message = Column(String)
|
| 11 |
+
response = Column(String)
|
| 12 |
+
timestamp = Column(DateTime, default=func.now())
|
| 13 |
+
|
| 14 |
+
user = relationship("User")
|
app/models/user.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, Integer, String
|
| 2 |
+
from app.database import Base
|
| 3 |
+
|
| 4 |
+
class User(Base):
|
| 5 |
+
__tablename__ = "users"
|
| 6 |
+
|
| 7 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 8 |
+
username = Column(String, unique=True, index=True)
|
| 9 |
+
email = Column(String, unique=True, index=True)
|
app/qdrant_client.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from qdrant_client import QdrantClient
|
| 3 |
+
from qdrant_client.models import Distance, VectorParams
|
| 4 |
+
from app.config import settings
|
| 5 |
+
|
| 6 |
+
# Initialize Qdrant client
|
| 7 |
+
qdrant_client = QdrantClient(
|
| 8 |
+
url=settings.QDRANT_URL,
|
| 9 |
+
api_key=settings.QDRANT_API_KEY,
|
| 10 |
+
)
|
| 11 |
+
|
| 12 |
+
COLLECTION_NAME = "book_embeddings"
|
| 13 |
+
|
| 14 |
+
def init_qdrant_collection():
|
| 15 |
+
"""Initialize Qdrant collection if it doesn't exist"""
|
| 16 |
+
try:
|
| 17 |
+
# Check if collection exists
|
| 18 |
+
collections = qdrant_client.get_collections().collections
|
| 19 |
+
collection_names = [col.name for col in collections]
|
| 20 |
+
|
| 21 |
+
if COLLECTION_NAME not in collection_names:
|
| 22 |
+
# Create collection with vector configuration
|
| 23 |
+
qdrant_client.create_collection(
|
| 24 |
+
collection_name=COLLECTION_NAME,
|
| 25 |
+
vectors_config=VectorParams(
|
| 26 |
+
size=1536, # OpenAI text-embedding-3-small dimension
|
| 27 |
+
distance=Distance.COSINE
|
| 28 |
+
)
|
| 29 |
+
)
|
| 30 |
+
print(f"✅ Created Qdrant collection: {COLLECTION_NAME}")
|
| 31 |
+
else:
|
| 32 |
+
print(f"✅ Qdrant collection already exists: {COLLECTION_NAME}")
|
| 33 |
+
except Exception as e:
|
| 34 |
+
print(f"⚠️ Warning: Could not initialize Qdrant collection: {e}")
|
| 35 |
+
|
| 36 |
+
def get_qdrant_client():
|
| 37 |
+
"""Dependency to get Qdrant client"""
|
| 38 |
+
return qdrant_client
|
app/routes/__pycache__/chat.cpython-311.pyc
ADDED
|
Binary file (3.74 kB). View file
|
|
|
app/routes/chat.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 6 |
+
from qdrant_client import QdrantClient
|
| 7 |
+
from app.qdrant_client import get_qdrant_client
|
| 8 |
+
from app.schemas.chat import ChatRequest, ChatResponse, ChatSelectionRequest
|
| 9 |
+
from app.services.rag_service import RAGService
|
| 10 |
+
from app.services.embeddings_service import EmbeddingsService
|
| 11 |
+
from app.services.openai_service import OpenAIService
|
| 12 |
+
import logging
|
| 13 |
+
|
| 14 |
+
logger = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
router = APIRouter(prefix="/api", tags=["chat"])
|
| 17 |
+
|
| 18 |
+
def get_rag_service(
|
| 19 |
+
qdrant_client: QdrantClient = Depends(get_qdrant_client)
|
| 20 |
+
):
|
| 21 |
+
embeddings_service = EmbeddingsService()
|
| 22 |
+
openai_service = OpenAIService()
|
| 23 |
+
return RAGService(qdrant_client, embeddings_service, openai_service)
|
| 24 |
+
|
| 25 |
+
@router.post("/chat", response_model=ChatResponse)
|
| 26 |
+
async def chat(
|
| 27 |
+
request: ChatRequest,
|
| 28 |
+
rag_service: RAGService = Depends(get_rag_service)
|
| 29 |
+
):
|
| 30 |
+
try:
|
| 31 |
+
# Retrieve context from vector database
|
| 32 |
+
context = await rag_service.retrieve_context(request.question, top_k=3)
|
| 33 |
+
|
| 34 |
+
# Generate response using OpenAI
|
| 35 |
+
answer = await rag_service.generate_response(request.question, context)
|
| 36 |
+
|
| 37 |
+
# Extract sources from context
|
| 38 |
+
sources = [f"Source {i+1}" for i in range(len(context))]
|
| 39 |
+
|
| 40 |
+
return ChatResponse(answer=answer, sources=sources)
|
| 41 |
+
except Exception as e:
|
| 42 |
+
logger.error(f"Error in chat endpoint: {str(e)}", exc_info=True)
|
| 43 |
+
raise HTTPException(status_code=500, detail=str(e))
|
| 44 |
+
|
| 45 |
+
@router.post("/chat-selection", response_model=ChatResponse)
|
| 46 |
+
async def chat_selection(
|
| 47 |
+
request: ChatSelectionRequest,
|
| 48 |
+
rag_service: RAGService = Depends(get_rag_service)
|
| 49 |
+
):
|
| 50 |
+
try:
|
| 51 |
+
# Use selected text as primary context
|
| 52 |
+
context = [request.selected_text]
|
| 53 |
+
|
| 54 |
+
# Generate response
|
| 55 |
+
answer = await rag_service.generate_response(request.question, context)
|
| 56 |
+
|
| 57 |
+
return ChatResponse(answer=answer, sources=["Selected Text"])
|
| 58 |
+
except Exception as e:
|
| 59 |
+
logger.error(f"Error in chat_selection endpoint: {str(e)}", exc_info=True)
|
| 60 |
+
raise HTTPException(status_code=500, detail=str(e))
|
app/schemas/__pycache__/chat.cpython-311.pyc
ADDED
|
Binary file (1.84 kB). View file
|
|
|
app/schemas/chat.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel
|
| 2 |
+
from typing import List, Optional
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
class Message(BaseModel):
|
| 6 |
+
content: str
|
| 7 |
+
role: str
|
| 8 |
+
|
| 9 |
+
class ChatRequest(BaseModel):
|
| 10 |
+
question: str
|
| 11 |
+
user_id: Optional[int] = None
|
| 12 |
+
|
| 13 |
+
class ChatResponse(BaseModel):
|
| 14 |
+
answer: str
|
| 15 |
+
sources: List[str] = []
|
| 16 |
+
|
| 17 |
+
class ChatSelectionRequest(BaseModel):
|
| 18 |
+
question: str
|
| 19 |
+
selected_text: str
|
| 20 |
+
user_id: Optional[int] = None
|
| 21 |
+
|
| 22 |
+
class ChatSelectionResponse(BaseModel):
|
| 23 |
+
response: str
|
app/services/__pycache__/embeddings_service.cpython-311.pyc
ADDED
|
Binary file (1.57 kB). View file
|
|
|
app/services/__pycache__/openai_service.cpython-311.pyc
ADDED
|
Binary file (1.84 kB). View file
|
|
|
app/services/__pycache__/rag_service.cpython-311.pyc
ADDED
|
Binary file (2.75 kB). View file
|
|
|
app/services/embeddings_service.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from openai import OpenAI
|
| 2 |
+
from app.config import settings
|
| 3 |
+
import httpx
|
| 4 |
+
import asyncio
|
| 5 |
+
|
| 6 |
+
class EmbeddingsService:
|
| 7 |
+
def __init__(self):
|
| 8 |
+
# Use httpx client without problematic kwargs
|
| 9 |
+
http_client = httpx.Client()
|
| 10 |
+
self.client = OpenAI(api_key=settings.OPENAI_API_KEY, http_client=http_client)
|
| 11 |
+
self.model = "text-embedding-3-small"
|
| 12 |
+
|
| 13 |
+
async def create_embedding(self, text: str):
|
| 14 |
+
text = text.replace("\n", " ")
|
| 15 |
+
# Run the blocking OpenAI call in a thread pool
|
| 16 |
+
response = await asyncio.to_thread(
|
| 17 |
+
self.client.embeddings.create,
|
| 18 |
+
input=[text],
|
| 19 |
+
model=self.model
|
| 20 |
+
)
|
| 21 |
+
return response.data[0].embedding
|
app/services/openai_service.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from openai import OpenAI
|
| 2 |
+
from app.config import settings
|
| 3 |
+
from typing import List
|
| 4 |
+
import httpx
|
| 5 |
+
import asyncio
|
| 6 |
+
|
| 7 |
+
class OpenAIService:
|
| 8 |
+
def __init__(self):
|
| 9 |
+
# Use httpx client without problematic kwargs
|
| 10 |
+
http_client = httpx.Client()
|
| 11 |
+
self.client = OpenAI(api_key=settings.OPENAI_API_KEY, http_client=http_client)
|
| 12 |
+
self.model = "gpt-4o-mini"
|
| 13 |
+
|
| 14 |
+
async def get_chat_response(self, prompt: str, history: List[dict] = None) -> str:
|
| 15 |
+
messages = []
|
| 16 |
+
if history:
|
| 17 |
+
messages.extend(history)
|
| 18 |
+
messages.append({"role": "user", "content": prompt})
|
| 19 |
+
|
| 20 |
+
# Run the blocking OpenAI call in a thread pool
|
| 21 |
+
response = await asyncio.to_thread(
|
| 22 |
+
self.client.chat.completions.create,
|
| 23 |
+
model=self.model,
|
| 24 |
+
messages=messages
|
| 25 |
+
)
|
| 26 |
+
return response.choices[0].message.content
|
app/services/rag_service.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import os
|
| 3 |
+
from qdrant_client import QdrantClient
|
| 4 |
+
from qdrant_client.models import NamedVector
|
| 5 |
+
from typing import List
|
| 6 |
+
|
| 7 |
+
from app.services.openai_service import OpenAIService
|
| 8 |
+
from app.services.embeddings_service import EmbeddingsService
|
| 9 |
+
|
| 10 |
+
class RAGService:
|
| 11 |
+
def __init__(self, qdrant_client: QdrantClient, embeddings_service: EmbeddingsService, openai_service: OpenAIService):
|
| 12 |
+
self.qdrant_client = qdrant_client
|
| 13 |
+
self.embeddings_service = embeddings_service
|
| 14 |
+
self.openai_service = openai_service
|
| 15 |
+
self.collection_name = os.getenv("QDRANT_COLLECTION_NAME", "book_embeddings")
|
| 16 |
+
|
| 17 |
+
async def retrieve_context(self, query: str, top_k: int = 3) -> List[str]:
|
| 18 |
+
query_vector = await self.embeddings_service.create_embedding(query)
|
| 19 |
+
|
| 20 |
+
search_result = self.qdrant_client.search(
|
| 21 |
+
collection_name=self.collection_name,
|
| 22 |
+
query_vector=query_vector,
|
| 23 |
+
limit=top_k,
|
| 24 |
+
with_payload=True,
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
context = [point.payload.get("content", "") for point in search_result if point.payload]
|
| 28 |
+
return context
|
| 29 |
+
|
| 30 |
+
async def generate_response(self, query: str, context: List[str]) -> str:
|
| 31 |
+
full_prompt = f"""Context: {' '.join(context)}
|
| 32 |
+
|
| 33 |
+
Question: {query}
|
| 34 |
+
|
| 35 |
+
Answer:"""
|
| 36 |
+
response = await self.openai_service.get_chat_response(full_prompt)
|
| 37 |
+
return response
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.111.0
|
| 2 |
+
uvicorn==0.30.1
|
| 3 |
+
openai==1.35.13
|
| 4 |
+
qdrant-client==1.9.0
|
| 5 |
+
psycopg2-binary==2.9.9
|
| 6 |
+
sqlalchemy==2.0.31
|
| 7 |
+
python-dotenv==1.0.1
|
| 8 |
+
pydantic==2.8.2
|
| 9 |
+
pydantic-settings==2.3.4
|
| 10 |
+
asyncpg==0.29.0
|
| 11 |
+
markdown==3.6
|
| 12 |
+
beautifulsoup4==4.12.3
|
scripts/ingest_content.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import argparse
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
import markdown
|
| 5 |
+
from bs4 import BeautifulSoup
|
| 6 |
+
from qdrant_client import QdrantClient
|
| 7 |
+
from qdrant_client.models import Distance, VectorParams, PointStruct
|
| 8 |
+
from dotenv import load_dotenv
|
| 9 |
+
|
| 10 |
+
# Add these to enable relative imports
|
| 11 |
+
import sys
|
| 12 |
+
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
|
| 13 |
+
|
| 14 |
+
from app.services.embeddings_service import EmbeddingsService
|
| 15 |
+
from app.qdrant_client import get_qdrant_client
|
| 16 |
+
|
| 17 |
+
load_dotenv(dotenv_path=Path(__file__).resolve().parent.parent / ".env")
|
| 18 |
+
|
| 19 |
+
QDRANT_COLLECTION_NAME = os.getenv("QDRANT_COLLECTION_NAME", "docs_collection")
|
| 20 |
+
|
| 21 |
+
def load_mdx_content(filepath: Path) -> str:
|
| 22 |
+
with open(filepath, 'r', encoding='utf-8') as f:
|
| 23 |
+
content = f.read()
|
| 24 |
+
# MDX is essentially Markdown, so we can convert to HTML then extract text
|
| 25 |
+
html = markdown.markdown(content)
|
| 26 |
+
soup = BeautifulSoup(html, 'html.parser')
|
| 27 |
+
return soup.get_text()
|
| 28 |
+
|
| 29 |
+
def chunk_text(text: str, chunk_size: int = 1000, overlap: int = 200) -> list[str]:
|
| 30 |
+
chunks = []
|
| 31 |
+
for i in range(0, len(text), chunk_size - overlap):
|
| 32 |
+
chunks.append(text[i:i + chunk_size])
|
| 33 |
+
return chunks
|
| 34 |
+
|
| 35 |
+
async def ingest_content(
|
| 36 |
+
docs_path: Path,
|
| 37 |
+
qdrant_client: QdrantClient,
|
| 38 |
+
embeddings_service: EmbeddingsService,
|
| 39 |
+
collection_name: str,
|
| 40 |
+
):
|
| 41 |
+
qdrant_client.recreate_collection(
|
| 42 |
+
collection_name=collection_name,
|
| 43 |
+
vectors_config=VectorParams(size=1536, distance=Distance.COSINE), # OpenAI embeddings size
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
points = []
|
| 47 |
+
point_id = 0
|
| 48 |
+
for mdx_file in docs_path.rglob("*.mdx"):
|
| 49 |
+
print(f"Processing {mdx_file}")
|
| 50 |
+
content = load_mdx_content(mdx_file)
|
| 51 |
+
chunks = chunk_text(content)
|
| 52 |
+
|
| 53 |
+
for chunk in chunks:
|
| 54 |
+
embedding = embeddings_service.create_embedding(chunk)
|
| 55 |
+
points.append(
|
| 56 |
+
PointStruct(
|
| 57 |
+
id=point_id,
|
| 58 |
+
vector=embedding,
|
| 59 |
+
payload={
|
| 60 |
+
"content": chunk,
|
| 61 |
+
"source": str(mdx_file.relative_to(docs_path))
|
| 62 |
+
}
|
| 63 |
+
)
|
| 64 |
+
)
|
| 65 |
+
point_id += 1
|
| 66 |
+
|
| 67 |
+
if len(points) >= 100: # Batch upsert
|
| 68 |
+
qdrant_client.upsert(
|
| 69 |
+
collection_name=collection_name,
|
| 70 |
+
points=points,
|
| 71 |
+
wait=True,
|
| 72 |
+
)
|
| 73 |
+
points = []
|
| 74 |
+
|
| 75 |
+
if points: # Upsert remaining points
|
| 76 |
+
qdrant_client.upsert(
|
| 77 |
+
collection_name=collection_name,
|
| 78 |
+
points=points,
|
| 79 |
+
wait=True,
|
| 80 |
+
)
|
| 81 |
+
|
| 82 |
+
print(f"Ingestion complete. Total points: {point_id}")
|
| 83 |
+
|
| 84 |
+
if __name__ == "__main__":
|
| 85 |
+
parser = argparse.ArgumentParser(description="Ingest MDX content into Qdrant.")
|
| 86 |
+
parser.add_argument(
|
| 87 |
+
"--docs_path",
|
| 88 |
+
type=str,
|
| 89 |
+
default="../physical-ai-humanoid-robotics/docs/",
|
| 90 |
+
help="Path to the directory containing MDX documentation files."
|
| 91 |
+
)
|
| 92 |
+
args = parser.parse_args()
|
| 93 |
+
|
| 94 |
+
qdrant_client = get_qdrant_client()
|
| 95 |
+
embeddings_service = EmbeddingsService()
|
| 96 |
+
|
| 97 |
+
# Run the async ingestion
|
| 98 |
+
import asyncio
|
| 99 |
+
asyncio.run(ingest_content(
|
| 100 |
+
docs_path=Path(args.docs_path),
|
| 101 |
+
qdrant_client=qdrant_client,
|
| 102 |
+
embeddings_service=embeddings_service,
|
| 103 |
+
collection_name=QDRANT_COLLECTION_NAME
|
| 104 |
+
))
|