| 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(): |
| |
| params = _parameters_for_tool("tmdb.movie.discover", "a quiet sci-fi movie", {}) |
| assert params["with_genres"] == "science fiction" |
|
|
| |
| params = _parameters_for_tool("tmdb.tv.discover", "anything", {"genres": ["comedy", "drama"]}) |
| assert params["with_genres"] == "comedy,drama" |
|
|
| |
| 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(): |
| |
| |
| 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" |
|
|
| |
| 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"} |
|
|
| |
| 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, |
| } |
| ] |
| ) |
| |
| |
| 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] |
| |
| 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 |
|
|