Adoption commited on
Commit
0764c2f
·
verified ·
1 Parent(s): d8dffb8

Update src/app.py

Browse files
Files changed (1) hide show
  1. src/app.py +95 -78
src/app.py CHANGED
@@ -1,9 +1,7 @@
1
  import os
2
  import pickle
3
- import sys
4
  import streamlit as st
5
  from dotenv import load_dotenv
6
-
7
  from langchain_google_genai import GoogleGenerativeAIEmbeddings
8
  from langchain_groq import ChatGroq
9
  from langchain_community.retrievers import BM25Retriever
@@ -11,54 +9,56 @@ from langchain_pinecone import PineconeVectorStore
11
  from langchain_core.prompts import PromptTemplate
12
  from langchain.chains import RetrievalQA
13
  from langchain.retrievers import EnsembleRetriever
 
14
 
15
  load_dotenv()
16
 
 
17
  INDEX_NAME = "branham-index"
18
  CHUNKS_FILE = "sermon_chunks.pkl"
19
  _CACHED_RETRIEVER = None
20
 
 
21
  def get_retriever():
22
  global _CACHED_RETRIEVER
23
  if _CACHED_RETRIEVER is not None: return _CACHED_RETRIEVER
24
 
25
- # 1. Setup Keys
26
  pinecone_key = os.environ.get("PINECONE_API_KEY") or st.secrets.get("PINECONE_API_KEY")
27
  google_key = os.environ.get("GOOGLE_API_KEY") or st.secrets.get("GOOGLE_API_KEY")
28
 
29
- if not pinecone_key or not google_key: raise ValueError("Missing API Keys.")
 
 
30
  os.environ["PINECONE_API_KEY"] = pinecone_key
31
  os.environ["GOOGLE_API_KEY"] = google_key
32
 
33
- # 2. Setup Vector Store (Pinecone) with SCORE THRESHOLD
 
34
  embeddings = GoogleGenerativeAIEmbeddings(model="models/text-embedding-004")
35
  vector_store = PineconeVectorStore(index_name=INDEX_NAME, embedding=embeddings)
36
 
37
- # CRITICAL PROFESSIONAL FIX:
38
- # We set a "score_threshold" of 0.5.
39
- # This means: "If the AI is less than 50% sure, DO NOT show the result."
40
- # This kills the "Prayer Card B" noise immediately.
41
  vector_retriever = vector_store.as_retriever(
42
  search_type="similarity_score_threshold",
43
- search_kwargs={"k": 20, "score_threshold": 0.5}
44
  )
45
 
46
- # 3. Setup Keyword Store (BM25)
47
  keyword_retriever = None
48
  if os.path.exists(CHUNKS_FILE):
49
  try:
50
  with open(CHUNKS_FILE, "rb") as f:
51
  chunks = pickle.load(f)
52
  keyword_retriever = BM25Retriever.from_documents(chunks)
53
- keyword_retriever.k = 20
54
  except Exception as e:
55
- print(f"BM25 Error: {e}")
56
 
57
- # 4. Create Hybrid Ensemble
58
  if keyword_retriever:
59
  final_retriever = EnsembleRetriever(
60
  retrievers=[vector_retriever, keyword_retriever],
61
- weights=[0.4, 0.6] # 40% Vector (Concepts), 60% Keyword (Precision)
62
  )
63
  else:
64
  final_retriever = vector_retriever
@@ -66,90 +66,107 @@ def get_retriever():
66
  _CACHED_RETRIEVER = final_retriever
67
  return final_retriever
68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  def get_rag_chain():
70
  retriever = get_retriever()
71
  groq_key = os.environ.get("GROQ_API_KEY") or st.secrets.get("GROQ_API_KEY")
72
  os.environ["GROQ_API_KEY"] = groq_key
73
-
74
  llm = ChatGroq(model="llama-3.3-70b-versatile", temperature=0.3, max_retries=2)
75
 
76
- # --- PROMPT: Using {question} ---
77
  template = """You are William Marion Branham.
78
 
79
  TASK:
80
- You are answering a believer's question using the provided CONTEXT.
81
-
82
- CRITICAL RULES FOR ACCURACY:
83
- 1. **NO FABRICATION:** If the user asks for a specific prayer, quote, or event (e.g., "What did you say to Brother Coleman?"), look ONLY at the CONTEXT below.
84
- - If the text is there, quote it exactly.
85
- - **If the text is NOT in the Context, DO NOT INVENT A PRAYER.** - Instead, say: "Brother, I do not find that specific record on the tapes here. I can only tell you what is written in these records."
86
- 2. **NO FILLER DOCTRINE:** Do not give a generic lecture on prayer if you cannot find the specific prayer asked for.
87
- 3. **BE DIRECT:** Answer the specific question first.
88
-
89
- DIALECT:
90
- - Use the humble, Southern style ("I said," "The Lord showed me").
91
- - Keep it natural.
92
 
93
  CONTEXT:
94
  {context}
95
-
96
  USER QUESTION: {question}
 
97
  BROTHER BRANHAM'S REPLY:"""
