lllin000_PaperForge/paperforge/commands/search.py
Research Assistant 2c21926444 feat: add OCR evidence-aware query routing for phase5
- Add evidence_query class to build_query_plan() for intent='evidence'
- Add ocr_evidence_available to enrich_query_plan_with_runtime()
- Add OCR evidence enrichment in search.py and retrieve.py
- Add routing contract tests (3 routing + 1 evidence hit)
- All pre-existing tests pass
2026-06-05 12:22:15 +08:00

116 lines
4.3 KiB
Python

from __future__ import annotations
import argparse
import sys
from paperforge import __version__ as PF_VERSION
from paperforge.core.errors import ErrorCode
from paperforge.core.result import PFError, PFResult
from paperforge.memory.db import get_connection, get_memory_db_path
from paperforge.memory.fts import search_papers
from paperforge.query_planning import build_query_plan, enrich_query_plan_with_runtime
def run(args: argparse.Namespace) -> int:
vault = args.vault_path
query = args.query
db_path = get_memory_db_path(vault)
if not db_path.exists():
result = PFResult(
ok=False,
command="search",
version=PF_VERSION,
error=PFError(
code=ErrorCode.PATH_NOT_FOUND,
message="Memory database not found. Run paperforge memory build.",
),
)
if args.json:
print(result.to_json())
else:
print(f"Error: {result.error.message}", file=sys.stderr)
return 1
conn = get_connection(db_path, read_only=True)
try:
results = search_papers(
conn,
query,
limit=args.limit,
domain=args.domain or "",
year_from=args.year_from or 0,
year_to=args.year_to or 0,
ocr_status=args.ocr or "",
deep_status=args.deep or "",
lifecycle=args.lifecycle or "",
next_step=args.next_step or "",
)
data = {
"query": query,
"matches": results,
"count": len(results),
"filters_applied": {
"domain": args.domain,
"year_from": args.year_from,
"year_to": args.year_to,
"ocr": args.ocr,
"deep": args.deep,
"lifecycle": args.lifecycle,
"next_step": args.next_step,
},
}
warnings: list[str] = []
next_actions: list[dict] = []
if len(results) == 0:
plan = enrich_query_plan_with_runtime(build_query_plan(query, "discover"), vault)
data["query_diagnostic"] = {
"query_class": plan["query_class"],
"recommended_primary": plan["recommended_primary"],
"query_writing_rules": plan["query_writing_rules"],
"scope_assessment": plan.get("scope_assessment"),
}
if plan["query_class"] in {"mixed_query", "author_year"}:
warnings.append("Zero results may reflect a noncanonical metadata query rather than library absence.")
evidence_plan = enrich_query_plan_with_runtime(build_query_plan(query, "evidence"), vault)
data["ocr_evidence_supported"] = evidence_plan.get("runtime", {}).get("ocr_evidence_available", False)
next_actions.append(
{
"command": "paperforge query-plan",
"reason": "Normalize the query and choose the correct first retrieval command.",
}
)
if plan["recommended_primary"]["command"] != "search":
next_actions.append(
{
"command": f"paperforge {plan['recommended_primary']['command']}",
"reason": "The planning layer recommends a different first command for this query.",
}
)
result = PFResult(
ok=True, command="search", version=PF_VERSION, data=data, warnings=warnings, next_actions=next_actions
)
except Exception as exc:
result = PFResult(
ok=False,
command="search",
version=PF_VERSION,
error=PFError(code=ErrorCode.INTERNAL_ERROR, message=str(exc)),
)
finally:
conn.close()
if args.json:
print(result.to_json())
else:
if result.ok:
matches = result.data["matches"]
print(f"Found {len(matches)} results for: {query}")
for m in matches:
rank_val = m.get("rank", "")
print(
f" [{m['lifecycle']:16}] {m['zotero_key']} | {m['year']} | {m['first_author']} | {m['title'][:60]}"
)
else:
print(f"Error: {result.error.message}", file=sys.stderr)
return 0 if result.ok else 1