Skip to content

feat: implement agentic DuckDuckGo web-search fallback - #516

Open
tejask011 wants to merge 7 commits into
FireFistisDead:masterfrom
tejask011:feature/agentic-web-fallback
Open

feat: implement agentic DuckDuckGo web-search fallback#516
tejask011 wants to merge 7 commits into
FireFistisDead:masterfrom
tejask011:feature/agentic-web-fallback

Conversation

@tejask011

@tejask011 tejask011 commented Jun 12, 2026

Copy link
Copy Markdown

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-search as 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

  • I ran the relevant checks locally
  • I verified the app still starts
  • I tested the affected flow end-to-end

Test Scenarios

  • Uploaded PDF and asked document-related questions.
  • Asked questions outside the scope of the uploaded PDF.
  • Verified fallback search triggers correctly when retrieval confidence is low.
  • Verified web citations render correctly in the frontend.
  • Verified external links open in a new tab.

Checklist:

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas

Notes

Deployment Requirements

  • The deployment environment must have internet access for DuckDuckGo search requests.

  • requirements.txt has been updated with:

    • duckduckgo-search>=5.0.0
  • Run:

pip install -r requirements.txt

during deployment to install the new dependency.

Security

  • No sensitive data included

labels

GSSOC

Summary by CodeRabbit

  • New Features
    • Automatic web-search fallback now provides web-based evidence to generate an overview when internal sources lack enough context; if no results are found, the existing insufficient-context responses (including streaming) remain unchanged.
    • Citations can now include web link metadata and open supported links in a new tab.
  • Bug Fixes
    • PDF summarization is now gated to run only when models are ready.
  • Style
    • Web citations use a globe icon, show a clickable pointer cursor, open http/https links in a new tab, and omit “Page X” suffixes for web sources.

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.
@vercel

vercel Bot commented Jun 12, 2026

Copy link
Copy Markdown

@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.

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds 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.

Changes

Web Search Fallback Feature

Layer / File(s) Summary
Web search foundation and citation metadata
rag-service/requirements.txt, rag-service/main.py
Adds the DuckDuckGo dependency, converts search results into LangChain documents, and includes source type and url in citation payloads.
Answer endpoint fallback
rag-service/main.py
Updates /ask and /ask/stream to use web results after evidence-gate rejection, or preserve refusal handling when searches return no results.
Web citation chip behavior
frontend/src/components/ChatPanel/MessageBubble.jsx
Uses globe icons for web sources, opens validated HTTP(S) URLs in new tabs, and omits page suffixes for web citations.
Summarization readiness wiring
rag-service/main.py
Gates /summarize on model readiness and removes its endpoint-local expired-session cleanup call.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: namraa310806

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
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The /summarize endpoint change to require model readiness and the cleanup removal are unrelated to the web-search fallback scope. Remove the /summarize gating and cleanup edits, or document why they are required for this feature.
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly names the main change: a DuckDuckGo web-search fallback.
Description check ✅ Passed The PR description covers the summary, related issue, testing, checklist, notes, and security sections expected by the template.
Linked Issues check ✅ Passed The changes add a web-search fallback, use results as context, and separate web citations in the UI, matching #515's goals.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added backend Express or API gateway work bug Something isn't working docs Documentation only enhancement New feature or request feature A new feature or improvement fix A targeted fix or cleanup frontend Frontend-related work level:advanced question Further information is requested rag-service FastAPI / model service work type:security type:testing level:beginner labels Jun 12, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between ba82aa9 and 8e9f8cd.

📒 Files selected for processing (3)
  • frontend/src/components/ChatPanel/MessageBubble.jsx
  • rag-service/main.py
  • rag-service/requirements.txt

