nikeshn commited on
Commit
b2f6cec
·
verified ·
1 Parent(s): eaac430

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +120 -48
app.py CHANGED
@@ -2,14 +2,14 @@
2
  Khalifa University Library RAG Backend
3
  LangChain + FAISS + FastAPI on Hugging Face Spaces
4
 
5
- This app:
6
- 1. Loads scraped library pages from the 'knowledge/' folder
7
- 2. Chunks and embeds them using OpenAI embeddings
8
- 3. Stores in a FAISS vector store
9
- 4. Exposes a /rag endpoint that retrieves relevant chunks and generates grounded answers
10
 
11
  Environment variables (set as HF Space Secrets):
12
- OPENAI_API_KEY for embeddings + LLM
 
13
  """
14
 
15
  import os
@@ -32,11 +32,29 @@ FAISS_INDEX_PATH = "faiss_index"
32
  CHUNK_SIZE = 800
33
  CHUNK_OVERLAP = 100
34
  EMBEDDING_MODEL = "text-embedding-3-small"
35
- LLM_MODEL = "gpt-4o-mini"
36
  TOP_K = 5
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  # ===== GLOBAL STATE =====
39
- qa_chain = None
40
  vectorstore = None
41
 
42
 
@@ -51,7 +69,6 @@ def load_documents():
51
  with open(filepath, "r", encoding="utf-8") as f:
52
  content = f.read()
53
 
54
- # Extract metadata from first two lines
55
  lines = content.split("\n", 3)
56
  source = ""
57
  title = ""
@@ -89,14 +106,12 @@ def build_vectorstore(docs):
89
 
90
  embeddings = OpenAIEmbeddings(model=EMBEDDING_MODEL)
91
 
92
- # Try to load existing index first
93
- if os.path.exists(FAISS_INDEX_PATH):
94
  print("Loading existing FAISS index...")
95
  store = FAISS.load_local(FAISS_INDEX_PATH, embeddings, allow_dangerous_deserialization=True)
96
  print(f"Loaded FAISS index with {store.index.ntotal} vectors")
97
  return store
98
 
99
- # Build new index
100
  print("Building new FAISS index...")
101
  store = FAISS.from_documents(chunks, embeddings)
102
  store.save_local(FAISS_INDEX_PATH)
@@ -104,34 +119,35 @@ def build_vectorstore(docs):
104
  return store
105
 
106
 
107
- def build_chain(store):
108
- """Build the LangChain RetrievalQA chain."""
109
- llm = ChatOpenAI(model=LLM_MODEL, temperature=0.2, max_tokens=500)
110
-
111
- prompt_template = PromptTemplate(
112
- input_variables=["context", "question"],
113
- template="""You are the Khalifa University Library AI Assistant in Abu Dhabi, UAE.
114
- KU means Khalifa University, NOT Kuwait University.
115
-
116
- Use ONLY the following context from the Khalifa University Library website to answer the question.
117
- If the context doesn't contain enough information, say "I don't have specific information about this in our library knowledge base" and suggest contacting Ask a Librarian at https://library.ku.ac.ae/AskUs
118
-
119
- Always include relevant URLs from the context when available.
120
- Keep answers concise (2-4 sentences) and helpful.
121
-
122
- Context:
123
- {context}
124
-
125
- Question: {question}
126
 
127
- Answer:"""
128
- )
129
 
 
 
 
130
  chain = RetrievalQA.from_chain_type(
131
  llm=llm,
132
  chain_type="stuff",
133
  retriever=store.as_retriever(search_kwargs={"k": TOP_K}),
134
- chain_type_kwargs={"prompt": prompt_template},
135
  return_source_documents=True,
136
  )
137
  return chain
@@ -140,20 +156,23 @@ Answer:"""
140
  # ===== STARTUP =====
141
  @asynccontextmanager
142
  async def lifespan(app: FastAPI):
143
- global qa_chain, vectorstore
144
  print("=== Starting KU Library RAG Backend ===")
145
 
146
  docs = load_documents()
147
  if docs:
148
  vectorstore = build_vectorstore(docs)
