Files
2026-07-13 13:22:34 +08:00

523 lines
19 KiB
Python

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