feat: implement agentic DuckDuckGo web-search fallback - #516
Conversation
Adds fallback logic to the RAG service when local FAISS context fails, injecting duckduckgo_search results as custom context. Updates the React UI to display web source citations with proper external links.
|
@tejask011 is attempting to deploy a commit to the firefistisdead's projects Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughAdds a DuckDuckGo web-search fallback when retrieved evidence is insufficient, integrates it into synchronous and streaming answers, propagates web citation metadata to the frontend, updates web citation interactions, and gates PDF summarization on model readiness. ChangesWeb Search Fallback Feature
Estimated code review effort: 3 (Moderate) | ~25 minutes Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant Client
participant RAGService
participant WebSearch
participant SessionStore
Client->>RAGService: submit query
RAGService->>RAGService: evaluate evidence gate
RAGService->>WebSearch: search when evidence is insufficient
WebSearch-->>RAGService: web documents with URLs
RAGService-->>Client: synthesized response with web citations
RAGService->>SessionStore: persist refusal when no results exist
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@frontend/src/components/ChatPanel/MessageBubble.jsx`:
- Around line 366-369: The onClick handler in MessageBubble.jsx opens
third-party URLs directly (window.open(source.url, '_blank')) without validating
the URL or preventing tabnabbing; update the handler (the branch using
isWebSource and source.url) to first parse and validate source.url (e.g., try
new URL(source.url) and ensure protocol is http: or https:, or prepend https://
if appropriate), then open it using window.open with noopener/noreferrer (e.g.,
window.open(validUrl, '_blank', 'noopener,noreferrer')), and as a fallback set
the returned window's opener to null (const w = window.open(...); if (w)
w.opener = null) to ensure window.opener is not exposed.
In `@rag-service/main.py`:
- Around line 3479-3484: The code replaces PDF docs with web_docs in the
evidence fallback (see logger.info, perform_web_search, docs, intent) but
doesn't propagate retrieval origin; update the flow to set and carry a
retrieval_origin flag (e.g., retrieval_origin = "web" or "pdf") whenever you
assign docs = web_docs and when original PDF retrieval succeeds, and thread that
flag through the answer-building and prompt-construction functions (including
the streaming prompt path and any functions that generate headers or extractive
intro text) so they switch wording/instructions when any doc.metadata.type ==
"web" (or retrieval_origin == "web"); ensure code paths that synthesize overview
vs PDF-specific language read the flag and produce web-appropriate provenance
statements.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 890edd76-693e-4451-aeac-9824130bba60
📒 Files selected for processing (3)
frontend/src/components/ChatPanel/MessageBubble.jsxrag-service/main.pyrag-service/requirements.txt
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
| "Evidence gate refused answer session_id=%s intent=%s best_score=%s retrieved_chunks=%s", | ||
| session_id, | ||
| intent, | ||
| best_score, |
| "Stream evidence gate refused session_id=%s intent=%s best_score=%s", | ||
| session_id, | ||
| intent, | ||
| best_score, |
|
@tejask011 check the tests as they are not passi |
please review it now |
|
@tejask011 there are merge conflicts |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
rag-service/main.py (1)
3410-3416:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winRoute handlers are shadowed, so
/askand/summarizenever execute their full logic.Both decorated handlers currently stop after
cleanup_expired_sessions(). The later same-nameddefblocks are not the registered FastAPI endpoints, so these routes returnnullinstead of running retrieval/generation logic.✅ Minimal patch
`@app.post`("/ask") def ask_question(data: Question, _ready: None = Depends(require_models_ready)): cleanup_expired_sessions() - - -def ask_question(data: Question): question = (data.question or "").strip() ...`@app.post`("/summarize") def summarize_pdf(data: SummarizeRequest, _ready: None = Depends(require_models_ready)): cleanup_expired_sessions() -def summarize_pdf(data: SummarizeRequest): session_id = str(data.session_id) ...Also applies to: 4211-4215
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rag-service/main.py` around lines 3410 - 3416, The FastAPI route handlers are shadowed by duplicate function definitions (e.g., the decorated `@app.post`("/ask") def ask_question(...) currently only calls cleanup_expired_sessions() while a later def ask_question(data: Question): contains the real logic); remove the duplicate/unregistered function and move the full handler logic into the decorated function (keep the dependency require_models_ready in the signature and call cleanup_expired_sessions() before running the question processing), and apply the same fix for the /summarize handler so the registered endpoints execute the retrieval/generation code instead of returning null.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@rag-service/main.py`:
- Around line 3988-3991: The merged list keeps per-session local rank values
which distort cross-session ranking used by diversify_retrieved_documents();
after sorting all_scored_candidates and slicing scored_candidates, recompute
global ranks by iterating scored_candidates in order and updating each
candidate's rank to its new global position (e.g., for i, cand in
enumerate(scored_candidates): set cand.rank = i+1 or rebuild the tuple as
(cand_doc, cand_score, i+1) if candidates are tuples) so
diversify_retrieved_documents() sees correct global ranks when applying
thresholds.
- Around line 3895-3907: The loop over session_id_list (for idx, session_id in
enumerate(session_id_list)) builds all_scored_candidates and
all_indexed_documents but later persistence uses a single session_id, so history
updates and dirty flags are only written for one session; fix by performing
per-session persistence: either move the refusal/success persistence logic into
the loop so each iteration uses the current session_id and secret_list[idx] (or
None) when appending chat history and marking dirty, and compute a
session-scoped cache_key (e.g., include session_id) before using it;
alternatively, keep aggregation but then after the loop iterate over
session_id_list and persist the corresponding subset of results for each
session_id (mapping candidates/documents back to each session), ensuring you
reference session_id_list, secret_list, the for idx, session_id loop,
all_scored_candidates, all_indexed_documents, and cache_key when making the
per-session writes.
---
Outside diff comments:
In `@rag-service/main.py`:
- Around line 3410-3416: The FastAPI route handlers are shadowed by duplicate
function definitions (e.g., the decorated `@app.post`("/ask") def
ask_question(...) currently only calls cleanup_expired_sessions() while a later
def ask_question(data: Question): contains the real logic); remove the
duplicate/unregistered function and move the full handler logic into the
decorated function (keep the dependency require_models_ready in the signature
and call cleanup_expired_sessions() before running the question processing), and
apply the same fix for the /summarize handler so the registered endpoints
execute the retrieval/generation code instead of returning null.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 997f9475-7967-42c1-9fef-16c26836d62d
📒 Files selected for processing (2)
rag-service/main.pyrag-service/requirements.txt
🚧 Files skipped from review as they are similar to previous changes (1)
- rag-service/requirements.txt
There was a problem hiding this comment.
Caution
Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
rag-service/main.py (1)
3410-3416:⚠️ Potential issue | 🔴 Critical | ⚡ Quick winRoute handlers are shadowed, so
/askand/summarizenever execute their full logic.Both decorated handlers currently stop after
cleanup_expired_sessions(). The later same-nameddefblocks are not the registered FastAPI endpoints, so these routes returnnullinstead of running retrieval/generation logic.✅ Minimal patch
`@app.post`("/ask") def ask_question(data: Question, _ready: None = Depends(require_models_ready)): cleanup_expired_sessions() - - -def ask_question(data: Question): question = (data.question or "").strip() ...`@app.post`("/summarize") def summarize_pdf(data: SummarizeRequest, _ready: None = Depends(require_models_ready)): cleanup_expired_sessions() -def summarize_pdf(data: SummarizeRequest): session_id = str(data.session_id) ...Also applies to: 4211-4215
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rag-service/main.py` around lines 3410 - 3416, The FastAPI route handlers are shadowed by duplicate function definitions (e.g., the decorated `@app.post`("/ask") def ask_question(...) currently only calls cleanup_expired_sessions() while a later def ask_question(data: Question): contains the real logic); remove the duplicate/unregistered function and move the full handler logic into the decorated function (keep the dependency require_models_ready in the signature and call cleanup_expired_sessions() before running the question processing), and apply the same fix for the /summarize handler so the registered endpoints execute the retrieval/generation code instead of returning null.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@rag-service/main.py`:
- Around line 3988-3991: The merged list keeps per-session local rank values
which distort cross-session ranking used by diversify_retrieved_documents();
after sorting all_scored_candidates and slicing scored_candidates, recompute
global ranks by iterating scored_candidates in order and updating each
candidate's rank to its new global position (e.g., for i, cand in
enumerate(scored_candidates): set cand.rank = i+1 or rebuild the tuple as
(cand_doc, cand_score, i+1) if candidates are tuples) so
diversify_retrieved_documents() sees correct global ranks when applying
thresholds.
- Around line 3895-3907: The loop over session_id_list (for idx, session_id in
enumerate(session_id_list)) builds all_scored_candidates and
all_indexed_documents but later persistence uses a single session_id, so history
updates and dirty flags are only written for one session; fix by performing
per-session persistence: either move the refusal/success persistence logic into
the loop so each iteration uses the current session_id and secret_list[idx] (or
None) when appending chat history and marking dirty, and compute a
session-scoped cache_key (e.g., include session_id) before using it;
alternatively, keep aggregation but then after the loop iterate over
session_id_list and persist the corresponding subset of results for each
session_id (mapping candidates/documents back to each session), ensuring you
reference session_id_list, secret_list, the for idx, session_id loop,
all_scored_candidates, all_indexed_documents, and cache_key when making the
per-session writes.
---
Outside diff comments:
In `@rag-service/main.py`:
- Around line 3410-3416: The FastAPI route handlers are shadowed by duplicate
function definitions (e.g., the decorated `@app.post`("/ask") def
ask_question(...) currently only calls cleanup_expired_sessions() while a later
def ask_question(data: Question): contains the real logic); remove the
duplicate/unregistered function and move the full handler logic into the
decorated function (keep the dependency require_models_ready in the signature
and call cleanup_expired_sessions() before running the question processing), and
apply the same fix for the /summarize handler so the registered endpoints
execute the retrieval/generation code instead of returning null.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 997f9475-7967-42c1-9fef-16c26836d62d
📒 Files selected for processing (2)
rag-service/main.pyrag-service/requirements.txt
🚧 Files skipped from review as they are similar to previous changes (1)
- rag-service/requirements.txt
🛑 Comments failed to post (2)
rag-service/main.py (2)
3895-3907:
⚠️ Potential issue | 🟠 Major | 🏗️ Heavy liftMulti-session retrieval writes chat history to only one session.
The loop collects documents from multiple sessions, but downstream refusal/success persistence still appends chat and marks dirty using a single
session_idvalue (the last iterated one). That drops history updates for other queried sessions and creates inconsistent per-session state.💡 Suggested direction
- for idx, session_id in enumerate(session_id_list): + validated_session_ids = [] + for idx, session_id in enumerate(session_id_list): ... _require_session_secret(session, secret) + validated_session_ids.append(session_id) ... - with sessions_lock: - current_session = sessions.get(session_id) - if current_session: - append_chat_exchange(...) - _mark_session_dirty(session_id) + with sessions_lock: + for sid in validated_session_ids: + current_session = sessions.get(sid) + if current_session: + append_chat_exchange(...) + _mark_session_dirty(sid)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rag-service/main.py` around lines 3895 - 3907, The loop over session_id_list (for idx, session_id in enumerate(session_id_list)) builds all_scored_candidates and all_indexed_documents but later persistence uses a single session_id, so history updates and dirty flags are only written for one session; fix by performing per-session persistence: either move the refusal/success persistence logic into the loop so each iteration uses the current session_id and secret_list[idx] (or None) when appending chat history and marking dirty, and compute a session-scoped cache_key (e.g., include session_id) before using it; alternatively, keep aggregation but then after the loop iterate over session_id_list and persist the corresponding subset of results for each session_id (mapping candidates/documents back to each session), ensuring you reference session_id_list, secret_list, the for idx, session_id loop, all_scored_candidates, all_indexed_documents, and cache_key when making the per-session writes.
3988-3991:
⚠️ Potential issue | 🟠 Major | ⚡ Quick winRecompute global ranks after merging cross-session retrieval results.
After sorting merged candidates by score, the code keeps per-session local
rankvalues.diversify_retrieved_documents()uses rank thresholds, so local ranks from different sessions distort relevance selection.🔧 Minimal fix
all_scored_candidates.sort(key=lambda x: x[1]) - scored_candidates = all_scored_candidates[:ASK_RETRIEVAL_CANDIDATES] + scored_candidates = [ + (doc, score, global_rank) + for global_rank, (doc, score, _local_rank) in enumerate( + all_scored_candidates[:ASK_RETRIEVAL_CANDIDATES] + ) + ] indexed_documents = all_indexed_documents🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rag-service/main.py` around lines 3988 - 3991, The merged list keeps per-session local rank values which distort cross-session ranking used by diversify_retrieved_documents(); after sorting all_scored_candidates and slicing scored_candidates, recompute global ranks by iterating scored_candidates in order and updating each candidate's rank to its new global position (e.g., for i, cand in enumerate(scored_candidates): set cand.rank = i+1 or rebuild the tuple as (cand_doc, cand_score, i+1) if candidates are tuples) so diversify_retrieved_documents() sees correct global ranks when applying thresholds.
|
check it now see i made changes |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (5)
rag-service/main.py (5)
4423-4469: 🩺 Stability & Availability | 🔴 Critical | ⚡ Quick winCrash: dead duplicate Groq-streaming code inside
except RuntimeError, using an undefinedgroq_api_key.The
except RuntimeErrorhandler (raised by_select_streaming_providerwhen no provider is configured) yields the error and then falls straight into an inline re-implementation of Groq streaming (Lines 4434-4465) that referencesgroq_api_key, which is never defined in this function's scope (it's only local tosynthesize_with_groq/_stream_with_groq). This raisesNameErrorinside theexcept RuntimeErrorsuite; a new exception there is not caught by the siblingexcept Exception as e:clause (they're mutually exclusive branches of the sametry), so it propagates out of the generator uncaught, breaking the SSE response whenever no streaming provider is configured. This logic also fully duplicates_stream_with_groq, which is already reachable via_select_streaming_provider.🐛 Proposed fix — remove the dead/duplicate block
except RuntimeError as exc: err = str(exc) yield err full_answer_parts.append(err) - import urllib.request - import json - - url = "https://api.groq.com/openai/v1/chat/completions" - headers = { - "Authorization": f"Bearer {groq_api_key}", - "Content-Type": "application/json", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" - } - payload = json.dumps({ - "model": "llama-3.1-8b-instant", - "messages": [{"role": "user", "content": prompt}], - "stream": True, - "temperature": 0 - }).encode("utf-8") - - req = urllib.request.Request(url, data=payload, headers=headers, method="POST") - with urllib.request.urlopen(req, timeout=30) as resp: - for line in resp: - decoded = line.decode('utf-8').strip() - if decoded.startswith('data: '): - data_str = decoded[6:] - if data_str == '[DONE]': - break - try: - data = json.loads(data_str) - token = data['choices'][0]['delta'].get('content', '') - if token: - full_answer_parts.append(token) - yield _sse_frame(token) - except Exception: - pass except Exception as e: err = f"LLM API Error: {str(e)}" yield err full_answer_parts.append(err)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rag-service/main.py` around lines 4423 - 4469, Remove the inline Groq HTTP streaming implementation from the RuntimeError handler in _generate_and_stream, including its imports, request construction, and response loop. Keep the handler limited to yielding and recording the original error, since _select_streaming_provider already routes through the existing Groq streaming implementation when configured.
3845-3852: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftWeb fallback loses provenance — prompt/context still frame docs as uploaded PDFs.
This mirrors the same substitution at Lines 4316-4323 in
ask_question_stream. In both places,docsis swapped toweb_docsandintentforced to"overview", but nothing marks the retrieval origin. Downstream, the prompt built at Lines 4067-4085 always says "answering questions over one or more uploaded PDF documents", and the per-source header at Lines 3892-3901 will renderPage Nonefor web results (which have nopagekey), producing a confusing/misleading context block fed to the LLM and shown in citations. This is the same concern raised in a prior review round — still unresolved at the new line numbers. Full fix proposed in the consolidated comment.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rag-service/main.py` around lines 3845 - 3852, Track retrieval provenance when the evidence-gate fallback replaces docs with web_docs, and apply the same change in ask_question_stream. Update downstream prompt construction and per-source formatting to distinguish web results from uploaded PDFs, avoiding PDF-specific wording and Page None citations; preserve the existing PDF behavior for document retrieval.
4316-4323: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftSame web-fallback provenance gap as the sync
/askpath.Mirrors Lines 3845-3852 —
docsis swapped toweb_docshere too, but the streaming prompt built at Lines 4399-4414 still uses the PDF-only framing, andformat_context(docs)(Line 4349) will hit the samepagemetadata mismatch for web documents. Same root cause as the sync-side comment; see consolidated comment for the proposed fix spanning both.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rag-service/main.py` around lines 4316 - 4323, Update the streaming fallback flow around passes_evidence_gate and format_context so web-search documents retain explicit provenance and are formatted without assuming PDF-only page metadata. Mark the fallback source and propagate it to the streaming prompt construction near the stream response logic, ensuring web results use web-specific framing while PDF results preserve the existing behavior.
3848-3848: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winAdd
ddgsalongside the DuckDuckGo dependency.
perform_web_searchis the web-search fallback for RAG, so rely on the maintainedddgspackage instead of only pinning the renamedduckduckgo-search>=5.0.0. Import fromddgsand addddgstorag-service/requirements.txt.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rag-service/main.py` at line 3848, Update perform_web_search and its web-search dependency to use the maintained ddgs package: change the relevant import to ddgs and add ddgs to requirements.txt, while preserving the existing fallback behavior and search flow.
2179-2207: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winLegacy provider-fallback chain differs between sync and streaming paths.
When
LLM_PROVIDERis unset,_select_cloud_synthesizertries Ollama → OpenAI → Anthropic (no Groq), while_select_streaming_provider's legacy chain tries OpenAI → Anthropic → Groq (no Ollama). A deployment relying on the default/legacy chain gets materially different provider precedence for/askvs/ask/stream— e.g. a configuredGROQ_API_KEYis silently never used for non-streaming answers, and a local Ollama instance is silently never used for streaming.🐛 Proposed fix — align both legacy chains
# Legacy fallback chain when no explicit provider is configured ollama_result = synthesize_with_ollama(prompt) if ollama_result: return ollama_result openai_result = synthesize_with_openai(prompt) if openai_result: return openai_result anthropic_result = synthesize_with_anthropic(prompt) if anthropic_result: return anthropic_result + groq_result = synthesize_with_groq(prompt) + if groq_result: + return groq_result return NoneAlso applies to: 2365-2389
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@rag-service/main.py` around lines 2179 - 2207, Align the legacy fallback behavior between _select_cloud_synthesizer and _select_streaming_provider. When LLM_PROVIDER is unset, make both paths attempt the same providers in the same precedence order, including Ollama and Groq, while preserving the explicit-provider branches and existing result handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@rag-service/main.py`:
- Around line 4423-4469: Remove the inline Groq HTTP streaming implementation
from the RuntimeError handler in _generate_and_stream, including its imports,
request construction, and response loop. Keep the handler limited to yielding
and recording the original error, since _select_streaming_provider already
routes through the existing Groq streaming implementation when configured.
- Around line 3845-3852: Track retrieval provenance when the evidence-gate
fallback replaces docs with web_docs, and apply the same change in
ask_question_stream. Update downstream prompt construction and per-source
formatting to distinguish web results from uploaded PDFs, avoiding PDF-specific
wording and Page None citations; preserve the existing PDF behavior for document
retrieval.
- Around line 4316-4323: Update the streaming fallback flow around
passes_evidence_gate and format_context so web-search documents retain explicit
provenance and are formatted without assuming PDF-only page metadata. Mark the
fallback source and propagate it to the streaming prompt construction near the
stream response logic, ensuring web results use web-specific framing while PDF
results preserve the existing behavior.
- Line 3848: Update perform_web_search and its web-search dependency to use the
maintained ddgs package: change the relevant import to ddgs and add ddgs to
requirements.txt, while preserving the existing fallback behavior and search
flow.
- Around line 2179-2207: Align the legacy fallback behavior between
_select_cloud_synthesizer and _select_streaming_provider. When LLM_PROVIDER is
unset, make both paths attempt the same providers in the same precedence order,
including Ollama and Groq, while preserving the explicit-provider branches and
existing result handling.
|
please merge it, its now about to go over a month |
Adds fallback logic to the RAG service when local FAISS context fails, injecting DuckDuckGo search results as additional context. Updates the React UI to display web source citations with proper external links and visual differentiation.
Summary
This PR addresses the "insufficient context" limitation in the RAG pipeline by introducing an Agentic Web-Search Fallback.
Changes Made
Backend (
rag-service/main.py)Added
duckduckgo-searchas a dependency.Implemented fallback logic that triggers when
passes_evidence_gate()fails.Executes a DuckDuckGo search using the user's query when retrieved document context is insufficient.
Injects top web search results into the LLM prompt as supplemental context.
Opens external URLs in a new browser tab when a web citation is clicked.
Related issue
Closes #515
Testing
Test Scenarios
Checklist:
Notes
Deployment Requirements
The deployment environment must have internet access for DuckDuckGo search requests.
requirements.txthas been updated with:duckduckgo-search>=5.0.0Run:
during deployment to install the new dependency.
Security
labels
GSSOC
Summary by CodeRabbit