""" echo/tools/research.py ---------------------- The world-grounding tool. Given a location + era, it returns real-world detail (cost of living, cultural moment, climate, notable events) that the Curator weaves in so an alternate life feels *anchored* rather than hallucinated. This grounding is the moment a judge thinks "wait, how does it know that?" ResearchTool is an interface with: * MockResearch — deterministic offline facts (for GPU-free testing), * WebResearch — wraps a real search call (web_search / an API) at deploy time. """ from __future__ import annotations from abc import ABC, abstractmethod class ResearchTool(ABC): @abstractmethod def ground(self, location: str, year: int, theme: str = "") -> str: """Return a short bundle of grounding facts for location/year.""" ... class MockResearch(ResearchTool): """Offline, deterministic grounding. Enough to exercise the pipeline.""" _NOTES = { "Lisbon": "steep tiled streets, cheap pastéis, a tech-expat wave, the 28 tram", "Tokyo": "rail punctual to the second, conbini at every corner, quiet density", "Berlin": "late winters, techno after-hours, a city still half rebuilt", "São Paulo": "endless gray sprawl, helicopters over traffic, rain that floods", "Reykjavik": "winter dark by 3pm, geothermal pools, wool against the wind", "Montreal": "bilingual signs, brutal Februaries, bagels better than New York", "Nairobi": "matatus in bright livery, high-altitude light, a startup hum", "Hanoi": "scooters like a river, broth at dawn, humidity that never lifts", } def ground(self, location: str, year: int, theme: str = "") -> str: note = self._NOTES.get(location, "a place with its own particular light") return f"{location} (~{year}): {note}." class WebResearch(ResearchTool): """ Deploy-time grounding via a real search function injected at construction. `search_fn(query) -> str` keeps this decoupled from any specific API and lets the Gradio app pass in web_search or a cached corpus. """ def __init__(self, search_fn): self.search_fn = search_fn def ground(self, location: str, year: int, theme: str = "") -> str: q = f"{location} {year} daily life cost of living culture {theme}".strip() try: return str(self.search_fn(q))[:600] except Exception: return f"{location} (~{year})."