149
- qa_chain = build_chain(vectorstore)
150
- print("RAG chain ready!")
151
  else:
152
- print("WARNING: No knowledge files found. RAG will not work.")
153
- print(f"Please add .txt files to the '{KNOWLEDGE_DIR}/' directory.")
154
 
155
- yield
 
 
 
156
 
 
157
  print("Shutting down...")
158
 
159
 
@@ -162,14 +181,16 @@ app = FastAPI(title="KU Library RAG", lifespan=lifespan)
162
 
163
  app.add_middleware(
164
  CORSMiddleware,
165
- allow_origins=["*"], # Restrict to your domains in production
166
  allow_methods=["POST", "GET"],
167
  allow_headers=["*"],
168
  )
169
 
170
 
 
171
  class QueryRequest(BaseModel):
172
  question: str
 
173
  top_k: int = 5
174
 
175
 
@@ -181,30 +202,40 @@ class SourceDoc(BaseModel):
181
 
182
  class QueryResponse(BaseModel):
183
  answer: str
 
184
  sources: list[SourceDoc]
185
  error: str | None = None
186
 
187
 
 
188
  @app.get("/")
189
  def health():
190
  return {
191
  "status": "ok",
192
- "rag_ready": qa_chain is not None,
 
 
 
 
193
  "service": "KU Library RAG Backend",
194
  }
195
 
196
 
197
  @app.post("/rag", response_model=QueryResponse)
198
  async def rag_query(req: QueryRequest):
199
- if not qa_chain:
200
  return QueryResponse(
201
  answer="RAG system not initialized. Knowledge base may be empty.",
 
202
  sources=[],
203
  error="No knowledge files loaded"
204
  )
205
 
 
 
206
  try:
207
- result = qa_chain.invoke({"query": req.question})
 
208
  answer = result.get("result", "No answer generated.")
209
  source_docs = result.get("source_documents", [])
210
 
@@ -222,11 +253,53 @@ async def rag_query(req: QueryRequest):
222
  snippet=doc.page_content[:200] + "..."
223
  ))
224
 
225
- return QueryResponse(answer=answer, sources=sources)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
226
 
227
  except Exception as e:
228
  return QueryResponse(
229
  answer="Sorry, I encountered an error processing your question.",
 
230
  sources=[],
231
  error=str(e)
232
  )
@@ -235,7 +308,7 @@ async def rag_query(req: QueryRequest):
235
  @app.post("/rebuild")
236
  async def rebuild_index():
237
  """Force rebuild the FAISS index from knowledge files."""
238
- global qa_chain, vectorstore
239
  try:
240
  if os.path.exists(FAISS_INDEX_PATH):
241
  import shutil
@@ -246,7 +319,6 @@ async def rebuild_index():
246
  return {"error": "No knowledge files found"}
247
 
248
  vectorstore = build_vectorstore(docs)
249
- qa_chain = build_chain(vectorstore)
250
  return {"status": "ok", "chunks": vectorstore.index.ntotal}
251
  except Exception as e:
252
- return {"error": str(e)}
 
2
  Khalifa University Library RAG Backend
3
  LangChain + FAISS + FastAPI on Hugging Face Spaces
4
 
5
+ Features:
6
+ - OpenAI embeddings (text-embedding-3-small) for vector search
7
+ - Switchable LLM: ChatGPT or Claude for answer generation
8
+ - FAISS vector store for fast similarity search
 
9
 
10
  Environment variables (set as HF Space Secrets):
