MuhammadSaad16 commited on
Commit
0cee4dc
·
1 Parent(s): cf3a37f

Add application file

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. DockerFile +18 -0
  2. app/__pycache__/config.cpython-311.pyc +0 -0
  3. app/__pycache__/config.cpython-313.pyc +0 -0
  4. app/__pycache__/database.cpython-311.pyc +0 -0
  5. app/__pycache__/database.cpython-313.pyc +0 -0
  6. app/__pycache__/main.cpython-311.pyc +0 -0
  7. app/__pycache__/main.cpython-313.pyc +0 -0
  8. app/__pycache__/qdrant_client.cpython-311.pyc +0 -0
  9. app/__pycache__/qdrant_client.cpython-313.pyc +0 -0
  10. app/config.py +26 -0
  11. app/database.py +17 -0
  12. app/main.py +35 -0
  13. app/models/__pycache__/chat.cpython-311.pyc +0 -0
  14. app/models/__pycache__/translation.cpython-313.pyc +0 -0
  15. app/models/__pycache__/user.cpython-311.pyc +0 -0
  16. app/models/__pycache__/user.cpython-313.pyc +0 -0
  17. app/models/chat.py +14 -0
  18. app/models/translation.py +13 -0
  19. app/models/user.py +31 -0
  20. app/qdrant_client.py +54 -0
  21. app/routes/__pycache__/auth.cpython-313.pyc +0 -0
  22. app/routes/__pycache__/chat.cpython-311.pyc +0 -0
  23. app/routes/__pycache__/chat.cpython-313.pyc +0 -0
  24. app/routes/__pycache__/personalize.cpython-313.pyc +0 -0
  25. app/routes/__pycache__/translate.cpython-313.pyc +0 -0
  26. app/routes/chat.py +60 -0
  27. app/routes/personalize.py +59 -0
  28. app/routes/translate.py +59 -0
  29. app/schemas/__pycache__/auth.cpython-313.pyc +0 -0
  30. app/schemas/__pycache__/chat.cpython-311.pyc +0 -0
  31. app/schemas/__pycache__/chat.cpython-313.pyc +0 -0
  32. app/schemas/__pycache__/personalize.cpython-313.pyc +0 -0
  33. app/schemas/__pycache__/translate.cpython-313.pyc +0 -0
  34. app/schemas/auth.py +51 -0
  35. app/schemas/chat.py +23 -0
  36. app/schemas/personalize.py +28 -0
  37. app/schemas/translate.py +25 -0
  38. app/services/__pycache__/auth.cpython-313.pyc +0 -0
  39. app/services/__pycache__/embeddings_service.cpython-311.pyc +0 -0
  40. app/services/__pycache__/embeddings_service.cpython-313.pyc +0 -0
  41. app/services/__pycache__/gemini_service.cpython-313.pyc +0 -0
  42. app/services/__pycache__/openai_service.cpython-311.pyc +0 -0
  43. app/services/__pycache__/openai_service.cpython-313.pyc +0 -0
  44. app/services/__pycache__/rag_service.cpython-311.pyc +0 -0
  45. app/services/__pycache__/rag_service.cpython-313.pyc +0 -0
  46. app/services/embeddings_service.py +19 -0
  47. app/services/openai_service.py +102 -0
  48. app/services/rag_service.py +75 -0
  49. history/prompts/004-urdu-translation/001-urdu-translation-spec.spec.prompt.md +76 -0
  50. history/prompts/004-urdu-translation/002-urdu-translation-plan.plan.prompt.md +81 -0
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", "app.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__/config.cpython-313.pyc ADDED
Binary file (1.03 kB). View file
 
app/__pycache__/database.cpython-311.pyc ADDED
Binary file (1.07 kB). View file
 
app/__pycache__/database.cpython-313.pyc ADDED
Binary file (964 Bytes). View file
 
app/__pycache__/main.cpython-311.pyc ADDED
Binary file (1.8 kB). View file
 
app/__pycache__/main.cpython-313.pyc ADDED
Binary file (1.51 kB). View file
 
app/__pycache__/qdrant_client.cpython-311.pyc ADDED
Binary file (2.11 kB). View file
 
app/__pycache__/qdrant_client.cpython-313.pyc ADDED
Binary file (2.66 kB). View file
 
