import json from typing import Any from unittest.mock import Mock, patch import pytest from mlflow.exceptions import MlflowException from mlflow.genai.optimize.optimizers.metaprompt_optimizer import MetaPromptOptimizer from mlflow.genai.optimize.types import EvaluationResultRecord, PromptOptimizerOutput _CALL_LLM = "mlflow.genai.utils.llm_utils._call_llm" @pytest.fixture def sample_train_data(): return [ { "inputs": {"question": "What is 2+2?"}, "outputs": "4", }, { "inputs": {"question": "What is the capital of France?"}, "outputs": "Paris", }, { "inputs": {"question": "What is 3*3?"}, "outputs": "9", }, { "inputs": {"question": "What color is the sky?"}, "outputs": "Blue", }, ] @pytest.fixture def sample_target_prompts(): return { "instruction": "Answer the following question: {{question}}", } @pytest.fixture def sample_target_prompts_multiple(): return { "system_prompt": "You are a helpful assistant.", "instruction": "Answer the following question: {{question}}", } def mock_eval_fn(candidate_prompts: dict[str, str], dataset: list[dict[str, Any]]): """Mock evaluation function that returns varied scores.""" # Return varied scores for diverse sampling scores = [0.9, 0.7, 0.4, 0.2] # High to low return [ EvaluationResultRecord( inputs=record["inputs"], outputs="mock output", expectations=record["outputs"], score=scores[i % len(scores)], trace=Mock(), # Use Mock for trace rationales={"correctness": f"Score {scores[i % len(scores)]}"}, ) for i, record in enumerate(dataset) ] @pytest.fixture def mock_llm_response(): """Mock LLM response with improved prompts.""" mock_response = Mock() mock_response.choices = [Mock()] mock_response.choices[0].message = Mock() mock_response.choices[0].message.content = json.dumps({ "instruction": "Improved: Answer this question carefully: {{question}}" }) return mock_response def test_metaprompt_optimizer_initialization(): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") assert optimizer.reflection_model == "openai:/gpt-4o" assert optimizer.lm_kwargs == {} def test_metaprompt_optimizer_initialization_with_custom_params(): optimizer = MetaPromptOptimizer( reflection_model="anthropic:/claude-3-5-sonnet-20241022", lm_kwargs={"temperature": 0.9, "max_tokens": 4096}, ) assert optimizer.reflection_model == "anthropic:/claude-3-5-sonnet-20241022" assert optimizer.lm_kwargs == {"temperature": 0.9, "max_tokens": 4096} def test_metaprompt_optimizer_invalid_lm_kwargs(): with pytest.raises(MlflowException, match="`lm_kwargs` must be a dictionary"): MetaPromptOptimizer(reflection_model="openai:/gpt-4o", lm_kwargs="invalid") with pytest.raises(MlflowException, match="`lm_kwargs` must be a dictionary"): MetaPromptOptimizer(reflection_model="openai:/gpt-4o", lm_kwargs=123) def test_extract_template_variables(): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") prompts = { "instruction": "Answer {{question}} about {{topic}}", "system": "You are a {{role}}", } variables = optimizer._extract_template_variables(prompts) assert variables["instruction"] == {"question", "topic"} assert variables["system"] == {"role"} def test_validate_template_variables_success(): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") original = {"instruction": "Answer {{question}}"} new = {"instruction": "Please answer this {{question}} carefully"} # Should not raise assert optimizer._validate_template_variables(original, new) is True def test_validate_template_variables_missing_var(): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") original = {"instruction": "Answer {{question}}"} new = {"instruction": "Answer the question"} # Missing {{question}} with pytest.raises(MlflowException, match="Missing.*question"): optimizer._validate_template_variables(original, new) def test_validate_template_variables_extra_var_stripped(): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") original = {"instruction": "Answer {{question}}"} new = {"instruction": "Answer {{question}} about {{topic}}"} assert optimizer._validate_template_variables(original, new) is True assert new["instruction"] == "Answer {{question}} about " def test_validate_template_variables_extra_var_no_original_vars(): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") original = {"system": "You are a helpful assistant."} new = {"system": "You are {{role}}, a helpful assistant."} assert optimizer._validate_template_variables(original, new) is True assert new["system"] == "You are , a helpful assistant." def test_validate_template_variables_multiple_extra_vars_stripped(): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") original = {"instruction": "Answer {{question}}"} new = {"instruction": "As a {{role}}, answer {{question}} about {{topic}}"} assert optimizer._validate_template_variables(original, new) is True assert new["instruction"] == "As a , answer {{question}} about " def test_validate_prompt_names_missing(): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") original = {"instruction": "Answer {{question}}", "system": "You are helpful"} new = {"instruction": "Answer {{question}}"} with pytest.raises(MlflowException, match="Prompts missing.*system"): optimizer._validate_prompt_names(original, new) def test_validate_prompt_names_unexpected(): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") original = {"instruction": "Answer {{question}}"} new = { "instruction": "Answer {{question}}", "extra_prompt": "This is unexpected", } with pytest.raises(MlflowException, match="Unexpected prompts.*extra_prompt"): optimizer._validate_prompt_names(original, new) def test_validate_prompt_names_success(): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") original = {"instruction": "Answer {{question}}", "system": "You are helpful"} new = {"instruction": "Answer {{question}}", "system": "You are an expert"} assert optimizer._validate_prompt_names(original, new) is True def test_build_zero_shot_meta_prompt(sample_target_prompts): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") template_vars = optimizer._extract_template_variables(sample_target_prompts) meta_prompt = optimizer._build_zero_shot_meta_prompt(sample_target_prompts, template_vars) assert "PROMPT ENGINEERING BEST PRACTICES" in meta_prompt assert "{{question}}" in meta_prompt or "question" in meta_prompt assert "instruction" in meta_prompt assert "JSON" in meta_prompt def test_build_few_shot_meta_prompt(sample_train_data, sample_target_prompts): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") template_vars = optimizer._extract_template_variables(sample_target_prompts) eval_results = mock_eval_fn(sample_target_prompts, sample_train_data) meta_prompt = optimizer._build_few_shot_meta_prompt( sample_target_prompts, template_vars, eval_results ) assert "EVALUATION EXAMPLES" in meta_prompt assert "Example 1:" in meta_prompt assert "Score:" in meta_prompt assert "JSON" in meta_prompt def test_build_few_shot_meta_prompt_empty_eval_results(sample_target_prompts): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") template_vars = optimizer._extract_template_variables(sample_target_prompts) with pytest.raises(MlflowException, match="Few-shot metaprompting requires evaluation results"): optimizer._build_few_shot_meta_prompt(sample_target_prompts, template_vars, []) def test_format_examples(sample_train_data): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") eval_results = mock_eval_fn({}, sample_train_data) formatted = optimizer._format_examples(eval_results[:2]) assert "Example 1:" in formatted assert "Example 2:" in formatted assert "Input:" in formatted assert "Output:" in formatted assert "Score:" in formatted def test_call_reflection_model_success(mock_llm_response): with patch(_CALL_LLM, return_value=mock_llm_response) as mock_call: optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") result = optimizer._call_reflection_model("test prompt") assert isinstance(result, dict) assert "instruction" in result assert "{{question}}" in result["instruction"] mock_call.assert_called_once() args, kwargs = mock_call.call_args assert args[0] == "openai:/gpt-4o" assert kwargs["json_mode"] is True def test_call_reflection_model_with_markdown(): # Test response with markdown code blocks mock_response = Mock() mock_response.choices = [Mock()] mock_response.choices[0].message = Mock() mock_response.choices[0].message.content = """```json { "instruction": "Improved: Answer {{question}}" } ```""" with patch(_CALL_LLM, return_value=mock_response): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") result = optimizer._call_reflection_model("test prompt") assert isinstance(result, dict) assert "instruction" in result def test_call_reflection_model_llm_failure(): with patch(_CALL_LLM, side_effect=Exception("API error")): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") with pytest.raises(MlflowException, match="Failed to call reflection model"): optimizer._call_reflection_model("test prompt") def test_call_reflection_model_with_lm_kwargs(mock_llm_response): custom_lm_kwargs = {"temperature": 0.5, "max_tokens": 2048, "top_p": 0.9} with patch(_CALL_LLM, return_value=mock_llm_response) as mock_call: optimizer = MetaPromptOptimizer( reflection_model="openai:/gpt-4o", lm_kwargs=custom_lm_kwargs ) result = optimizer._call_reflection_model("test prompt") assert isinstance(result, dict) # Verify that custom lm_kwargs were passed as inference_params mock_call.assert_called_once() _, kwargs = mock_call.call_args assert kwargs["inference_params"] == custom_lm_kwargs def test_optimize_zero_shot_mode(sample_target_prompts, mock_llm_response): with patch(_CALL_LLM, return_value=mock_llm_response) as mock_call: optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") result = optimizer.optimize( eval_fn=Mock(), # Not used in zero-shot train_data=[], # Empty triggers zero-shot target_prompts=sample_target_prompts, enable_tracking=False, ) assert isinstance(result, PromptOptimizerOutput) assert result.initial_eval_score is None # No evaluation in zero-shot assert result.final_eval_score is None assert "instruction" in result.optimized_prompts assert "{{question}}" in result.optimized_prompts["instruction"] # Zero-shot uses single pass assert mock_call.call_count == 1 def test_optimize_few_shot_mode(sample_train_data, sample_target_prompts, mock_llm_response): with patch(_CALL_LLM, return_value=mock_llm_response) as mock_call: optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") result = optimizer.optimize( eval_fn=mock_eval_fn, train_data=sample_train_data, target_prompts=sample_target_prompts, enable_tracking=False, ) assert isinstance(result, PromptOptimizerOutput) assert result.initial_eval_score is not None assert result.final_eval_score is not None # Sanity check evaluation on train data assert "instruction" in result.optimized_prompts assert mock_call.call_count == 1 # Single pass def test_optimize_few_shot_with_baseline_eval(sample_train_data, sample_target_prompts): # Mock LLM to return improved prompts mock_response = Mock() mock_response.choices = [Mock()] mock_response.choices[0].message = Mock() mock_response.choices[0].message.content = json.dumps({ "instruction": "Better: Answer {{question}}" }) # Mock eval_fn that returns scores def mock_eval_fn(candidate_prompts, dataset): return [ EvaluationResultRecord( inputs=record["inputs"], outputs="mock output", expectations=record["outputs"], score=0.7, trace=Mock(), rationales={}, ) for record in dataset ] with patch(_CALL_LLM, return_value=mock_response): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") result = optimizer.optimize( eval_fn=mock_eval_fn, train_data=sample_train_data, target_prompts=sample_target_prompts, enable_tracking=False, ) # Should have both baseline and final eval scores (sanity check) assert result.initial_eval_score is not None assert result.final_eval_score is not None assert "Better" in result.optimized_prompts["instruction"] def test_optimize_strips_extra_vars_from_no_variable_prompt(sample_train_data): mock_response = Mock() mock_response.choices = [Mock()] mock_response.choices[0].message = Mock() mock_response.choices[0].message.content = json.dumps({ "system": "You are an expert {{topic}} assistant.", "instruction": "Please answer: {{question}}", }) prompts = { "system": "You are a helpful assistant.", "instruction": "Answer {{question}}", } with patch("litellm.completion", return_value=mock_response): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") result = optimizer.optimize( eval_fn=mock_eval_fn, train_data=sample_train_data, target_prompts=prompts, enable_tracking=False, ) assert result.optimized_prompts["system"] == "You are an expert assistant." assert result.optimized_prompts["instruction"] == "Please answer: {{question}}" def test_optimize_preserves_template_variables(sample_train_data): # Mock response that drops the {{question}} variable mock_response = Mock() mock_response.choices = [Mock()] mock_response.choices[0].message = Mock() mock_response.choices[0].message.content = json.dumps( {"instruction": "Answer the question"} # Missing {{question}} ) prompts = {"instruction": "Answer {{question}}"} with patch(_CALL_LLM, return_value=mock_response): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") result = optimizer.optimize( eval_fn=mock_eval_fn, train_data=sample_train_data, target_prompts=prompts, enable_tracking=False, ) # Should keep original prompts due to validation failure # (caught as exception and logged as warning) assert "{{question}}" in result.optimized_prompts["instruction"] def test_optimize_with_multiple_prompts(sample_train_data, sample_target_prompts_multiple): mock_response = Mock() mock_response.choices = [Mock()] mock_response.choices[0].message = Mock() mock_response.choices[0].message.content = json.dumps({ "system_prompt": "Improved: You are an expert assistant.", "instruction": "Improved: Answer {{question}}", }) with patch(_CALL_LLM, return_value=mock_response): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") result = optimizer.optimize( eval_fn=mock_eval_fn, train_data=sample_train_data, target_prompts=sample_target_prompts_multiple, enable_tracking=False, ) assert "system_prompt" in result.optimized_prompts assert "instruction" in result.optimized_prompts assert "{{question}}" in result.optimized_prompts["instruction"] def test_build_zero_shot_meta_prompt_with_guidelines(sample_target_prompts): custom_guidelines = "Focus on concise, accurate answers for finance domain." optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o", guidelines=custom_guidelines) template_vars = optimizer._extract_template_variables(sample_target_prompts) meta_prompt = optimizer._build_zero_shot_meta_prompt(sample_target_prompts, template_vars) # Verify structure assert "CUSTOM GUIDELINES:" in meta_prompt assert custom_guidelines in meta_prompt assert "TEMPLATE VARIABLES:" in meta_prompt assert "PROMPT ENGINEERING BEST PRACTICES:" in meta_prompt def test_build_few_shot_meta_prompt_with_guidelines(sample_target_prompts): custom_guidelines = "Focus on concise, accurate answers for finance domain." optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o", guidelines=custom_guidelines) template_vars = optimizer._extract_template_variables(sample_target_prompts) # Create sample evaluation results eval_results = [ EvaluationResultRecord( inputs={"question": "test"}, outputs="answer", expectations="answer", score=0.8, trace=Mock(), rationales={"correctness": "Good"}, ) ] meta_prompt = optimizer._build_few_shot_meta_prompt( sample_target_prompts, template_vars, eval_results ) # Verify structure assert "CUSTOM GUIDELINES:" in meta_prompt assert custom_guidelines in meta_prompt assert "TEMPLATE VARIABLES:" in meta_prompt assert "EVALUATION EXAMPLES" in meta_prompt # Now includes score in header assert "Current Score:" in meta_prompt def test_compute_per_scorer_scores(): optimizer = MetaPromptOptimizer(reflection_model="openai:/gpt-4o") # Test with multiple results having individual scores eval_results = [ EvaluationResultRecord( inputs={"q": "1"}, outputs="a", expectations="a", score=0.8, trace=Mock(), rationales={}, individual_scores={"Correctness": 0.9, "Safety": 0.7}, ), EvaluationResultRecord( inputs={"q": "2"}, outputs="b", expectations="b", score=0.6, trace=Mock(), rationales={}, individual_scores={"Correctness": 0.7, "Safety": 0.5}, ), ] per_scorer = optimizer._compute_per_scorer_scores(eval_results) assert per_scorer == {"Correctness": 0.8, "Safety": 0.6} # Average of each scorer