File size: 7,985 Bytes
22fd41f 56da115 22fd41f 56da115 22fd41f 56da115 22fd41f 56da115 22fd41f 56da115 22fd41f 56da115 22fd41f 56da115 22fd41f 56da115 22fd41f 56da115 22fd41f 56da115 22fd41f 7ae27cd 56da115 22fd41f 56da115 22fd41f 56da115 22fd41f 56da115 22fd41f 56da115 7ae27cd 56da115 22fd41f 56da115 a7bda33 22fd41f 56da115 22fd41f 56da115 22fd41f 56da115 22fd41f a7bda33 56da115 22fd41f 56da115 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 | """
app.py β FastAPI backend server for the Sacred Texts RAG application.
Endpoints:
POST /ask β Ask a question, get a streamed answer with sources
POST /clear β Clear conversation history for a session
GET /history β Retrieve conversation history for a session
GET /health β Health check
GET /books β List books currently in the knowledge base
Run with:
python app.py
"""
import os
import uuid
from fastapi import FastAPI, HTTPException, Request, Response
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel, Field
from dotenv import load_dotenv
from fastapi.responses import StreamingResponse, FileResponse, JSONResponse
from rag_chain import (
query_sacred_texts,
get_embeddings,
get_vector_store,
clear_session,
get_history,
)
from langchain_core.messages import HumanMessage, AIMessage
load_dotenv()
# βββ App Setup ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
app = FastAPI(
title="Sacred Texts RAG API",
description="Ask questions answered exclusively from Bhagavad Gita, Quran, Bible, and Guru Granth Sahib",
version="2.0.0",
)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
expose_headers=["X-Session-Id"],
)
SESSION_COOKIE = "rag_session_id"
# βββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def get_or_create_session(request: Request, response: Response) -> str:
"""
Read the session ID from the cookie (or X-Session-Id header).
If absent, generate a new one and set it on the response cookie.
"""
session_id = (
request.cookies.get(SESSION_COOKIE)
or request.headers.get("X-Session-Id")
)
if not session_id:
session_id = str(uuid.uuid4())
response.set_cookie(
key=SESSION_COOKIE,
value=session_id,
httponly=True,
samesite="lax",
max_age=60 * 60 * 24, # 24 hours
)
return session_id
# βββ Request / Response Models ββββββββββββββββββββββββββββββββββββββββββββββββ
class AskRequest(BaseModel):
question: str = Field(..., min_length=3, max_length=1000,
example="What do the scriptures say about compassion?")
session_id: str | None = Field(
default=None,
description="Optional session ID for multi-turn conversations. "
"If omitted, the server reads/creates one via cookie.",
)
class HealthResponse(BaseModel):
status: str
message: str
class BooksResponse(BaseModel):
books: list[str]
total_chunks: int
class ClearRequest(BaseModel):
session_id: str | None = None
class HistoryItem(BaseModel):
role: str # "human" | "ai"
content: str
class HistoryResponse(BaseModel):
session_id: str
turns: int
messages: list[HistoryItem]
# βββ Routes βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@app.get("/health", response_model=HealthResponse, tags=["System"])
def health_check():
return {"status": "ok", "message": "Sacred Texts RAG is running ποΈ"}
@app.get("/books", response_model=BooksResponse, tags=["Knowledge Base"])
def list_books():
try:
embeddings = get_embeddings()
vector_store = get_vector_store(embeddings)
collection = vector_store._collection
results = collection.get(include=["metadatas"])
metadatas = results.get("metadatas", [])
books = sorted(set(m.get("book", "Unknown") for m in metadatas if m))
return {"books": books, "total_chunks": len(metadatas)}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Could not read knowledge base: {e}")
@app.post("/ask", tags=["Query"])
async def ask(request_body: AskRequest, request: Request, response: Response):
"""
Ask a spiritual or philosophical question.
Streams the answer as NDJSON (one JSON object per line).
Maintains per-session conversation history automatically via cookie or
the `session_id` field in the request body.
"""
if not request_body.question.strip():
raise HTTPException(status_code=400, detail="Question cannot be empty.")
# Resolve session: body field > cookie/header > new
if request_body.session_id:
session_id = request_body.session_id
else:
session_id = get_or_create_session(request, response)
try:
stream = query_sacred_texts(request_body.question, session_id=session_id)
# We need to forward the session_id so the frontend can persist it
headers = {"X-Session-Id": session_id}
return StreamingResponse(
stream,
media_type="application/x-ndjson",
headers=headers,
)
except FileNotFoundError:
raise HTTPException(
status_code=503,
detail="Knowledge base not found. Run `python ingest.py` first.",
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.post("/clear", tags=["Session"])
async def clear_conversation(body: ClearRequest, request: Request, response: Response):
"""
Clear the conversation history for the given session.
If session_id is omitted, clears the session identified by cookie.
"""
session_id = body.session_id or request.cookies.get(SESSION_COOKIE)
if not session_id:
raise HTTPException(status_code=400, detail="No session to clear.")
clear_session(session_id)
return {"status": "cleared", "session_id": session_id}
@app.get("/history", response_model=HistoryResponse, tags=["Session"])
async def conversation_history(session_id: str | None = None, request: Request = None):
"""
Return the conversation history for a session (for debugging / display).
"""
sid = session_id or (request.cookies.get(SESSION_COOKIE) if request else None)
if not sid:
raise HTTPException(status_code=400, detail="Provide session_id query param or cookie.")
messages = get_history(sid)
items = []
for msg in messages:
if isinstance(msg, HumanMessage):
items.append(HistoryItem(role="human", content=msg.content))
elif isinstance(msg, AIMessage):
items.append(HistoryItem(role="ai", content=msg.content))
return HistoryResponse(
session_id=sid,
turns=len(items) // 2,
messages=items,
)
@app.get("/", include_in_schema=False)
async def serve_frontend():
frontend_path = "frontend/index.html"
if os.path.exists(frontend_path):
return FileResponse(frontend_path)
return {"message": "Sacred Texts RAG API is live. Visit /docs for Swagger UI."}
# βββ Entry Point ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if __name__ == "__main__":
import uvicorn
host = os.getenv("HOST", "0.0.0.0")
port = int(os.getenv("PORT", "7860"))
print(f"\nποΈ Sacred Texts RAG β API Server v2.0")
print(f"{'β' * 40}")
print(f"π Running at : http://{host}:{port}")
print(f"π§ Multi-turn conversation: ENABLED")
print(f"{'β' * 40}\n")
uvicorn.run("app:app", host=host, port=port, reload=False) |