import os import tempfile from unittest.mock import mock_open, patch import pytest from semantic_router.encoders import BaseEncoder, CohereEncoder, OpenAIEncoder from semantic_router.layer import LayerConfig, RouteLayer from semantic_router.route import Route def mock_encoder_call(utterances): # Define a mapping of utterances to return values mock_responses = { "Hello": [0.1, 0.2, 0.3], "Hi": [0.4, 0.5, 0.6], "Goodbye": [0.7, 0.8, 0.9], "Bye": [1.0, 1.1, 1.2], "Au revoir": [1.3, 1.4, 1.5], } return [mock_responses.get(u, [0, 0, 0]) for u in utterances] def layer_json(): return """{ "encoder_type": "cohere", "encoder_name": "embed-english-v3.0", "routes": [ { "name": "politics", "utterances": [ "isn't politics the best thing ever", "why don't you tell me about your political opinions" ], "description": null, "function_schema": null }, { "name": "chitchat", "utterances": [ "how's the weather today?", "how are things going?" ], "description": null, "function_schema": null } ] }""" def layer_yaml(): return """encoder_name: embed-english-v3.0 encoder_type: cohere routes: - description: null function_schema: null name: politics utterances: - isn't politics the best thing ever - why don't you tell me about your political opinions - description: null function_schema: null name: chitchat utterances: - how's the weather today? - how are things going? """ @pytest.fixture def base_encoder(): return BaseEncoder(name="test-encoder", score_threshold=0.5) @pytest.fixture def cohere_encoder(mocker): mocker.patch.object(CohereEncoder, "__call__", side_effect=mock_encoder_call) return CohereEncoder(name="test-cohere-encoder", cohere_api_key="test_api_key") @pytest.fixture def openai_encoder(mocker): mocker.patch.object(OpenAIEncoder, "__call__", side_effect=mock_encoder_call) return OpenAIEncoder(name="test-openai-encoder", openai_api_key="test_api_key") @pytest.fixture def routes(): return [ Route(name="Route 1", utterances=["Hello", "Hi"]), Route(name="Route 2", utterances=["Goodbye", "Bye", "Au revoir"]), ] @pytest.fixture def dynamic_routes(): return [ Route(name="Route 1", utterances=["Hello", "Hi"], function_schema="test"), Route(name="Route 2", utterances=["Goodbye", "Bye", "Au revoir"]), ] class TestRouteLayer: def test_initialization(self, openai_encoder, routes): route_layer = RouteLayer(encoder=openai_encoder, routes=routes) assert openai_encoder.score_threshold == 0.82 assert route_layer.score_threshold == 0.82 assert len(route_layer.index) if route_layer.index is not None else 0 == 5 assert ( len(set(route_layer.categories)) if route_layer.categories is not None else 0 == 2 ) def test_initialization_different_encoders(self, cohere_encoder, openai_encoder): route_layer_cohere = RouteLayer(encoder=cohere_encoder) assert cohere_encoder.score_threshold == 0.3 assert route_layer_cohere.score_threshold == 0.3 route_layer_openai = RouteLayer(encoder=openai_encoder) assert route_layer_openai.score_threshold == 0.82 def test_initialization_no_encoder(self, openai_encoder): with patch("semantic_router.encoders.OpenAIEncoder") as _: route_layer_none = RouteLayer(encoder=None) assert route_layer_none.score_threshold == openai_encoder.score_threshold def test_initialization_dynamic_route(self, cohere_encoder, openai_encoder): route_layer_cohere = RouteLayer(encoder=cohere_encoder) assert route_layer_cohere.score_threshold == 0.3 route_layer_openai = RouteLayer(encoder=openai_encoder) assert openai_encoder.score_threshold == 0.82 assert route_layer_openai.score_threshold == 0.82 def test_add_route(self, openai_encoder): route_layer = RouteLayer(encoder=openai_encoder) route1 = Route(name="Route 1", utterances=["Yes", "No"]) route2 = Route(name="Route 2", utterances=["Maybe", "Sure"]) route_layer.add(route=route1) assert route_layer.index is not None and route_layer.categories is not None assert route_layer.index.shape[0] == 2 assert len(set(route_layer.categories)) == 1 assert set(route_layer.categories) == {"Route 1"} route_layer.add(route=route2) assert route_layer.index.shape[0] == 4 assert len(set(route_layer.categories)) == 2 assert set(route_layer.categories) == {"Route 1", "Route 2"} del route_layer def test_add_multiple_routes(self, openai_encoder, routes): route_layer = RouteLayer(encoder=openai_encoder) route_layer._add_routes(routes=routes) assert route_layer.index is not None and route_layer.categories is not None assert route_layer.index.shape[0] == 5 assert len(set(route_layer.categories)) == 2 def test_query_and_classification(self, openai_encoder, routes): route_layer = RouteLayer(encoder=openai_encoder, routes=routes) query_result = route_layer("Hello").name assert query_result in ["Route 1", "Route 2"] def test_query_with_no_index(self, openai_encoder): route_layer = RouteLayer(encoder=openai_encoder) assert route_layer("Anything").name is None def test_semantic_classify(self, openai_encoder, routes): route_layer = RouteLayer(encoder=openai_encoder, routes=routes) classification, score = route_layer._semantic_classify( [ {"route": "Route 1", "score": 0.9}, {"route": "Route 2", "score": 0.1}, ] ) assert classification == "Route 1" assert score == [0.9] def test_semantic_classify_multiple_routes(self, openai_encoder, routes): route_layer = RouteLayer(encoder=openai_encoder, routes=routes) classification, score = route_layer._semantic_classify( [ {"route": "Route 1", "score": 0.9}, {"route": "Route 2", "score": 0.1}, {"route": "Route 1", "score": 0.8}, ] ) assert classification == "Route 1" assert score == [0.9, 0.8] def test_pass_threshold(self, openai_encoder): route_layer = RouteLayer(encoder=openai_encoder) assert not route_layer._pass_threshold([], 0.5) assert route_layer._pass_threshold([0.6, 0.7], 0.5) def test_failover_score_threshold(self, base_encoder): route_layer = RouteLayer(encoder=base_encoder) assert route_layer.score_threshold == 0.5 def test_json(self, openai_encoder, routes): with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as temp: os.environ["OPENAI_API_KEY"] = "test_api_key" route_layer = RouteLayer(encoder=openai_encoder, routes=routes) route_layer.to_json(temp.name) assert os.path.exists(temp.name) route_layer_from_file = RouteLayer.from_json(temp.name) assert ( route_layer_from_file.index is not None and route_layer_from_file.categories is not None ) os.remove(temp.name) def test_yaml(self, openai_encoder, routes): with tempfile.NamedTemporaryFile(suffix=".yaml", delete=False) as temp: os.environ["OPENAI_API_KEY"] = "test_api_key" route_layer = RouteLayer(encoder=openai_encoder, routes=routes) route_layer.to_yaml(temp.name) assert os.path.exists(temp.name) route_layer_from_file = RouteLayer.from_yaml(temp.name) assert ( route_layer_from_file.index is not None and route_layer_from_file.categories is not None ) os.remove(temp.name) def test_config(self, openai_encoder, routes): os.environ["OPENAI_API_KEY"] = "test_api_key" route_layer = RouteLayer(encoder=openai_encoder, routes=routes) # confirm route creation functions as expected layer_config = route_layer.to_config() assert layer_config.routes == routes # now load from config and confirm it's the same route_layer_from_config = RouteLayer.from_config(layer_config) assert (route_layer_from_config.index == route_layer.index).all() assert (route_layer_from_config.categories == route_layer.categories).all() assert route_layer_from_config.score_threshold == route_layer.score_threshold # Add more tests for edge cases and error handling as needed. class TestLayerConfig: def test_init(self): layer_config = LayerConfig() assert layer_config.routes == [] def test_to_file_json(self): route = Route(name="test", utterances=["utterance"]) layer_config = LayerConfig(routes=[route]) with patch("builtins.open", mock_open()) as mocked_open: layer_config.to_file("data/test_output.json") mocked_open.assert_called_once_with("data/test_output.json", "w") def test_to_file_yaml(self): route = Route(name="test", utterances=["utterance"]) layer_config = LayerConfig(routes=[route]) with patch("builtins.open", mock_open()) as mocked_open: layer_config.to_file("data/test_output.yaml") mocked_open.assert_called_once_with("data/test_output.yaml", "w") def test_to_file_invalid(self): route = Route(name="test", utterances=["utterance"]) layer_config = LayerConfig(routes=[route]) with pytest.raises(ValueError): layer_config.to_file("test_output.txt") def test_from_file_json(self): mock_json_data = layer_json() with patch("builtins.open", mock_open(read_data=mock_json_data)) as mocked_open: layer_config = LayerConfig.from_file("data/test.json") mocked_open.assert_called_once_with("data/test.json", "r") assert isinstance(layer_config, LayerConfig) def test_from_file_yaml(self): mock_yaml_data = layer_yaml() with patch("builtins.open", mock_open(read_data=mock_yaml_data)) as mocked_open: layer_config = LayerConfig.from_file("data/test.yaml") mocked_open.assert_called_once_with("data/test.yaml", "r") assert isinstance(layer_config, LayerConfig) def test_from_file_invalid(self): with open("test.txt", "w") as f: f.write("dummy content") with pytest.raises(ValueError): LayerConfig.from_file("test.txt") os.remove("test.txt") def test_to_dict(self): route = Route(name="test", utterances=["utterance"]) layer_config = LayerConfig(routes=[route]) assert layer_config.to_dict()["routes"] == [route.to_dict()] def test_add(self): route = Route(name="test", utterances=["utterance"]) route2 = Route(name="test2", utterances=["utterance2"]) layer_config = LayerConfig() layer_config.add(route) # confirm route added assert layer_config.routes == [route] # add second route and check updates layer_config.add(route2) assert layer_config.routes == [route, route2] def test_get(self): route = Route(name="test", utterances=["utterance"]) layer_config = LayerConfig(routes=[route]) assert layer_config.get("test") == route def test_get_not_found(self): route = Route(name="test", utterances=["utterance"]) layer_config = LayerConfig(routes=[route]) assert layer_config.get("not_found") is None def test_remove(self): route = Route(name="test", utterances=["utterance"]) layer_config = LayerConfig(routes=[route]) layer_config.remove("test") assert layer_config.routes == []