app/config.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/config.py
2
+ from pydantic_settings import BaseSettings
3
+
4
+
5
+ class Settings(BaseSettings):
6
+ # OpenAI Configuration (Required)
7
+ OPENAI_API_KEY: str
8
+
9
+ # Database Configuration (Required)
10
+ NEON_DATABASE_URL: str
11
+
12
+ # Qdrant Vector Database (Required)
13
+ QDRANT_URL: str
14
+ QDRANT_API_KEY: str
15
+
16
+ # OpenAI Model Configuration (Optional - defaults provided)
17
+ OPENAI_MODEL_CHAT: str = "gpt-4o-mini"
18
+ OPENAI_MODEL_EMBEDDING: str = "text-embedding-3-small"
19
+
20
+ class Config:
21
+ env_file = ".env"
22
+ env_file_encoding = 'utf-8'
23
+ extra = "ignore" # Ignore extra env vars like legacy gemini_api_key
24
+
25
+
26
+ 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,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dotenv import load_dotenv
2
+
3
+
4
+ # Load environment variables FIRST
5
+ load_dotenv()
6
+
7
+ from fastapi import FastAPI
8
+ from fastapi.middleware.cors import CORSMiddleware
9
+ from app.routes import chat, translate, personalize
10
+ from app.database import engine, Base
11
+ from app.qdrant_client import init_qdrant_collection
12
+
13
+ app = FastAPI(title="RAG Chatbot API")
14
+
15
+ # CORS Configuration
16
+ app.add_middleware(
17
+ CORSMiddleware,
18
+ allow_origins=["http://localhost:3000", "http://127.0.0.1:3000","http://localhost:3001", "http://127.0.0.1:3001"],
19
+ allow_credentials=True,
20
+ allow_methods=["*"],
21
+ allow_headers=["*"],
22
+ )
23
+
24
+ # Include routers
25
+ app.include_router(chat.router)
26
+ app.include_router(translate.router)
27
+ app.include_router(personalize.router)
28
+
29
+ @app.get("/")
30
+ async def root():
31
+ return {"message": "RAG Chatbot API"}
32
+
33
+ @app.get("/api/health")
34
+ async def health():
35
+ return {"status": "ok"}
app/models/__pycache__/chat.cpython-311.pyc ADDED
Binary file (1.17 kB). View file
 
app/models/__pycache__/translation.cpython-313.pyc ADDED
Binary file (1.01 kB). View file
 
app/models/__pycache__/user.cpython-311.pyc ADDED
Binary file (794 Bytes). View file
 
