File size: 18,806 Bytes
fa9a583
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
#######################################################################################################################
#
# geval.py
#
# Description: This file contains the code to evaluate the generated text using G-Eval metric.
#
# Scripts taken from https://github.com/microsoft/promptflow/tree/main/examples/flows/evaluation/eval-summarization and modified.
#
import configparser
import json

import gradio as gr
import inspect
import logging
import re
from typing import Dict, Callable, List, Any
from tenacity import (
    RetryError,
    Retrying,
    after_log,
    before_sleep_log,
    stop_after_attempt,
    wait_random_exponential,
)

from App_Function_Libraries.Local_Summarization_Lib import summarize_with_llama, summarize_with_kobold, \
    summarize_with_oobabooga, summarize_with_tabbyapi, summarize_with_vllm, summarize_with_local_llm, \
    summarize_with_ollama
from App_Function_Libraries.Summarization_General_Lib import summarize_with_openai, summarize_with_anthropic, \
    summarize_with_cohere, summarize_with_groq, summarize_with_openrouter, summarize_with_deepseek, \
    summarize_with_huggingface, summarize_with_mistral

#
#######################################################################################################################
#
# Start of G-Eval.py

logger = logging.getLogger(__name__)
config = configparser.ConfigParser()
config.read('config.txt')

def aggregate(

    fluency_list: List[float],

    consistency_list: List[float],

    relevance_list: List[float],

    coherence_list: List[float],

) -> Dict[str, float]:
    """

    Takes list of scores for 4 dims and outputs average for them.



    Args:

        fluency_list (List(float)): list of fluency scores

        consistency_list (List(float)): list of consistency scores

        relevance_list (List(float)): list of relevance scores

        coherence_list (List(float)): list of coherence scores



    Returns:

        Dict[str, float]: Returns average scores

    """
    average_fluency = sum(fluency_list) / len(fluency_list)
    average_consistency = sum(consistency_list) / len(consistency_list)
    average_relevance = sum(relevance_list) / len(relevance_list)
    average_coherence = sum(coherence_list) / len(coherence_list)

    log_metric("average_fluency", average_fluency)
    log_metric("average_consistency", average_consistency)
    log_metric("average_relevance", average_relevance)
    log_metric("average_coherence", average_coherence)

    return {
        "average_fluency": average_fluency,
        "average_consistency": average_consistency,
        "average_relevance": average_relevance,
        "average_coherence": average_coherence,
    }