11
+ OPENAI_API_KEY required (embeddings + ChatGPT answers)
12
+ ANTHROPIC_API_KEY — optional (enables Claude answers)
13
  """
14
 
15
  import os
 
32
  CHUNK_SIZE = 800
33
  CHUNK_OVERLAP = 100
34
  EMBEDDING_MODEL = "text-embedding-3-small"
 
35
  TOP_K = 5
36
 
37
+ # ===== RAG PROMPT =====
38
+ RAG_PROMPT = PromptTemplate(
39
+ input_variables=["context", "question"],
40
+ template="""You are the Khalifa University Library AI Assistant in Abu Dhabi, UAE.
41
+ KU means Khalifa University, NOT Kuwait University.
42
+
43
+ Use ONLY the following context from the Khalifa University Library website to answer the question.
44
+ If the context doesn't contain enough information, say "I don't have specific information about this in our library knowledge base" and suggest contacting Ask a Librarian at https://library.ku.ac.ae/AskUs
45
+
46
+ Always include relevant URLs from the context when available.
47
+ Keep answers concise (2-4 sentences) and helpful.
48
+
49
+ Context:
50
+ {context}
51
+
52
+ Question: {question}
53
+
54
+ Answer:"""
55
+ )
56
+
57
  # ===== GLOBAL STATE =====
 
58
  vectorstore = None
59
 
60
 
 
69
  with open(filepath, "r", encoding="utf-8") as f:
70
  content = f.read()
71
 
 
72
  lines = content.split("\n", 3)
73
  source = ""
74
  title = ""
 
106
 
107
  embeddings = OpenAIEmbeddings(model=EMBEDDING_MODEL)
108
 
109
+ if os.path.exists(os.path.join(FAISS_INDEX_PATH, "index.faiss")):
 
110
  print("Loading existing FAISS index...")
111
  store = FAISS.load_local(FAISS_INDEX_PATH, embeddings, allow_dangerous_deserialization=True)
112
  print(f"Loaded FAISS index with {store.index.ntotal} vectors")
113
  return store
114
 
 
115
  print("Building new FAISS index...")
116
  store = FAISS.from_documents(chunks, embeddings)
117
  store.save_local(FAISS_INDEX_PATH)
 
119
  return store
120
 
121
 
122
+ def get_llm(model_name: str = "gpt"):
123
+ """Get LLM based on model selection."""
124
+ if model_name == "claude":
125
+ api_key = os.environ.get("ANTHROPIC_API_KEY")
126
+ if not api_key:
127
+ raise ValueError("ANTHROPIC_API_KEY not configured. Switch to ChatGPT or add the key in Space Secrets.")
128
+ from langchain_anthropic import ChatAnthropic
129
+ return ChatAnthropic(
130
+ model="claude-sonnet-4-20250514",
131
+ temperature=0.2,
132
+ max_tokens=500,
133
+ anthropic_api_key=api_key,
134
+ )
135
+ else:
136
+ return ChatOpenAI(
137
+ model="gpt-4o-mini",
138
+ temperature=0.2,
139
+ max_tokens=500,
140
+ )
141
 
 
 
142
 
143
+ def build_chain(store, model_name: str = "gpt"):
144
+ """Build the LangChain RetrievalQA chain with the selected LLM."""
145
+ llm = get_llm(model_name)
146
  chain = RetrievalQA.from_chain_type(
147
  llm=llm,
148
  chain_type="stuff",
149
  retriever=store.as_retriever(search_kwargs={"k": TOP_K}),
150
+ chain_type_kwargs={"prompt": RAG_PROMPT},
151
  return_source_documents=True,
152
  )
153
  return chain
 
156
  # ===== STARTUP =====
157
  @asynccontextmanager
158
  async def lifespan(app: FastAPI):
159
+ global vectorstore
160
  print("=== Starting KU Library RAG Backend ===")
161
 
162
  docs = load_documents()
163
  if docs:
164
  vectorstore = build_vectorstore(docs)
165
+ print("Vector store ready!")
 
166
  else:
167
+ print("WARNING: No knowledge files found.")
168
+ print(f"Add .txt files to '{KNOWLEDGE_DIR}/' and restart.")
169
 
170
+ # Check which LLMs are available
171
+ has_openai = bool(os.environ.get("OPENAI_API_KEY"))
172
+ has_claude = bool(os.environ.get("ANTHROPIC_API_KEY"))
173
+ print(f"LLMs available: ChatGPT={'YES' if has_openai else 'NO'}, Claude={'YES' if has_claude else 'NO'}")
174
 
175
+ yield
176
  print("Shutting down...")
177
 
178
 
 
181
 
182
  app.add_middleware(
183
  CORSMiddleware,
184
+ allow_origins=["*"],
185
  allow_methods=["POST", "GET"],
186
  allow_headers=["*"],
187
  )
188
 
189
 
190
+ # ===== REQUEST/RESPONSE MODELS =====
191
  class QueryRequest(BaseModel):
192
  question: str
193
+ model: str = "gpt" # "gpt" or "claude"
194
  top_k: int = 5
195
 
196
 
 
202
 
203
  class QueryResponse(BaseModel):
204
  answer: str
205
+ model_used: str
206
  sources: list[SourceDoc]
207
  error: str | None = None
208
 
209
 
210
+ # ===== ENDPOINTS =====
211
  @app.get("/")
212
  def health():
213
  return {
214
  "status": "ok",
215
+ "vectorstore_ready": vectorstore is not None,
216
+ "models": {
217
+ "gpt": bool(os.environ.get("OPENAI_API_KEY")),
218
+ "claude": bool(os.environ.get("ANTHROPIC_API_KEY")),
219
+ },
220
  "service": "KU Library RAG Backend",
221
  }
222
 
223
 
224
  @app.post("/rag", response_model=QueryResponse)
225
  async def rag_query(req: QueryRequest):
226
+ if not vectorstore:
227
  return QueryResponse(
228
  answer="RAG system not initialized. Knowledge base may be empty.",
229
+ model_used=req.model,
230
  sources=[],
231
  error="No knowledge files loaded"
232
  )
233
 
234
+ model_name = req.model if req.model in ("gpt", "claude") else "gpt"
235
+
236
  try:
237
+ chain = build_chain(vectorstore, model_name)
238
+ result = chain.invoke({"query": req.question})
239
  answer = result.get("result", "No answer generated.")
240
  source_docs = result.get("source_documents", [])
241
 
 
253
  snippet=doc.page_content[:200] + "..."
254
  ))
255
 
256
+ return QueryResponse(
257
+ answer=answer,
258
+ model_used=model_name,
259
+ sources=sources,
260
+ )
261
+
262
+ except ValueError as e:
263
+ # Model not available — fallback to the other one
264
+ fallback = "gpt" if model_name == "claude" else "claude"
265
+ try:
266
+ chain = build_chain(vectorstore, fallback)
267
+ result = chain.invoke({"query": req.question})
268
+ answer = result.get("result", "No answer generated.")
269
+ source_docs = result.get("source_documents", [])
270
+
271
+ sources = []
272
+ seen = set()
273
+ for doc in source_docs:
274
+ src = doc.metadata.get("source", "")
275
+ title = doc.metadata.get("title", "")
276
+ key = src or title
277
+ if key and key not in seen:
278
+ seen.add(key)
279
+ sources.append(SourceDoc(
280
+ title=title,
281
+ source=src,
282
+ snippet=doc.page_content[:200] + "..."
283
+ ))
284
+
285
+ return QueryResponse(
286
+ answer=answer,
287
+ model_used=fallback,
288
+ sources=sources,
289
+ error=f"{model_name} unavailable, used {fallback} instead"
290
+ )
291
+ except Exception as e2:
292
+ return QueryResponse(
293
+ answer="Both models failed. Please check API keys in Space Secrets.",
294
+ model_used="none",
295
+ sources=[],
296
+ error=str(e2)
297
+ )
298
 
299
  except Exception as e:
300
  return QueryResponse(
301
  answer="Sorry, I encountered an error processing your question.",
302
+ model_used=model_name,
303
  sources=[],
304
  error=str(e)
305
  )
 
308
  @app.post("/rebuild")
309
  async def rebuild_index():
310
  """Force rebuild the FAISS index from knowledge files."""
311
+ global vectorstore
312
  try:
313
  if os.path.exists(FAISS_INDEX_PATH):
314
  import shutil
 
319
  return {"error": "No knowledge files found"}
320
 
321
  vectorstore = build_vectorstore(docs)
 
322
  return {"status": "ok", "chunks": vectorstore.index.ntotal}
323
  except Exception as e:
324
+ return {"error": str(e)}