Muhammad Saad commited on
Commit
7d2a2eb
·
1 Parent(s): 1fac55e
.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
+ ))