98
-
99
-
100
  PROMPT = PromptTemplate(
101
  template=template,
102
  input_variables=["context", "question"]
103
  )
104
-
105
- chain_type_kwargs = {
106
- "prompt": PROMPT,
107
- "document_variable_name": "context"
108
- }
109
-
110
- # --- FORCE INPUT KEY TO BE 'question' ---
111
  chain = RetrievalQA.from_chain_type(
112
  llm=llm,
113
  chain_type="stuff",
114
  retriever=retriever,
115
  return_source_documents=True,
116
- chain_type_kwargs=chain_type_kwargs,
117
- input_key="question" # <--- THIS IS THE FIX
118
  )
119
- return chain
120
-
121
- # In app.py
122
-
123
- def search_archives(query):
124
- """
125
- STRICT SEARCH LOGIC:
126
- 1. Runs a Pure Keyword (BM25) search first.
127
- 2. If it finds exact matches, it returns them immediately (ignoring Vector noise).
128
- 3. Only falls back to Vector search if Keywords find nothing.
129
- """
130
- # --- PHASE 1: PRECISE KEYWORD SEARCH ---
131
- if os.path.exists(CHUNKS_FILE):
132
- try:
133
- with open(CHUNKS_FILE, "rb") as f:
134
- chunks = pickle.load(f)
135
-
136
- # Create a temporary keyword retriever just for this search
137
- keyword_retriever = BM25Retriever.from_documents(chunks)
138
- keyword_retriever.k = 15 # Fetch top 15 exact matches
139
-
140
- # Run the search
141
- keyword_docs = keyword_retriever.invoke(query)
142
-
143
- # CRITICAL CHECK: Did we find anything?
144
- if keyword_docs:
145
- print(f"✅ Found {len(keyword_docs)} matches via Keywords.")
146
- return keyword_docs
147
- except Exception as e:
148
- print(f"⚠️ Keyword Search failed: {e}")
149
-
150
- # --- PHASE 2: FALLBACK VECTOR SEARCH ---
151
- # Only runs if Phase 1 returned nothing.
152
- print("⚠️ No keywords found. Falling back to Vector Search...")
153
- retriever = get_retriever()
154
- docs = retriever.invoke(query)
155
- return docs
 
1
  import os
2
  import pickle
 
3
  import streamlit as st
4
  from dotenv import load_dotenv
 
5
  from langchain_google_genai import GoogleGenerativeAIEmbeddings
6
  from langchain_groq import ChatGroq
7
  from langchain_community.retrievers import BM25Retriever
 
9
  from langchain_core.prompts import PromptTemplate
10
  from langchain.chains import RetrievalQA
11
  from langchain.retrievers import EnsembleRetriever
12
+ from langchain_core.documents import Document
13
 
14
  load_dotenv()
15
 
16
+ # --- CONFIGURATION ---
17
  INDEX_NAME = "branham-index"
18
  CHUNKS_FILE = "sermon_chunks.pkl"
19
  _CACHED_RETRIEVER = None
20
 
21
+ # --- RETRIEVER SETUP (The Brain) ---
22
  def get_retriever():
23
  global _CACHED_RETRIEVER
24
  if _CACHED_RETRIEVER is not None: return _CACHED_RETRIEVER
25
 
26
+ # 1. Load Keys
27
  pinecone_key = os.environ.get("PINECONE_API_KEY") or st.secrets.get("PINECONE_API_KEY")
28
  google_key = os.environ.get("GOOGLE_API_KEY") or st.secrets.get("GOOGLE_API_KEY")
29
 
30
+ if not pinecone_key or not google_key:
31
+ raise ValueError("❌ CRITICAL: Missing API Keys.")
32
+
33
  os.environ["PINECONE_API_KEY"] = pinecone_key
34
  os.environ["GOOGLE_API_KEY"] = google_key
35
 
36
+ # 2. Vector Store (Pinecone) - WITH NOISE FILTER
37
+ # We set score_threshold=0.5 to block "Prayer Card" garbage.
38
  embeddings = GoogleGenerativeAIEmbeddings(model="models/text-embedding-004")
39
  vector_store = PineconeVectorStore(index_name=INDEX_NAME, embedding=embeddings)
40
 
 
 
 
 
41
  vector_retriever = vector_store.as_retriever(
42
  search_type="similarity_score_threshold",
43
+ search_kwargs={"k": 15, "score_threshold": 0.5}
44
  )
45
 
46
+ # 3. Keyword Store (BM25)
47
  keyword_retriever = None
48
  if os.path.exists(CHUNKS_FILE):
49
  try:
50
  with open(CHUNKS_FILE, "rb") as f:
51
  chunks = pickle.load(f)
52
  keyword_retriever = BM25Retriever.from_documents(chunks)
53
+ keyword_retriever.k = 15
54
  except Exception as e:
