#!/usr/bin/env python3 """ Unit tests for the guarded_ai.py module. Tests the core feedback generation functions including: - Legacy single feedback system - New multi-prompt feedback system - Both systems together - OpenAI client initialization - Categorization and feedback generation """ import unittest from unittest.mock import patch, MagicMock, call import sys from pathlib import Path import json # Add parent directory to path to import guarded_ai sys.path.insert(0, str(Path(__file__).parent.parent.parent / "research")) from guarded_ai import ( provide_feedback, provide_feedback_prompts, categorize_response, generate_ai_feedback, get_openai_client_and_model, initialize_model_map, ) class TestGuardedAI(unittest.TestCase): """Test cases for guarded_ai functions""" def setUp(self): """Set up test fixtures""" self.sample_metadata = { "player_health": 100, "enemy_health": 80, "user_shot": "A5", "ai_shot": "B3", "user_hit_result": "hit", "ai_hit_result": "miss", } self.sample_transition = { "ai_feedback": { "tokens_for_ai": "Additional transition-specific instructions" }, "metadata_feedback_filter": [ "user_shot", "ai_shot", "user_hit_result", "ai_hit_result", ], } @patch("guarded_ai.get_openai_client_and_model") def test_categorize_response(self, mock_get_client): """Test response categorization""" # Setup mock mock_client = MagicMock() mock_completion = MagicMock() mock_completion.choices[0].message.content = "correct_answer" mock_client.chat.completions.create.return_value = mock_completion mock_get_client.return_value = (mock_client, "test-model") # Test categorization question = "What is 2+2?" response = "Four" buckets = ["correct_answer", "wrong_answer"] tokens_for_ai = "Categorize math answers" category = categorize_response(question, response, buckets, tokens_for_ai) # Verify result self.assertEqual(category, "correct_answer") # Verify client was called correctly mock_client.chat.completions.create.assert_called_once() call_args = mock_client.chat.completions.create.call_args[1] self.assertEqual(call_args["model"], "test-model") self.assertEqual(call_args["max_tokens"], 5) self.assertEqual(call_args["temperature"], 0) # Check message content messages = call_args["messages"] self.assertEqual(len(messages), 2) self.assertIn("correct_answer, wrong_answer", messages[0]["content"]) @patch("guarded_ai.get_openai_client_and_model") def test_generate_ai_feedback(self, mock_get_client): """Test AI feedback generation""" # Setup mock mock_client = MagicMock() mock_completion = MagicMock() mock_completion.choices[0].message.content = "Great job on the math!" mock_client.chat.completions.create.return_value = mock_completion mock_get_client.return_value = (mock_client, "test-model") # Test feedback generation category = "correct_answer" question = "What is 2+2?" user_response = "Four" tokens_for_ai = "Provide encouraging feedback" metadata = {"score": 100} feedback = generate_ai_feedback( category, question, user_response, tokens_for_ai, metadata ) # Verify result self.assertEqual(feedback, "Great job on the math!") # Verify client was called correctly mock_client.chat.completions.create.assert_called_once() call_args = mock_client.chat.completions.create.call_args[1] self.assertEqual(call_args["model"], "test-model") self.assertEqual(call_args["max_tokens"], 250) self.assertEqual(call_args["temperature"], 0.7) @patch("guarded_ai.generate_ai_feedback") def test_provide_feedback_legacy(self, mock_generate_feedback): """Test legacy single feedback system""" mock_generate_feedback.return_value = "Good work! Try again." # Test data transition = self.sample_transition category = "partial_understanding" question = "What is the capital of France?" user_response = "Paris is nice" user_language = "English" tokens_for_ai = "Provide geography feedback" metadata = {"attempts": 1} # Call function feedback = provide_feedback( transition, category, question, user_response, user_language, tokens_for_ai, metadata, ) # Verify feedback was generated self.assertIn("AI Feedback:", feedback) self.assertIn("Good work! Try again.", feedback) # Verify generate_ai_feedback was called with filtered metadata mock_generate_feedback.assert_called_once() call_args = mock_generate_feedback.call_args[0] self.assertEqual(call_args[0], category) # category self.assertEqual(call_args[1], question) # question self.assertEqual(call_args[2], user_response) # user_response # Check tokens_for_ai includes language and transition instructions tokens_arg = call_args[3] self.assertIn("English", tokens_arg) self.assertIn("Additional transition-specific instructions", tokens_arg) # Check metadata was filtered filtered_metadata = call_args[4] expected_filtered = { k: v for k, v in self.sample_metadata.items() if k in transition["metadata_feedback_filter"] } # Since our test metadata doesn't have the filtered keys, it should be empty or contain only matching keys # But the function should have passed what it received @patch("guarded_ai.generate_ai_feedback") def test_provide_feedback_prompts(self, mock_generate_feedback): """Test new multi-prompt feedback system""" # Setup mock to return different feedback for each prompt mock_generate_feedback.side_effect = [ "Hit at A5, miss at B3", "No ships were sunk this round", ] # Test data transition = self.sample_transition category = "valid_move" question = "Where do you want to shoot?" feedback_prompts = [ { "name": "hit_miss", "tokens_for_ai": "Report the hit/miss results for both players", }, { "name": "ship_sinking", "tokens_for_ai": "Report any ships that were sunk", }, ] user_response = "A5" user_language = "English" metadata = self.sample_metadata # Call function feedback_messages = provide_feedback_prompts( transition, category, question, feedback_prompts, user_response, user_language, metadata, "", ) # Verify we got the expected number of feedback messages self.assertEqual(len(feedback_messages), 2) # Verify message structure self.assertEqual(feedback_messages[0]["name"], "hit_miss") self.assertEqual(feedback_messages[0]["content"], "Hit at A5, miss at B3") self.assertEqual(feedback_messages[1]["name"], "ship_sinking") self.assertEqual( feedback_messages[1]["content"], "No ships were sunk this round" ) # Verify generate_ai_feedback was called twice self.assertEqual(mock_generate_feedback.call_count, 2) @patch("guarded_ai.generate_ai_feedback") def test_provide_feedback_prompts_empty_responses(self, mock_generate_feedback): """Test that empty feedback responses are filtered out""" # Setup mock to return empty/whitespace responses mock_generate_feedback.side_effect = [ "", # Empty response " ", # Whitespace only "Valid feedback", # Valid response ] transition = {} category = "test" question = "Test?" feedback_prompts = [ {"name": "empty", "tokens_for_ai": "Empty prompt"}, {"name": "whitespace", "tokens_for_ai": "Whitespace prompt"}, {"name": "valid", "tokens_for_ai": "Valid prompt"}, ] user_response = "Test response" user_language = "English" metadata = {} feedback_messages = provide_feedback_prompts( transition, category, question, feedback_prompts, user_response, user_language, metadata, "", ) # Should only return the valid feedback message self.assertEqual(len(feedback_messages), 1) self.assertEqual(feedback_messages[0]["name"], "valid") self.assertEqual(feedback_messages[0]["content"], "Valid feedback") def test_provide_feedback_no_ai_feedback_config(self): """Test legacy feedback when no ai_feedback config in transition""" transition = {} # No ai_feedback key category = "test" question = "Test?" user_response = "Response" user_language = "English" tokens_for_ai = "Base tokens" metadata = {} with patch("guarded_ai.generate_ai_feedback") as mock_generate: mock_generate.return_value = "" # Should not be called feedback = provide_feedback( transition, category, question, user_response, user_language, tokens_for_ai, metadata, ) # Should NOT call generate_ai_feedback when no ai_feedback in transition mock_generate.assert_not_called() self.assertEqual(feedback, "") @patch.dict( "os.environ", {"MODEL_ENDPOINT_0": "http://test.com", "MODEL_API_KEY_0": "test-key"}, ) def test_initialize_model_map(self): """Test model map initialization from environment variables""" with patch("guarded_ai.get_client_for_endpoint") as mock_get_client: mock_client = MagicMock() mock_get_client.return_value = mock_client # Clear and reinitialize import guarded_ai guarded_ai.MODEL_CLIENT_MAP = {} initialize_model_map() # Verify client was created and stored mock_get_client.assert_called_with("http://test.com", "test-key") self.assertIn("endpoint_0", guarded_ai.MODEL_CLIENT_MAP) self.assertEqual(guarded_ai.MODEL_CLIENT_MAP["endpoint_0"][0], mock_client) def test_get_openai_client_and_model_default(self): """Test getting OpenAI client with default model""" with patch("guarded_ai.MODEL_CLIENT_MAP", {}): with patch("guarded_ai.get_client_for_endpoint") as mock_get_client: mock_client = MagicMock() mock_get_client.return_value = mock_client client, model = get_openai_client_and_model() # Should return default model name self.assertEqual(model, "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic") self.assertEqual(client, mock_client) def test_get_openai_client_and_model_from_map(self): """Test getting OpenAI client from model map""" mock_client = MagicMock() test_map = {"endpoint_0": (mock_client, "http://test.com")} with patch("guarded_ai.MODEL_CLIENT_MAP", test_map): client, model = get_openai_client_and_model("test-model") # Should return client from map self.assertEqual(client, mock_client) self.assertEqual(model, "test-model") @patch("guarded_ai.get_openai_client_and_model") def test_categorize_response_error_handling(self, mock_get_client): """Test error handling in categorize_response""" # Setup mock to raise exception mock_client = MagicMock() mock_client.chat.completions.create.side_effect = Exception("API Error") mock_get_client.return_value = (mock_client, "test-model") category = categorize_response("Test?", "Answer", ["bucket1"], "tokens") # Should return error string self.assertIn("Error:", category) @patch("guarded_ai.get_openai_client_and_model") def test_generate_ai_feedback_error_handling(self, mock_get_client): """Test error handling in generate_ai_feedback""" # Setup mock to raise exception mock_client = MagicMock() mock_client.chat.completions.create.side_effect = Exception("API Error") mock_get_client.return_value = (mock_client, "test-model") feedback = generate_ai_feedback("cat", "Q?", "A", "tokens", {}) # Should return error string self.assertIn("Error:", feedback) if __name__ == "__main__": unittest.main(verbosity=2)