search.py 2.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. """Search dispatch helpers leveraging HelloAgents SearchTool."""
  2. from __future__ import annotations
  3. import logging
  4. from typing import Any, Optional, Tuple
  5. from hello_agents.tools import SearchTool
  6. from config import Configuration
  7. from utils import (
  8. deduplicate_and_format_sources,
  9. format_sources,
  10. get_config_value,
  11. )
  12. logger = logging.getLogger(__name__)
  13. MAX_TOKENS_PER_SOURCE = 2000
  14. _GLOBAL_SEARCH_TOOL = SearchTool(backend="hybrid")
  15. def dispatch_search(
  16. query: str,
  17. config: Configuration,
  18. loop_count: int,
  19. ) -> Tuple[dict[str, Any] | None, list[str], Optional[str], str]:
  20. """Execute configured search backend and normalise response payload."""
  21. search_api = get_config_value(config.search_api)
  22. try:
  23. raw_response = _GLOBAL_SEARCH_TOOL.run(
  24. {
  25. "input": query,
  26. "backend": search_api,
  27. "mode": "structured",
  28. "fetch_full_page": config.fetch_full_page,
  29. "max_results": 5,
  30. "max_tokens_per_source": MAX_TOKENS_PER_SOURCE,
  31. "loop_count": loop_count,
  32. }
  33. )
  34. except Exception as exc: # pragma: no cover - defensive logging
  35. logger.exception("Search backend %s failed: %s", search_api, exc)
  36. raise
  37. if isinstance(raw_response, str):
  38. notices = [raw_response]
  39. logger.warning("Search backend %s returned text notice: %s", search_api, raw_response)
  40. payload: dict[str, Any] = {
  41. "results": [],
  42. "backend": search_api,
  43. "answer": None,
  44. "notices": notices,
  45. }
  46. else:
  47. payload = raw_response
  48. notices = list(payload.get("notices") or [])
  49. backend_label = str(payload.get("backend") or search_api)
  50. answer_text = payload.get("answer")
  51. results = payload.get("results", [])
  52. if notices:
  53. for notice in notices:
  54. logger.info("Search notice (%s): %s", backend_label, notice)
  55. logger.info(
  56. "Search backend=%s resolved_backend=%s answer=%s results=%s",
  57. search_api,
  58. backend_label,
  59. bool(answer_text),
  60. len(results),
  61. )
  62. return payload, notices, answer_text, backend_label
  63. def prepare_research_context(
  64. search_result: dict[str, Any] | None,
  65. answer_text: Optional[str],
  66. config: Configuration,
  67. ) -> tuple[str, str]:
  68. """Build structured context and source summary for downstream agents."""
  69. sources_summary = format_sources(search_result)
  70. context = deduplicate_and_format_sources(
  71. search_result or {"results": []},
  72. max_tokens_per_source=MAX_TOKENS_PER_SOURCE,
  73. fetch_full_page=config.fetch_full_page,
  74. )
  75. if answer_text:
  76. context = f"AI直接答案:\n{answer_text}\n\n{context}"
  77. return sources_summary, context