jtdearmon commited on
Commit
5fea842
Β·
verified Β·
1 Parent(s): c5e4098

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +332 -0
app.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Fri Feb 7 13:26:43 2025
4
+
5
+ @author: Jacob Dearmon
6
+ """
7
+ import os
8
+ import time
9
+ import csv
10
+ import datetime
11
+ import base64
12
+ import gradio as gr
13
+ import openai
14
+ import io
15
+ from PIL import Image
16
+ from pinecone import Pinecone
17
+
18
+ # ---------------------------------------------------
19
+ # 1. Convert local SERMONS logo (JFIF) to PIL Image
20
+ # ---------------------------------------------------
21
+ def to_base64(path_to_img):
22
+ """Convert an image file to Base64 string."""
23
+ with open(path_to_img, "rb") as f:
24
+ encoded = base64.b64encode(f.read()).decode("utf-8")
25
+ return encoded
26
+
27
+ def base64_to_image(base64_string):
28
+ """Convert Base64 string back to PIL Image."""
29
+ image_data = base64.b64decode(base64_string)
30
+ # Pillow can handle JFIF as it’s effectively a JPEG
31
+ return Image.open(io.BytesIO(image_data))
32
+
33
+ # Update the path to your JFIF logo file here
34
+ SERMONS_LOGO_B64 = to_base64(r"D:\Dearmon\Legacy\DP\Sermons\DP_logo.jfif")
35
+ SERMONS_LOGO_IMG = base64_to_image(SERMONS_LOGO_B64)
36
+
37
+ # ---------------------------------------------------
38
+ # 2. Configuration
39
+ # ---------------------------------------------------
40
+
41
+ openai.api_key = os.getenv("OPENAI_API_KEY")
42
+ PINECONE_API_KEY = os.getenv("PINECONE_API_KEY")
43
+
44
+ # From your screenshot: "Cloud: AWS | Region: us-east-1 | Dimension: 1536"
45
+ PINECONE_ENV = "us-east-1"
46
+ INDEX_NAME = "idx-sermons-1536" # name from Pinecone console
47
+ EMBED_DIMENSION = 1536 # matches your screenshot
48
+ EMBED_MODEL = "text-embedding-ada-002"
49
+ CHAT_MODEL = "gpt-4o"
50
+ TOP_K = 20
51
+ SIMILARITY_THRESHOLD = 0.4
52
+
53
+ NEGATIVE_FEEDBACK_CSV = "negative_feedback.csv"
54
+ NEUTRAL_FEEDBACK_CSV = "neutral_feedback.csv"
55
+ SESSION_HISTORY_CSV = "session_history.csv"
56
+
57
+ # ---------------------------------------------------
58
+ # 2.5. Automatically Initialize Pinecone Index
59
+ # ---------------------------------------------------
60
+ def init_pinecone_index(index_name=INDEX_NAME, dimension=EMBED_DIMENSION):
61
+ """
62
+ Creates (or reuses) the Pinecone index with the given name and dimension.
63
+ Returns a Pinecone index object.
64
+ """
65
+ pc = Pinecone(api_key=PINECONE_API_KEY, environment=PINECONE_ENV)
66
+ existing_indexes = pc.list_indexes().names() # get list of index names
67
+ if index_name not in existing_indexes:
68
+ print(f"[Info] Creating Pinecone index '{index_name}' in env '{PINECONE_ENV}'...")
69
+ pc.create_index(name=index_name, dimension=dimension)
70
+ time.sleep(5) # short pause
71
+ else:
72
+ print(f"[Info] Reusing existing Pinecone index '{index_name}' in env '{PINECONE_ENV}'.")
73
+ return pc.Index(index_name)
74
+
75
+ # Initialize Pinecone Index
76
+ pc_index = init_pinecone_index()
77
+
78
+ # ---------------------------------------------------
79
+ # 3. Session Memory
80
+ # ---------------------------------------------------
81
+ session_history = [
82
+ {
83
+ "role": "system",
84
+ "content": "You are a helpful AI assistant specialized in sermons and biblical questions. Answer in a compassionate and loving tone, while recognizing the emotive content of the question - if any."
85
+ }
86
+ ]
87
+
88
+ # ---------------------------------------------------
89
+ # 4. Helper Functions
90
+ # ---------------------------------------------------
91
+ def embed_text(text: str):
92
+ """Get embeddings from OpenAI."""
93
+ try:
94
+ resp = openai.Embedding.create(model=EMBED_MODEL, input=[text])
95
+ return resp["data"][0]["embedding"]
96
+ except Exception as e:
97
+ print(f"[Error] Embedding failed: {e}")
98
+ return None
99
+
100
+ def query_index(user_query: str, top_k=TOP_K):
101
+ """Query Pinecone for relevant matches based on 'user_query' embeddings."""
102
+ vector = embed_text(user_query)
103
+ if vector is None:
104
+ return []
105
+ try:
106
+ response = pc_index.query(vector=vector, top_k=top_k, include_metadata=True)
107
+ return response.matches
108
+ except Exception as e:
109
+ print(f"[Error] Pinecone query failed: {e}")
110
+ return []
111
+
112
+ def build_rag_answer(user_query, matches):
113
+ """
114
+ Build a RAG-based answer using retrieved chunks as context for the LLM.
115
+ """
116
+ # Combine top matches into a context string
117
+ combined_context = "\n\n".join(
118
+ f"Chunk ID: {m.id}\n{m.metadata.get('text', '')}"
119
+ for m in matches
120
+ )
121
+
122
+ # Create a system message with retrieved context
123
+ context_system_message = {
124
+ "role": "system",
125
+ "content": (
126
+ "Relevant reference text from Pinecone:\n"
127
+ f"CONTEXT:\n{combined_context}\n\n"
128
+ "Answer the user's question using this context where helpful."
129
+ )
130
+ }
131
+
132
+ # Full conversation: existing history + new system context + user query
133
+ conversation = session_history + [
134
+ context_system_message,
135
+ {"role": "user", "content": user_query}
136
+ ]
137
+
138
+ try:
139
+ response = openai.ChatCompletion.create(
140
+ model=CHAT_MODEL,
141
+ messages=conversation,
142
+ temperature=0.2,
143
+ max_tokens=1750
144
+ )
145
+ final_answer = response["choices"][0]["message"]["content"].strip()
146
+ except Exception as e:
147
+ print(f"[Error] ChatCompletion failed: {e}")
148
+ final_answer = "Error generating RAG answer."
149
+
150
+ # Append the new assistant message to session history
151
+ session_history.append({"role": "assistant", "content": final_answer})
152
+ return final_answer
153
+
154
+ def direct_llm_call(user_query):
155
+ """
156
+ If no relevant results or below threshold, do a direct LLM call with session history only.
157
+ """
158
+ conversation = session_history + [
159
+ {"role": "user", "content": user_query}
160
+ ]
161
+
162
+ try:
163
+ response = openai.ChatCompletion.create(
164
+ model=CHAT_MODEL,
165
+ messages=conversation,
166
+ temperature=0.2
167
+ )
168
+ final_answer = response["choices"][0]["message"]["content"].strip()
169
+ except Exception as e:
170
+ print(f"[Error] Direct LLM call failed: {e}")
171
+ final_answer = "Error generating direct LLM answer."
172
+
173
+ session_history.append({"role": "assistant", "content": final_answer})
174
+ return final_answer
175
+
176
+ def query_rag(user_query: str) -> str:
177
+ """
178
+ Main pipeline:
179
+ 1) Add user query to session history
180
+ 2) Query Pinecone
181
+ 3) If top match above threshold -> build RAG answer
182
+ else do direct call
183
+ """
184
+ user_query = user_query.strip()
185
+ if not user_query:
186
+ return "Please enter a valid query."
187
+
188
+ # Add user query to session memory
189
+ session_history.append({"role": "user", "content": user_query})
190
+
191
+ # Retrieve relevant context from Pinecone
192
+ matches = query_index(user_query, top_k=TOP_K)
193
+ if not matches:
194
+ # If no matches, do direct LLM call
195
+ return direct_llm_call(user_query)
196
+
197
+ top_score = matches[0].score or 0.0
198
+ if top_score >= SIMILARITY_THRESHOLD:
199
+ return build_rag_answer(user_query, matches)
200
+ else:
201
+ return direct_llm_call(user_query)
202
+
203
+ # ---------------------------------------------------
204
+ # 5. Feedback + Logging
205
+ # ---------------------------------------------------
206
+ def incorporate_feedback_into_pinecone(user_query, answer):
207
+ """
208
+ If thumbs-up, store Q&A as a new chunk in Pinecone.
209
+ """
210
+ text_chunk = f"User Query: {user_query}\nAI Answer: {answer}"
211
+ vector = embed_text(text_chunk)
212
+ if vector is None:
213
+ return
214
+ feedback_id = f"feedback_{int(time.time())}"
215
+ metadata = {"source": "feedback", "text": text_chunk}
216
+ try:
217
+ pc_index.upsert([
218
+ {"id": feedback_id, "values": vector, "metadata": metadata}
219
+ ])
220
+ print("[Info] User feedback upserted to Pinecone.")
221
+ except Exception as e:
222
+ print(f"[Error] Could not upsert feedback: {e}")
223
+
224
+ def store_feedback_to_csv(user_query, answer, csv_path):
225
+ """
226
+ Log negative/neutral feedback in separate CSV.
227
+ """
228
+ file_exists = os.path.exists(csv_path)
229
+ with open(csv_path, mode="a", newline="", encoding="utf-8") as f:
230
+ fieldnames = ["timestamp", "query", "answer"]
231
+ writer = csv.DictWriter(f, fieldnames=fieldnames)
232
+ if not file_exists:
233
+ writer.writeheader()
234
+ writer.writerow({
235
+ "timestamp": datetime.datetime.now().isoformat(),
236
+ "query": user_query,
237
+ "answer": answer
238
+ })
239
+ print(f"[Info] Feedback logged to {csv_path}.")
240
+
241
+ def store_session_history(user_query, answer, feedback):
242
+ """
243
+ Log (Q, A, feedback) to a single CSV: session_history.csv
244
+ """
245
+ file_exists = os.path.exists(SESSION_HISTORY_CSV)
246
+ with open(SESSION_HISTORY_CSV, mode="a", newline="", encoding="utf-8") as f:
247
+ fieldnames = ["timestamp", "user_query", "ai_answer", "feedback"]
248
+ writer = csv.DictWriter(f, fieldnames=fieldnames)
249
+ if not file_exists:
250
+ writer.writeheader()
251
+ writer.writerow({
252
+ "timestamp": datetime.datetime.now().isoformat(),
253
+ "user_query": user_query,
254
+ "ai_answer": answer,
255
+ "feedback": feedback
256
+ })
257
+ print(f"[Info] Session Q&A stored in {SESSION_HISTORY_CSV}.")
258
+
259
+ def handle_feedback(user_query, answer, feedback_option):
260
+ """
261
+ Called when user selects feedback in Gradio UI.
262
+ """
263
+ if not user_query.strip() or not answer.strip():
264
+ return "No valid Q&A to provide feedback on."
265
+
266
+ if feedback_option == "πŸ‘":
267
+ incorporate_feedback_into_pinecone(user_query, answer)
268
+ store_session_history(user_query, answer, "positive")
269
+ return "πŸ‘ Your Q&A has been stored in Pinecone (and logged)."
270
+ elif feedback_option == "βš–οΈ":
271
+ store_feedback_to_csv(user_query, answer, NEUTRAL_FEEDBACK_CSV)
272
+ store_session_history(user_query, answer, "neutral")
273
+ return "βš–οΈ Q&A logged to neutral_feedback.csv and session_history.csv."
274
+ else: # "πŸ‘Ž"
275
+ store_feedback_to_csv(user_query, answer, NEGATIVE_FEEDBACK_CSV)
276
+ store_session_history(user_query, answer, "negative")
277
+ return "πŸ‘Ž Q&A logged to negative_feedback.csv and session_history.csv."
278
+
279
+ # ---------------------------------------------------
280
+ # 6. Gradio Interface
281
+ # ---------------------------------------------------
282
+ def run_query(user_query):
283
+ return query_rag(user_query)
284
+
285
+ with gr.Blocks() as demo:
286
+ # Row with two columns: (1) SERMONS jfif logo, (2) headings
287
+ with gr.Row():
288
+ with gr.Column(scale=1, min_width=100):
289
+ gr.Image(
290
+ value=SERMONS_LOGO_IMG,
291
+ label=None,
292
+ show_label=False,
293
+ width=80,
294
+ height=80
295
+ )
296
+ with gr.Column(scale=6):
297
+ gr.Markdown("## Derek Prince RAG Demo")
298
+ gr.Markdown("Ask questions about DP's sermons data, stored in Pinecone.\n"
299
+ "Now with session memory!")
300
+
301
+ with gr.Column():
302
+ user_query = gr.Textbox(
303
+ label="Your Query",
304
+ lines=1,
305
+ placeholder="Ask about a sermon..."
306
+ )
307
+ get_answer_btn = gr.Button("Get Answer")
308
+
309
+ answer_output = gr.Textbox(label="AI Answer", lines=4)
310
+
311
+ feedback_radio = gr.Radio(
312
+ choices=["πŸ‘", "βš–οΈ", "πŸ‘Ž"],
313
+ value="βš–οΈ",
314
+ label="Feedback"
315
+ )
316
+ feedback_btn = gr.Button("Submit Feedback")
317
+ feedback_result = gr.Label()
318
+
319
+ get_answer_btn.click(fn=run_query, inputs=[user_query], outputs=[answer_output])
320
+ feedback_btn.click(
321
+ fn=handle_feedback,
322
+ inputs=[user_query, answer_output, feedback_radio],
323
+ outputs=[feedback_result]
324
+ )
325
+
326
+ if __name__ == "__main__":
327
+ demo.launch(
328
+ server_name="0.0.0.0",
329
+ server_port=7860,
330
+ share=True,
331
+ auth=("DP", "DP#1234")
332
+ )