Spaces:
Sleeping
Sleeping
Update src/app.py
Browse files- 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 (
|
| 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 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 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 (
|
| 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 |
-
#
|
| 114 |
-
retriever = vector_store.as_retriever(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 (
|
| 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
|
| 135 |
temperature=0.3,
|
| 136 |
convert_system_message_to_human=True
|
| 137 |
)
|
| 138 |
|
| 139 |
-
# 3. PROMPT
|
| 140 |
-
template = """
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
PROMPT = PromptTemplate(template=template, input_variables=["context", "question"])
|
| 159 |
-
|
| 160 |
chain = RetrievalQA.from_chain_type(
|
| 161 |
llm=llm,
|
| 162 |
-
chain_type="
|
| 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
|