Files
2025-09-27 20:55:31 +01:00

334 lines
12 KiB
Python

import pytest
from cleverswarm_python_client.libs.evaluation.detailed_eval import (
calculate_metrics, normalize_string, normalize_triple,
evaluate_entity_presence, evaluate_entity_linking,
evaluate_relation_linking, calculate_precision_recall_f1,
evaluate_triples, read_jsonl, write_jsonl, load_ontology,
calculate_average_metrics, evaluate_ontology, CustomEncoder,
EvaluateResults
)
import json
import tempfile
import os
@pytest.fixture
def sample_ontology():
return {
"Relations": {
"works_at": [["Person", "Organization"]],
"part_of": [["Organization", "Organization"]],
"located_in": [["Organization", "Location"]]
}
}
@pytest.fixture
def sample_ground_truth():
return {
"id": "test1",
"sent": "John works at Apple in California",
"triples": [
{"sub": "John", "rel": "works_at", "obj": "Apple"},
{"sub": "Apple", "rel": "located_in", "obj": "California"}
]
}
@pytest.fixture
def sample_response():
return {
"id": "test1",
"ents": [
{"text": "John", "class": "Person"},
{"text": "Apple", "class": "Organization"},
{"text": "California", "class": "Location"}
],
"triples": [
["John", "works_at", "Apple"],
["Apple", "located_in", "California"]
]
}
def test_calculate_metrics():
# Test normal case
precision, recall, f1 = calculate_metrics(tp=5, fp=2, fn=1)
assert precision == round(5/7, 2)
assert recall == round(5/6, 2)
assert f1 == round(2 * (5/7 * 5/6) / (5/7 + 5/6), 2)
# Test edge case with zero values
precision, recall, f1 = calculate_metrics(tp=0, fp=0, fn=0)
assert precision == 0
assert recall == 0
assert f1 == 0
def test_normalize_string():
test_cases = [
("The Test String", "teststring"),
("Test (with parentheses)", "test"),
("Test123abc", "test123"),
("Company, Inc.", "company"),
("Multiple Spaces", "multiplespace"),
("Numbers123", "numbers123"),
("The Company", "company"),
("Products & Services", "productsservice")
]
for input_str, expected in test_cases:
result = normalize_string(input_str)
assert result == expected, f"Failed for input '{input_str}': expected '{expected}' but got '{result}'"
def test_normalize_triple():
test_cases = [
("Subject", "Relation", "Object", "subjectrelationobject"),
("The Company", "has_product", "Item", "companyhasproductitem"),
("Person", "works_at", None, "personworksatnone"),
("Test", "test", "", "testtest")
]
for sub, rel, obj, expected in test_cases:
result = normalize_triple(sub, rel, obj)
assert result == expected, f"Failed for input ({sub}, {rel}, {obj}): expected '{expected}' but got '{result}'"
def test_normalize_string_edge_cases():
test_cases = [
("", ""), # Empty string
(" ", ""), # Only spaces
("The", "the"), # Single word with 'The'
("A B C", "abc"), # Multiple single letters
("Test123Test", "test123"), # Mixed alphanumeric
("Test-123_Test", "test123"), # With special characters
]
for input_str, expected in test_cases:
result = normalize_string(input_str)
assert result == expected, f"Failed for input '{input_str}': expected '{expected}' but got '{result}'"
def test_normalize_triple_edge_cases():
test_cases = [
("", "", "", ""), # All empty
("A", "B", "C", "abc"), # Single letters
("The A", "The B", "The C", "abc"), # With articles
("Test(1)", "Test,2", "Test.3", "testtesttest3"), # With special chars and numbers
]
for sub, rel, obj, expected in test_cases:
result = normalize_triple(sub, rel, obj)
assert result == expected, f"Failed for input ({sub}, {rel}, {obj}): expected '{expected}' but got '{result}'"
def test_normalize_triple_none_handling():
# Test with None as object - this works as the function handles it
result = normalize_triple("Person", "works_at", None)
assert result == "personworksatnone", f"Failed for None object: expected 'personworksatnone' but got '{result}'"
# Test with empty string instead of None for subject - since function doesn't handle None for subject
result = normalize_triple("", "works_at", "Company")
assert result == "worksatcompany", f"Failed for empty subject: expected 'worksatcompany' but got '{result}'"
def test_evaluate_entity_presence(sample_ground_truth, sample_response):
result = evaluate_entity_presence(sample_ground_truth, sample_response)
assert isinstance(result, dict)
assert all(key in result for key in ['precision', 'recall', 'f1', 'has_entities'])
assert result['has_entities'] == True
assert result['precision'] > 0
assert result['recall'] > 0
assert result['f1'] > 0
def test_evaluate_entity_linking(sample_ground_truth, sample_response, sample_ontology):
result = evaluate_entity_linking(sample_ground_truth, sample_response, sample_ontology)
assert isinstance(result, dict)
assert all(key in result for key in ['accuracy', 'correct_links', 'total_links',
'gt_class_instances', 'correctly_linked_entities',
'has_links'])
assert result['has_links'] == True
assert result['accuracy'] > 0
def test_evaluate_relation_linking(sample_ground_truth, sample_response):
correctly_linked_entities = ["john", "apple", "california"]
result = evaluate_relation_linking(sample_ground_truth, sample_response, correctly_linked_entities)
assert isinstance(result, dict)
assert all(key in result for key in ['correct', 'missing', 'wrong', 'correct_triples',
'missing_triples', 'wrong_triples', 'total_triples',
'has_triples'])
assert result['has_triples'] == True
def test_evaluate_triples(sample_ground_truth, sample_response):
result = evaluate_triples(sample_ground_truth, sample_response)
assert isinstance(result, dict)
assert all(key in result for key in ['triples_precision', 'triples_recall', 'triples_f1'])
assert result['triples_precision'] > 0
assert result['triples_recall'] > 0
assert result['triples_f1'] > 0
def test_calculate_precision_recall_f1():
gold = {"triple1", "triple2", "triple3"}
pred = {"triple1", "triple2", "triple4"}
precision, recall, f1 = calculate_precision_recall_f1(gold, pred)
assert precision == 2/3
assert recall == 2/3
assert f1 == 2/3
# Test empty prediction
precision, recall, f1 = calculate_precision_recall_f1(gold, set())
assert precision == 0
assert recall == 0
assert f1 == 0
def test_jsonl_operations():
test_data = [{"test": 1}, {"test": 2}]
with tempfile.NamedTemporaryFile(mode='w+', delete=False) as f:
write_jsonl(test_data, f.name)
result = read_jsonl(f.name)
assert result == test_data
os.unlink(f.name)
def test_calculate_average_metrics():
results = [
{
'triples_evaluation': {'triples_precision': 0.8, 'triples_recall': 0.7, 'triples_f1': 0.75},
'entity_presence': {'precision': 0.9, 'recall': 0.85, 'f1': 0.87},
'entity_linking': {'accuracy': 0.8, 'has_links': True},
'relation_linking': {'correct': 0.7, 'missing': 0.2, 'wrong': 0.1, 'has_triples': True}
}
]
avg_metrics = calculate_average_metrics(results, True)
assert isinstance(avg_metrics, dict)
assert avg_metrics['triples_evaluation_precision'] == 0.8
assert avg_metrics['entity_presence_f1'] == 0.87
assert avg_metrics['entity_linking_accuracy'] == 0.8
def test_evaluate_ontology(tmp_path):
# Create temporary test files
config = {
'path_patterns': {
'sys': str(tmp_path / 'outputs' / '$$onto$$_output.jsonl'),
'gt': str(tmp_path / 'ground_truth' / '$$onto$$_gt.jsonl'),
'onto': str(tmp_path / 'ontologies' / '$$onto$$.json'),
'output': str(tmp_path / 'metrics' / 'ont_$$onto$$_eval_results.jsonl')
}
}
# Create necessary directories
os.makedirs(tmp_path / 'outputs', exist_ok=True)
os.makedirs(tmp_path / 'ground_truth', exist_ok=True)
os.makedirs(tmp_path / 'ontologies', exist_ok=True)
# Create test files
test_output = [
{
"id": "test1",
"ents": [{"text": "John", "class": "Person"}],
"triples": [["John", "works_at", "Company"]]
}
]
test_gt = [
{
"id": "test1",
"sent": "John works at Company",
"triples": [{"sub": "John", "rel": "works_at", "obj": "Company"}]
}
]
test_onto = {
"Relations": {
"works_at": [["Person", "Organization"]]
}
}
# Write test files
write_jsonl(test_output, str(tmp_path / 'outputs' / 'test_output.jsonl'))
write_jsonl(test_gt, str(tmp_path / 'ground_truth' / 'test_gt.jsonl'))
with open(str(tmp_path / 'ontologies' / 'test.json'), 'w') as f:
json.dump(test_onto, f)
results = evaluate_ontology('test', config)
assert isinstance(results, list)
assert len(results) > 0
assert 'triples_evaluation' in results[0]
assert 'entity_presence' in results[0]
assert 'entity_linking' in results[0]
def test_evaluate_results_with_config(tmp_path):
config = {
'onto_list': ['test'],
'path_patterns': {
'sys': str(tmp_path / 'outputs' / '$$onto$$_output.jsonl'),
'gt': str(tmp_path / 'ground_truth' / '$$onto$$_gt.jsonl'),
'onto': str(tmp_path / 'ontologies' / '$$onto$$.json'),
'output': str(tmp_path / 'metrics' / 'ont_$$onto$$_eval_results.jsonl')
},
'overall_output': str(tmp_path / 'overall_metrics.json')
}
# Create test files similar to test_evaluate_ontology
os.makedirs(tmp_path / 'outputs', exist_ok=True)
os.makedirs(tmp_path / 'ground_truth', exist_ok=True)
os.makedirs(tmp_path / 'ontologies', exist_ok=True)
test_output = [{"id": "test1", "ents": [], "triples": []}]
test_gt = [{"id": "test1", "sent": "test", "triples": []}]
test_onto = {"Relations": {}}
write_jsonl(test_output, str(tmp_path / 'outputs' / 'test_output.jsonl'))
write_jsonl(test_gt, str(tmp_path / 'ground_truth' / 'test_gt.jsonl'))
with open(str(tmp_path / 'ontologies' / 'test.json'), 'w') as f:
json.dump(test_onto, f)
# Test both detailed and non-detailed evaluations
from cleverswarm_python_client.libs.evaluation.detailed_eval import EvaluateResults
EvaluateResults(config, is_detailed_metrics=True)
assert os.path.exists(config['overall_output'])
EvaluateResults(config, is_detailed_metrics=False)
assert os.path.exists(config['overall_output'])
def test_custom_encoder():
test_data = {"set_field": {1, 2, 3}}
encoded = json.dumps(test_data, cls=CustomEncoder)
decoded = json.loads(encoded)
assert isinstance(decoded['set_field'], list)
assert set(decoded['set_field']) == {1, 2, 3}
def test_calculate_average_metrics_edge_cases():
# Test with empty results
empty_results = []
avg_metrics = calculate_average_metrics(empty_results, True)
assert isinstance(avg_metrics, dict)
# Test with all required fields with minimal data
incomplete_results = [{
'triples_evaluation': {
'triples_precision': 0.8,
'triples_recall': 0.7,
'triples_f1': 0.75
},
'entity_presence': {
'precision': 0.7,
'recall': 0.8,
'f1': 0.75,
'has_entities': True
},
'entity_linking': {
'accuracy': 0.6,
'has_links': True
},
'relation_linking': {
'correct': 0.5,
'missing': 0.3,
'wrong': 0.2,
'has_triples': True
}
}]
# Test with detailed metrics
avg_metrics = calculate_average_metrics(incomplete_results, True)
assert isinstance(avg_metrics, dict)
assert 'triples_evaluation_precision' in avg_metrics
assert 'triples_evaluation_recall' in avg_metrics
assert 'entity_linking_accuracy' in avg_metrics
# Test with non-detailed metrics
avg_metrics = calculate_average_metrics(incomplete_results, False)
assert isinstance(avg_metrics, dict)
assert 'entity_presence_precision' in avg_metrics
assert 'triples_evaluation_precision' in avg_metrics