Files
ragflow/rag/advanced_rag/harness/orchestrator/decompose.py
Kevin Hu 454dea686e Feat: agentic search framework (#16859)
### Summary

Agentic search

<img width="1149" height="1575" alt="image"
src="https://github.com/user-attachments/assets/bce9a3e7-0517-4fb2-80a2-5d2a81a4da78"
/>

---------

Co-authored-by: Yingfeng Zhang <yingfeng.zhang@gmail.com>
2026-07-15 23:46:23 +08:00

113 lines
3.9 KiB
Python

"""Medium mode: decompose → parallel search → sufficiency check."""
import asyncio
import logging
from rag.advanced_rag.harness.types import ClaimTarget, AgentResult, OrchestratorContext
from rag.advanced_rag.harness.config import get_mode
from rag.advanced_rag.harness.sufficiency import (
cross_check_claim,
compute_fusion_score,
route_sufficiency_verdict,
)
from rag.advanced_rag.harness.tools.search import hybrid_search
_LOG = logging.getLogger(__name__)
async def decompose_and_search(state: dict, tools) -> dict:
"""Decompose → parallel search → merge → sufficiency check → iterate."""
question = state.get("question", "")
keywords = state.get("keywords", "")
claims_raw = state.get("claims", [])
mode_label = state.get("route", {}).thinking_mode if state.get("route") else "medium"
mode = get_mode(mode_label)
claims = [ClaimTarget(**c) if isinstance(c, dict) else c for c in claims_raw]
ctx = OrchestratorContext(question=question, claims=claims, mode=mode_label)
for cycle in range(mode.max_orchestrator_cycles):
ctx.iteration = cycle
unverified = [c for c in ctx.claims if not c.is_verified]
if not unverified:
break
# Parallel search on unverified claims
tasks = []
for c in unverified:
tasks.append(hybrid_search(tools, query=c.description, keywords=keywords))
results = await asyncio.gather(*tasks)
for c, result in zip(unverified, results):
if result.get("chunks"):
c.is_verified = True
c.confidence = 0.8
c.agent_result = AgentResult(
claim_id=c.claim_id,
report=_summarize(result),
is_verified=True,
confidence=0.8,
evidence_ids=list(range(len(result.get("chunks", [])))),
)
_merge_kbinfos(tools, result)
else:
c.agent_result = AgentResult(
claim_id=c.claim_id,
report="",
is_verified=False,
confidence=0.0,
)
all_chunks = {i: c for i, c in enumerate(tools.kbinfos.get("chunks", []))}
agent_results = [c.agent_result for c in ctx.claims if c.agent_result]
cross_results = [cross_check_claim(r, all_chunks) for r in agent_results]
verdict = compute_fusion_score(agent_results, cross_results, mode)
action, should_continue = route_sufficiency_verdict(
verdict,
mode_label,
cycle,
mode.max_orchestrator_cycles,
)
if action in ("ANSWER", "ANSWER_PARTIAL"):
return {
"verdict": verdict.__dict__,
"partial_answer": action == "ANSWER_PARTIAL",
"kbinfos": tools.kbinfos,
}
if action == "ABSTAIN":
tools.kbinfos["chunks"] = []
return {"verdict": verdict.__dict__, "abstain": True}
return {"kbinfos": tools.kbinfos}
def _merge_kbinfos(tools, result: dict):
if not result or not result.get("chunks"):
return
seen = {_chunk_key(c) for c in tools.kbinfos.get("chunks", [])}
for c in result.get("chunks", []):
k = _chunk_key(c)
if k in seen:
continue
seen.add(k)
tools.kbinfos.setdefault("chunks", []).append(c)
dseen = {d.get("doc_id") for d in tools.kbinfos.get("doc_aggs", [])}
for d in result.get("doc_aggs", []):
if d.get("doc_id") in dseen:
continue
dseen.add(d.get("doc_id"))
tools.kbinfos.setdefault("doc_aggs", []).append(d)
def _chunk_key(ck: dict) -> str:
return ck.get("chunk_id") or ck.get("id") or str(id(ck))
def _summarize(result: dict) -> str:
chunks = result.get("chunks", [])
texts = [c.get("content_with_weight", "")[:200] for c in chunks[:3]]
return " | ".join(texts)