-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrun_ab_test.py
More file actions
169 lines (123 loc) · 5.9 KB
/
Copy pathrun_ab_test.py
File metadata and controls
169 lines (123 loc) · 5.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
"""
A/B Prompt Test using Langfuse experiments + Mistral 8B.
Runs the same dataset against two prompt versions (labels "a" and "b"),
scores each response, and prints a comparison.
Usage:
python run_ab_test.py
"""
from dotenv import load_dotenv
load_dotenv()
from langfuse import Langfuse
from mistralai import Mistral
import os
PROMPT_NAME = "mistral-ab-test"
DATASET_NAME = "ab-test-eval-set"
langfuse = Langfuse()
mistral = Mistral(api_key=os.environ["MISTRAL_API_KEY"])
# ── Task factory: one per prompt label ───────────────────────────────
def make_task(prompt_label: str):
"""Return a task function bound to a specific prompt label."""
def task(*, item, **kwargs):
prompt = langfuse.get_prompt(PROMPT_NAME, label=prompt_label, type="chat")
messages = prompt.compile(**item["input"])
generation = langfuse.generation(
name=f"mistral-{prompt_label}",
input=messages,
model=prompt.config.get("model", "ministral-14b-latest"),
metadata={"prompt_label": prompt_label},
)
response = mistral.chat.complete(
model=prompt.config.get("model", "ministral-14b-latest"),
messages=messages,
temperature=prompt.config.get("temperature", 0.5),
)
output = response.choices[0].message.content
generation.end(output=output)
return output
return task
# ── Evaluators ───────────────────────────────────────────────────────
def keyword_overlap(*, output, expected_output, **kwargs):
"""Score based on keyword overlap between output and expected answer."""
if not expected_output or not output:
return {"name": "keyword_overlap", "value": 0.0}
expected_words = set(expected_output.lower().split())
output_words = set(output.lower().split())
# Remove very short/common words
stop_words = {"is", "a", "an", "the", "of", "in", "to", "and", "or", "for", "with", "that", "it", "by", "from", "on", "are", "was", "be", "has", "its"}
expected_words -= stop_words
output_words -= stop_words
if not expected_words:
return {"name": "keyword_overlap", "value": 0.0}
overlap = expected_words & output_words
score = len(overlap) / len(expected_words)
return {"name": "keyword_overlap", "value": round(score, 2)}
def response_length(*, output, **kwargs):
"""Score based on response length (penalizes very short or very long)."""
if not output:
return {"name": "response_length", "value": 0.0}
length = len(output)
# Sweet spot: 50-500 chars
if 50 <= length <= 500:
score = 1.0
elif length < 50:
score = length / 50
else:
score = max(0.2, 500 / length)
return {"name": "response_length", "value": round(score, 2)}
# ── Run experiments ──────────────────────────────────────────────────
def run():
dataset = langfuse.get_dataset(DATASET_NAME)
results = {}
for label in ["a", "b"]:
run_name = f"ab-test-prompt-{label}"
print(f"\n{'='*50}")
print(f"Running experiment: {run_name}")
print(f"{'='*50}")
scores_overlap = []
scores_length = []
for item in dataset.items:
print(f"\n Q: {item.input['user_question']}")
with item.run(run_name=run_name) as root_span:
# Get prompt and call Mistral
prompt = langfuse.get_prompt(PROMPT_NAME, label=label, type="chat")
messages = prompt.compile(**item.input)
response = mistral.chat.complete(
model=prompt.config.get("model", "ministral-14b-latest"),
messages=messages,
temperature=prompt.config.get("temperature", 0.5),
)
output = response.choices[0].message.content
print(f" A: {output[:120]}...")
# Evaluate
s_overlap = keyword_overlap(
output=output, expected_output=item.expected_output
)
s_length = response_length(output=output)
# Score the trace in Langfuse
root_span.score_trace(name="keyword_overlap", value=s_overlap["value"])
root_span.score_trace(name="response_length", value=s_length["value"])
scores_overlap.append(s_overlap["value"])
scores_length.append(s_length["value"])
avg_overlap = sum(scores_overlap) / len(scores_overlap) if scores_overlap else 0
avg_length = sum(scores_length) / len(scores_length) if scores_length else 0
results[label] = {
"avg_keyword_overlap": round(avg_overlap, 3),
"avg_response_length": round(avg_length, 3),
"n_items": len(scores_overlap),
}
# ── Summary ──────────────────────────────────────────────────────
print("RESULTS COMPARISON")
print(f"{'Metric':<25} {'Prompt A':>12} {'Prompt B':>12} {'Winner':>10}")
print(f"{'-'*60}")
for metric in ["avg_keyword_overlap", "avg_response_length"]:
val_a = results["a"][metric]
val_b = results["b"][metric]
winner = "A" if val_a > val_b else ("B" if val_b > val_a else "Tie")
print(f"{metric:<25} {val_a:>12.3f} {val_b:>12.3f} {winner:>10}")
print(f"\nItems evaluated: {results['a']['n_items']}")
print(f"\nPrompt A: Concise and direct (temp=0.3)")
print(f"Prompt B: Chain-of-thought detailed (temp=0.7)")
print(f"\nResults are also available in the Langfuse dashboard.")
langfuse.flush()
if __name__ == "__main__":
run()