Spaces:
No application file
No application file
Commit ·
0cee4dc
1
Parent(s): cf3a37f
Add application file
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- DockerFile +18 -0
- app/__pycache__/config.cpython-311.pyc +0 -0
- app/__pycache__/config.cpython-313.pyc +0 -0
- app/__pycache__/database.cpython-311.pyc +0 -0
- app/__pycache__/database.cpython-313.pyc +0 -0
- app/__pycache__/main.cpython-311.pyc +0 -0
- app/__pycache__/main.cpython-313.pyc +0 -0
- app/__pycache__/qdrant_client.cpython-311.pyc +0 -0
- app/__pycache__/qdrant_client.cpython-313.pyc +0 -0
- app/config.py +26 -0
- app/database.py +17 -0
- app/main.py +35 -0
- app/models/__pycache__/chat.cpython-311.pyc +0 -0
- app/models/__pycache__/translation.cpython-313.pyc +0 -0
- app/models/__pycache__/user.cpython-311.pyc +0 -0
- app/models/__pycache__/user.cpython-313.pyc +0 -0
- app/models/chat.py +14 -0
- app/models/translation.py +13 -0
- app/models/user.py +31 -0
- app/qdrant_client.py +54 -0
- app/routes/__pycache__/auth.cpython-313.pyc +0 -0
- app/routes/__pycache__/chat.cpython-311.pyc +0 -0
- app/routes/__pycache__/chat.cpython-313.pyc +0 -0
- app/routes/__pycache__/personalize.cpython-313.pyc +0 -0
- app/routes/__pycache__/translate.cpython-313.pyc +0 -0
- app/routes/chat.py +60 -0
- app/routes/personalize.py +59 -0
- app/routes/translate.py +59 -0
- app/schemas/__pycache__/auth.cpython-313.pyc +0 -0
- app/schemas/__pycache__/chat.cpython-311.pyc +0 -0
- app/schemas/__pycache__/chat.cpython-313.pyc +0 -0
- app/schemas/__pycache__/personalize.cpython-313.pyc +0 -0
- app/schemas/__pycache__/translate.cpython-313.pyc +0 -0
- app/schemas/auth.py +51 -0
- app/schemas/chat.py +23 -0
- app/schemas/personalize.py +28 -0
- app/schemas/translate.py +25 -0
- app/services/__pycache__/auth.cpython-313.pyc +0 -0
- app/services/__pycache__/embeddings_service.cpython-311.pyc +0 -0
- app/services/__pycache__/embeddings_service.cpython-313.pyc +0 -0
- app/services/__pycache__/gemini_service.cpython-313.pyc +0 -0
- app/services/__pycache__/openai_service.cpython-311.pyc +0 -0
- app/services/__pycache__/openai_service.cpython-313.pyc +0 -0
- app/services/__pycache__/rag_service.cpython-311.pyc +0 -0
- app/services/__pycache__/rag_service.cpython-313.pyc +0 -0
- app/services/embeddings_service.py +19 -0
- app/services/openai_service.py +102 -0
- app/services/rag_service.py +75 -0
- history/prompts/004-urdu-translation/001-urdu-translation-spec.spec.prompt.md +76 -0
- 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
|