WaysAhead Global commited on
Commit
4e8a55f
·
verified ·
1 Parent(s): dcad7e7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +356 -57
app.py CHANGED
@@ -1,17 +1,36 @@
1
- from fastapi import FastAPI, Request, UploadFile
2
  from fastapi.responses import JSONResponse
3
- import os, json, logging, ast, psycopg2, hashlib
 
 
4
  from typing import List
 
 
 
 
 
 
5
  from langchain_community.vectorstores import FAISS
6
  from langchain_openai import ChatOpenAI, OpenAIEmbeddings
7
  from langchain_core.messages import AIMessage, HumanMessage
8
  from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
9
  from langchain.chains import create_history_aware_retriever, create_retrieval_chain
10
  from langchain.chains.combine_documents import create_stuff_documents_chain
11
- from functools import lru_cache
12
-
13
- # ----------------- Configuration ----------------
 
14
  FAISS_INDEX_PATH = "faiss_index"
 
 
 
 
 
 
 
 
 
 
15
  DB_CONFIG = {
16
  "host": "45.194.46.142",
17
  "port": 5432,
@@ -21,15 +40,149 @@ DB_CONFIG = {
21
  }
22
  TABLE_NAME = "tb_vector_embeddings"
23
 
24
- # ------------------ Setup ------------------
25
- app = FastAPI()
26
- logging.basicConfig(level=logging.INFO)
27
 
28
- vector_store = None
29
- chat_history = [AIMessage(content="Hello! I am your AI assistant. Ask me questions based on your documents.")]
30
- response_cache = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
 
32
- # ------------------ PGVector Load ------------------
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  def fetch_pgvector_documents():
34
  conn = psycopg2.connect(**DB_CONFIG)
35
  cursor = conn.cursor()
@@ -44,29 +197,42 @@ def fetch_pgvector_documents():
44
  emb_array = ast.literal_eval(emb_array)
45
  if not emb_array or len(emb_array) != 1536:
46
  continue
 
 
 
 
47
  texts.append(text)
48
  embeddings.append(list(emb_array))
49
- metadatas.append(json.loads(meta) if isinstance(meta, str) else meta)
50
-
51
  return texts, embeddings, metadatas
52
 
53
- # ------------------ Initialize Vector Store ------------------
54
  def initialize_vector_store():
55
  global vector_store
 
 
 
 
 
 
 
 
56
  try:
57
  texts, embeddings, metadatas = fetch_pgvector_documents()
58
- text_embedding_pairs = list(zip(texts, embeddings))
59
- vector_store = FAISS.from_embeddings(text_embedding_pairs, OpenAIEmbeddings(), metadatas=metadatas)
 
 
 
60
  vector_store.save_local(FAISS_INDEX_PATH)
61
- logging.info("✅ FAISS index created from PostgreSQL embeddings.")
62
  except Exception as e:
63
- logging.error(f"❌ Failed to initialize FAISS from PG: {e}")
64
  vector_store = None
65
 
66
- # ------------------ LangChain Chain ------------------
67
  def get_context_retriever_chain():
68
  llm = ChatOpenAI(model="gpt-4o-mini-2024-07-18", temperature=0)
69
- retriever = vector_store.as_retriever()
70
  prompt = ChatPromptTemplate.from_messages([
71
  MessagesPlaceholder(variable_name="chat_history"),
72
  ("user", "{input}"),
@@ -75,67 +241,200 @@ def get_context_retriever_chain():
75
  return create_history_aware_retriever(llm, retriever, prompt)
76
 
77
  def get_conversational_rag_chain(retriever_chain):
78
- llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
79
  prompt = ChatPromptTemplate.from_messages([
80
- ("system", "You are a helpful assistant. Only answer using the provided context:\n\n{context}"),
81
  MessagesPlaceholder(variable_name="chat_history"),
82
  ("user", "{input}")
83
  ])
84
  doc_chain = create_stuff_documents_chain(llm=llm, prompt=prompt)
85
  return create_retrieval_chain(retriever_chain, doc_chain)
86
 
87
- # ------------------ Q&A ------------------
88
  def ask_question(question):
89
  global chat_history
90
  if vector_store is None:
91
- return "No documents loaded yet.", []
92
 
93
- # Caching using hash key
94
- question_hash = hashlib.md5(question.strip().encode()).hexdigest()
95
- if question_hash in response_cache:
96
- return response_cache[question_hash]
97
 
 
98
  retriever_chain = get_context_retriever_chain()
99
  rag_chain = get_conversational_rag_chain(retriever_chain)
100
-
101
- result = rag_chain.invoke({
102
- "input": question.strip(),
103
- "chat_history": chat_history
104
- })
105
 
106
  answer = result.get("answer", "No answer found.")
107
- chat_history.append(HumanMessage(content=question.strip()))
108
  chat_history.append(AIMessage(content=answer))
109
 
110
- docs = vector_store.similarity_search(question.strip(), k=3)
111
- sources = [{
112
- "source": doc.metadata.get("file_name", doc.metadata.get("source", "Unknown")),
113
- "page": str(doc.metadata.get("page", "Unknown")),
114
- "snippet": doc.page_content[:300]
115
- } for doc in docs]
116
 
117
- response_cache[question_hash] = (answer, sources)
118
- return answer, sources
119
 
120
- # ------------------ FastAPI Endpoints ------------------
121
- @app.on_event("startup")
122
- def startup_event():
123
- initialize_vector_store()
 
124
 
125
- @app.get("/refresh")
126
- def refresh_faiss():
127
- initialize_vector_store()
128
- return {"status": "reinitialized" if vector_store else "failed"}
129
 
130
- @app.get("/status")
131
- def get_status():
132
- return {"status": "ready" if vector_store else "not_initialized"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
  @app.post("/ask")
135
- def ask_from_documents(payload: dict):
136
  question = payload.get("question", "").strip()
137
  if not question:
138
  return JSONResponse(status_code=400, content={"error": "Question cannot be empty"})
 
139
 
140
- answer, sources = ask_question(question)
141
- return {"answer": answer, "sources": sources}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, UploadFile, Form, Request
2
  from fastapi.responses import JSONResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ import os, shutil, hashlib, logging, re, json
5
+ from uuid import uuid4
6
  from typing import List
7
+ import numpy as np
8
+ from deepface import DeepFace
9
+ from scipy.spatial.distance import cosine
10
+ from duckduckgo_search import DDGS
11
+ import spacy
12
+
13
  from langchain_community.vectorstores import FAISS
14
  from langchain_openai import ChatOpenAI, OpenAIEmbeddings
15
  from langchain_core.messages import AIMessage, HumanMessage
16
  from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
17
  from langchain.chains import create_history_aware_retriever, create_retrieval_chain
18
  from langchain.chains.combine_documents import create_stuff_documents_chain
19
+ import psycopg2, ast
20
+ # ---------------- App Setup ----------------
21
+ app = FastAPI()
22
+ UPLOAD_FOLDER = "user_photos"
23
  FAISS_INDEX_PATH = "faiss_index"
24
+ os.makedirs(UPLOAD_FOLDER, exist_ok=True)
25
+ app.mount("/gallery", StaticFiles(directory=UPLOAD_FOLDER), name="gallery")
26
+
27
+ # ---------------- Globals ----------------
28
+ photo_db = [] # list of {name, email, photo_url, full_url, embedding}
29
+ chat_history = [AIMessage(content="Hello! I am your AI assistant.")]
30
+ response_cache = {}
31
+ vector_store = None
32
+ nlp = spacy.load("en_core_web_sm")
33
+
34
  DB_CONFIG = {
35
  "host": "45.194.46.142",
36
  "port": 5432,
 
40
  }
41
  TABLE_NAME = "tb_vector_embeddings"
42
 
43
+ # Add this above the function for simple caching
44
+ s3_url_cache = {}
 
45
 
46
+ import boto3
47
+
48
+
49
+ s3_client = boto3.client(
50
+ "s3",
51
+ region_name="ap-south-1", # Your bucket's region
52
+ aws_access_key_id="AKIA2IQBLLIAS6MIFZHJ",
53
+ aws_secret_access_key="TK5jDf5fEyQrvgnyHuX8ALBKcNdFRmgAQP8ICPge"
54
+ )
55
+
56
+ from urllib.parse import unquote
57
+
58
+ def get_s3_url_by_filename(file_name: str) -> str:
59
+ try:
60
+ conn = psycopg2.connect(
61
+ host="45.194.46.142",
62
+ port=5432,
63
+ dbname="IMDL_DWH_DEV",
64
+ user="dwh_user",
65
+ password="ImagesDWH@2025"
66
+ )
67
+ cursor = conn.cursor()
68
+ cursor.execute("SELECT s3_url FROM tb_fact_magazines WHERE title = %s LIMIT 1", (file_name,))
69
+ row = cursor.fetchone()
70
+ cursor.close()
71
+ conn.close()
72
+
73
+ if row and row[0]:
74
+ raw_s3 = row[0].replace("s3://", "")
75
+ bucket, key = raw_s3.split("/", 1)
76
+
77
+ # Decode any %2F and + and return actual key
78
+ key = unquote(key).replace("+", " ")
79
+
80
+ print(f"✅ S3 Key for signing: {key}")
81
+
82
+ signed_url = s3_client.generate_presigned_url(
83
+ ClientMethod='get_object',
84
+ Params={'Bucket': bucket, 'Key': key},
85
+ ExpiresIn=600
86
+ )
87
+ return signed_url
88
+ except Exception as e:
89
+ logging.warning(f"⚠️ Failed to generate signed URL for {file_name}: {e}")
90
+
91
+ return ""
92
+
93
+
94
+
95
+
96
+
97
+ # ---------------- Upload Photo ----------------
98
+ @app.post("/upload_photo")
99
+ async def upload_photo(file: UploadFile, name: str = Form(...), email: str = Form(...)):
100
+ if file.content_type not in ["image/jpeg", "image/png"]:
101
+ return JSONResponse(status_code=400, content={"error": "Invalid image format."})
102
+
103
+ extension = os.path.splitext(file.filename)[1]
104
+ photo_id = f"{uuid4().hex}{extension}"
105
+ photo_path = os.path.join(UPLOAD_FOLDER, photo_id)
106
+
107
+ with open(photo_path, "wb") as buffer:
108
+ shutil.copyfileobj(file.file, buffer)
109
+
110
+ try:
111
+ embedding = DeepFace.represent(img_path=photo_path, model_name='Facenet512')[0]['embedding']
112
+ except Exception as e:
113
+ return JSONResponse(status_code=400, content={"error": f"Face not detected: {e}"})
114
+
115
+ photo_db.append({
116
+ "name": name.strip(),
117
+ "email": email.strip(),
118
+ "photo_url": f"/gallery/{photo_id}",
119
+ "full_url": f"http://localhost:8000/gallery/{photo_id}",
120
+ "embedding": embedding
121
+ })
122
+
123
+ return {"message": "Photo uploaded", "photo_url": f"http://localhost:8000/gallery/{photo_id}"}
124
+
125
+ # ---------------- Disk Scan ----------------
126
+ def scan_photos_from_disk():
127
+ for filename in os.listdir(UPLOAD_FOLDER):
128
+ if not filename.lower().endswith((".jpg", ".jpeg", ".png")):
129
+ continue
130
+ try:
131
+ filepath = os.path.join(UPLOAD_FOLDER, filename)
132
+ name_guess = os.path.splitext(filename)[0].replace("_", " ").title()
133
+ embedding = DeepFace.represent(img_path=filepath, model_name='Facenet512')[0]['embedding']
134
+ photo_db.append({
135
+ "name": name_guess,
136
+ "email": "auto@scanned.local",
137
+ "photo_url": f"/gallery/{filename}",
138
+ "full_url": f"http://localhost:8000/gallery/{filename}",
139
+ "embedding": embedding
140
+ })
141
+ logging.info(f"Scanned and added {filename} as {name_guess}")
142
+ except Exception as e:
143
+ logging.warning(f"Skipped {filename}: {e}")
144
+
145
+ # ---------------- Face Lookup ----------------
146
+ from difflib import get_close_matches
147
+
148
+ # Enhanced fuzzy and case-insensitive photo lookup with logging
149
+ def get_person_photos_by_name(name: str):
150
+ if not name:
151
+ return []
152
+
153
+ name = name.lower().strip()
154
+ all_names = [entry["name"].lower().strip() for entry in photo_db]
155
 
156
+ # Try exact match first
157
+ if name in all_names:
158
+ matched = [entry["full_url"] for entry in photo_db if entry["name"].lower().strip() == name]
159
+ logging.info(f"✅ Exact photo match for '{name}': {len(matched)} images found.")
160
+ return matched
161
+
162
+ # Fuzzy match fallback
163
+ closest = get_close_matches(name, all_names, n=1, cutoff=0.7)
164
+ if closest:
165
+ matched_name = closest[0]
166
+ logging.info(f"🔍 Fuzzy matched '{name}' to '{matched_name}' in photo_db.")
167
+ return [entry["full_url"] for entry in photo_db if entry["name"].lower().strip() == matched_name]
168
+
169
+ logging.warning(f"⚠️ No photo match found for '{name}' in photo_db.")
170
+ return []
171
+
172
+
173
+
174
+ def search_web_images(query: str, max_results: int = 3) -> List[str]:
175
+ try:
176
+ results = []
177
+ with DDGS() as ddgs:
178
+ for r in ddgs.images(query, max_results=max_results):
179
+ results.append(r["image"])
180
+ return results
181
+ except Exception as e:
182
+ logging.warning(f"Web image search failed for {query}: {e}")
183
+ return []
184
+
185
+ # ---------------- VectorStore Init ----------------
186
  def fetch_pgvector_documents():
187
  conn = psycopg2.connect(**DB_CONFIG)
188
  cursor = conn.cursor()
 
197
  emb_array = ast.literal_eval(emb_array)
198
  if not emb_array or len(emb_array) != 1536:
199
  continue
200
+ try:
201
+ metadata = json.loads(meta) if isinstance(meta, str) else meta
202
+ except Exception:
203
+ metadata = {}
204
  texts.append(text)
205
  embeddings.append(list(emb_array))
206
+ metadatas.append(metadata)
 
207
  return texts, embeddings, metadatas
208
 
 
209
  def initialize_vector_store():
210
  global vector_store
211
+ try:
212
+ if os.path.exists(os.path.join(FAISS_INDEX_PATH, "index.faiss")):
213
+ vector_store = FAISS.load_local(FAISS_INDEX_PATH, OpenAIEmbeddings(), allow_dangerous_deserialization=True)
214
+ logging.info("FAISS index loaded from disk.")
215
+ return
216
+ except Exception as e:
217
+ logging.warning(f"Failed to load FAISS index from disk: {e}")
218
+
219
  try:
220
  texts, embeddings, metadatas = fetch_pgvector_documents()
221
+ if not texts:
222
+ logging.error("No documents found in the database to build the vector store.")
223
+ vector_store = None
224
+ return
225
+ vector_store = FAISS.from_embeddings(list(zip(texts, embeddings)), OpenAIEmbeddings(), metadatas)
226
  vector_store.save_local(FAISS_INDEX_PATH)
227
+ logging.info("FAISS index created and saved.")
228
  except Exception as e:
229
+ logging.error(f"FAISS init failed: {e}")
230
  vector_store = None
231
 
232
+ # ---------------- LangChain ----------------
233
  def get_context_retriever_chain():
234
  llm = ChatOpenAI(model="gpt-4o-mini-2024-07-18", temperature=0)
235
+ retriever = vector_store.as_retriever(search_kwargs={"k": 20}) # Increased k for better context
236
  prompt = ChatPromptTemplate.from_messages([
237
  MessagesPlaceholder(variable_name="chat_history"),
238
  ("user", "{input}"),
 
241
  return create_history_aware_retriever(llm, retriever, prompt)
242
 
243
  def get_conversational_rag_chain(retriever_chain):
244
+ llm = ChatOpenAI(model="gpt-4o-mini-2024-07-18", temperature=0) # Use same model for consistency
245
  prompt = ChatPromptTemplate.from_messages([
246
+ ("system", "You are a helpful assistant for Images Group. Only answer using the provided context:\n\n{context}"),
247
  MessagesPlaceholder(variable_name="chat_history"),
248
  ("user", "{input}")
249
  ])
250
  doc_chain = create_stuff_documents_chain(llm=llm, prompt=prompt)
251
  return create_retrieval_chain(retriever_chain, doc_chain)
252
 
253
+ # ---------------- Main Ask Logic ----------------
254
  def ask_question(question):
255
  global chat_history
256
  if vector_store is None:
257
+ return {"error": "Vector store not initialized."}
258
 
259
+ q_hash = hashlib.md5(question.encode()).hexdigest()
260
+ if q_hash in response_cache:
261
+ return response_cache[q_hash]
 
262
 
263
+ # Step 1: Run RAG
264
  retriever_chain = get_context_retriever_chain()
265
  rag_chain = get_conversational_rag_chain(retriever_chain)
266
+ result = rag_chain.invoke({"input": question, "chat_history": chat_history})
 
 
 
 
267
 
268
  answer = result.get("answer", "No answer found.")
269
+ chat_history.append(HumanMessage(content=question))
270
  chat_history.append(AIMessage(content=answer))
271
 
272
+ # Step 2: Extract all PERSON entities from answer
273
+ doc = nlp(answer)
274
+ detected_people = [ent.text for ent in doc.ents if ent.label_ == "PERSON"]
 
 
 
275
 
276
+ people_output = []
277
+ seen_names = set()
278
 
279
+ for name in detected_people:
280
+ key = name.lower().strip()
281
+ if key in seen_names:
282
+ continue
283
+ seen_names.add(key)
284
 
285
+ local_photos = get_person_photos_by_name(name)
286
+ web_photos = search_web_images(name)
 
 
287
 
288
+ people_output.append({
289
+ "name": name,
290
+ "local_photos": local_photos,
291
+ "web_photos": web_photos
292
+ })
293
+
294
+ # Step 3: Source documents with S3 URLs
295
+ docs = vector_store.similarity_search(question, k=5)
296
+ sources = []
297
+ for doc in docs:
298
+ file_name = doc.metadata.get("file_name", doc.metadata.get("source", "Unknown"))
299
+ s3_url = get_s3_url_by_filename(file_name)
300
+ sources.append({
301
+ "source": file_name,
302
+ "page": str(doc.metadata.get("page", "Unknown")),
303
+ "snippet": doc.page_content[:300],
304
+ "download": s3_url
305
+ })
306
+
307
+ # Step 4: Generate insights
308
+ insights = [
309
+ f"{name} was detected in the answer and linked to visual/media content."
310
+ for name in seen_names
311
+ ]
312
+ if sources:
313
+ insights.append(f"{len(sources)} source documents contributed to this answer.")
314
+ insights.append(f"The most relevant document is: {sources[0]['source']}")
315
+
316
+ # Step 5: Build response with tabs
317
+ response = {
318
+ "answer": answer,
319
+ "tabs": {
320
+ "answer": answer,
321
+ "images": people_output,
322
+ "videos": [],
323
+ "sources": sources,
324
+ "insights": insights
325
+ }
326
+ }
327
+
328
+ response_cache[q_hash] = response
329
+ return response
330
+
331
+ # ---------------- Endpoints ----------------
332
+ @app.on_event("startup")
333
+ def on_start():
334
+ initialize_vector_store()
335
 
336
  @app.post("/ask")
337
+ def ask(payload: dict):
338
  question = payload.get("question", "").strip()
339
  if not question:
340
  return JSONResponse(status_code=400, content={"error": "Question cannot be empty"})
341
+ return ask_question(question)
342
 
343
+ @app.get("/status")
344
+ def status():
345
+ return {"status": "ready" if vector_store else "not_initialized"}
346
+
347
+ @app.get("/refresh")
348
+ def refresh():
349
+ initialize_vector_store()
350
+ scan_photos_from_disk()
351
+ return {"status": "refreshed", "photos": len(photo_db)}
352
+ logging.info("Application started")
353
+ logging.info("Vector store initialized")
354
+ logging.info("Photos scanned from disk")
355
+ logging.info("Question received")
356
+ logging.info("RAG chain invoked")
357
+ logging.info("Answer generated")
358
+ logging.info("Entities extracted")
359
+ logging.info("Local and web photos retrieved")
360
+ logging.info("Source documents retrieved")
361
+ logging.info("Insights generated")
362
+ logging.info("Response generated")# No selection was provided, so I'll generate a code snippet that can be inserted at the cursor position.
363
+ # This snippet includes some improvements to the existing code, such as adding error handling and logging.
364
+
365
+ try:
366
+ # Initialize the vector store and scan photos from disk
367
+ initialize_vector_store()
368
+ scan_photos_from_disk()
369
+ except Exception as e:
370
+ logging.error(f"Failed to initialize vector store or scan photos: {e}")
371
+
372
+ # Define a function to handle the ask endpoint
373
+ def ask_question(question):
374
+ try:
375
+ # Run the RAG chain and get the answer
376
+ retriever_chain = get_context_retriever_chain()
377
+ rag_chain = get_conversational_rag_chain(retriever_chain)
378
+ result = rag_chain.invoke({"input": question, "chat_history": chat_history})
379
+
380
+ # Extract the answer and entities
381
+ answer = result.get("answer", "No answer found.")
382
+ doc = nlp(answer)
383
+ detected_people = [ent.text for ent in doc.ents if ent.label_ == "PERSON"]
384
+
385
+ # Get local and web photos
386
+ people_output = []
387
+ seen_names = set()
388
+ for name in detected_people:
389
+ key = name.lower().strip()
390
+ if key in seen_names:
391
+ continue
392
+ seen_names.add(key)
393
+
394
+ local_photos = get_person_photos_by_name(name)
395
+ web_photos = search_web_images(name)
396
+
397
+ people_output.append({
398
+ "name": name,
399
+ "local_photos": local_photos,
400
+ "web_photos": web_photos
401
+ })
402
+
403
+ # Get source documents with S3 URLs
404
+ docs = vector_store.similarity_search(question, k=5)
405
+ sources = []
406
+ for doc in docs:
407
+ file_name = doc.metadata.get("file_name", doc.metadata.get("source", "Unknown"))
408
+ s3_url = get_s3_url_by_filename(file_name)
409
+ sources.append({
410
+ "source": file_name,
411
+ "page": str(doc.metadata.get("page", "Unknown")),
412
+ "snippet": doc.page_content[:300],
413
+ "download": s3_url
414
+ })
415
+
416
+ # Generate insights
417
+ insights = [
418
+ f"{name} was detected in the answer and linked to visual/media content."
419
+ for name in seen_names
420
+ ]
421
+ if sources:
422
+ insights.append(f"{len(sources)} source documents contributed to this answer.")
423
+ insights.append(f"The most relevant document is: {sources[0]['source']}")
424
+
425
+ # Build the response
426
+ response = {
427
+ "answer": answer,
428
+ "tabs": {
429
+ "answer": answer,
430
+ "images": people_output,
431
+ "videos": [],
432
+ "sources": sources,
433
+ "insights": insights
434
+ }
435
+ }
436
+
437
+ return response
438
+ except Exception as e:
439
+ logging.error(f"Failed to process question: {e}")
440
+ return {"error": "Failed to process question"}