def run_geval(document, summary, api_key, api_name=None, save=False):
    prompts = {
        "coherence": """You will be given one summary written for a source document.



        Your task is to rate the summary on one metric.



        Please make sure you read and understand these instructions carefully. Please keep this document open while reviewing, and refer to it as needed.



        Evaluation Criteria:



        Coherence (1-5) - the collective quality of all sentences. We align this dimension with the DUC quality question of structure and coherence whereby "the summary should be well-structured and well-organized. The summary should not just be a heap of related information, but should build from sentence to a coherent body of information about a topic."



        Evaluation Steps:



        1. Read the source document carefully and identify the main topic and key points.

        2. Read the summary and compare it to the source document. Check if the summary covers the main topic and key points of the source document, and if it presents them in a clear and logical order.

        3. Assign a score for coherence on a scale of 1 to 5, where 1 is the lowest and 5 is the highest based on the Evaluation Criteria.





        Example:





        Source Document:



        {{Document}}



        Summary:



        {{Summary}}





        Evaluation Form (scores ONLY):



        - Coherence:""",
        "consistency": """You will be given a source document. You will then be given one summary written for this source document.



        Your task is to rate the summary on one metric.



        Please make sure you read and understand these instructions carefully. Please keep this document open while reviewing, and refer to it as needed.





        Evaluation Criteria:



        Consistency (1-5) - the factual alignment between the summary and the summarized source. A factually consistent summary contains only statements that are entailed by the source document. Annotators were also asked to penalize summaries that contained hallucinated facts. 



        Evaluation Steps:



        1. Read the source document carefully and identify the main facts and details it presents.

        2. Read the summary and compare it to the source document. Check if the summary contains any factual errors that are not supported by the source document.

        3. Assign a score for consistency based on the Evaluation Criteria.





        Example:





        Source Document: 



        {{Document}}



        Summary: 



        {{Summary}}





        Evaluation Form (scores ONLY):



        - Consistency:""",
        "fluency": """You will be given one summary written for a source document.



        Your task is to rate the summary on one metric.



        Please make sure you read and understand these instructions carefully. Please keep this document open while reviewing, and refer to it as needed.





        Evaluation Criteria:



        Fluency (1-3): the quality of the summary in terms of grammar, spelling, punctuation, word choice, and sentence structure.



        - 1: Poor. The summary has many errors that make it hard to understand or sound unnatural.

        - 2: Fair. The summary has some errors that affect the clarity or smoothness of the text, but the main points are still comprehensible.

        - 3: Good. The summary has few or no errors and is easy to read and follow.





        Example:



        Summary:



        {{Summary}}





        Evaluation Form (scores ONLY):



        - Fluency (1-3):""",
        "relevance": """You will be given one summary written for a source document.



        Your task is to rate the summary on one metric.



        Please make sure you read and understand these instructions carefully. Please keep this document open while reviewing, and refer to it as needed.



        Evaluation Criteria:



        Relevance (1-5) - selection of important content from the source. The summary should include only important information from the source document. Annotators were instructed to penalize summaries which contained redundancies and excess information.



        Evaluation Steps:



        1. Read the summary and the source document carefully.

        2. Compare the summary to the source document and identify the main points of the source document.

        3. Assess how well the summary covers the main points of the source document, and how much irrelevant or redundant information it contains.

        4. Assign a relevance score from 1 to 5.





        Example:





        Source Document:



        {{Document}}



        Summary:



        {{Summary}}





        Evaluation Form (scores ONLY):



        - Relevance:"""
    }

    scores = {}
    for metric, prompt in prompts.items():
        full_prompt = prompt.replace("{{Document}}", document).replace("{{Summary}}", summary)
        try:
            score = geval_summarization(full_prompt, 5 if metric != "fluency" else 3, api_name, api_key)
            scores[metric] = score
        except Exception as e:
            error_message = detailed_api_error(api_name, e)
            return error_message

    avg_scores = aggregate([scores['fluency']], [scores['consistency']],
                           [scores['relevance']], [scores['coherence']])

    results = {
        "scores": scores,
        "average_scores": avg_scores
    }
    logging.debug("Results: %s", results)

    if save:
        logging.debug("Saving results to geval_results.json")
        save_eval_results(results)
        logging.debug("Results saved to geval_results.json")

    return (f"Coherence: {scores['coherence']:.2f}\n"
            f"Consistency: {scores['consistency']:.2f}\n"
            f"Fluency: {scores['fluency']:.2f}\n"
            f"Relevance: {scores['relevance']:.2f}\n"
            f"Average Scores:\n"
            f"  Fluency: {avg_scores['average_fluency']:.2f}\n"
            f"  Consistency: {avg_scores['average_consistency']:.2f}\n"
            f"  Relevance: {avg_scores['average_relevance']:.2f}\n"
            f"  Coherence: {avg_scores['average_coherence']:.2f}\n"
            f"Results saved to geval_results.json")


def create_geval_tab():
    with gr.Tab("G-Eval"):
        gr.Markdown("# G-Eval Summarization Evaluation")
        with gr.Row():
            with gr.Column():
                document_input = gr.Textbox(label="Source Document", lines=10)
                summary_input = gr.Textbox(label="Summary", lines=5)
                api_name_input = gr.Dropdown(
                    choices=["OpenAI", "Anthropic", "Cohere", "Groq", "OpenRouter", "DeepSeek", "HuggingFace", "Mistral", "Llama.cpp", "Kobold", "Ooba", "Tabbyapi", "VLLM", "Local-LLM", "Ollama"],
                    label="Select API"
                )
                api_key_input = gr.Textbox(label="API Key (if required)", type="password")
                save_value = gr.Checkbox(label="Save Results to a JSON file(geval_results.json)")
                evaluate_button = gr.Button("Evaluate Summary")
            with gr.Column():
                output = gr.Textbox(label="Evaluation Results", lines=10)

        evaluate_button.click(
            fn=run_geval,
            inputs=[document_input, summary_input, api_name_input, api_key_input, save_value],
            outputs=output
        )

    return document_input, summary_input, api_name_input, api_key_input, evaluate_button, output


