File size: 9,022 Bytes
8e874f5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
"""
Answer evaluation utilities for comparing and scoring RAG responses.

This module provides functions to evaluate multiple answers to the same question
based on criteria like comprehensiveness, diversity, logicality, relevance, and coherence.

This is a standalone module for post-hoc quality assessment, separate from the
core answering pipeline.
"""

import re
import json
from .utils import logger


async def evaluate_multiple_answers(
    query: str,
    answers: list[str],
    use_llm_func: callable,
) -> dict:
    """
    Evaluate multiple answers to the same question based on five criteria.

    Parameters:
    -----------
    query : str
        The original question
    answers : list[str]
        List of answers to evaluate
    use_llm_func : callable
        LLM function to use for evaluation

    Returns:
    --------
    dict
        Evaluation results with scores and rankings for each answer
    """
    if len(answers) < 2:
        logger.warning("Need at least 2 answers for evaluation")
        return {}

    # Create evaluation prompt for multiple answers
    answers_text = ""
    for i, answer in enumerate(answers, 1):
        answers_text += f"Answer {i}: {answer}\n\n"

    prompt = f"""---Role---
You are an expert tasked with evaluating multiple answers to the same question based on five criteria: Comprehensiveness, Diversity, Logicality, Relevance, and Coherence.

---Goal---
You will evaluate {len(answers)} answers to the same question based on five criteria:
- Comprehensiveness: How much detail does the answer provide to cover all aspects and details of the question?
- Diversity: How varied and rich is the answer in providing different perspectives and insights on the question?
- Logicality: How logically does the answer respond to all parts of the question?
- Relevance: How relevant is the answer to the question, staying focused and addressing the intended topic or issue?
- Coherence: How well does the answer maintain internal logical connections between its parts, ensuring a smooth and consistent structure?

Here is the question: {query}

Here are the {len(answers)} answers:
{answers_text}

For each criterion, assign a score from 1 to 10 to each answer, where:
- 1-2: Poor performance
- 3-4: Below average
- 5-6: Average
- 7-8: Good
- 9-10: Excellent

Then provide an overall ranking of the answers from best to worst.

Output your evaluation in the following JSON format:
{{
    "criterion_scores": {{
        "Comprehensiveness": {{
            "Answer 1": [score],
            "Answer 2": [score],
            ...
        }},
        "Diversity": {{
            "Answer 1": [score],
            "Answer 2": [score],
            ...
        }},
        "Logicality": {{
            "Answer 1": [score],
            "Answer 2": [score],
            ...
        }},
        "Relevance": {{
            "Answer 1": [score],
            "Answer 2": [score],
            ...
        }},
        "Coherence": {{
            "Answer 1": [score],
            "Answer 2": [score],
            ...
        }}
    }},
    "overall_scores": {{
        "Answer 1": [total_score],
        "Answer 2": [total_score],
        ...
    }},
    "ranking": ["Answer X", "Answer Y", ...],
    "best_answer": "Answer X",
    "explanations": {{
        "Answer 1": "Brief explanation of strengths and weaknesses",
        "Answer 2": "Brief explanation of strengths and weaknesses",
        ...
    }}
}}"""

    try:
        response = await use_llm_func(prompt, max_tokens=1000)

        # Try to find JSON in the response
        json_match = re.search(r'\{.*\}', response, re.DOTALL)
        if json_match:
            try:
                evaluation_result = json.loads(json_match.group())
                return evaluation_result
            except json.JSONDecodeError as e:
                logger.error(f"Failed to parse JSON from LLM response: {e}")
                logger.error(f"Response: {response}")
                return {}
        else:
            logger.error(f"No JSON found in LLM response: {response}")
            return {}

    except Exception as e:
        logger.error(f"Error during answer evaluation: {e}")
        return {}


