search_image_tool.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. """Wikipedia image search tool for hello_agents framework"""
  2. import json
  3. import logging
  4. import requests
  5. from typing import Any, Dict, List, Optional
  6. from hello_agents.tools.base import Tool, ToolParameter
  7. logger = logging.getLogger("game.tools")
  8. # Wikipedia REST API endpoints (no auth required)
  9. _ZH_SUMMARY_URL = "https://zh.wikipedia.org/api/rest_v1/page/summary/{title}"
  10. _EN_SUMMARY_URL = "https://en.wikipedia.org/api/rest_v1/page/summary/{title}"
  11. # Fake browser User-Agent to avoid 403 from Wikipedia
  12. _HEADERS = {
  13. "User-Agent": (
  14. "Mozilla/5.0 (compatible; GuessWhoAmI/1.0; "
  15. "+https://github.com/ieafei/hello-agents)"
  16. )
  17. }
  18. class SearchImageTool(Tool):
  19. """Wikipedia image search tool - fetch figure portrait from Wikipedia page summary."""
  20. def __init__(self):
  21. super().__init__(
  22. name="wikipedia_image_search",
  23. description=(
  24. "Search Wikipedia for a portrait image of a historical or fictional figure. "
  25. "Returns a list of image URLs from the Wikipedia page thumbnail."
  26. )
  27. )
  28. logger.info("[TOOL] SearchImageTool (Wikipedia) initialized")
  29. # ── Internal helpers ──────────────────────────────────────────────────────
  30. def _fetch_summary(self, title: str, lang: str = "zh") -> Optional[Dict]:
  31. """Fetch Wikipedia page summary (includes thumbnail) by exact title."""
  32. url_tpl = _ZH_SUMMARY_URL if lang == "zh" else _EN_SUMMARY_URL
  33. try:
  34. resp = requests.get(
  35. url_tpl.format(title=requests.utils.quote(title, safe="")),
  36. headers=_HEADERS,
  37. timeout=8,
  38. )
  39. resp.raise_for_status()
  40. return resp.json()
  41. except Exception as e:
  42. logger.warning(f"[TOOL] Wikipedia summary ({lang}) failed for {title!r}: {e}")
  43. return None
  44. def _get_photo_from_summary(self, summary: Dict, query: str) -> Optional[Dict[str, str]]:
  45. """Extract photo dict from a Wikipedia summary response."""
  46. thumbnail = summary.get("thumbnail")
  47. if not thumbnail:
  48. return None
  49. original = summary.get("originalimage", {})
  50. return {
  51. "url": original.get("source") or thumbnail.get("source", ""),
  52. "thumb": thumbnail.get("source", ""),
  53. "description": summary.get("title", query),
  54. "photographer": "Wikipedia",
  55. }
  56. def _lookup(self, query: str) -> List[Dict[str, str]]:
  57. """
  58. Directly call REST Summary API with the figure name (zh first, then en).
  59. Skips the w/api.php search step which is often blocked (403).
  60. Returns a list with at most 1 photo dict.
  61. """
  62. for lang in ("zh", "en"):
  63. summary = self._fetch_summary(query, lang)
  64. if not summary:
  65. continue
  66. photo = self._get_photo_from_summary(summary, query)
  67. if photo:
  68. logger.info(
  69. f"[TOOL] Wikipedia image found | lang={lang} title={query!r} url={photo['url']!r}"
  70. )
  71. return [photo]
  72. logger.warning(f"[TOOL] No Wikipedia image found for query={query!r}")
  73. return []
  74. # ── Tool interface ────────────────────────────────────────────────────────
  75. def run(self, parameters: Dict[str, Any]) -> str:
  76. """
  77. Search Wikipedia for images matching the query.
  78. Args:
  79. parameters: dict with key 'query' - the search keyword (e.g. figure name)
  80. Returns:
  81. JSON string with image list, or error message
  82. """
  83. query = parameters.get("query", "").strip()
  84. if not query:
  85. return "Error: search query cannot be empty"
  86. logger.info(f"[TOOL] Wikipedia image search | query={query!r}")
  87. photos = self._lookup(query)
  88. return json.dumps(photos, ensure_ascii=False)
  89. def search_photos(self, query: str, per_page: int = 3) -> List[Dict[str, str]]:
  90. """
  91. Convenience method: search and return parsed photo list directly.
  92. Args:
  93. query: search keyword (figure name)
  94. per_page: ignored (Wikipedia returns at most 1 portrait per page)
  95. Returns:
  96. List of photo dicts with url/thumb/description/photographer
  97. """
  98. raw = self.run({"query": query})
  99. try:
  100. return json.loads(raw) if raw.startswith("[") else []
  101. except Exception:
  102. return []
  103. def get_first_photo_url(self, query: str) -> Optional[str]:
  104. """Return the URL of the first matching photo, or None."""
  105. photos = self.search_photos(query)
  106. return photos[0]["url"] if photos else None
  107. def get_parameters(self) -> List[ToolParameter]:
  108. return [
  109. ToolParameter(
  110. name="query",
  111. type="string",
  112. description="Search keyword, e.g. the name of a historical figure",
  113. required=True,
  114. ),
  115. ]