def parse_output(output: str, max: float) -> float:
    """

    Function that extracts numerical score from the beginning of string



    Args:

        output (str): String to search

        max (float): Maximum score allowed



    Returns:

        float: The extracted score

    """
    matched: List[str] = re.findall(r"(?<!\S)\d+(?:\.\d+)?", output)
    if matched:
        if len(matched) == 1:
            score = float(matched[0])
            if score > max:
                raise ValueError(f"Parsed number: {score} was larger than max score: {max}")
        else:
            raise ValueError(f"More than one number detected in input. Input to parser was: {output}")
    else:
        raise ValueError(f'No number detected in input. Input to parser was "{output}". ')
    return score

def geval_summarization(

    prompt_with_src_and_gen: str,

    max_score: float,

    api_name: str,

    api_key: str,

) -> float:
    summarize_function = get_summarize_function(api_name)
    model = get_model_from_config(api_name)

    try:
        for attempt in Retrying(
            reraise=True,
            before_sleep=before_sleep_log(logger, logging.INFO),
            after=after_log(logger, logging.INFO),
            wait=wait_random_exponential(multiplier=1, min=1, max=120),
            stop=stop_after_attempt(10),
        ):
            with attempt:
                response = summarize_function(api_key, prompt_with_src_and_gen, "", temp=0.7, system_prompt="You are a helpful AI assistant")
    except RetryError:
        logger.exception(f"geval {api_name} call failed\nInput prompt was: {prompt_with_src_and_gen}")
        raise

    try:
        score = parse_output(response, max_score)
    except ValueError as e:
        logger.warning(f"Error parsing output: {e}")
        score = 0

    return score


def get_summarize_function(api_name: str):
    summarize_functions = {
        "openai": summarize_with_openai,
        "anthropic": summarize_with_anthropic,
        "cohere": summarize_with_cohere,
        "groq": summarize_with_groq,
        "openrouter": summarize_with_openrouter,
        "deepseek": summarize_with_deepseek,
        "huggingface": summarize_with_huggingface,
        "mistral": summarize_with_mistral,
        "llama.cpp": summarize_with_llama,
        "kobold": summarize_with_kobold,
        "ooba": summarize_with_oobabooga,
        "tabbyapi": summarize_with_tabbyapi,
        "vllm": summarize_with_vllm,
        "local-llm": summarize_with_local_llm,
        "ollama": summarize_with_ollama
    }
    api_name_lower = api_name.lower()
    if api_name_lower not in summarize_functions:
        raise ValueError(f"Unsupported API: {api_name}")
    return summarize_functions[api_name_lower]



def get_model_from_config(api_name: str) -> str:
    model = config.get('models', api_name)
    if isinstance(model, dict):
        # If the model is a dictionary, return a specific key or a default value
        return model.get('name', str(model))  # Adjust 'name' to the appropriate key if needed
    return str(model) if model is not None else ""

def aggregate_llm_scores(llm_responses: List[str], max_score: float) -> float:
    """Parse and average valid scores from the generated responses of

    the G-Eval LLM call.



    Args:

        llm_responses (List[str]): List of scores from multiple LLMs

        max_score (float): The maximum score allowed.



    Returns:

        float: The average of all the valid scores

    """
    all_scores = []
    error_count = 0
    for generated in llm_responses:
        try:
            parsed = parse_output(generated, max_score)
            all_scores.append(parsed)
        except ValueError as e:
            logger.warning(e)
            error_count += 1
    if error_count:
        logger.warning(f"{error_count} out of 20 scores were discarded due to corrupt g-eval generation")
    score = sum(all_scores) / len(all_scores)
    return score