async def compare_two_answers(
    query: str,
    answer1: str,
    answer2: str,
    use_llm_func: callable,
) -> dict:
    """
    Compare two answers to the same question based on five criteria.

    Parameters:
    -----------
    query : str
        The original question
    answer1 : str
        First answer to evaluate
    answer2 : str
        Second answer to evaluate
    use_llm_func : callable
        LLM function to use for evaluation

    Returns:
    --------
    dict
        Comparison results with winner for each criterion and overall winner
    """
    prompt = f"""---Role---
You are an expert tasked with evaluating two answers to the same question based on five criteria: Comprehensiveness, Diversity, Logicality, Relevance, and Coherence.

---Goal---
You will evaluate two answers to the same question based on five criteria:
- Comprehensiveness: How much detail does the answer provide to cover all aspects and details of the question?
- Diversity: How varied and rich is the answer in providing different perspectives and insights on the question?
- Logicality: How logically does the answer respond to all parts of the question?
- Relevance: How relevant is the answer to the question, staying focused and addressing the intended topic or issue?
- Coherence: How well does the answer maintain internal logical connections between its parts, ensuring a smooth and consistent structure?

Here is the question: {query}

Here are the two answers:
Answer 1: {answer1}

Answer 2: {answer2}

For each criterion, choose the better answer (either Answer 1 or Answer 2) and explain why. Then, select an overall winner based on these five criteria.

Output your evaluation in the following JSON format:
{{
    "Comprehensiveness": {{ "Winner": "[Answer 1 or Answer 2]", "Explanation": "[Provide explanation here]" }},
    "Diversity": {{ "Winner": "[Answer 1 or Answer 2]", "Explanation": "[Provide explanation here]" }},
    "Logicality": {{ "Winner": "[Answer 1 or Answer 2]", "Explanation": "[Provide explanation here]" }},
    "Relevance": {{ "Winner": "[Answer 1 or Answer 2]", "Explanation": "[Provide explanation here]" }},
    "Coherence": {{ "Winner": "[Answer 1 or Answer 2]", "Explanation": "[Provide explanation here]" }},
    "Overall Winner": {{ "Winner": "[Answer 1 or Answer 2]", "Explanation": "[Summarize why this answer is the overall winner based on the five criteria]" }}
}}"""

    try:
        response = await use_llm_func(prompt, max_tokens=800)

        # Try to find JSON in the response
        json_match = re.search(r'\{.*\}', response, re.DOTALL)
        if json_match:
            try:
                comparison_result = json.loads(json_match.group())
                return comparison_result
            except json.JSONDecodeError as e:
                logger.error(f"Failed to parse JSON from LLM response: {e}")
                logger.error(f"Response: {response}")
                return {}
        else:
            logger.error(f"No JSON found in LLM response: {response}")
            return {}

    except Exception as e:
        logger.error(f"Error during answer comparison: {e}")
        return {}


def calculate_evaluation_metrics(evaluation_result: dict) -> dict:
    """
    Calculate additional metrics from evaluation results.

    Parameters:
    -----------
    evaluation_result : dict
        Result from evaluate_multiple_answers function

    Returns:
    --------
    dict
        Additional metrics including average scores, standard deviations, etc.
    """
    if not evaluation_result or "criterion_scores" not in evaluation_result:
        return {}

    metrics = {}
    criterion_scores = evaluation_result["criterion_scores"]

    # Calculate average scores for each criterion
    for criterion, scores in criterion_scores.items():
        if isinstance(scores, dict):
            values = [v for v in scores.values() if isinstance(v, (int, float))]
            if values:
                metrics[f"{criterion}_average"] = sum(values) / len(values)
                metrics[f"{criterion}_max"] = max(values)
                metrics[f"{criterion}_min"] = min(values)

    # Calculate overall statistics
    if "overall_scores" in evaluation_result:
        overall_scores = evaluation_result["overall_scores"]
        if isinstance(overall_scores, dict):
            values = [v for v in overall_scores.values() if isinstance(v, (int, float))]
            if values:
                metrics["overall_average"] = sum(values) / len(values)
                metrics["overall_max"] = max(values)
                metrics["overall_min"] = min(values)
                metrics["score_range"] = max(values) - min(values)

    return metrics


__all__ = [
    "evaluate_multiple_answers",
    "compare_two_answers",
    "calculate_evaluation_metrics",
]