MetaRec / MetaRec-backend /tests /test_generic_graph.py
Hanny658's picture
dev: harden hotel recommendation sources
a1b14c0
Raw
History Blame Contribute Delete
9.77 kB
import pytest
from langgraph_metarec.graphs.generic_graph import (
GenericGraphAdapters,
_parameters_for_tool,
_relaxation_actions,
normalize_tool_items,
run_generic_domain_graph,
)
from langgraph_metarec.tool_registry import ToolRegistry, ToolSpec
@pytest.mark.backend_unit
def test_parameters_for_tool_derives_discover_genres():
# Genres inferred from the query when no explicit preference is set.
params = _parameters_for_tool("tmdb.movie.discover", "a quiet sci-fi movie", {})
assert params["with_genres"] == "science fiction"
# Explicit preference genres take priority over inference.
params = _parameters_for_tool("tmdb.tv.discover", "anything", {"genres": ["comedy", "drama"]})
assert params["with_genres"] == "comedy,drama"
# Search tools just carry the query through.
assert _parameters_for_tool("tmdb.movie.search", "jaws", {}) == {"max_results": 10, "query": "jaws"}
assert _parameters_for_tool("amazon.product.search", "laptop under 2000 SGD", {"use_case": "work"}) == {
"max_results": 10,
"query": "laptop under 2000 SGD work",
}
assert _parameters_for_tool(
"amazon.product.search",
"recommend an iOS testing phone",
{"product": "iPhone", "model": "iPhone 14-16", "budget": "<= 1600 SGD", "use_case": "iOS testing"},
) == {
"max_results": 10,
"query": "recommend an iOS testing phone iPhone iPhone 14-16 <= 1600 SGD",
}
@pytest.mark.backend_unit
def test_parameters_for_tool_composes_hotel_search_query():
# The stay filters (stars, amenities, budget, destination) enrich the text
# query the same way the product search does.
params = _parameters_for_tool(
"gmap.hotel.search",
"Find me a place to stay",
{"location": "Sentosa", "stars": "4", "amenities": "pool, free wifi", "budget": "< 200 SGD"},
)
assert params["query"] == "Find me a place to stay hotels 4-star pool free wifi < 200 SGD in Sentosa"
# Tokens already present in the query are not duplicated; "any" is ignored.
params = _parameters_for_tool("gmap.hotel.search", "4-star hotels in Kyoto", {"location": "Kyoto", "stars": "any"})
assert params["query"] == "4-star hotels in Kyoto"
@pytest.mark.backend_unit
def test_parameters_for_tool_hotel_discover_needs_destination():
params = _parameters_for_tool("osm.hotel.discover", "somewhere nice", {"location": "Chinatown", "stars": "4"})
assert params == {"max_results": 10, "location": "Chinatown", "stars": "4"}
# No usable destination -> no structured filter contributed ("any" is noise).
assert _parameters_for_tool("osm.hotel.discover", "somewhere nice", {"location": "any"}) == {"max_results": 10}
assert _parameters_for_tool("osm.hotel.discover", "somewhere nice", {}) == {"max_results": 10}
@pytest.mark.backend_unit
def test_normalize_tool_items_maps_hotel_tools():
gmap_items = normalize_tool_items(
"gmap.hotel.search",
[
{
"title": "Grand Palace Hotel",
"address": "1 Beach Rd",
"rating": 4.5,
"reviews": 980,
"type": "Hotel",
"price": "$$$",
"place_id": "place-123",
"link": "https://maps.google.com/?cid=123",
"thumbnail": "https://img.example/hotel.jpg",
"gps_coordinates": {"latitude": 1.29, "longitude": 103.85},
}
],
"hotel",
)
assert gmap_items[0]["id"] == "place-123"
assert gmap_items[0]["title"] == "Grand Palace Hotel"
assert gmap_items[0]["subtitle"] == "1 Beach Rd"
assert gmap_items[0]["url"] == "https://maps.google.com/?cid=123"
assert gmap_items[0]["image_url"] == "https://img.example/hotel.jpg"
assert gmap_items[0]["rating"] == 4.5
assert gmap_items[0]["reviews_count"] == 980
assert gmap_items[0]["source"] == "Google Maps"
assert gmap_items[0]["tags"] == ["Hotel", "$$$"]
osm_items = normalize_tool_items(
"osm.hotel.discover",
[
{
"title": "Riverside Guest House",
"tourism": "guest_house",
"stars": 3.0,
"address": "12 River St",
"website": "https://riverside.example",
"link": "https://www.openstreetmap.org/node/42",
"searched_location": "Clarke Quay",
}
],
"hotel",
)
assert osm_items[0]["title"] == "Riverside Guest House"
assert osm_items[0]["subtitle"] == "12 River St"
assert osm_items[0]["url"] == "https://riverside.example"
assert osm_items[0]["source"] == "OpenStreetMap"
assert osm_items[0]["tags"] == ["guest_house", "3-star"]
@pytest.mark.backend_unit
def test_relaxation_ladder_drops_hotel_stars_but_keeps_destination():
actions = _relaxation_actions(
[
{
"tool": "osm.hotel.discover",
"parameters": {"max_results": 10, "location": "Chinatown", "stars": "5"},
"count": 0,
}
]
)
# Stars is droppable; the destination is the keep-last filter, so exactly
# one relaxation step exists and it still carries the location.
assert actions == [{"tool": "osm.hotel.discover", "parameters": {"max_results": 10, "location": "Chinatown"}}]
@pytest.mark.backend_unit
@pytest.mark.asyncio
async def test_generic_graph_runs_hotel_domain_end_to_end():
registry = ToolRegistry()
registry.register(
ToolSpec(
name="gmap.hotel.search",
domain="hotel",
tags={"#place", "#hotel"},
input_schema={"type": "object"},
output_schema={"type": "array"},
adapter=lambda params: [
{"title": "Palm View Hotel", "address": "7 Palm Ave", "rating": 4.6, "reviews": 512, "type": "Hotel"}
],
)
)
registry.register(
ToolSpec(
name="osm.hotel.discover",
domain="hotel",
tags={"#place", "#hotel"},
input_schema={"type": "object"},
output_schema={"type": "array"},
adapter=lambda params: [
{"title": "Palm Hostel", "tourism": "hostel", "address": "9 Palm Ave", "searched_location": params.get("location")}
],
)
)
result = await run_generic_domain_graph(
query="a hotel near Palm Avenue",
domain="hotel",
preferences={"location": "Palm Avenue"},
tool_tags=["#place", "#hotel"],
adapters=GenericGraphAdapters(tool_registry=registry),
)
assert result.metadata["domain"] == "hotel"
assert result.metadata["selected_tools"] == ["gmap.hotel.search", "osm.hotel.discover"]
titles = [item["title"] for item in result.items]
# Rated gmap candidates rank above unrated OSM ones.
assert titles[0] == "Palm View Hotel"
assert "Palm Hostel" in titles
assert all(item["domain"] == "hotel" for item in result.items)
@pytest.mark.backend_unit
@pytest.mark.asyncio
async def test_generic_graph_dispatches_active_scoped_tools_and_normalizes_items():
registry = ToolRegistry()
registry.register(
ToolSpec(
name="amazon.product.search",
domain="product",
tags={"#thing", "#product", "#shopping"},
input_schema={"type": "object"},
output_schema={"type": "array"},
adapter=lambda params: [
{
"product_id": "asin-1",
"title": f"{params['query']} headphones",
"brand": "Acme",
"rating": 4.7,
"reviews": 1200,
"thumbnail": "https://example.test/headphones.jpg",
"link": "https://example.test/headphones",
"tags": ["audio"],
}
],
)
)
result = await run_generic_domain_graph(
query="noise cancelling",
domain="product",
tool_tags=["#thing", "#product"],
adapters=GenericGraphAdapters(tool_registry=registry),
)
assert result.metadata["domain"] == "product"
assert result.items[0]["id"] == "asin-1"
assert result.items[0]["title"] == "noise cancelling headphones"
assert result.items[0]["subtitle"] == "Acme"
assert result.items[0]["rating"] == 4.7
assert result.items[0]["reviews_count"] == 1200
assert result.items[0]["source"] == "Amazon"
assert result.metadata["selected_tools"] == ["amazon.product.search"]
assert result.metadata["skipped_tools"] == []
assert result.metadata["items_count"] == 1
@pytest.mark.backend_unit
@pytest.mark.asyncio
async def test_generic_graph_reports_inactive_scoped_tools_without_dispatching():
registry = ToolRegistry()
registry.register(
ToolSpec(
name="tmdb.movie.search",
domain="movie",
tags={"#thing", "#movie"},
input_schema={"type": "object"},
output_schema={"type": "array"},
adapter=lambda params: pytest.fail("inactive adapter should not be dispatched"),
status="missing_credentials:TMDB_API_ACCESS_TOKEN",
)
)
result = await run_generic_domain_graph(
query="quiet sci-fi movie",
domain="movie",
tool_tags=["#thing", "#movie"],
adapters=GenericGraphAdapters(tool_registry=registry),
)
assert result.items == []
assert result.metadata["selected_tools"] == []
assert result.metadata["skipped_tools"] == [
{"name": "tmdb.movie.search", "status": "missing_credentials:TMDB_API_ACCESS_TOKEN"}
]
assert result.metadata["items_count"] == 0