Spaces:
Running on Zero
Running on Zero
File size: 5,715 Bytes
b1aba72 51e9502 b1aba72 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 | """
retrieval.py
-------------------
Retrieval nodes and pipeline orchestration.
"""
import time
import numpy as np
from tavily import TavilyClient
import vectorstore as vs_module
from config import CONFIDENCE_THRESHOLD, TAVILY_API_KEY
from logging_config import get_logger
logger = get_logger(__name__)
# Node 1: FAISS retrieval
def retrieve_node(state: dict) -> dict:
"""
Search the FAISS index and decide whether web search is needed.
Confidence scoring: sim = 1 / (1 + L2_distance). If best similarity
< CONFIDENCE_THRESHOLD, sets needs_web=True to route to web_node.
"""
query = state["query"]
# Access the current FAISS index via the module — never cache this
# as a local variable because chunk_and_index() updates it in place.
vs = vs_module.vectorstore
if vs is None:
logger.info("FAISS store is empty -> routing to web search")
return {
"retrieved_docs": "",
"needs_web": True,
"confidence": 0.0,
"source_type": "none",
}
try:
docs_with_scores = vs.similarity_search_with_score(query, k=6)
except Exception as e:
logger.warning(f"FAISS search failed: {e} -> web fallback")
return {
"retrieved_docs": "",
"needs_web": True,
"confidence": 0.0,
"source_type": "none",
}
if not docs_with_scores:
logger.info("No matches -> routing to web search")
return {
"retrieved_docs": "",
"needs_web": True,
"confidence": 0.0,
"source_type": "none",
}
distances = np.array([score for _, score in docs_with_scores])
similarities = 1.0 / (1.0 + distances)
best_score = float(np.max(similarities))
logger.info(f"best_sim={best_score:.3f} (threshold={CONFIDENCE_THRESHOLD})")
docs_text = "\n\n".join(
f"[Source: {doc.metadata.get('source', 'unknown')}]\n"
f"{doc.page_content[:800]}"
for doc, _ in docs_with_scores
)
needs_web = best_score < CONFIDENCE_THRESHOLD
if needs_web:
logger.info("Low confidence -> fetching from Tavily")
else:
logger.info("Sufficient confidence -> skipping web search")
return {
"retrieved_docs": docs_text,
"needs_web": needs_web,
"confidence": best_score,
"source_type": "faiss",
}
# Node 2: Tavily web search
def web_node(state: dict) -> dict:
"""
Fetch fresh results from Tavily and index them into FAISS.
Full raw content is indexed for future queries; a capped display
snippet (1500 chars/source) goes into the answer prompt.
"""
query = state["query"]
logger.info(f"Searching Tavily: {query!r}")
if not TAVILY_API_KEY:
logger.warning("TAVILY_API_KEY not set — skipping web search")
return {"web_results": "", "sources_count": 0, "source_type": "none"}
try:
client = TavilyClient(api_key=TAVILY_API_KEY)
response = client.search(
query=query,
max_results=5,
search_depth="basic",
include_raw_content=True,
include_answer=False,
)
except Exception as e:
logger.error(f"Tavily failed: {e}")
return {"web_results": "", "sources_count": 0, "source_type": "none"}
results = response.get("results", [])
if not results:
logger.info("Tavily returned no results")
return {"web_results": "", "sources_count": 0, "source_type": "none"}
parts = []
indexed_count = 0
for item in results:
title = item.get("title", "Untitled")
url = item.get("url", "")
raw = (item.get("raw_content") or item.get("content", "")).strip()
display = raw[:1500]
if not display:
continue
if url and len(raw) > 100:
try:
vs_module.chunk_and_index(raw, source_url=url)
indexed_count += 1
except Exception as e:
logger.warning(f"Indexing failed for {url}: {e}")
parts.append(f"TITLE: {title}\nURL: {url}\n\n{display}")
logger.info(f"{len(parts)} source(s) fetched | {indexed_count} indexed into FAISS")
separator = "\n\n" + ("─" * 60) + "\n\n"
web_results = separator.join(parts) if parts else ""
return {
"web_results": web_results,
"source_type": "web",
"sources_count": len(parts),
}
# Pipeline orchestrator
def run_pipeline_phase1(query: str, history: list = None) -> dict:
"""
Call retrieve_node and (optionally) web_node in sequence.
Direct node calls (not app.invoke()) let app.py intercept between
retrieval and generation — necessary to show a 'Generating...'
status update to the user before the (now much faster, but still
non-zero) GPU generation call runs.
"""
from ui_helpers import build_retrieval_query
if history is None:
history = []
retrieval_query = build_retrieval_query(query, history)
if retrieval_query != query:
logger.debug(f"Follow-up enriched: {retrieval_query!r}")
state = {
"query": retrieval_query,
"retrieved_docs": "",
"web_results": "",
"answer": "",
"needs_web": False,
"confidence": 0.0,
"source_type": "none",
"sources_count": 0,
"prompt_tokens": 0,
"t_retrieve": 0.0,
"t_web": 0.0,
"history": history,
}
t0 = time.perf_counter()
state.update(retrieve_node(state))
state["t_retrieve"] = time.perf_counter() - t0
if state.get("needs_web", False):
t1 = time.perf_counter()
state.update(web_node(state))
state["t_web"] = time.perf_counter() - t1
# Restore original query — the model answers what the user typed,
# not the internally enriched search string
state["query"] = query
return state
|