plan_helpers.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. """Derived helpers for ResolvedSearchPlan."""
  2. from __future__ import annotations
  3. from typing import TYPE_CHECKING
  4. from .method_acronym import resolve_method_acronym
  5. def _venues(plan: "ResolvedSearchPlan") -> list[str]:
  6. return [str(v).strip() for v in (plan.venues or []) if str(v).strip()]
  7. def _keywords(plan: "ResolvedSearchPlan") -> list[str]:
  8. return [str(k).strip() for k in (plan.keywords or []) if str(k).strip()]
  9. if TYPE_CHECKING:
  10. from .recall_context import RecallContext
  11. from .search_plan import ResolvedSearchPlan
  12. def primary_venue(plan: "ResolvedSearchPlan") -> str | None:
  13. v = _venues(plan)
  14. return v[0] if v else None
  15. def method_acronym_for(plan: "ResolvedSearchPlan", ctx: "RecallContext | None" = None) -> str:
  16. if (plan.method_acronym or "").strip():
  17. return str(plan.method_acronym).strip()
  18. if ctx is not None:
  19. return str(ctx.search_kwargs.get("method_acronym") or "").strip()
  20. return ""
  21. def is_pinned_single_year(plan: "ResolvedSearchPlan") -> bool:
  22. yf, yt = plan.year_from, plan.year_to
  23. return bool(_venues(plan) and isinstance(yf, int) and isinstance(yt, int) and yf == yt)
  24. def is_strict_venue_match(plan: "ResolvedSearchPlan") -> bool:
  25. return is_pinned_single_year(plan) and bool(plan.main_conference_proceedings_only)
  26. def use_venue_proceedings_journal(plan: "ResolvedSearchPlan") -> bool:
  27. return bool(_venues(plan))
  28. def pinned_research_topic(plan: "ResolvedSearchPlan") -> str:
  29. """Search topic after removing pinned venue/year terms."""
  30. from ...core.search.normalize import extract_pinned_topic_terms
  31. venue = primary_venue(plan) or ""
  32. year = plan.year_from if isinstance(plan.year_from, int) else None
  33. return extract_pinned_topic_terms(
  34. query=plan.query or "",
  35. merged_kw=list(plan.keywords or []),
  36. venue=venue,
  37. year=year,
  38. ).strip()
  39. def is_venue_browse_plan(plan: "ResolvedSearchPlan") -> bool:
  40. """True for pure venue+year browsing."""
  41. if not is_pinned_single_year(plan) or not plan.main_conference_proceedings_only or not _venues(plan):
  42. return False
  43. if method_acronym_for(plan) or resolve_method_acronym(plan.query or "", _keywords(plan)):
  44. return False
  45. if plan.target_titles or plan.authors:
  46. return False
  47. return not bool(pinned_research_topic(plan))
  48. def effective_max_results(plan: "ResolvedSearchPlan", requested: int) -> int:
  49. return max(int(requested), 30) if is_venue_browse_plan(plan) else int(requested)
  50. def effective_recall_max_candidates(plan: "ResolvedSearchPlan", current: int) -> int:
  51. cur = int(current or 24)
  52. return max(cur, 24) if is_venue_browse_plan(plan) else cur
  53. def tavily_configured() -> bool:
  54. try:
  55. from ...settings import get_settings
  56. return bool(str(getattr(get_settings(), "tavily_api_key", "") or "").strip())
  57. except Exception:
  58. return False
  59. def should_supplement_from_proceedings_site(plan: "ResolvedSearchPlan") -> bool:
  60. """Venue searches can use proceedings supplement when Tavily is configured."""
  61. return (
  62. bool(_venues(plan))
  63. and tavily_configured()
  64. )
  65. def should_supplement_from_intent_dict(intent: dict[str, Any]) -> bool:
  66. venues = [str(v).strip() for v in (intent.get("venues") or []) if str(v).strip()]
  67. if not venues:
  68. return False
  69. yf, yt = intent.get("year_from"), intent.get("year_to")
  70. try:
  71. pinned = yf is not None and yt is not None and int(yf) == int(yt)
  72. except (TypeError, ValueError):
  73. pinned = False
  74. return pinned and bool(intent.get("main_conference_proceedings_only")) and tavily_configured()