add place link metadata for suggestions

This commit is contained in:
alex wiesner
2026-03-14 11:57:14 +00:00
parent 8f12aee5e1
commit 1b004b9e65
3 changed files with 273 additions and 21 deletions

View File

@@ -228,6 +228,47 @@ def _parse_address(tags):
return ", ".join([p for p in parts if p])
def _extract_official_website(tags):
if not isinstance(tags, dict):
return None
website_fields = [
"website",
"contact:website",
"official_website",
"url",
]
for field in website_fields:
value = tags.get(field)
if isinstance(value, str):
normalized = value.strip()
if normalized:
return normalized
return None
def _build_osm_map_url(item):
if not isinstance(item, dict):
return None
osm_type = item.get("type")
osm_id = item.get("id")
if osm_type in {"node", "way", "relation"} and osm_id is not None:
return f"https://www.openstreetmap.org/{osm_type}/{osm_id}"
latitude = item.get("lat")
longitude = item.get("lon")
if latitude is None or longitude is None:
center = item.get("center") or {}
latitude = center.get("lat")
longitude = center.get("lon")
if latitude is None or longitude is None:
return None
return f"https://www.openstreetmap.org/?mlat={latitude}&mlon={longitude}"
@agent_tool(
name="search_places",
description=(
@@ -309,6 +350,10 @@ def search_places(
if latitude is None or longitude is None:
continue
official_website = _extract_official_website(tags)
map_url = _build_osm_map_url(item)
preferred_link = official_website or map_url
results.append(
{
"name": name,
@@ -316,6 +361,9 @@ def search_places(
"latitude": latitude,
"longitude": longitude,
"category": category,
"link": preferred_link,
"official_website": official_website,
"map_url": map_url,
}
)

View File

@@ -1521,6 +1521,91 @@ class SearchPlaces429NonRetryableTests(TestCase):
)
class SearchPlacesLinkMetadataTests(TestCase):
@patch("chat.agent_tools.requests.post")
@patch("chat.agent_tools.requests.get")
def test_search_places_prefers_official_website_over_map_fallback(
self,
mock_get,
mock_post,
):
mock_get_response = MagicMock()
mock_get_response.json.return_value = [{"lat": "48.85837", "lon": "2.294481"}]
mock_get_response.raise_for_status.return_value = None
mock_get.return_value = mock_get_response
mock_post_response = MagicMock()
mock_post_response.json.return_value = {
"elements": [
{
"type": "node",
"id": 12345,
"lat": 48.85837,
"lon": 2.294481,
"tags": {
"name": "Eiffel Tower",
"website": "https://www.toureiffel.paris/en",
"contact:website": "https://fallback.example.com",
},
}
]
}
mock_post_response.raise_for_status.return_value = None
mock_post.return_value = mock_post_response
result = search_places(user=None, location="Paris, France", category="tourism")
self.assertIn("results", result)
self.assertEqual(len(result["results"]), 1)
place = result["results"][0]
self.assertEqual(place.get("link"), "https://www.toureiffel.paris/en")
self.assertEqual(
place.get("official_website"),
"https://www.toureiffel.paris/en",
)
self.assertTrue(place.get("map_url", "").startswith("https://"))
@patch("chat.agent_tools.requests.post")
@patch("chat.agent_tools.requests.get")
def test_search_places_uses_map_fallback_when_no_official_website(
self,
mock_get,
mock_post,
):
mock_get_response = MagicMock()
mock_get_response.json.return_value = [{"lat": "41.9028", "lon": "12.4964"}]
mock_get_response.raise_for_status.return_value = None
mock_get.return_value = mock_get_response
mock_post_response = MagicMock()
mock_post_response.json.return_value = {
"elements": [
{
"type": "way",
"id": 98765,
"center": {"lat": 41.8902, "lon": 12.4922},
"tags": {
"name": "Colosseum",
},
}
]
}
mock_post_response.raise_for_status.return_value = None
mock_post.return_value = mock_post_response
result = search_places(user=None, location="Rome, Italy", category="tourism")
self.assertIn("results", result)
self.assertEqual(len(result["results"]), 1)
place = result["results"][0]
self.assertIsNone(place.get("official_website"))
self.assertEqual(place.get("link"), place.get("map_url"))
self.assertEqual(
place.get("map_url"),
"https://www.openstreetmap.org/way/98765",
)
class GetWeatherCoordFallbackTests(APITransactionTestCase):
"""get_weather lat/lng required param should be retried with collection location coords."""
@@ -2015,3 +2100,116 @@ class DaySuggestionsCoordinateEnrichmentTests(TestCase):
self.assertEqual(len(enriched), 1)
self.assertNotIn("latitude", enriched[0])
self.assertNotIn("longitude", enriched[0])
def test_preserves_existing_coordinates_and_link_when_present(self):
suggestions = [
{
"name": "Known Place",
"location": "Somewhere",
"latitude": 10.5,
"longitude": 20.5,
"link": "https://existing.example.com",
}
]
place_candidates = [
{
"name": "Known Place",
"address": "Somewhere",
"latitude": 1.0,
"longitude": 2.0,
"link": "https://candidate.example.com",
}
]
enriched = self.view._enrich_suggestions_with_coordinates(
suggestions,
place_candidates,
)
self.assertEqual(enriched[0]["latitude"], 10.5)
self.assertEqual(enriched[0]["longitude"], 20.5)
self.assertEqual(enriched[0]["link"], "https://existing.example.com")
def test_fills_missing_coordinates_and_link_from_place_match(self):
suggestions = [
{
"name": "Roscioli",
"location": "Via dei Giubbonari, Rome",
}
]
place_candidates = [
{
"name": "Roscioli",
"address": "Via dei Giubbonari, Rome",
"latitude": 41.8933,
"longitude": 12.4722,
"link": "https://www.roscioli.com",
}
]
enriched = self.view._enrich_suggestions_with_coordinates(
suggestions,
place_candidates,
)
self.assertEqual(len(enriched), 1)
self.assertEqual(enriched[0]["latitude"], 41.8933)
self.assertEqual(enriched[0]["longitude"], 12.4722)
self.assertEqual(enriched[0]["link"], "https://www.roscioli.com")
def test_preserves_existing_link_while_filling_missing_coordinates(self):
suggestions = [
{
"name": "Borough Market",
"location": "Southwark",
"link": "https://existing.example.com/borough",
}
]
place_candidates = [
{
"name": "Borough Market",
"address": "8 Southwark St, London",
"latitude": 51.5055,
"longitude": -0.0904,
"link": "https://candidate.example.com/borough",
}
]
enriched = self.view._enrich_suggestions_with_coordinates(
suggestions,
place_candidates,
)
self.assertEqual(len(enriched), 1)
self.assertEqual(enriched[0]["latitude"], 51.5055)
self.assertEqual(enriched[0]["longitude"], -0.0904)
self.assertEqual(enriched[0]["link"], "https://existing.example.com/borough")
def test_preserves_existing_coordinates_while_filling_missing_link(self):
suggestions = [
{
"name": "City Museum",
"location": "Old Town",
"latitude": 40.7128,
"longitude": -74.006,
}
]
place_candidates = [
{
"name": "City Museum",
"address": "Old Town",
"latitude": 40.7128,
"longitude": -74.006,
"link": "https://citymuseum.example.com",
}
]
enriched = self.view._enrich_suggestions_with_coordinates(
suggestions,
place_candidates,
)
self.assertEqual(len(enriched), 1)
self.assertEqual(enriched[0]["latitude"], 40.7128)
self.assertEqual(enriched[0]["longitude"], -74.006)
self.assertEqual(enriched[0]["link"], "https://citymuseum.example.com")

View File

@@ -370,28 +370,34 @@ class DaySuggestionsView(APIView):
if not isinstance(suggestion, dict):
continue
if (
suggestion.get("latitude") is not None
and suggestion.get("longitude") is not None
):
enriched.append(suggestion)
continue
matched_place = self._best_place_match(suggestion, place_candidates)
if not matched_place:
enriched.append(suggestion)
continue
if (
matched_place.get("latitude") is None
or matched_place.get("longitude") is None
):
enriched.append(suggestion)
continue
merged = dict(suggestion)
merged["latitude"] = matched_place.get("latitude")
merged["longitude"] = matched_place.get("longitude")
has_coordinates = (
merged.get("latitude") is not None
and merged.get("longitude") is not None
)
has_link = bool(merged.get("link"))
if has_coordinates and has_link:
enriched.append(merged)
continue
matched_place = self._best_place_match(merged, place_candidates)
if not matched_place:
enriched.append(merged)
continue
if not has_coordinates and (
matched_place.get("latitude") is not None
and matched_place.get("longitude") is not None
):
merged["latitude"] = matched_place.get("latitude")
merged["longitude"] = matched_place.get("longitude")
if not has_link:
matched_link = matched_place.get("link")
if matched_link:
merged["link"] = matched_link
merged["location"] = merged.get("location") or matched_place.get("address")
enriched.append(merged)