Studio: skip RAG tool + system prompt nudge when scope has no docs
If RAG is toggled on but the active scope (thread or KB) has zero indexed documents, exposing search_knowledge_base to the model just wastes a tool-call turn — the model calls the tool, gets back 'no matching chunks', and has to re-plan. The system prompt nudge that instructs the model to call the tool before answering is similarly counterproductive. Frontend: before computing ragToolPathTaken in chat-adapter, fetch the document list for the current scope (KB or thread) and require docs to exist. The flag now gates both the system prompt injection and the enabled_tools list. Defensive fallback: if the docs lookup fails the flag stays true so we don't silently swallow RAG. Backend: add _drop_rag_tool_if_scope_empty in routes/inference.py that counts rag_documents for the request's rag_scope and strips search_knowledge_base from the tool list when 0. Applied at both chat-completion tool-filter sites so the protection works regardless of which streaming path serves the request.
This commit is contained in:
parent
336ad815b3
commit
5edffa3feb
2 changed files with 75 additions and 1 deletions
|
|
@ -302,6 +302,46 @@ def _effective_enable_tools(payload) -> Optional[bool]:
|
|||
return policy if policy is not None else payload.enable_tools
|
||||
|
||||
|
||||
def _drop_rag_tool_if_scope_empty(tools: list, rag_scope: Optional[dict]) -> list:
|
||||
"""Strip ``search_knowledge_base`` when the request's rag_scope has no docs.
|
||||
|
||||
Exposing the tool to the LLM when there's nothing to retrieve wastes a
|
||||
tool-call turn — the model hits the tool, gets back "no chunks", and
|
||||
has to re-plan. We do the doc count here as defence-in-depth; the
|
||||
frontend also avoids requesting the tool in this case.
|
||||
"""
|
||||
if not rag_scope:
|
||||
return tools
|
||||
kb_id = rag_scope.get("kb_id")
|
||||
thread_id = rag_scope.get("thread_id")
|
||||
if not kb_id and not thread_id:
|
||||
return tools
|
||||
try:
|
||||
from storage.studio_db import get_connection
|
||||
|
||||
with get_connection() as conn:
|
||||
if kb_id:
|
||||
row = conn.execute(
|
||||
"SELECT COUNT(*) FROM rag_documents WHERE kb_id = ?",
|
||||
(kb_id,),
|
||||
).fetchone()
|
||||
else:
|
||||
row = conn.execute(
|
||||
"SELECT COUNT(*) FROM rag_documents WHERE thread_id = ?",
|
||||
(thread_id,),
|
||||
).fetchone()
|
||||
doc_count = row[0] if row else 0
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.warning("RAG scope-has-docs check failed", error = str(exc))
|
||||
return tools
|
||||
if doc_count > 0:
|
||||
return tools
|
||||
return [
|
||||
t for t in tools
|
||||
if t.get("function", {}).get("name") != "search_knowledge_base"
|
||||
]
|
||||
|
||||
|
||||
# Cancel registry. Proxies (e.g. Colab) can swallow client fetch aborts
|
||||
# so is_disconnected() never fires. POST /inference/cancel looks up
|
||||
# in-flight cancel_events here by cancel_id (per-run) or session_id /
|
||||
|
|
@ -2387,6 +2427,9 @@ async def openai_chat_completions(
|
|||
]
|
||||
else:
|
||||
tools_to_use = ALL_TOOLS
|
||||
tools_to_use = _drop_rag_tool_if_scope_empty(
|
||||
tools_to_use, payload.rag_scope
|
||||
)
|
||||
|
||||
# ── Tool-use system prompt nudge ──────────────────────
|
||||
_tool_names = {t["function"]["name"] for t in tools_to_use}
|
||||
|
|
@ -2884,6 +2927,9 @@ async def openai_chat_completions(
|
|||
]
|
||||
else:
|
||||
_sf_tools_to_use = ALL_TOOLS
|
||||
_sf_tools_to_use = _drop_rag_tool_if_scope_empty(
|
||||
_sf_tools_to_use, payload.rag_scope
|
||||
)
|
||||
|
||||
_sf_tool_names = {t["function"]["name"] for t in _sf_tools_to_use}
|
||||
_sf_has_web = "web_search" in _sf_tool_names
|
||||
|
|
|
|||
|
|
@ -59,6 +59,8 @@ import {
|
|||
import {
|
||||
type SearchHit,
|
||||
type SearchRequest,
|
||||
listKBDocuments,
|
||||
listThreadDocuments,
|
||||
search as ragSearch,
|
||||
} from "@/features/rag/api/rag-api";
|
||||
import {
|
||||
|
|
@ -1051,8 +1053,34 @@ export function createOpenAIStreamAdapter(): ChatModelAdapter {
|
|||
|
||||
const ragSource = runtime.ragSource;
|
||||
const ragToolEnabled = runtime.ragToolEnabled;
|
||||
// Even when RAG is toggled on, the tool + system-prompt nudge are
|
||||
// useless if the active scope has no indexed documents — the model
|
||||
// would call the tool, get back "no chunks", and waste a turn. Do
|
||||
// a lightweight scope-has-docs check up front and treat the empty
|
||||
// scope as effectively "off" for this turn.
|
||||
let ragScopeHasDocs = false;
|
||||
if (ragToolEnabled && ragSource.kind !== "off") {
|
||||
try {
|
||||
if (ragSource.kind === "kb") {
|
||||
const docs = await listKBDocuments(ragSource.kbId);
|
||||
ragScopeHasDocs = docs.length > 0;
|
||||
} else if (ragSource.kind === "thread" && resolvedThreadId) {
|
||||
const docs = await listThreadDocuments(resolvedThreadId);
|
||||
ragScopeHasDocs = docs.length > 0;
|
||||
}
|
||||
} catch (err) {
|
||||
// If the doc-list endpoint is unreachable we err on the side
|
||||
// of letting the tool through — better to attempt retrieval
|
||||
// and surface an error than to silently skip RAG.
|
||||
console.warn("RAG scope-has-docs check failed:", err);
|
||||
ragScopeHasDocs = true;
|
||||
}
|
||||
}
|
||||
const ragToolPathTaken =
|
||||
ragToolEnabled && supportsTools && !isExternalRequest;
|
||||
ragToolEnabled
|
||||
&& supportsTools
|
||||
&& !isExternalRequest
|
||||
&& ragScopeHasDocs;
|
||||
|
||||
const safeSystemPrompt =
|
||||
typeof params.systemPrompt === "string" ? params.systemPrompt : "";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue