From a25ff22acac18c09fd2af49598f1eddb57ef659c Mon Sep 17 00:00:00 2001 From: Kevin Hu Date: Fri, 7 Aug 2026 14:51:54 +0800 Subject: [PATCH] Refactor: check the evidences while formalize answer. (#17951) ### Summary Check the evidences while formalize answer. --- rag/advanced_rag/agentic_rag_graph.py | 107 ++++- .../harness/orchestrator/agentic.py | 16 +- .../harness/orchestrator/decompose.py | 437 ++++++++++++++++-- .../harness/prompts/report_prompt.py | 7 + rag/advanced_rag/harness/tools/search.py | 2 +- 5 files changed, 521 insertions(+), 48 deletions(-) diff --git a/rag/advanced_rag/agentic_rag_graph.py b/rag/advanced_rag/agentic_rag_graph.py index 86ce6f4468..37c27d4c5b 100644 --- a/rag/advanced_rag/agentic_rag_graph.py +++ b/rag/advanced_rag/agentic_rag_graph.py @@ -43,6 +43,30 @@ from rag.prompts.generator import form_message, kb_prompt, message_fit_in _LOG = logging.getLogger(__name__) +_ANSWER_TARGET_SYSTEM = """You are an answer-target verifier for a multi-hop RAG system. +Resolve what entity, value, or fact the user's question is asking for. +Do not choose bridge entities that merely explain a clue. In inverse-relation +questions, a later clue may identify the target's partner, relative, employer, +team, or source; keep tracing until the requested answer role is satisfied. +Return JSON only.""" + +_ANSWER_TARGET_USER = """Question: +{question} + +Research summary: +{pre_summary} + +Evidence: +{evidence} + +Return JSON: +{{ + "target_role": "the role the final answer must satisfy", + "must_satisfy": ["short evidence-backed condition the answer must meet"], + "bridge_entities": ["entities that are useful clues but should not be the answer unless they also satisfy target_role"], + "reason": "one short explanation of the answer shape" +}}""" + def _snip(value: Any, limit: int = 240) -> str: try: @@ -55,6 +79,79 @@ def _snip(value: Any, limit: int = 240) -> str: return s +def _extract_json(text: str) -> dict: + text = re.sub(r"^.*", "", text or "", flags=re.DOTALL).strip() + text = re.sub(r"```(?:json)?\s*|\s*```", "", text).strip() + try: + import json_repair + + return json_repair.loads(text) + except Exception: + try: + return json.loads(text) + except Exception: + return {} + + +def _compact_text(text: str, limit: int) -> str: + text = text or "" + if len(text) <= limit: + return text + return text[:limit] + f"\n...(+{len(text) - limit} chars)" + + +def _string_items(value) -> list[str]: + if isinstance(value, str): + return [value.strip()] if value.strip() else [] + if not isinstance(value, list): + return [] + return [str(v).strip() for v in value if str(v).strip()] + + +def _format_answer_target_contract(contract: dict) -> str: + target_role = str(contract.get("target_role") or "").strip() + reason = str(contract.get("reason") or "").strip() + lines = [] + if target_role: + lines.append(f"Final answer must satisfy: {target_role}") + must = _string_items(contract.get("must_satisfy")) + if must: + lines.append("Must meet these conditions:") + lines.extend(f"- {item}" for item in must) + bridges = _string_items(contract.get("bridge_entities")) + if bridges: + lines.append("Bridge entities to verify but not answer with:") + lines.extend(f"- {item}" for item in bridges) + if reason: + lines.append(f"Reasoning guard: {reason}") + lines.append("Do not answer with an intermediate clue entity unless it also satisfies the final answer role.") + return "\n".join(lines) + + +async def _answer_target_contract(tools, question: str, kbinfos: dict, evidence: str) -> str: + """Build a compact guardrail that tells final synthesis what to answer.""" + fallback = "Final answer must directly satisfy the user's top-level who/what request. Use bridge entities only as clues, and verify any proposed answer against the evidence." + pre_summary = kbinfos.get("pre_summary") or "" + if not question or not pre_summary: + return fallback + try: + user = _ANSWER_TARGET_USER.format( + question=question, + pre_summary=_compact_text(pre_summary, 2400), + evidence=_compact_text(evidence, 6000), + ) + msg = await tools._fit_messages(_ANSWER_TARGET_SYSTEM, user) + ans = await tools.chat_mdl.async_chat(msg[0]["content"], msg[1:], {"temperature": 0.0}) + if isinstance(ans, tuple): + ans = ans[0] + parsed = _extract_json(ans) + formatted = _format_answer_target_contract(parsed) if parsed else "" + return formatted or fallback + except Exception: + _LOG.exception("[Composing the answer] Failed to build answer-target contract") + return fallback + + class AgenticState(TypedDict, total=False): messages: list question: str @@ -70,6 +167,7 @@ class AgenticState(TypedDict, total=False): empty_result: bool final_answer: str loop: int + max_loops: int feedback: str # replanning feedback @@ -263,9 +361,14 @@ def build_agentic_graph(tools, token_queue: asyncio.Queue, gen_conf: dict | None return {"final_answer": tools.empty_response} # Build evidence - evidence = kb_prompt(kbinfos, tools.chat_mdl.max_length) + evidence_blocks = kb_prompt(kbinfos, tools.chat_mdl.max_length) + evidence = "\n".join(evidence_blocks) if isinstance(evidence_blocks, list) else str(evidence_blocks) parts = [f"Question:\n{question}\n"] + answer_target = await _answer_target_contract(tools, question, kbinfos, evidence) + if answer_target: + parts.append(f"Answer Target Contract:\n{answer_target}\n") + if no_evidence: parts.append("No supporting evidence was retrieved. State clearly that the available sources are insufficient, and do not answer from general knowledge.\n") @@ -335,7 +438,7 @@ async def run_agentic_rag(tools, messages: list, max_loops: int = 3, gen_conf: d async def _drive(): try: holder["state"] = await graph.ainvoke( - {"messages": messages}, + {"messages": messages, "max_loops": max_loops}, {"recursion_limit": max(25, max_loops * 8)}, ) except Exception: diff --git a/rag/advanced_rag/harness/orchestrator/agentic.py b/rag/advanced_rag/harness/orchestrator/agentic.py index 7d9363d0ec..af0ef5a1f0 100644 --- a/rag/advanced_rag/harness/orchestrator/agentic.py +++ b/rag/advanced_rag/harness/orchestrator/agentic.py @@ -249,13 +249,25 @@ def _merge_agent_results(ctx: OrchestratorContext, tools): if combined: tools.kbinfos["pre_summary"] = "\n\n".join(combined) - # Collect evidence chunks from agent results + # Collect the chunks the agents actually cited across all claims. These + # indices share the same positional space as kb_prompt's ``[ID:n]`` markers + # (both index tools.kbinfos["chunks"]). for c in ctx.claims: if c.agent_result and c.agent_result.evidence_ids: for eid in c.agent_result.evidence_ids: - if eid not in seen_evidence: + if isinstance(eid, int): seen_evidence.add(eid) + # Drop chunks no claim ever cited (e.g. pre_search recall that didn't pan + # out) so the final-answer LLM call only sees the useful evidence. Preserve + # order so the re-numbered [ID:n] citations stay stable. Defensive: never + # filter to empty — if nothing was cited, keep the full pool. + all_chunks = tools.kbinfos.get("chunks") or [] + keep = sorted(i for i in seen_evidence if 0 <= i < len(all_chunks)) + if keep and len(keep) < len(all_chunks): + _LOG.info("[Agentic research] Trimming evidence for the final answer: %d of %d chunk(s) were cited.", len(keep), len(all_chunks)) + tools.kbinfos["chunks"] = [all_chunks[i] for i in keep] + async def _get_compilation_map(tools) -> dict[str, set[str]]: """Build compilation map from RAGTools - check which KBs have compilation artifacts.""" diff --git a/rag/advanced_rag/harness/orchestrator/decompose.py b/rag/advanced_rag/harness/orchestrator/decompose.py index 52ebbef6b3..315b9ae77e 100644 --- a/rag/advanced_rag/harness/orchestrator/decompose.py +++ b/rag/advanced_rag/harness/orchestrator/decompose.py @@ -1,7 +1,9 @@ -"""Medium mode: decompose → parallel search → sufficiency check.""" +"""Medium mode: decompose -> parallel search -> evidence-guided follow-up.""" import asyncio +import json import logging +import re from rag.advanced_rag.harness.types import ClaimTarget, AgentResult, OrchestratorContext from rag.advanced_rag.harness.config import get_mode @@ -14,84 +16,433 @@ from rag.advanced_rag.harness.tools.search import hybrid_search _LOG = logging.getLogger(__name__) +_MAX_EVIDENCE_SNIPPETS = 6 +_MAX_NEXT_QUERIES = 3 + +_EVIDENCE_ANALYSIS_SYSTEM = """You are controlling a multi-hop RAG retrieval loop. +Judge whether the retrieved passages verify the claim using only the provided evidence. +If the claim is not verified, produce targeted next search queries that use entities, +dates, names, or relationships discovered in the evidence and move closer to the +original question. +Distinguish final-answer entities from bridge entities. If the passages identify +only a clue node in the chain, keep the claim incomplete and search for the +remaining relation needed by the original question. Return JSON only.""" + +_EVIDENCE_ANALYSIS_USER = """Original question: +{question} + +Claim to verify: +{claim} + +Search query used this round: +{query} + +Round: {cycle} of {max_cycles} + +Retrieved evidence snippets: +{evidence} + +Return JSON: +{{ + "is_verified": true, + "confidence": 0.0, + "report": "Short evidence-backed finding, or what was learned so far.", + "gaps": ["specific missing fact or relationship"], + "next_queries": ["standalone follow-up search query"] +}}""" + async def decompose_and_search(state: dict, tools) -> dict: - """Decompose → parallel search → merge → sufficiency check → iterate.""" + """Decompose, retrieve, analyze evidence, then iterate with next-hop queries.""" 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" + route = state.get("route") + mode_label = _mode_label(route) mode = get_mode(mode_label) + max_cycles = _cycle_budget(state, mode.max_orchestrator_cycles) claims = [ClaimTarget(**c) if isinstance(c, dict) else c for c in claims_raw] ctx = OrchestratorContext(question=question, claims=claims, mode=mode_label) + attempted_queries: dict[str, set[str]] = {c.claim_id: set() for c in ctx.claims} + pending_queries: dict[str, list[str]] = {c.claim_id: [] for c in ctx.claims} + completed_cycles = 0 - for cycle in range(mode.max_orchestrator_cycles): + for cycle in range(max_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) + _LOG.info( + "[Decompose search] Round %d of %d: researching %d unresolved claim(s).", + cycle + 1, + max_cycles, + len(unverified), + ) - for c, result in zip(unverified, results): - if result.get("chunks"): - _merge_kbinfos(tools, result) - 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=_global_evidence_ids(tools, result), + tasks = [] + searched_claims = [] + for c in unverified: + query = _pick_next_query( + question, + c, + attempted_queries.setdefault(c.claim_id, set()), + pending_queries.setdefault(c.claim_id, []), + ) + if not query: + _LOG.info("[Decompose search] No unused follow-up query remains for claim %s.", c.claim_id) + continue + attempted_queries[c.claim_id].add(_normalize_query(query)) + searched_claims.append((c, query)) + tasks.append(hybrid_search(tools, query=query, keywords=keywords, use_compiled=True)) + + if not tasks: + break + + results = await asyncio.gather(*tasks, return_exceptions=True) + analysis_inputs = [] + + for (c, query), result in zip(searched_claims, results): + if isinstance(result, Exception): + _LOG.exception("[Decompose search] Search failed for claim %s.", c.claim_id, exc_info=result) + result = {"chunks": [], "doc_aggs": []} + + chunks = result.get("chunks", []) or [] + _merge_kbinfos(tools, result) + evidence_ids = _evidence_ids(tools, chunks) + analysis_inputs.append((c, query, result, evidence_ids)) + + analyses = await asyncio.gather( + *[ + _analyze_claim_evidence( + question=question, + claim=c, + query=query, + result=result, + evidence_ids=evidence_ids, + cycle=cycle, + max_cycles=max_cycles, + tools=tools, ) - else: - c.agent_result = AgentResult( - claim_id=c.claim_id, - report="", - is_verified=False, - confidence=0.0, + for c, query, result, evidence_ids in analysis_inputs + ], + return_exceptions=True, + ) + + for (c, query, result, evidence_ids), analysis in zip(analysis_inputs, analyses): + if isinstance(analysis, Exception): + _LOG.exception("[Decompose search] Evidence analysis failed for claim %s.", c.claim_id, exc_info=analysis) + analysis = _fallback_analysis(result, cycle, max_cycles) + + c.is_verified = analysis["is_verified"] + c.confidence = analysis["confidence"] + c.agent_result = AgentResult( + claim_id=c.claim_id, + report=analysis["report"], + is_verified=c.is_verified, + confidence=c.confidence, + evidence_ids=evidence_ids, + gaps=analysis["gaps"], + discovered_claims=[], + ) + ctx.agent_results[c.claim_id] = c.agent_result + + next_queries = _new_queries( + analysis.get("next_queries", []), + attempted_queries.setdefault(c.claim_id, set()), + ) + if not c.is_verified and next_queries: + pending_queries.setdefault(c.claim_id, []).extend(next_queries) + _LOG.info( + "[Decompose search] Claim %s needs another hop; queued %d targeted query/queries.", + c.claim_id, + len(next_queries), ) + _LOG.info( + '[Decompose search] Claim %s after "%s": %s (confidence %.0f%%).', + c.claim_id, + _snip(query), + "verified" if c.is_verified else "still incomplete", + c.confidence * 100, + ) + + completed_cycles = cycle + 1 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) + ctx.verdict = verdict action, should_continue = route_sufficiency_verdict( verdict, mode_label, cycle, - mode.max_orchestrator_cycles, + max_cycles, ) if action in ("ANSWER", "ANSWER_PARTIAL"): - return { - "verdict": verdict.__dict__, - "partial_answer": action == "ANSWER_PARTIAL", - "kbinfos": tools.kbinfos, - } + return _finalize(ctx, tools, partial=action == "ANSWER_PARTIAL", loop=completed_cycles) if action == "ABSTAIN": if getattr(tools, "text_attachments_content", ""): return {"verdict": verdict.__dict__, "kbinfos": tools.kbinfos} tools.kbinfos["chunks"] = [] - return {"verdict": verdict.__dict__, "abstain": True} + return {"verdict": verdict.__dict__, "abstain": True, "loop": completed_cycles} + if action == "FALLBACK_LLM": + return _finalize(ctx, tools, partial=True, loop=completed_cycles) + if not should_continue: + break - # Cycle exhaustion: flag the answer as partial so the final-answer node - # prepends the partial-answer preamble instead of presenting an incomplete - # answer as complete. Partial when (a) some claim is still unverified, or - # (b) every claim is verified but the final verdict is not SUFFICIENT (e.g. - # cross-check flagged conflicts/mismatches) — in both cases an exhaustive - # answer was not reached. - verdict_status = getattr(verdict, "status", None) - partial = (any(not c.is_verified for c in ctx.claims) or (verdict_status is not None and verdict_status != "SUFFICIENT")) and bool(tools.kbinfos.get("chunks")) - return {"kbinfos": tools.kbinfos, "partial_answer": partial} + if not tools.kbinfos.get("chunks"): + return {"empty_result": True, "kbinfos": tools.kbinfos, "loop": completed_cycles} + + partial = any(not c.is_verified for c in ctx.claims) + return _finalize(ctx, tools, partial=partial, loop=completed_cycles) + + +async def _analyze_claim_evidence( + *, + question: str, + claim: ClaimTarget, + query: str, + result: dict, + evidence_ids: list[int], + cycle: int, + max_cycles: int, + tools, +) -> dict: + chunks = result.get("chunks", []) or [] + if not chunks: + return { + "is_verified": False, + "confidence": 0.0, + "report": "", + "gaps": ["no evidence found"], + "next_queries": _fallback_queries(question, claim), + } + + try: + user = _EVIDENCE_ANALYSIS_USER.format( + question=question, + claim=claim.description, + query=query, + cycle=cycle + 1, + max_cycles=max_cycles, + evidence=_format_evidence(chunks), + ) + msg = await tools._fit_messages(_EVIDENCE_ANALYSIS_SYSTEM, user) + ans = await tools.chat_mdl.async_chat(msg[0]["content"], msg[1:], {"temperature": 0.1}) + if isinstance(ans, tuple): + ans = ans[0] + parsed = _extract_json(ans) + return _normalize_analysis(parsed, result, evidence_ids, question, claim, cycle, max_cycles) + except Exception: + _LOG.exception("[Decompose search] Evidence analysis LLM call failed.") + return _fallback_analysis(result, cycle, max_cycles, question, claim) + + +def _mode_label(route) -> str: + if not route: + return "medium" + if isinstance(route, dict): + return route.get("thinking_mode", "medium") + return getattr(route, "thinking_mode", "medium") + + +def _cycle_budget(state: dict, default_cycles: int) -> int: + try: + requested = int(state.get("max_loops") or default_cycles) + except (TypeError, ValueError): + requested = default_cycles + return max(1, min(default_cycles, requested)) + + +def _extract_json(text: str) -> dict: + text = re.sub(r"^.*", "", text or "", flags=re.DOTALL).strip() + text = re.sub(r"```(?:json)?\s*|\s*```", "", text).strip() + try: + import json_repair + + return json_repair.loads(text) + except Exception: + try: + return json.loads(text) + except Exception: + _LOG.warning("[Decompose search] Failed to parse evidence analysis output: %s", text[:200]) + return {} + + +def _normalize_analysis( + parsed: dict, + result: dict, + evidence_ids: list[int], + question: str, + claim: ClaimTarget, + cycle: int, + max_cycles: int, +) -> dict: + confidence = _clamp_float(parsed.get("confidence"), 0.0, 1.0) + is_verified = bool(parsed.get("is_verified")) and bool(evidence_ids) and confidence >= 0.55 + report = str(parsed.get("report") or "").strip() or _summarize(result) + gaps = _string_list(parsed.get("gaps")) + next_queries = _string_list(parsed.get("next_queries"))[:_MAX_NEXT_QUERIES] + + if is_verified: + gaps = [] + next_queries = [] + confidence = max(confidence, 0.65) + elif not next_queries and cycle + 1 < max_cycles: + next_queries = _fallback_queries(question, claim) + + return { + "is_verified": is_verified, + "confidence": confidence, + "report": report, + "gaps": gaps, + "next_queries": next_queries, + } + + +def _fallback_analysis( + result: dict, + cycle: int, + max_cycles: int, + question: str = "", + claim: ClaimTarget | None = None, +) -> dict: + chunks = result.get("chunks", []) or [] + is_last_cycle = cycle + 1 >= max_cycles + is_verified = bool(chunks) and is_last_cycle + next_queries = [] if is_last_cycle or claim is None else _fallback_queries(question, claim) + return { + "is_verified": is_verified, + "confidence": 0.55 if is_verified else (0.35 if chunks else 0.0), + "report": _summarize(result), + "gaps": [] if is_verified else ["need more specific evidence"], + "next_queries": next_queries, + } + + +def _pick_next_query( + question: str, + claim: ClaimTarget, + attempted: set[str], + pending: list[str], +) -> str: + while pending: + query = (pending.pop(0) or "").strip() + normalized = _normalize_query(query) + if normalized and normalized not in attempted: + return query + + candidates = [] + if not attempted: + candidates.append(claim.description) + candidates.extend(_fallback_queries(question, claim)) + + for query in candidates: + query = (query or "").strip() + normalized = _normalize_query(query) + if normalized and normalized not in attempted: + return query + return "" + + +def _fallback_queries(question: str, claim: ClaimTarget) -> list[str]: + candidates = [] + for gap in _agent_result_gaps(claim.agent_result): + candidates.append(f"{claim.description} {gap}") + if question: + candidates.append(f"{question} {claim.description}") + candidates.append(claim.description) + return candidates[:_MAX_NEXT_QUERIES] + + +def _new_queries(raw_queries: list[str], attempted: set[str]) -> list[str]: + queries = [] + seen = set(attempted) + for query in raw_queries: + query = (query or "").strip() + normalized = _normalize_query(query) + if not normalized or normalized in seen: + continue + seen.add(normalized) + queries.append(query) + if len(queries) >= _MAX_NEXT_QUERIES: + break + return queries + + +def _agent_result_gaps(agent_result) -> list[str]: + if not agent_result: + return [] + if isinstance(agent_result, dict): + return _string_list(agent_result.get("gaps")) + return _string_list(getattr(agent_result, "gaps", [])) + + +def _normalize_query(query: str) -> str: + return " ".join((query or "").lower().split()) + + +def _format_evidence(chunks: list[dict]) -> str: + snippets = [] + for i, chunk in enumerate(chunks[:_MAX_EVIDENCE_SNIPPETS], start=1): + text = chunk.get("content_with_weight") or chunk.get("content") or chunk.get("text") or "" + source = chunk.get("docnm_kwd") or chunk.get("doc_name") or chunk.get("doc_id") or "source" + snippets.append(f"[{i}] {source}: {_snip(text, 900)}") + return "\n\n".join(snippets) or "(no evidence)" + + +def _string_list(value) -> list[str]: + if isinstance(value, str): + return [value.strip()] if value.strip() else [] + if not isinstance(value, list): + return [] + return [str(v).strip() for v in value if str(v).strip()] + + +def _clamp_float(value, lo: float, hi: float) -> float: + try: + number = float(value) + except (TypeError, ValueError): + number = 0.0 + return min(hi, max(lo, number)) + + +def _evidence_ids(tools, chunks: list[dict]) -> list[int]: + all_chunks = tools.kbinfos.get("chunks", []) + index_by_key = {_chunk_key(c): i for i, c in enumerate(all_chunks)} + ids = [] + for chunk in chunks: + idx = index_by_key.get(_chunk_key(chunk)) + if idx is not None and idx not in ids: + ids.append(idx) + return ids + + +def _finalize(ctx: OrchestratorContext, tools, partial: bool, loop: int) -> dict: + combined = [] + for claim in ctx.claims: + if claim.agent_result and claim.agent_result.report: + status = "verified" if claim.is_verified else "incomplete" + combined.append(f"[{claim.claim_id}] {status} ({claim.description}): {claim.agent_result.report[:500]}") + if combined: + tools.kbinfos["pre_summary"] = "Research findings. These may include bridge entities; the final answer must still satisfy the original question's requested role.\n\n" + "\n\n".join(combined) + + return { + "verdict": ctx.verdict.__dict__ if ctx.verdict else None, + "partial_answer": partial, + "kbinfos": tools.kbinfos, + "loop": loop, + } + + +def _snip(text: str, limit: int = 160) -> str: + text = (text or "").replace("\n", " ").strip() + return text if len(text) <= limit else text[: limit - 3] + "..." def _merge_kbinfos(tools, result: dict): @@ -136,5 +487,5 @@ def _global_evidence_ids(tools, result: dict) -> list[int]: def _summarize(result: dict) -> str: chunks = result.get("chunks", []) - texts = [c.get("content_with_weight", "")[:200] for c in chunks[:3]] + texts = [(c.get("content_with_weight") or c.get("content") or c.get("text") or "")[:200] for c in chunks[:3]] return " | ".join(texts) diff --git a/rag/advanced_rag/harness/prompts/report_prompt.py b/rag/advanced_rag/harness/prompts/report_prompt.py index f6de3d191c..e10b63549b 100644 --- a/rag/advanced_rag/harness/prompts/report_prompt.py +++ b/rag/advanced_rag/harness/prompts/report_prompt.py @@ -2,6 +2,13 @@ FINAL_ANSWER_SYSTEM = """You are a smart agent. Answer the user's question using ONLY the evidence provided below. Do not invent facts: if the evidence cannot support a claim, say so plainly instead of guessing. +# Answer target +First resolve the exact role requested by the user's question. Multi-hop questions +often mention bridge entities that are only clues. Do not answer with a bridge +entity just because it satisfies a later clue; answer the entity, value, or fact +that satisfies the top-level question. If an Answer Target Contract is provided, +obey it over any research-summary wording. + # Citation rules {cite_rules} diff --git a/rag/advanced_rag/harness/tools/search.py b/rag/advanced_rag/harness/tools/search.py index 0b74f42e51..f3fb1046bd 100644 --- a/rag/advanced_rag/harness/tools/search.py +++ b/rag/advanced_rag/harness/tools/search.py @@ -114,7 +114,7 @@ def _highlight_keywords(text: str, kwds: list[str]) -> str: if not terms: return text pattern = re.compile("|".join(re.escape(term) for term in terms), re.IGNORECASE) - return pattern.sub(lambda m: f"{m.group(0)}", text) + return pattern.sub(lambda m: f"*{m.group(0)}*", text) def _narrow_by_keywords(chunks: list[dict], keywords: str) -> list[dict]: