- assertion_anchored_graph_retriever 断言锚定候选与 top30 合并 - pipeline/agentic_controller/llm_client 重试与 Q0 软回退 - question_decomposition JSON repair、citation_contract grouped 归一化 - api endpoint_path 全参数、retrieval 禁用 PC/SQLite、canonical-only 校验 - bounded_canonical/direct_flow 白名单、subquestion_orchestrator required_review - scripts/qa_v1_1_73_server.py 新服务入口(backend_guard + gate_policy) - 旧 start_qa_v1_1_73_*.sh 移入 docs/legacy_startup_scripts/
137 lines
5.0 KiB
Python
137 lines
5.0 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, List, Tuple
|
|
|
|
from .schemas import EvidenceItem
|
|
from .v16_retriever import V16IndependentRetriever
|
|
|
|
|
|
def _text(value: Any) -> str:
|
|
return str(value or "").strip()
|
|
|
|
|
|
DEFAULT_RETRIEVAL_OPTIONS: Dict[str, bool] = {
|
|
"use_v15": True,
|
|
"use_pc": False,
|
|
"use_sqlite": False,
|
|
"use_qdrant": True,
|
|
"use_neo4j": True,
|
|
"use_graph_expansion": True,
|
|
}
|
|
|
|
|
|
def normalize_retrieval_options(options: Dict[str, Any] | None) -> Dict[str, bool]:
|
|
normalized = dict(DEFAULT_RETRIEVAL_OPTIONS)
|
|
if isinstance(options, dict):
|
|
if options.get("use_pc") or options.get("use_sqlite"):
|
|
raise ValueError(
|
|
"PC and SQLite retrieval backends are disabled; use Neo4j and Qdrant"
|
|
)
|
|
for key in list(normalized):
|
|
if key in options:
|
|
normalized[key] = bool(options[key])
|
|
# If Neo4j is disabled, expansion cannot run.
|
|
if not normalized["use_neo4j"]:
|
|
normalized["use_graph_expansion"] = False
|
|
return normalized
|
|
|
|
|
|
class DualKBRetriever:
|
|
"""Use stable public KB clients without importing previous QA server scripts."""
|
|
|
|
def __init__(self) -> None:
|
|
self.v15 = V16IndependentRetriever()
|
|
|
|
def retrieve_queries(
|
|
self,
|
|
queries: Dict[str, List[str]],
|
|
*,
|
|
top_k_per_query: int,
|
|
graph_hops: int,
|
|
include_candidate: bool,
|
|
retrieval_options: Dict[str, Any] | None = None,
|
|
) -> Tuple[List[EvidenceItem], List[Dict[str, Any]]]:
|
|
options = normalize_retrieval_options(retrieval_options)
|
|
output: List[EvidenceItem] = []
|
|
audit: List[Dict[str, Any]] = []
|
|
for lane, lane_queries in queries.items():
|
|
for query in lane_queries:
|
|
before = len(output)
|
|
if options["use_v15"]:
|
|
output.extend(
|
|
self._retrieve_v15(
|
|
query,
|
|
lane,
|
|
top_k_per_query,
|
|
graph_hops,
|
|
include_candidate,
|
|
options,
|
|
)
|
|
)
|
|
audit.append(
|
|
{
|
|
"lane": lane,
|
|
"query": query,
|
|
"new_rows": len(output) - before,
|
|
"retrieval_options": dict(options),
|
|
}
|
|
)
|
|
return output, audit
|
|
|
|
def _retrieve_v15(
|
|
self,
|
|
query: str,
|
|
lane: str,
|
|
top_k: int,
|
|
graph_hops: int,
|
|
include_candidate: bool,
|
|
options: Dict[str, bool],
|
|
) -> List[EvidenceItem]:
|
|
items: List[EvidenceItem] = []
|
|
try:
|
|
rows = self.v15.search(
|
|
query,
|
|
top_k=top_k,
|
|
max_hops=graph_hops,
|
|
include_candidate=include_candidate,
|
|
use_sqlite=False,
|
|
use_qdrant=options["use_qdrant"],
|
|
use_neo4j=options["use_neo4j"],
|
|
use_graph_expansion=options["use_graph_expansion"],
|
|
)
|
|
except Exception:
|
|
rows = []
|
|
for row in rows:
|
|
method = str(row.get("_retrieval_method") or "v16_hybrid")
|
|
items.append(self._from_row(row, "V16/V15", f"{lane}:{method}", query))
|
|
return items
|
|
|
|
@staticmethod
|
|
def _from_row(row: Dict[str, Any], kb: str, method: str, query: str) -> EvidenceItem:
|
|
metadata = row.get("metadata") if isinstance(row.get("metadata"), dict) else {}
|
|
return EvidenceItem(
|
|
evidence_id="",
|
|
kb=kb,
|
|
source=_text(row.get("source") or metadata.get("source")),
|
|
relation=_text(row.get("relation") or metadata.get("relation")),
|
|
target=_text(row.get("target") or metadata.get("target")),
|
|
evidence=_text(row.get("evidence") or row.get("content")),
|
|
paper_id=_text(row.get("paper_id") or row.get("canonical_paper_id") or metadata.get("paper_id")),
|
|
title=_text(row.get("title") or row.get("paper_title") or metadata.get("title")),
|
|
pmid=_text(row.get("pmid") or row.get("pubmed_id") or metadata.get("pmid")),
|
|
pmcid=_text(row.get("pmcid") or metadata.get("pmcid")),
|
|
doi=_text(row.get("doi") or metadata.get("doi")),
|
|
species=_text(row.get("species") or metadata.get("species")),
|
|
section=_text(row.get("section") or metadata.get("section")),
|
|
output_layer=_text(row.get("output_layer") or metadata.get("output_layer")),
|
|
claim_scope=_text(row.get("claim_scope") or metadata.get("claim_scope")),
|
|
relation_id=_text(row.get("relation_id") or metadata.get("relation_id")),
|
|
retrieval_methods=[method],
|
|
retrieval_queries=[query],
|
|
vector_score=float(row.get("_vector_score") or row.get("score") or 0.0),
|
|
raw=dict(row),
|
|
)
|
|
|
|
def close(self) -> None:
|
|
self.v15.close()
|