Skip to content

Commit c4392db

Browse files
tgeilenTobias GeilenMoSchmidt
authored
Prompt benchmark (#34)
* Add prompt evaluation system for keyword extraction benchmarking - Implement automated prompt evaluation framework with Jaccard similarity scoring - Add dataset generator: creates ground truth test cases from keywords using LLM - Add prompt evaluator: tests prompts against dataset and calculates metrics - Add batch evaluator: compares multiple prompts and generates ranking report - Create 6 alternative prompt variations for testing (v1-v6) - Add metrics module with Jaccard similarity calculation and keyword normalization - Include rate limiting support (configurable delay) for API requests - Organize outputs: default paths use evaluation_data/ and evaluation_results/ directories - Update .gitignore to exclude evaluation data and results directories The system enables systematic benchmarking of keyword extraction prompts, with prompt_v1_concise showing 4.6x improvement over baseline (0.3465 vs 0.0747 Jaccard score). * FIXES MADE BY CURSOR - fixed pylint * Add comprehensive prompt engineering evaluation system with best practices testing - Add 10 new prompt variants (v7-v16) systematically testing prompt engineering best practices: * Individual practices: persona, delimiters, explicit steps, few-shot examples, detailed instructions, format specification * Combined practices: persona+examples, steps+delimiters, persona+steps+examples, all best practices - Expand evaluation dataset from 10 to 28 test cases covering diverse ML/NLP/CV domains - Enhance metrics module with Precision, Recall, and F1 score calculations - Update evaluation scripts to calculate and report all metrics (Jaccard, Precision, Recall, F1) - Update prompts README with documentation of new variants and best practices tested - Results show 3.8x improvement over baseline with best-performing prompt (prompt_v2_single_words) * fixed pylint #1 * fixed pylint #2 * fixed pylint #3 * fixed error in OpenAI provider * fixed the one trailing white space, which caused the pylint to reject this minor change (had 9.99/10 code quality) * revert changes in openAI provider.py --------- Co-authored-by: Tobias Geilen <tgeilen@mail.uni-mannheim.de> Co-authored-by: Moritz <moritzschmidt1@gmail.com>
1 parent 731aaf4 commit c4392db

27 files changed

Lines changed: 1715 additions & 161 deletions

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,10 @@ backend/ingestion/data
1919
backend/inquiro-env/
2020

2121
# Claude Code
22-
.claude
22+
.claude
23+
24+
# Evaluation data and results
25+
backend/evaluation_data/
26+
backend/evaluation_results/
27+
backend/dataset.json
28+
backend/results.json
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# Prompt Evaluation System
2+
3+
This module provides tools for benchmarking and comparing different keyword extraction prompts.
4+
5+
## Overview
6+
7+
The evaluation system works in two stages:
8+
9+
1. **Dataset Generation**: Create ground truth test cases (keywords → user inputs)
10+
2. **Prompt Evaluation**: Test prompts against the dataset and calculate Jaccard similarity scores
11+
12+
## Quick Start
13+
14+
### 1. Generate Evaluation Dataset
15+
16+
First, create a keywords file with one keyword set per line (comma-separated):
17+
18+
```bash
19+
# keywords.txt
20+
transformer, attention mechanism, BERT
21+
reinforcement learning, Q-learning, DQN
22+
convolutional neural network, CNN, image classification
23+
```
24+
25+
Then generate the dataset:
26+
27+
```bash
28+
cd backend
29+
python -m app.llm.evaluation.dataset_generator \
30+
--keywords-file keywords.txt \
31+
--output evaluation_data/dataset.json
32+
```
33+
34+
Note: The default output is `evaluation_data/dataset.json` (directories are created automatically).
35+
36+
This will create the dataset file with test cases containing:
37+
- `ground_truth_keywords`: The original keywords
38+
- `user_input`: Generated natural language query
39+
- `metadata`: Generation info
40+
41+
### 2. Evaluate a Prompt
42+
43+
Create a prompt file (e.g., `my_prompt.txt`):
44+
45+
```
46+
You are an expert in academic information retrieval. Extract 5 short search queries...
47+
```
48+
49+
Then evaluate it:
50+
51+
```bash
52+
python -m app.llm.evaluation.prompt_evaluator \
53+
--prompt-file my_prompt.txt \
54+
--dataset evaluation_data/dataset.json \
55+
--output evaluation_results/results.json
56+
```
57+
58+
Note: The default output is `evaluation_results/results.json` (directories are created automatically).
59+
60+
The results will include:
61+
- `mean_jaccard`: Average Jaccard similarity score
62+
- `std_jaccard`: Standard deviation
63+
- `min_jaccard` / `max_jaccard`: Score range
64+
- `per_case_scores`: Detailed results for each test case
65+
66+
## Metrics
67+
68+
**Jaccard Similarity**: Measures overlap between extracted and ground truth keywords
69+
- Formula: `|A ∩ B| / |A ∪ B|`
70+
- Range: 0.0 (no overlap) to 1.0 (perfect match)
71+
- Keywords are normalized (lowercase, stripped) before comparison
72+
73+
## File Structure
74+
75+
```
76+
app/llm/evaluation/
77+
├── __init__.py
78+
├── metrics.py # Jaccard similarity calculation
79+
├── dataset_generator.py # Generate test dataset
80+
├── prompt_evaluator.py # Evaluate prompts
81+
└── README.md # This file
82+
```
83+
84+
## Example Workflow
85+
86+
```bash
87+
# 1. Create keywords file
88+
cat > keywords.txt << EOF
89+
transformer, attention, BERT
90+
reinforcement learning, DQN, policy gradient
91+
CNN, image classification, ResNet
92+
EOF
93+
94+
# 2. Generate dataset
95+
python -m app.llm.evaluation.dataset_generator \
96+
--keywords-file keywords.txt \
97+
--output evaluation/dataset.json \
98+
--delay 20.0
99+
100+
# 3. Evaluate a single prompt
101+
python -m app.llm.evaluation.prompt_evaluator \
102+
--prompt-file app/llm/openai/prompts.py \
103+
--dataset dataset.json \
104+
--output results_baseline.json \
105+
--delay 20.0
106+
107+
# 4. Evaluate all prompts and compare (RECOMMENDED)
108+
python -m app.llm.evaluation.evaluate_all_prompts \
109+
--prompts-dir app/llm/evaluation/prompts \
110+
--dataset evaluation_data/dataset.json \
111+
--output-dir evaluation_results \
112+
--delay 20.0 \
113+
--include-baseline
114+
```
115+
116+
## Evaluating Multiple Prompts
117+
118+
The `evaluate_all_prompts.py` script evaluates all prompts in a directory and generates a comparison report:
119+
120+
```bash
121+
python -m app.llm.evaluation.evaluate_all_prompts \
122+
--prompts-dir app/llm/evaluation/prompts \
123+
--dataset evaluation_data/dataset.json \
124+
--output-dir evaluation_results \
125+
--delay 20.0 \
126+
--include-baseline
127+
```
128+
129+
This will:
130+
- Evaluate all prompts matching the pattern (default: `prompt_*.txt`)
131+
- Save individual results for each prompt
132+
- Generate a comparison report ranking prompts by mean Jaccard score
133+
- Optionally include the baseline prompt from `app/llm/openai/prompts.py`
134+
135+
## Notes
136+
137+
- Requires `OPENAI_API_KEY` to be set in your environment
138+
- Dataset generation uses the same OpenAI model as production
139+
- Evaluation runs sequentially (one test case at a time)
140+
- Results are saved as JSON for programmatic analysis
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Evaluation module for prompt benchmarking."""
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Common utilities for evaluation scripts."""
2+
3+
import argparse
4+
import logging
5+
from typing import Dict, List, Optional
6+
7+
from app.core.config import settings
8+
from app.llm.openai.provider import OpenAIProvider
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
def add_delay_argument(parser: argparse.ArgumentParser) -> None:
14+
"""
15+
Add the standard --delay argument to an argument parser.
16+
17+
Args:
18+
parser: ArgumentParser instance to add the argument to
19+
"""
20+
parser.add_argument(
21+
"--delay",
22+
type=float,
23+
default=20.0,
24+
help="Delay in seconds between API requests (default: 20.0 for 3 RPM limit)",
25+
)
26+
27+
28+
def add_dataset_argument(parser: argparse.ArgumentParser) -> None:
29+
"""
30+
Add the standard --dataset argument to an argument parser.
31+
32+
Args:
33+
parser: ArgumentParser instance to add the argument to
34+
"""
35+
parser.add_argument(
36+
"--dataset",
37+
type=str,
38+
required=True,
39+
help="Path to dataset JSON file",
40+
)
41+
42+
43+
def validate_openai_api_key() -> bool:
44+
"""
45+
Validate that OPENAI_API_KEY is set in the environment.
46+
47+
Returns:
48+
True if API key is set, False otherwise
49+
"""
50+
if settings.OPENAI_API_KEY is None:
51+
logger.error("OPENAI_API_KEY is not set. Please configure it in your environment.")
52+
return False
53+
return True
54+
55+
56+
async def call_openai_api(
57+
provider: OpenAIProvider,
58+
developer_prompt: str,
59+
user_content: Optional[str] = None,
60+
reasoning_effort: str = "low",
61+
) -> str:
62+
"""
63+
Make a standardized OpenAI API call for evaluation scripts.
64+
65+
Args:
66+
provider: OpenAIProvider instance
67+
developer_prompt: Prompt for the developer role
68+
user_content: Optional user content (if None, only developer prompt is sent)
69+
reasoning_effort: Reasoning effort level (default: "low")
70+
71+
Returns:
72+
Response text from the API
73+
74+
Raises:
75+
Exception: If the API call fails
76+
"""
77+
input_messages: List[Dict[str, str]] = [
78+
{
79+
"role": "developer",
80+
"content": developer_prompt,
81+
},
82+
]
83+
84+
if user_content is not None:
85+
input_messages.append(
86+
{
87+
"role": "user",
88+
"content": user_content,
89+
}
90+
)
91+
92+
# Accessing _model is necessary for evaluation scripts
93+
# pylint: disable=protected-access
94+
response = await provider.client.responses.create(
95+
model=provider._model,
96+
reasoning={"effort": reasoning_effort},
97+
input=input_messages,
98+
)
99+
return response.output_text.strip()

0 commit comments

Comments
 (0)