app/models/__pycache__/user.cpython-313.pyc ADDED
Binary file (1.91 kB). 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/translation.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Column, Integer, String, Text, DateTime
2
+ from sqlalchemy.sql import func
3
+ from app.database import Base
4
+
5
+
6
+ class Translation(Base):
7
+ __tablename__ = "translations"
8
+
9
+ id = Column(Integer, primary_key=True, index=True)
10
+ cache_key = Column(String(255), unique=True, index=True, nullable=False)
11
+ english_text = Column(Text, nullable=False)
12
+ urdu_text = Column(Text, nullable=False)
13
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
app/models/user.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from enum import Enum
2
+ from sqlalchemy import Column, Integer, String, Text, DateTime
3
+ from sqlalchemy.sql import func
4
+ from app.database import Base
5
+
6
+
7
+ class SoftwareLevel(str, Enum):
8
+ """User's software development experience level"""
9
+ beginner = "beginner"
10
+ intermediate = "intermediate"
11
+ advanced = "advanced"
12
+
13
+
14
+ class HardwareLevel(str, Enum):
15
+ """User's hardware/electronics experience level"""
16
+ none = "none"
17
+ basic = "basic"
18
+ experienced = "experienced"
19
+
20
+
21
+ class User(Base):
22
+ __tablename__ = "users"
23
+
24
+ id = Column(Integer, primary_key=True, index=True)
25
+ username = Column(String, unique=True, index=True, nullable=True)
26
+ email = Column(String(255), unique=True, index=True, nullable=False)
27
+ hashed_password = Column(String(60), nullable=False)
28
+ software_level = Column(String(20), nullable=False, default="beginner")
29
+ hardware_level = Column(String(20), nullable=False, default="none")
30
+ learning_goals = Column(Text, nullable=False, default="")
31
+ created_at = Column(DateTime(timezone=True), server_default=func.now())
app/qdrant_client.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/qdrant_client.py
2
+ from qdrant_client import QdrantClient
3
+ from qdrant_client.models import Distance, VectorParams
4
+ from app.config import settings
5
+
6
+ # OpenAI text-embedding-3-small produces 1536-dimensional vectors
7
+ EMBEDDING_DIMENSION = 1536
8
+
9
+ # Initialize Qdrant client
10
+ qdrant_client = QdrantClient(
11
+ url=settings.QDRANT_URL,
12
+ api_key=settings.QDRANT_API_KEY,
13
+ )
14
+
15
+ COLLECTION_NAME = "book_embeddings"
16
+
17
+
18
+ def init_qdrant_collection(recreate: bool = False):
19
+ """Initialize Qdrant collection if it doesn't exist (or recreate if flagged)"""
20
+ try:
21
+ # Check if collection exists
22
+ collections = qdrant_client.get_collections().collections
23
+ collection_names = [col.name for col in collections]
24
+
25
+ if recreate and COLLECTION_NAME in collection_names:
26
+ qdrant_client.delete_collection(collection_name=COLLECTION_NAME)
27
+ print(f"Deleted existing Qdrant collection: {COLLECTION_NAME} (for dimension fix)")
28
+
29
+ if COLLECTION_NAME not in collection_names:
30
+ # Create collection with vector configuration
31
+ qdrant_client.create_collection(
32
+ collection_name=COLLECTION_NAME,
33
+ vectors_config=VectorParams(
34
+ size=EMBEDDING_DIMENSION, # OpenAI text-embedding-3-small dimension
35
+ distance=Distance.COSINE
36
+ )
37
+ )
38
+ print(f"Created Qdrant collection: {COLLECTION_NAME}")
39
+ else:
40
+ # Verify dimensions match (optional safety check)
41
+ info = qdrant_client.get_collection(COLLECTION_NAME)
42
+ if info.config.params.vectors.size != EMBEDDING_DIMENSION:
43
+ raise ValueError(
44
+ f"Collection {COLLECTION_NAME} has wrong size {info.config.params.vectors.size}; "
45
+ f"expected {EMBEDDING_DIMENSION}. Recreate with flag."
46
+ )
47
+ print(f"Qdrant collection already exists with correct dims: {COLLECTION_NAME}")
48
+ except Exception as e:
49
+ print(f"Warning: Could not initialize Qdrant collection: {e}")
50
+
51
+
52
+ def get_qdrant_client():
53
+ """Dependency to get Qdrant client"""
54
+ return qdrant_client
app/routes/__pycache__/auth.cpython-313.pyc ADDED
Binary file (4.26 kB). View file
 
app/routes/__pycache__/chat.cpython-311.pyc ADDED
Binary file (3.74 kB). View file
 
app/routes/__pycache__/chat.cpython-313.pyc ADDED
Binary file (3.2 kB). View file
 
app/routes/__pycache__/personalize.cpython-313.pyc ADDED
Binary file (2.9 kB). View file
 
