WaysAhead Global commited on
Commit
d084e35
·
verified ·
1 Parent(s): 6045b98

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +211 -0
app.py ADDED
@@ -0,0 +1,211 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request, UploadFile, Form
2
+ from fastapi.responses import JSONResponse
3
+ import os, io, json, tempfile, logging
4
+ import boto3
5
+ from typing import List
6
+
7
+ from langchain_community.document_loaders import PyMuPDFLoader
8
+ from langchain.text_splitter import RecursiveCharacterTextSplitter
9
+ from langchain_community.vectorstores import Chroma
10
+ from langchain_openai import ChatOpenAI, OpenAIEmbeddings
11
+ from langchain_core.messages import AIMessage, HumanMessage
12
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
13
+ from langchain.chains import create_history_aware_retriever, create_retrieval_chain
14
+ from langchain.chains.combine_documents import create_stuff_documents_chain
15
+ from chromadb.config import Settings
16
+
17
+ # ------------------ Configuration ------------------
18
+ S3_BUCKET = "imagesdatalibrary"
19
+ S3_PREFIX = "RetailME Magazine/"
20
+ AWS_REGION = "ap-south-1"
21
+ VECTOR_STORE_DIR = "persistent_vector_store"
22
+ COLLECTION_NAME = "my_documents"
23
+ PROCESSED_FILES_JSON = "processed_files.json"
24
+
25
+ # ------------------ Setup ------------------
26
+ app = FastAPI()
27
+ logging.basicConfig(level=logging.INFO)
28
+
29
+ s3 = boto3.client(
30
+ "s3",
31
+ aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID", "").strip(),
32
+ aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY", "").strip(),
33
+ region_name=AWS_REGION
34
+ )
35
+
36
+ vector_store = None
37
+ processed_files = set()
38
+ chat_history = [AIMessage(content="Hello! I am your AI assistant. Ask me questions based on your documents.")]
39
+
40
+ # ------------------ Utilities ------------------
41
+
42
+ def load_processed_files():
43
+ global processed_files
44
+ if os.path.exists(PROCESSED_FILES_JSON):
45
+ with open(PROCESSED_FILES_JSON, "r") as f:
46
+ processed_files = set(json.load(f))
47
+
48
+ def save_processed_files():
49
+ with open(PROCESSED_FILES_JSON, "w") as f:
50
+ json.dump(list(processed_files), f)
51
+
52
+ def get_chroma_settings():
53
+ return Settings(
54
+ anonymized_telemetry=False,
55
+ is_persistent=True,
56
+ persist_directory=VECTOR_STORE_DIR
57
+ )
58
+
59
+ def initialize_vector_store():
60
+ global vector_store
61
+ if os.path.exists(VECTOR_STORE_DIR):
62
+ vector_store = Chroma(
63
+ persist_directory=VECTOR_STORE_DIR,
64
+ embedding_function=OpenAIEmbeddings(),
65
+ collection_name=COLLECTION_NAME,
66
+ client_settings=get_chroma_settings()
67
+ )
68
+ else:
69
+ vector_store = None
70
+
71
+ # ------------------ Document Processing ------------------
72
+
73
+ def list_s3_pdfs():
74
+ paginator = s3.get_paginator("list_objects_v2")
75
+ pages = paginator.paginate(Bucket=S3_BUCKET, Prefix=S3_PREFIX)
76
+ for page in pages:
77
+ for obj in page.get("Contents", []):
78
+ key = obj["Key"]
79
+ if key.endswith(".pdf"):
80
+ yield key.strip()
81
+
82
+ def process_new_s3_files():
83
+ global vector_store, processed_files
84
+ files_found = False
85
+ try:
86
+ s3_keys = list(list_s3_pdfs())
87
+ unprocessed = [key for key in s3_keys if key not in processed_files]
88
+ for s3_key in unprocessed:
89
+ files_found = True
90
+ file_stream = io.BytesIO()
91
+ s3.download_fileobj(S3_BUCKET, s3_key, file_stream)
92
+ file_stream.seek(0)
93
+
94
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file:
95
+ tmp_file.write(file_stream.read())
96
+ tmp_path = tmp_file.name
97
+
98
+ loader = PyMuPDFLoader(tmp_path)
99
+ docs = loader.load()
100
+ os.remove(tmp_path)
101
+
102
+ for idx, doc in enumerate(docs):
103
+ doc.metadata["source"] = s3_key.strip()
104
+ doc.metadata["page"] = idx + 1
105
+
106
+ if not docs:
107
+ logging.warning(f"No text extracted from {s3_key}")
108
+ continue
109
+
110
+ splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)
111
+ chunks = splitter.split_documents(docs)
112
+
113
+ if vector_store:
114
+ vector_store.add_documents(chunks)
115
+ else:
116
+ vector_store = Chroma.from_documents(
117
+ chunks,
118
+ OpenAIEmbeddings(),
119
+ persist_directory=VECTOR_STORE_DIR,
120
+ collection_name=COLLECTION_NAME,
121
+ client_settings=get_chroma_settings()
122
+ )
123
+
124
+ processed_files.add(s3_key.strip())
125
+ save_processed_files()
126
+
127
+ return {"status": "success", "processed": len(unprocessed)}
128
+
129
+ except Exception as e:
130
+ logging.error(f"Error: {e}")
131
+ return {"status": "error", "message": str(e)}
132
+
133
+ # ------------------ LangChain Chain ------------------
134
+
135
+ def get_context_retriever_chain(vector_store):
136
+ llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
137
+ retriever = vector_store.as_retriever()
138
+ prompt = ChatPromptTemplate.from_messages([
139
+ MessagesPlaceholder(variable_name="chat_history"),
140
+ ("user", "{input}"),
141
+ ("user", "Generate a search query to help retrieve documents.")
142
+ ])
143
+ return create_history_aware_retriever(llm, retriever, prompt)
144
+
145
+ def get_conversational_rag_chain(retriever_chain):
146
+ llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
147
+ prompt = ChatPromptTemplate.from_messages([
148
+ ("system", "You are a helpful assistant. Only answer using the provided context:\n\n{context}"),
149
+ MessagesPlaceholder(variable_name="chat_history"),
150
+ ("user", "{input}")
151
+ ])
152
+ doc_chain = create_stuff_documents_chain(llm=llm, prompt=prompt)
153
+ return create_retrieval_chain(retriever_chain, doc_chain)
154
+
155
+ # ------------------ Q&A ------------------
156
+
157
+ def ask_question(question):
158
+ global chat_history
159
+ if vector_store is None:
160
+ return "No documents loaded yet.", []
161
+
162
+ retriever_chain = get_context_retriever_chain(vector_store)
163
+ rag_chain = get_conversational_rag_chain(retriever_chain)
164
+
165
+ result = rag_chain.invoke({
166
+ "input": question.strip(),
167
+ "chat_history": chat_history
168
+ })
169
+
170
+ answer = result.get("answer", "No answer found.")
171
+ chat_history.append(HumanMessage(content=question.strip()))
172
+ chat_history.append(AIMessage(content=answer))
173
+
174
+ docs = vector_store.similarity_search(question.strip(), k=3)
175
+ sources = []
176
+ for doc in docs:
177
+ meta = doc.metadata
178
+ sources.append({
179
+ "source": meta.get("source", "Unknown").strip(),
180
+ "page": meta.get("page", "N/A"),
181
+ "snippet": doc.page_content.strip()[:300]
182
+ })
183
+
184
+ return answer, sources
185
+
186
+ # ------------------ FastAPI Endpoints ------------------
187
+
188
+ @app.on_event("startup")
189
+ def startup_event():
190
+ load_processed_files()
191
+ initialize_vector_store()
192
+ process_new_s3_files()
193
+
194
+ @app.get("/status")
195
+ def get_status():
196
+ return {"processed_files_count": len(processed_files)}
197
+
198
+ @app.post("/sync")
199
+ def sync_s3():
200
+ result = process_new_s3_files()
201
+ initialize_vector_store()
202
+ return result
203
+
204
+ @app.post("/ask")
205
+ def ask_from_documents(payload: dict):
206
+ question = payload.get("question", "").strip()
207
+ if not question:
208
+ return JSONResponse(status_code=400, content={"error": "Question cannot be empty"})
209
+
210
+ answer, sources = ask_question(question)
211
+ return {"answer": answer, "sources": sources}