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)