def validate_inputs(document: str, summary: str, api_name: str, api_key: str) -> None:
    """

    Validate inputs for the G-Eval function.



    Args:

        document (str): The source document

        summary (str): The summary to evaluate

        api_name (str): The name of the API to use

        api_key (str): The API key



    Raises:

        ValueError: If any of the inputs are invalid

    """
    if not document.strip():
        raise ValueError("Source document cannot be empty")
    if not summary.strip():
        raise ValueError("Summary cannot be empty")
    if api_name.lower() not in ["openai", "anthropic", "cohere", "groq", "openrouter", "deepseek", "huggingface",
                                "mistral", "llama.cpp", "kobold", "ooba", "tabbyapi", "vllm", "local-llm", "ollama"]:
        raise ValueError(f"Unsupported API: {api_name}")
    if not api_key.strip() and api_name.lower() not in ["local-llm", "ollama"]:
        raise ValueError("API key is required for non-local APIs")


def detailed_api_error(api_name: str, error: Exception) -> str:
    """

    Generate a detailed error message for API failures.



    Args:

        api_name (str): The name of the API that failed

        error (Exception): The exception that was raised



    Returns:

        str: A detailed error message

    """
    error_type = type(error).__name__
    error_message = str(error)
    return f"API Failure: {api_name}\nError Type: {error_type}\nError Message: {error_message}\nPlease check your API key and network connection, and try again."


def save_eval_results(results: Dict[str, Any], filename: str = "geval_results.json") -> None:
    """

    Save evaluation results to a JSON file.



    Args:

        results (Dict[str, Any]): The evaluation results

        filename (str): The name of the file to save results to

    """
    with open(filename, 'w') as f:
        json.dump(results, f, indent=2)
    print(f"Results saved to {filename}")


#######################################################################################################################
#
# Taken from: https://github.com/microsoft/promptflow/blob/b5a68f45e4c3818a29e2f79a76f2e73b8ea6be44/src/promptflow-core/promptflow/_core/metric_logger.py

class MetricLoggerManager:
    _instance = None

    def __init__(self):
        self._metric_loggers = []

    @staticmethod
    def get_instance() -> "MetricLoggerManager":
        if MetricLoggerManager._instance is None:
            MetricLoggerManager._instance = MetricLoggerManager()
        return MetricLoggerManager._instance

    def log_metric(self, key, value, variant_id=None):
        for logger in self._metric_loggers:
            if len(inspect.signature(logger).parameters) == 2:
                logger(key, value)  # If the logger only accepts two parameters, we don't pass variant_id
            else:
                logger(key, value, variant_id)

    def add_metric_logger(self, logger_func: Callable):
        existing_logger = next((logger for logger in self._metric_loggers if logger is logger_func), None)
        if existing_logger:
            return
        if not callable(logger_func):
            return
        sign = inspect.signature(logger_func)
        # We accept two kinds of metric loggers:
        # def log_metric(k, v)
        # def log_metric(k, v, variant_id)
        if len(sign.parameters) not in [2, 3]:
            return
        self._metric_loggers.append(logger_func)

    def remove_metric_logger(self, logger_func: Callable):
        self._metric_loggers.remove(logger_func)


def log_metric(key, value, variant_id=None):
    """Log a metric for current promptflow run.



    :param key: Metric name.

    :type key: str

    :param value: Metric value.

    :type value: float

    :param variant_id: Variant id for the metric.

    :type variant_id: str

    """
    MetricLoggerManager.get_instance().log_metric(key, value, variant_id)


def add_metric_logger(logger_func: Callable):
    MetricLoggerManager.get_instance().add_metric_logger(logger_func)


def remove_metric_logger(logger_func: Callable):
    MetricLoggerManager.get_instance().remove_metric_logger(logger_func)
#
# End of G-Eval.py
#######################################################################################################################