search_plan.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. """Resolved search plan + FallbackPolicy — single source of truth between intent and pipeline."""
  2. from __future__ import annotations
  3. from dataclasses import dataclass, field
  4. from .search_recipe import SearchRecipe, finalize_plan_recipe
  5. @dataclass
  6. class FallbackPolicy:
  7. allow_arxiv_only: bool = True
  8. reason: str = "auto"
  9. @dataclass
  10. class ResolvedSearchPlan:
  11. """All search parameters resolved once, used by pipeline. No LLM calls in retrieval layer."""
  12. query: str = ""
  13. keywords: list[str] = field(default_factory=list)
  14. authors: list[str] = field(default_factory=list)
  15. venues: list[str] = field(default_factory=list)
  16. year_from: int | None = None
  17. year_to: int | None = None
  18. sources: list[str] = field(default_factory=list)
  19. sort: str = "relevance"
  20. ranking_profile: str = "accuracy"
  21. use_llm_rank: bool = True
  22. recall_max_candidates: int = 24
  23. target_titles: list[str] = field(default_factory=list)
  24. arxiv_id_list: list[str] = field(default_factory=list)
  25. main_conference_proceedings_only: bool = False
  26. raw_user_message: str = ""
  27. wants_recent: bool = False
  28. wants_classic: bool = False
  29. fallback: FallbackPolicy = field(default_factory=FallbackPolicy)
  30. use_tavily: bool = False
  31. max_results: int = 10
  32. recipe: SearchRecipe = SearchRecipe.GENERAL
  33. method_acronym: str | None = None
  34. @classmethod
  35. def from_search_intent(cls, intent) -> "ResolvedSearchPlan":
  36. plan = cls(
  37. query=(intent.query or "").strip()[:500],
  38. keywords=list(intent.keywords or [])[:16],
  39. authors=list(getattr(intent, "authors", []) or [])[:8],
  40. venues=list(intent.venues or []),
  41. year_from=_norm_year(intent.year_from),
  42. year_to=_norm_year(intent.year_to),
  43. sources=_resolve_sources(intent),
  44. sort=_resolve_sort(intent),
  45. ranking_profile=_resolve_profile(intent),
  46. use_llm_rank=bool(getattr(intent, "use_llm_rank", True)),
  47. recall_max_candidates=_resolve_recall_max(intent),
  48. target_titles=list(getattr(intent, "target_titles", []) or [])[:6],
  49. arxiv_id_list=list(getattr(intent, "arxiv_id_list", []) or [])[:16],
  50. main_conference_proceedings_only=bool(getattr(intent, "main_conference_proceedings_only", False)),
  51. raw_user_message=(getattr(intent, "raw_user_message", "") or "")[:3200],
  52. wants_recent=bool(getattr(intent, "wants_recent", False)),
  53. wants_classic=bool(getattr(intent, "wants_classic", False)),
  54. use_tavily=_resolve_use_tavily(intent),
  55. max_results=max(5, min(30, int(getattr(intent, "max_results", 10) or 10))),
  56. )
  57. return _finalize_plan_for_retrieval(plan)
  58. def _finalize_plan_for_retrieval(plan: ResolvedSearchPlan) -> ResolvedSearchPlan:
  59. """RECIPE_RULES 判定并应用策略;派生状态见 plan_helpers。"""
  60. return finalize_plan_recipe(plan)
  61. def _resolve_sort(intent) -> str:
  62. rk = getattr(intent, "ranking_strategy", None)
  63. if rk == "date":
  64. return "date"
  65. if rk == "relevance":
  66. return "relevance"
  67. if getattr(intent, "wants_recent", False):
  68. return "date"
  69. if getattr(intent, "wants_classic", False):
  70. return "relevance"
  71. return str(getattr(intent, "sort", "relevance") or "relevance")
  72. def _resolve_profile(intent) -> str:
  73. if getattr(intent, "wants_classic", False):
  74. return "classic"
  75. if getattr(intent, "wants_recent", False):
  76. return "novelty"
  77. return "accuracy"
  78. def _resolve_sources(intent) -> list[str]:
  79. llm_src = getattr(intent, "sources", []) or []
  80. allowed = {"arxiv", "dblp", "openalex"}
  81. if llm_src:
  82. resolved = [s for s in llm_src if s in allowed]
  83. if resolved:
  84. return resolved
  85. return ["arxiv", "dblp", "openalex"]
  86. def _resolve_use_tavily(intent) -> bool:
  87. llm_src = [str(s).strip().lower() for s in (getattr(intent, "sources", []) or [])]
  88. return "tavily" in llm_src or bool(getattr(intent, "use_tavily_presearch", False))
  89. def _resolve_recall_max(intent) -> int:
  90. try:
  91. from ...settings import get_settings
  92. cap = int(get_settings().papergraph_recall_max_candidates)
  93. except Exception:
  94. cap = 24
  95. raw = int(getattr(intent, "rerank_recall_max", 24) or 24)
  96. return max(8, min(cap, raw))
  97. def _norm_year(y) -> int | None:
  98. if y is None:
  99. return None
  100. try:
  101. yi = int(y)
  102. return yi if 1900 <= yi <= 2100 else None
  103. except (TypeError, ValueError):
  104. return None