app/routes/__pycache__/translate.cpython-313.pyc ADDED
Binary file (2.96 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 Gemini
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/routes/personalize.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from sqlalchemy.orm import Session
3
+ from app.database import get_db
4
+ from app.models.user import User
5
+ from app.schemas.personalize import PersonalizeRequest, PersonalizeResponse
6
+ from app.services.openai_service import OpenAIService
7
+ import logging
8
+ import json
9
+
10
+ logger = logging.getLogger(__name__)
11
+ router = APIRouter(prefix="/api", tags=["personalization"])
12
+
13
+
14
+ @router.post("/personalize", response_model=PersonalizeResponse)
15
+ async def personalize_content(
16
+ request: PersonalizeRequest,
17
+ db: Session = Depends(get_db)
18
+ ):
19
+ """
20
+ Personalize content based on user's background.
21
+
22
+ - Fetches user profile from database
23
+ - Uses Gemini to adapt content complexity based on:
24
+ * software_level (beginner/intermediate/advanced)
25
+ * hardware_level (none/basic/experienced)
26
+ * learning_goals (free text)
27
+ - Returns personalized content with description of adjustments
28
+ """
29
+ # Fetch user profile
30
+ user = db.query(User).filter(User.id == request.user_id).first()
31
+ if not user:
32
+ raise HTTPException(status_code=404, detail="User not found")
33
+
34
+ # Personalize via OpenAI SDK + Gemini
35
+ try:
36
+ openai_service = OpenAIService()
37
+ result = await openai_service.personalize_content(
38
+ content=request.content,
39
+ software_level=user.software_level,
40
+ hardware_level=user.hardware_level,
41
+ learning_goals=user.learning_goals or ""
42
+ )
43
+ except json.JSONDecodeError as e:
44
+ logger.error(f"Invalid JSON from Gemini: {e}")
45
+ raise HTTPException(
46
+ status_code=500,
47
+ detail="Invalid response from personalization service"
48
+ )
49
+ except Exception as e:
50
+ logger.error(f"Gemini personalization error: {e}")
51
+ raise HTTPException(
52
+ status_code=503,
53
+ detail="Personalization service temporarily unavailable"
54
+ )
55
+
56
+ return PersonalizeResponse(
57
+ personalized_content=result.get("personalized_content", ""),
58
+ adjustments_made=result.get("adjustments_made", "")
59
+ )
app/routes/translate.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from sqlalchemy.orm import Session
3
+ from sqlalchemy.exc import IntegrityError
4
+ from app.database import get_db
5
+ from app.models.translation import Translation
6
+ from app.schemas.translate import TranslateRequest, TranslateResponse
7
+ from app.services.openai_service import OpenAIService
8
+ import logging
9
+
10
+ logger = logging.getLogger(__name__)
11
+ router = APIRouter(prefix="/api", tags=["translation"])
12
+
13
+
14
+ @router.post("/translate/urdu", response_model=TranslateResponse)
15
+ async def translate_to_urdu(
16
+ request: TranslateRequest,
17
+ db: Session = Depends(get_db)
18
+ ):
19
+ """
20
+ Translate English text to Urdu.
21
+
22
+ - Checks cache first for existing translation
23
+ - If not cached, calls Gemini for translation
24
+ - Stores new translations in database for future requests
25
+ """
26
+ # T007: Check cache first
27
+ cached = db.query(Translation).filter_by(cache_key=request.cache_key).first()
28
+ if cached:
29
+ return TranslateResponse(urdu_text=cached.urdu_text, cached=True)
30
+
31
+ # Perform translation via OpenAI SDK + Gemini
32
+ try:
33
+ openai_service = OpenAIService()
34
+ urdu_text = await openai_service.translate_to_urdu(request.content)
35
+ except Exception as e:
36
+ logger.error(f"Gemini translation error: {e}")
37
+ raise HTTPException(status_code=503, detail="Translation service temporarily unavailable")
38
+
39
+ # T008 & T009: Store in cache with race condition handling
40
+ try:
41
+ translation = Translation(
42
+ cache_key=request.cache_key,
43
+ english_text=request.content,
44
+ urdu_text=urdu_text
45
+ )
46
+ db.add(translation)
47
+ db.commit()
48
+ except IntegrityError:
49
+ db.rollback()
50
+ # Race condition - another request cached this key
51
+ cached = db.query(Translation).filter_by(cache_key=request.cache_key).first()
52
+ if cached:
53
+ return TranslateResponse(urdu_text=cached.urdu_text, cached=True)
54
+ except Exception as e:
55
+ logger.error(f"Database error: {e}")
56
+ # Return translation even if caching fails
57
+ return TranslateResponse(urdu_text=urdu_text, cached=False)
58
+
59
+ return TranslateResponse(urdu_text=urdu_text, cached=False)
app/schemas/__pycache__/auth.cpython-313.pyc ADDED
Binary file (3.04 kB). View file
 
app/schemas/__pycache__/chat.cpython-311.pyc ADDED
Binary file (1.84 kB). View file
 
app/schemas/__pycache__/chat.cpython-313.pyc ADDED
Binary file (1.59 kB). View file
 
app/schemas/__pycache__/personalize.cpython-313.pyc ADDED
Binary file (1.65 kB). View file
 
app/schemas/__pycache__/translate.cpython-313.pyc ADDED
Binary file (1.58 kB). View file
 
app/schemas/auth.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from typing import Optional
3
+ from pydantic import BaseModel, EmailStr, Field, field_validator
4
+ from app.models.user import SoftwareLevel, HardwareLevel
5
+
6
+
7
+ class SignupRequest(BaseModel):
8
+ """Request schema for user registration"""
9
+ email: EmailStr
10
+ password: str = Field(..., min_length=8, description="Password must be at least 8 characters")
11
+ software_level: SoftwareLevel
12
+ hardware_level: HardwareLevel
13
+ learning_goals: str = Field(..., max_length=1000, description="Learning objectives (max 1000 chars)")
14
+
15
+ @field_validator('email')
16
+ @classmethod
17
+ def normalize_email(cls, v: str) -> str:
18
+ """Normalize email to lowercase"""
19
+ return v.lower().strip()
20
+
21
+
22
+ class SigninRequest(BaseModel):
23
+ """Request schema for user authentication"""
24
+ email: EmailStr
25
+ password: str
26
+
27
+ @field_validator('email')
28
+ @classmethod
29
+ def normalize_email(cls, v: str) -> str:
30
+ """Normalize email to lowercase"""
31
+ return v.lower().strip()
32
+
33
+
34
+ class TokenResponse(BaseModel):
35
+ """Response schema for successful authentication"""
36
+ access_token: str
37
+ token_type: str = "bearer"
38
+
39
+
40
+ class UserResponse(BaseModel):
41
+ """Response schema for user profile data"""
42
+ id: int
43
+ email: str
44
+ username: Optional[str] = None
45
+ software_level: str
46
+ hardware_level: str
47
+ learning_goals: str
48
+ created_at: datetime
49
+
50
+ class Config:
51
+ from_attributes = True
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/schemas/personalize.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, field_validator
2
+
3
+
4
+ class PersonalizeRequest(BaseModel):
5
+ content: str
6
+ user_id: int
7
+
8
+ @field_validator('content')
9
+ @classmethod
10
+ def content_not_empty(cls, v):
11
+ if not v or not v.strip():
12
+ raise ValueError('Content cannot be empty')
13
+ v = v.strip()
14
+ if len(v) > 50000:
15
+ raise ValueError('Content exceeds maximum length of 50000 characters')
16
+ return v
17
+
18
+ @field_validator('user_id')
19
+ @classmethod
20
+ def user_id_positive(cls, v):
21
+ if v <= 0:
22
+ raise ValueError('User ID must be a positive integer')
23
+ return v
24
+
25
+
26
+ class PersonalizeResponse(BaseModel):
27
+ personalized_content: str
28
+ adjustments_made: str
app/schemas/translate.py ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, field_validator
2
+
3
+
4
+ class TranslateRequest(BaseModel):
5
+ content: str
6
+ cache_key: str
7
+
8
+ @field_validator('content')
9
+ @classmethod
10
+ def content_not_empty(cls, v):
11
+ if not v or not v.strip():
12
+ raise ValueError('Content cannot be empty')
13
+ return v.strip()
14
+
15
+ @field_validator('cache_key')
16
+ @classmethod
17
+ def cache_key_not_empty(cls, v):
18
+ if not v or not v.strip():
19
+ raise ValueError('Cache key cannot be empty')
20
+ return v.strip()
21
+
22
+
23
+ class TranslateResponse(BaseModel):
24
+ urdu_text: str
25
+ cached: bool
app/services/__pycache__/auth.cpython-313.pyc ADDED
Binary file (3.72 kB). View file
 
app/services/__pycache__/embeddings_service.cpython-311.pyc ADDED
Binary file (1.57 kB). View file
 
app/services/__pycache__/embeddings_service.cpython-313.pyc ADDED
Binary file (1.32 kB). View file
 
app/services/__pycache__/gemini_service.cpython-313.pyc ADDED
Binary file (5.49 kB). View file
 
app/services/__pycache__/openai_service.cpython-311.pyc ADDED
Binary file (1.84 kB). View file
 
app/services/__pycache__/openai_service.cpython-313.pyc ADDED
Binary file (4.72 kB). View file
 
app/services/__pycache__/rag_service.cpython-311.pyc ADDED
Binary file (2.75 kB). View file
 
app/services/__pycache__/rag_service.cpython-313.pyc ADDED
Binary file (2.54 kB). View file
 
app/services/embeddings_service.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/services/embeddings_service.py
2
+ from openai import OpenAI
3
+ from app.config import settings
4
+
5
+
6
+ class EmbeddingsService:
7
+ def __init__(self):
8
+ self.client = OpenAI(
9
+ api_key=settings.OPENAI_API_KEY
10
+ )
11
+ self.model = settings.OPENAI_MODEL_EMBEDDING
12
+
13
+ def create_embedding(self, text: str):
14
+ """Generate embedding for text using OpenAI API."""
15
+ response = self.client.embeddings.create(
16
+ model=self.model,
17
+ input=text
18
+ )
19
+ return response.data[0].embedding
app/services/openai_service.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # app/services/openai_service.py
2
+ from openai import OpenAI
3
+ from app.config import settings
4
+ from typing import List
5
+ import json
6
+
7
+
8
+ class OpenAIService:
9
+ def __init__(self):
10
+ self.client = OpenAI(
11
+ api_key=settings.OPENAI_API_KEY
12
+ )
13
+ self.model = settings.OPENAI_MODEL_CHAT
14
+
15
+ async def get_chat_response(self, prompt: str, history: List[dict] = None) -> str:
16
+ """Generate chat response using OpenAI API."""
17
+ messages = []
18
+
19
+ if history:
20
+ for msg in history:
21
+ if msg["role"] != "system":
22
+ messages.append({
23
+ "role": msg["role"],
24
+ "content": msg["content"]
25
+ })
26
+
27
+ messages.append({"role": "user", "content": prompt})
28
+
29
+ response = self.client.chat.completions.create(
30
+ model=self.model,
31
+ messages=messages
32
+ )
33
+ return response.choices[0].message.content
34
+
35
+ async def translate_to_urdu(self, content: str) -> str:
36
+ """Translate English content to Urdu using OpenAI API."""
37
+ messages = [
38
+ {
39
+ "role": "system",
40
+ "content": "You are a professional translator. Translate the following English text to Urdu. Maintain technical terms. Provide only the Urdu translation without any explanation or additional text."
41
+ },
42
+ {
43
+ "role": "user",
44
+ "content": content
45
+ }
46
+ ]
47
+
48
+ response = self.client.chat.completions.create(
49
+ model=self.model,
50
+ messages=messages
51
+ )
52
+ return response.choices[0].message.content
53
+
54
+ async def personalize_content(
55
+ self,
56
+ content: str,
57
+ software_level: str,
58
+ hardware_level: str,
59
+ learning_goals: str
60
+ ) -> dict:
61
+ """Personalize content based on user's background."""
62
+ system_prompt = f"""You are an expert educational content adapter. Your task is to personalize the following content based on the user's background.
63
+
64
+ USER PROFILE:
65
+ - Software/Programming Level: {software_level}
66
+ - Hardware/Electronics Level: {hardware_level}
67
+ - Learning Goals: {learning_goals if learning_goals else 'Not specified'}
68
+
69
+ PERSONALIZATION RULES:
70
+
71
+ For Software Level:
72
+ - beginner: Add detailed explanations, use simpler terminology, break down complex concepts, provide examples
73
+ - intermediate: Maintain moderate complexity, brief explanations for advanced concepts only
74
+ - advanced: Add technical depth, skip basic explanations, use precise technical terminology
75
+
76
+ For Hardware Level:
77
+ - none: Explain all hardware concepts from scratch, use analogies
78
+ - basic: Brief hardware explanations, define technical terms
79
+ - experienced: Use technical hardware terminology without explanation
80
+
81
+ If learning goals are specified, emphasize and connect content to those objectives.
82
+
83
+ OUTPUT FORMAT:
84
+ Respond with a JSON object containing exactly two fields:
85
+ 1. "personalized_content": The adapted content
86
+ 2. "adjustments_made": A brief description of what changes were made
87
+
88
+ Example response format:
89
+ {{"personalized_content": "...", "adjustments_made": "..."}}"""
90
+
91
+ messages = [
92
+ {"role": "system", "content": system_prompt},
93
+ {"role": "user", "content": content}
94
+ ]
95
+
96
+ response = self.client.chat.completions.create(
97
+ model=self.model,
98
+ messages=messages
99
+ )
100
+
101
+ result = json.loads(response.choices[0].message.content)
102
+ return result
app/services/rag_service.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, gemini_service: OpenAIService):
12
+ # self.qdrant_client = qdrant_client
13
+ # self.embeddings_service = embeddings_service
14
+ # self.gemini_service = gemini_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 = self.embeddings_service.create_embedding(query)
19
+
20
+ # search_result = self.qdrant_client.query_points(
21
+ # collection_name=self.collection_name,
22
+ # query=query_vector,
23
+ # limit=top_k,
24
+ # with_payload=True,
25
+ # ).points
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.gemini_service.get_chat_response(full_prompt)
37
+ # return response
38
+
39
+
40
+ import os
41
+ from qdrant_client import QdrantClient
42
+ from qdrant_client.models import NamedVector
43
+ from typing import List
44
+
45
+ from app.services.openai_service import OpenAIService
46
+ from app.services.embeddings_service import EmbeddingsService
47
+
48
+ class RAGService:
49
+ def __init__(self, qdrant_client: QdrantClient, embeddings_service: EmbeddingsService, gemini_service: OpenAIService):
50
+ self.qdrant_client = qdrant_client
51
+ self.embeddings_service = embeddings_service
52
+ self.gemini_service = gemini_service
53
+ self.collection_name = os.getenv("QDRANT_COLLECTION_NAME", "book_embeddings")
54
+
55
+ async def retrieve_context(self, query: str, top_k: int = 3) -> List[str]:
56
+ query_vector = self.embeddings_service.create_embedding(query)
57
+
58
+ search_result = self.qdrant_client.query_points(
59
+ collection_name=self.collection_name,
60
+ query=query_vector,
61
+ limit=top_k,
62
+ with_payload=True,
63
+ ).points
64
+
65
+ context = [point.payload.get("content", "") for point in search_result if point.payload]
66
+ return context
67
+
68
+ async def generate_response(self, query: str, context: List[str]) -> str:
69
+ full_prompt = f"""Context: {' '.join(context)}
70
+
71
+ Question: {query}
72
+
73
+ Answer:"""
74
+ response = await self.gemini_service.get_chat_response(full_prompt)
75
+ return response
history/prompts/004-urdu-translation/001-urdu-translation-spec.spec.prompt.md ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ id: 001
3
+ title: Urdu Translation API Spec Creation
4
+ stage: spec
5
+ date: 2025-12-14
6
+ surface: agent
7
+ model: claude-opus-4-5-20251101
8
+ feature: 004-urdu-translation
9
+ branch: 004-urdu-translation
10
+ user: user
11
+ command: /sp.specify
12
+ labels: ["translation", "urdu", "openai", "caching", "api"]
13
+ links:
14
+ spec: specs/004-urdu-translation/spec.md
15
+ ticket: null
16
+ adr: null
17
+ pr: null
18
+ files:
19
+ - specs/004-urdu-translation/spec.md
20
+ - specs/004-urdu-translation/checklists/requirements.md
21
+ - history/prompts/004-urdu-translation/001-urdu-translation-spec.spec.prompt.md
22
+ tests:
23
+ - N/A - specification phase
24
+ ---
25
+
26
+ ## Prompt
27
+
28
+ Add Urdu translation API endpoint.
29
+
30
+ **Feature:**
31
+ - POST /api/translate/urdu
32
+ - Request: {content: string, cache_key: string}
33
+ - Use OpenAI GPT-4 for translation
34
+ - Cache translations in Neon database (translations table)
35
+ - Response: {urdu_text: string, cached: boolean}
36
+
37
+ **Tables:**
38
+ - translations: id, cache_key, english_text, urdu_text, created_at
39
+
40
+ Files:
41
+ - app/routes/translate.py
42
+ - app/models/translation.py
43
+
44
+ ## Response snapshot
45
+
46
+ Created feature specification for Urdu Translation API on branch `004-urdu-translation`:
47
+
48
+ 1. **User Stories (3 total)**:
49
+ - P1: Translate English Content to Urdu - send English text and receive Urdu translation via OpenAI GPT-4
50
+ - P1: Retrieve Cached Translations - return cached translations without making new AI requests
51
+ - P2: Translation Data Persistence - store translations in database for retrieval and audit
52
+
53
+ 2. **Functional Requirements (9 total)**: FR-001 through FR-009 covering POST endpoint at /api/translate/urdu, request validation, cache lookup, OpenAI GPT-4 translation, database storage, response format with cached boolean, and error handling.
54
+
55
+ 3. **Key Entity**: Translation with id, cache_key, english_text, urdu_text, and created_at fields.
56
+
57
+ 4. **Success Criteria (5 total)**: Measurable outcomes including new translations < 10 sec, cached responses < 500ms, 100% cache hit rate for existing keys, acceptable translation quality, and 50 concurrent requests support.
58
+
59
+ 5. **Edge Cases**: AI service unavailability, special characters/emoji handling, database connection failures, concurrent requests with same cache key.
60
+
61
+ 6. **Quality Checklist**: All items passed - no clarifications needed, technology-agnostic, testable requirements.
62
+
63
+ ## Outcome
64
+
65
+ - Status: Specification created, ready for planning phase
66
+ - Tests: N/A - specification phase
67
+ - Files: spec.md, requirements.md checklist created
68
+ - Next prompts: /sp.plan or /sp.clarify
69
+ - Reflection: User requirements were clear and comprehensive, enabling complete spec without clarification markers
70
+
71
+ ## Evaluation notes (flywheel)
72
+
73
+ - Failure modes observed: None - requirements were explicit with clear table schema and API contract
74
+ - Graders run and results (PASS/FAIL): Checklist validation PASS (all items checked)
75
+ - Prompt variant (if applicable): N/A
76
+ - Next experiment (smallest change to try): N/A
history/prompts/004-urdu-translation/002-urdu-translation-plan.plan.prompt.md ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ id: 002
3
+ title: Urdu Translation Implementation Plan
4
+ stage: plan
5
+ date: 2025-12-14
6
+ surface: agent
7
+ model: claude-opus-4-5-20251101
8
+ feature: 004-urdu-translation
9
+ branch: 004-urdu-translation
10
+ user: user
11
+ command: /sp.plan
12
+ labels: ["translation", "urdu", "openai", "architecture", "api-design"]
13
+ links:
14
+ spec: specs/004-urdu-translation/spec.md
15
+ ticket: null
16
+ adr: null
17
+ pr: null
18
+ files:
19
+ - specs/004-urdu-translation/plan.md
20
+ - specs/004-urdu-translation/research.md
21
+ - specs/004-urdu-translation/data-model.md
22
+ - specs/004-urdu-translation/contracts/openapi.yaml
23
+ - specs/004-urdu-translation/quickstart.md
24
+ - history/prompts/004-urdu-translation/002-urdu-translation-plan.plan.prompt.md
25
+ tests:
26
+ - N/A - planning phase
27
+ ---
28
+
29
+ ## Prompt
30
+
31
+ /sp.plan
32
+
33
+ Execute the implementation planning workflow for the Urdu Translation API feature (004-urdu-translation).
34
+
35
+ ## Response snapshot
36
+
37
+ Created implementation plan for Urdu Translation API with the following artifacts:
38
+
39
+ 1. **research.md**: Technical decisions resolved
40
+ - OpenAI GPT-4 for translation (quality for Urdu script)
41
+ - Database-first caching strategy with cache_key lookup
42
+ - Error handling patterns matching existing codebase
43
+ - Race condition handling with UNIQUE constraint
44
+
45
+ 2. **data-model.md**: Translation entity defined
46
+ - Fields: id, cache_key (unique), english_text, urdu_text, created_at
47
+ - Indexes: Primary key on id, unique index on cache_key
48
+ - SQLAlchemy model ready for implementation
49
+
50
+ 3. **contracts/openapi.yaml**: API specification
51
+ - POST /api/translate/urdu endpoint
52
+ - Request: {content, cache_key}
53
+ - Response: {urdu_text, cached}
54
+ - Error responses: 400, 500, 503
55
+
56
+ 4. **quickstart.md**: Developer guide
57
+ - curl examples for testing
58
+ - Cache key best practices
59
+ - Files to implement list
60
+
61
+ 5. **plan.md**: Implementation plan
62
+ - Architecture diagram
63
+ - Code snippets for all components
64
+ - 6-step implementation order
65
+ - Error handling strategy
66
+ - Testing strategy
67
+
68
+ ## Outcome
69
+
70
+ - Status: Planning complete, ready for task generation
71
+ - Tests: N/A - planning phase
72
+ - Files: 5 planning artifacts created
73
+ - Next prompts: /sp.tasks to generate implementation tasks
74
+ - Reflection: Existing codebase patterns (OpenAIService, SQLAlchemy, FastAPI) enabled clear implementation path
75
+
76
+ ## Evaluation notes (flywheel)
77
+
78
+ - Failure modes observed: None - clear requirements and existing patterns
79
+ - Graders run and results (PASS/FAIL): Constitution compliance PASS
80
+ - Prompt variant (if applicable): N/A
81
+ - Next experiment (smallest change to try): N/A