Adoption commited on
Commit
6469141
Β·
verified Β·
1 Parent(s): ae6d6c8

Update src/app.py

Browse files
Files changed (1) hide show
  1. src/app.py +73 -68
src/app.py CHANGED
@@ -12,6 +12,8 @@ from langchain.chains import RetrievalQA
12
  from langchain_core.documents import Document
13
  from langchain_core.retrievers import BaseRetriever
14
  from langchain_core.callbacks import CallbackManagerForRetrieverRun
 
 
15
 
16
  load_dotenv()
17
 
@@ -25,29 +27,23 @@ if not os.path.exists(CHUNKS_FILE):
25
  print(f"⚠️ WARNING: Pickle file not found at: {CHUNKS_FILE}")
26
  else:
27
  print(f"βœ… SUCCESS: Pickle file found at: {CHUNKS_FILE}")
28
-
29
  def search_archives(query):
30
- """
31
- This function strictly scans the local file.
32
- It does NOT use Pinecone.
33
- It returns ALL matches found.
34
- """
35
  status_log = []
36
  results = []
37
-
38
  if os.path.exists(CHUNKS_FILE):
39
  try:
40
  # 1. Load the Data
41
  with open(CHUNKS_FILE, "rb") as f:
42
  chunks = pickle.load(f)
43
-
44
  status_log.append(f"πŸ“‚ Scanning {len(chunks)} local paragraphs...")
45
  query_lower = query.lower().strip()
46
-
47
  # 2. Find ALL Matches (No Limit)
48
  # We check every single chunk.
49
  results = [doc for doc in chunks if query_lower in doc.page_content.lower()]
50
-
51
  # 3. Safety Check
52
  # If we find > 1000 results, we show the first 1000 to keep the browser from freezing.
53
  total_found = len(results)
@@ -56,9 +52,7 @@ def search_archives(query):
56
  status_log.append(f"⚠️ Found {total_found} matches! Showing first 1000 to prevent crash.")
57
  else:
58
  status_log.append(f"βœ… Found {total_found} exact matches.")
59
-
60
  return results, status_log
61
-
62
  except Exception as e:
63
  status_log.append(f"❌ Local Load Error: {e}")
64
  return [], status_log
@@ -68,7 +62,6 @@ def search_archives(query):
68
 
69
  # --- RAG CHAIN (The Chat Tool - POWERED BY GEMINI) ---
70
  def get_rag_chain():
71
-
72
  class SmartRetriever(BaseRetriever):