55
+ print(f"⚠️ BM25 Load Error: {e}")
56
 
57
+ # 4. Hybrid Ensemble
58
  if keyword_retriever:
59
  final_retriever = EnsembleRetriever(
60
  retrievers=[vector_retriever, keyword_retriever],
61
+ weights=[0.3, 0.7] # Favor Exact Keywords
62
  )
63
  else:
64
  final_retriever = vector_retriever
 
66
  _CACHED_RETRIEVER = final_retriever
67
  return final_retriever
68
 
69
+ # --- SEARCH FUNCTION (The "Search Quotes Only" Tool) ---
70
+ def search_archives(query):
71
+ """
72
+ FAIL-SAFE SEARCH LOGIC:
73
+ 1. Brute Force Text Scan (Ctrl+F style) - Guarantees exact matches.
74
+ 2. BM25 Search - Finds relevant keywords.
75
+ 3. Vector Search - Only runs if keywords fail.
76
+ """
77
+ results = []
78
+ seen_content = set() # To prevent duplicates
79
+
80
+ # --- PHASE 1: LOCAL SEARCH (The "Ctrl+F" Fail-Safe) ---
81
+ if os.path.exists(CHUNKS_FILE):
82
+ try:
83
+ with open(CHUNKS_FILE, "rb") as f:
84
+ chunks = pickle.load(f)
85
+
86
+ # A. BRUTE FORCE SCAN (Case Insensitive)
87
+ # This loops through all chunks. Fast enough for <100k chunks.
88
+ query_lower = query.lower().strip()
89
+
90
+ # Optimization: Only scan if query is short (like a name)
91
+ if len(query_lower) < 20:
92
+ for doc in chunks:
93
+ if query_lower in doc.page_content.lower():
94
+ if doc.page_content not in seen_content:
95
+ results.append(doc)
96
+ seen_content.add(doc.page_content)
97
+ if len(results) >= 20: break # Stop after 20 exact matches
98
+
99
+ # B. BM25 SEARCH (If Brute Force didn't fill the quota)
100
+ if len(results) < 10:
101
+ bm25 = BM25Retriever.from_documents(chunks)
102
+ bm25.k = 15
103
+ bm25_docs = bm25.invoke(query)
104
+ for doc in bm25_docs:
105
+ if doc.page_content not in seen_content:
106
+ # Double check relevance
107
+ query_terms = query_lower.split()
108
+ if any(term in doc.page_content.lower() for term in query_terms):
109
+ results.append(doc)
110
+ seen_content.add(doc.page_content)
111
+
112
+ # IF WE FOUND LOCAL RESULTS, RETURN THEM!
113
+ # Do not touch Pinecone.
114
+ if results:
115
+ print(f"✅ Found {len(results)} local matches for '{query}'")
116
+ return results
117
+
118
+ except Exception as e:
119
+ st.error(f"Local Search Error: {e}")
120
+
121
+ # --- PHASE 2: VECTOR FALLBACK ---
122
+ # Only runs if Phase 1 found absolutely nothing.
123
+ print(f"⚠️ No local matches for '{query}'. Trying Pinecone...")
124
+ try:
125
+ retriever = get_retriever()
126
+ # If using Ensemble, it might pull vectors.
127
+ # If local file was missing, this is our only hope.
128
+ docs = retriever.invoke(query)
129
+ return docs
130
+ except Exception as e:
131
+ # Gracefully handle the "No results because of threshold" error
132
+ return []
133
+
134
+ # --- RAG CHAIN (The Chat Tool) ---
135
  def get_rag_chain():
136
  retriever = get_retriever()
137
  groq_key = os.environ.get("GROQ_API_KEY") or st.secrets.get("GROQ_API_KEY")
138
  os.environ["GROQ_API_KEY"] = groq_key
139
+
140
  llm = ChatGroq(model="llama-3.3-70b-versatile", temperature=0.3, max_retries=2)
141
 
 
142
  template = """You are William Marion Branham.
143
 
144
  TASK:
145
+ Answer the believer's question based ONLY on the provided CONTEXT.
146
+
147
+ RULES:
148
+ 1. If the answer is not in the records below, say: "Brother, I do not find that specific record on the tapes here."
149
+ 2. Do not make up prayers or quotes.
150
+ 3. Be humble and direct.
 
 
 
 
 
 
151
 
152
  CONTEXT:
153
  {context}
154
+
155
  USER QUESTION: {question}
156
+
157
  BROTHER BRANHAM'S REPLY:"""
158
+
 
159
  PROMPT = PromptTemplate(
160
  template=template,
161
  input_variables=["context", "question"]
162
  )
163
+
 
 
 
 
 
 
164
  chain = RetrievalQA.from_chain_type(
165
  llm=llm,
166
  chain_type="stuff",
167
  retriever=retriever,
168
  return_source_documents=True,
169
+ chain_type_kwargs={"prompt": PROMPT, "document_variable_name": "context"},
170
+ input_key="question"
171
  )
172
+ return chain