Comment thread frontend/src/components/ChatPanel/MessageBubble.jsx
Comment thread rag-service/main.py Outdated
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Comment thread rag-service/main.py
"Evidence gate refused answer session_id=%s intent=%s best_score=%s retrieved_chunks=%s",
session_id,
intent,
best_score,
Comment thread rag-service/main.py
"Stream evidence gate refused session_id=%s intent=%s best_score=%s",
session_id,
intent,
best_score,
@FireFistisDead

Copy link
Copy Markdown
Owner

@tejask011 check the tests as they are not passi

@tejask011

Copy link
Copy Markdown
Author

@tejask011 check the tests as they are not passi

please review it now

@FireFistisDead

Copy link
Copy Markdown
Owner

@tejask011 there are merge conflicts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Route handlers are shadowed, so /ask and /summarize never execute their full logic.

Both decorated handlers currently stop after cleanup_expired_sessions(). The later same-named def blocks are not the registered FastAPI endpoints, so these routes return null instead 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

📥 Commits

Reviewing files that changed from the base of the PR and between be85d26 and 50894a1.

📒 Files selected for processing (2)
  • rag-service/main.py
  • rag-service/requirements.txt
🚧 Files skipped from review as they are similar to previous changes (1)
  • rag-service/requirements.txt

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Route handlers are shadowed, so /ask and /summarize never execute their full logic.

Both decorated handlers currently stop after cleanup_expired_sessions(). The later same-named def blocks are not the registered FastAPI endpoints, so these routes return null instead 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

📥 Commits

Reviewing files that changed from the base of the PR and between be85d26 and 50894a1.

📒 Files selected for processing (2)
  • rag-service/main.py
  • rag-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 lift

Multi-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_id value (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 win

Recompute global ranks after merging cross-session retrieval results.

After sorting merged candidates by score, the code keeps per-session local rank values. 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.

@tejask011

Copy link
Copy Markdown
Author

check it now see i made changes

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Crash: dead duplicate Groq-streaming code inside except RuntimeError, using an undefined groq_api_key.

The except RuntimeError handler (raised by _select_streaming_provider when no provider is configured) yields the error and then falls straight into an inline re-implementation of Groq streaming (Lines 4434-4465) that references groq_api_key, which is never defined in this function's scope (it's only local to synthesize_with_groq/_stream_with_groq). This raises NameError inside the except RuntimeError suite; a new exception there is not caught by the sibling except Exception as e: clause (they're mutually exclusive branches of the same try), 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 lift

Web 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, docs is swapped to web_docs and intent forced 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 render Page None for web results (which have no page key), 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 lift

Same web-fallback provenance gap as the sync /ask path.

Mirrors Lines 3845-3852 — docs is swapped to web_docs here too, but the streaming prompt built at Lines 4399-4414 still uses the PDF-only framing, and format_context(docs) (Line 4349) will hit the same page metadata 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 win

Add ddgs alongside the DuckDuckGo dependency.

perform_web_search is the web-search fallback for RAG, so rely on the maintained ddgs package instead of only pinning the renamed duckduckgo-search>=5.0.0. Import from ddgs and add ddgs to rag-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 win

Legacy provider-fallback chain differs between sync and streaming paths.

When LLM_PROVIDER is unset, _select_cloud_synthesizer tries 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 /ask vs /ask/stream — e.g. a configured GROQ_API_KEY is 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 None

Also 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.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 265a7bb1-984e-4bd9-9800-ddabfa3e213c

📥 Commits

Reviewing files that changed from the base of the PR and between 50894a1 and 3b8f52e.

📒 Files selected for processing (1)
  • rag-service/main.py

@tejask011

Copy link
Copy Markdown
Author

please merge it, its now about to go over a month

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Express or API gateway work bug Something isn't working docs Documentation only enhancement New feature or request feature A new feature or improvement fix A targeted fix or cleanup frontend Frontend-related work gssoc:approved level:advanced level:beginner quality:clean question Further information is requested rag-service FastAPI / model service work type:security type:testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Agentic Web-Search Fallback for Out-of-Context Queries

3 participants