73
  def _get_relevant_documents(
74
  self, query: str, *, run_manager: CallbackManagerForRetrieverRun = None
@@ -76,93 +69,105 @@ def get_rag_chain():
76
  print(f"🧠 Chat is thinking about: '{query}'")
77
  final_docs = []
78
  seen_content = set()
79
-
80
- # --- PHASE A: LOCAL LOOKUP (The "Vacuum") ---
81
- # Suck up everything that even vaguely matches keywords
82
  if os.path.exists(CHUNKS_FILE):
83
  try:
84
  with open(CHUNKS_FILE, "rb") as f:
85
  chunks = pickle.load(f)
86
-
87
- keywords = [w.lower() for w in query.split() if len(w) > 3]
88
-
89
- if keywords:
90
- local_matches = []
91
- for doc in chunks:
92
- content_lower = doc.page_content.lower()
93
- # If ANY keyword is present, grab it.
94
- if any(k in content_lower for k in keywords):
95
- local_matches.append(doc)
96
-
97
- # Gemini is powerful: Send top 40 matches
98
- for doc in local_matches[:40]:
99
- if doc.page_content not in seen_content:
100
- final_docs.append(doc)
101
- seen_content.add(doc.page_content)
102
-
103
- print(f"βœ… Vacuumed {len(final_docs)} local matches.")
104
  except Exception as e:
105
  print(f"⚠️ Local Search Warning: {e}")
106
-
107
- # --- PHASE B: CLOUD LOOKUP (The "Net") ---
108
  print("☁️ Checking Cloud...")
109
  try:
110
  embeddings = GoogleGenerativeAIEmbeddings(model="models/text-embedding-004")
111
  vector_store = PineconeVectorStore(index_name=INDEX_NAME, embedding=embeddings)
112
-
113
- # Grab top 20 concepts
114
- retriever = vector_store.as_retriever(search_kwargs={"k": 20})
 
 
 
 
 
115
  cloud_docs = retriever.invoke(query)
116
-
117
  for doc in cloud_docs:
118
  if doc.page_content not in seen_content:
119
  final_docs.append(doc)
120
  seen_content.add(doc.page_content)
121
-
122
  print(f"βœ… Added {len(cloud_docs)} cloud matches.")
123
  except Exception as e:
124
  print(f"❌ Cloud Error: {e}")
125
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  return final_docs
127
 
128
- # 2. SETUP LLM (Gemini 2.5 Flash)
129
  google_key = os.environ.get("GOOGLE_API_KEY") or st.secrets.get("GOOGLE_API_KEY")
130
  os.environ["GOOGLE_API_KEY"] = google_key
131
-
132
- # Using gemini-2.5-flash
133
  llm = ChatGoogleGenerativeAI(
134
- model="gemini-2.5-flash-lite",
135
  temperature=0.3,
136
  convert_system_message_to_human=True
137
  )
138
 
139
- # 3. PROMPT
140
- template = """You are William Marion Branham ai.
141
-
142
- INSTRUCTIONS:
143
- - You are a helpful evangelist. Answer the question comprehensively using the Context below.
144
- - Use a humble, 1950s Southern preaching dialect.
145
- - VARY YOUR OPENINGS. Do not always start with "My dear brother." Speak naturally.
146
- - If the Context mentions the topic but isn't complete, use your general knowledge of the Message to fill in the gaps.
147
- - IGNORE irrelevant "noise" in the search results (e.g. tape gaps, random prayer lines).
148
- - NEVER generate new revelations, interpretations, or content beyond the CONTEXT; if insufficient, note limitations and suggest consulting original sermons.
149
- - Promote ethical AI use: Emphasize that this is a tool for study, not a substitute for prayerful listening to the tapes or Holy Spirit-led understanding.
150
- - Minimize biases: Present doctrines factually from CONTEXT, prioritizing 1963–1965 sermons as the mature unveiling for progressive revelations, treating earlier ones as foundational shadows without forcing harmony.
151
-
152
- CONTEXT: {context}
153
-
154
- USER QUESTION: {question}
155
-
156
- BROTHER BRANHAM'S REPLY:"""
157
-
 
 
 
 
 
 
 
 
158
  PROMPT = PromptTemplate(template=template, input_variables=["context", "question"])
159
-
160
  chain = RetrievalQA.from_chain_type(
161
  llm=llm,
162
- chain_type="stuff",
163
- retriever=SmartRetriever(),
164
  return_source_documents=True,
165
  chain_type_kwargs={"prompt": PROMPT, "document_variable_name": "context"},
166
  input_key="question"
167
  )
168
- return chain
 
12
  from langchain_core.documents import Document
13
  from langchain_core.retrievers import BaseRetriever
14
  from langchain_core.callbacks import CallbackManagerForRetrieverRun
15
+ from langchain_community.retrievers import BM25Retriever # Added for upgraded local retriever
16
+ from sentence_transformers import CrossEncoder # Added for reranking; install via pip if needed (but note env limits)
17
 
18
  load_dotenv()
19
 
 
27
  print(f"⚠️ WARNING: Pickle file not found at: {CHUNKS_FILE}")
28
  else:
29
  print(f"βœ… SUCCESS: Pickle file found at: {CHUNKS_FILE}")
30
+
31
  def search_archives(query):
32
+ """ This function strictly scans the local file. It does NOT use Pinecone. It returns ALL matches found. """
 
 
 
 
33
  status_log = []
34
  results = []
 
35
  if os.path.exists(CHUNKS_FILE):
36
  try:
37
  # 1. Load the Data
38
  with open(CHUNKS_FILE, "rb") as f:
39
  chunks = pickle.load(f)
 
40
  status_log.append(f"πŸ“‚ Scanning {len(chunks)} local paragraphs...")
41
  query_lower = query.lower().strip()
42
+
43
  # 2. Find ALL Matches (No Limit)
44
  # We check every single chunk.
45
  results = [doc for doc in chunks if query_lower in doc.page_content.lower()]
46
+
47
  # 3. Safety Check
48
  # If we find > 1000 results, we show the first 1000 to keep the browser from freezing.
49
  total_found = len(results)
 
52
  status_log.append(f"⚠️ Found {total_found} matches! Showing first 1000 to prevent crash.")
53
  else:
54
  status_log.append(f"βœ… Found {total_found} exact matches.")
 
55
  return results, status_log
 
56
  except Exception as e:
57
  status_log.append(f"❌ Local Load Error: {e}")
58
  return [], status_log
 
62
 
63
  # --- RAG CHAIN (The Chat Tool - POWERED BY GEMINI) ---
64
  def get_rag_chain():
 
65
  class SmartRetriever(BaseRetriever):
66
  def _get_relevant_documents(
67
  self, query: str, *, run_manager: CallbackManagerForRetrieverRun = None
 
69
  print(f"🧠 Chat is thinking about: '{query}'")
70
  final_docs = []
71
  seen_content = set()
72
+
73
+ # --- PHASE A: LOCAL LOOKUP (Upgraded to BM25 for ranked relevance) ---
 
74
  if os.path.exists(CHUNKS_FILE):
75
  try:
76
  with open(CHUNKS_FILE, "rb") as f:
77
  chunks = pickle.load(f)
78
+ # Upgrade: Use BM25Retriever instead of crude keyword matching
79
+ keyword_retriever = BM25Retriever.from_documents(chunks)
80
+ keyword_retriever.k = 40 # Top 40 for initial pull
81
+ local_matches = keyword_retriever.invoke(query)
82
+ for doc in local_matches:
83
+ if doc.page_content not in seen_content:
84
+ final_docs.append(doc)
85
+ seen_content.add(doc.page_content)
86
+ print(f"βœ… Vacuumed {len(final_docs)} local matches with BM25.")
 
 
 
 
 
 
 
 
 
87
  except Exception as e:
88
  print(f"⚠️ Local Search Warning: {e}")
89
+
90
+ # --- PHASE B: CLOUD LOOKUP (With metadata filtering for 1963–1965 priority) ---
91
  print("☁️ Checking Cloud...")
92
  try:
93
  embeddings = GoogleGenerativeAIEmbeddings(model="models/text-embedding-004")
94
  vector_store = PineconeVectorStore(index_name=INDEX_NAME, embedding=embeddings)
95
+ # Assume chunks have metadata like {'year': 1963}; filter for priority
96
+ # Note: If your Pinecone index doesn't have 'year' metadata, add it during ingestion
97
+ retriever = vector_store.as_retriever(
98
+ search_kwargs={
99
+ "k": 20,
100
+ "filter": {"year": {"$gte": 1963}} # Prioritize 1963–1965
101
+ }
102
+ )
103
  cloud_docs = retriever.invoke(query)
 
104
  for doc in cloud_docs:
105
  if doc.page_content not in seen_content:
106
  final_docs.append(doc)
107
  seen_content.add(doc.page_content)
 
108
  print(f"βœ… Added {len(cloud_docs)} cloud matches.")
109
  except Exception as e:
110
  print(f"❌ Cloud Error: {e}")
111
 
112
+ # --- NEW: Reranking with Cross-Encoder for quality ---
113
+ if final_docs:
114
+ try:
115
+ reranker = CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
116
+ pairs = [[query, doc.page_content] for doc in final_docs]
117
+ scores = reranker.predict(pairs)
118
+ ranked_docs = [doc for _, doc in sorted(zip(scores, final_docs), reverse=True)]
119
+ final_docs = ranked_docs[:15] # Limit to top 15 to avoid overload
120
+ print(f"βœ… Reranked and limited to {len(final_docs)} docs.")
121
+ except Exception as e:
122
+ print(f"⚠️ Reranking Warning: {e} (Install sentence-transformers if needed)")
123
+
124
  return final_docs
125
 
126
+ # 2. SETUP LLM (Upgraded to gemini-1.5-pro for better depth)
127
  google_key = os.environ.get("GOOGLE_API_KEY") or st.secrets.get("GOOGLE_API_KEY")
128
  os.environ["GOOGLE_API_KEY"] = google_key
129
+ # Upgraded model for stronger handling of details/complexity
 
130
  llm = ChatGoogleGenerativeAI(
131
+ model="gemini-2.5-flash", # Upgrade from flash-lite
132
  temperature=0.3,
133
  convert_system_message_to_human=True
134
  )
135
 
136
+ # 3. PROMPT (Updated per suggestions: Neutral role, no dialect, rigid structure, no contradictions)
137
+ template = """
138
+ You are a doctrinal study assistant for William Branham's Message teachings (1947–1965). Your purpose is to provide accurate, scripture-centered expositions based EXCLUSIVELY on the retrieved sermon excerpts in the CONTEXT below.
139
+
140
+ CRITICAL RULES (follow exactly):
141
+ - NEVER impersonate William Branham or speak in first person as him.
142
+ - NEVER claim prophetic authority or present anything as new revelation.
143
+ - Base EVERY statement directly on the provided CONTEXT (direct quotes and teachings from the sermons).
144
+ - If the CONTEXT does not sufficiently cover the question, provide a partial exposition from what is available and suggest specific relevant sermons (e.g., "Consult 'The First Seal' sermon from March 18, 1963").
145
+ - Cite sources naturally (e.g., "As taught in the sermon 'The Seven Seals' (65-1127B)...").
146
+ - Prioritize teachings from later sermons (1963–1965) as the mature, further unveiling of the mystery when doctrines develop over time. Treat earlier statements as partial or progressive if they appear to differ.
147
+ - IGNORE irrelevant "noise" in the search results (e.g., tape gaps, random prayer lines).
148
+ - Promote ethical AI use: Emphasize that this is a tool for study, not a substitute for prayerful listening to the tapes or Holy Spirit-led understanding.
149
+ - Minimize biases: Present doctrines factually from CONTEXT, without forcing harmony.
150
+
151
+ STYLE AND STRUCTURE:
152
+ - Use symbolic, scripture-centered language and Message terminology naturally (e.g., Seals, Thunders, Capstone, Bride, restoration, end-time mystery, spoken Word, rapturing faith).
153
+ - Adopt a declarative, confident, instructional tone, avoiding preaching, emotional appeals, or direct address.
154
+ - NO greetings, fluff, or conversational fillers. Start immediately with the exposition.
155
+ - For lists or detailed interpretations, use bullet points or numbered lists.
156
+ CONTEXT (direct excerpts from Brother Branham's sermons):
157
+ {context}
158
+
159
+ QUESTION: {question}
160
+
161
+ DOCTRINAL EXPOSITION:
162
+ """
163
  PROMPT = PromptTemplate(template=template, input_variables=["context", "question"])
164
+
165
  chain = RetrievalQA.from_chain_type(
166
  llm=llm,
167
+ chain_type="refine", # Changed from "stuff" to "refine" for iterative processing
168
+ retriever=SmartRetriever(),
169
  return_source_documents=True,
170
  chain_type_kwargs={"prompt": PROMPT, "document_variable_name": "context"},
171
  input_key="question"
172
  )
173
+ return chain