-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsummarization.py
More file actions
193 lines (160 loc) · 5.93 KB
/
Copy pathsummarization.py
File metadata and controls
193 lines (160 loc) · 5.93 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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
"""Built-in summarization task.
This task combines lexical overlap, heuristic grounding checks, and rubric-based
LLM judgment so the final score captures both surface fidelity and deeper summary
quality.
"""
from __future__ import annotations
import re
from pathlib import Path
from eval_harness.providers.base import ModelProvider
from eval_harness.scorers.base import Scorer
from eval_harness.scorers.composite import CompositeScorer
from eval_harness.scorers.llm_judge import COHERENCE, FAITHFULNESS, LLMJudgeScorer
from eval_harness.scorers.rouge import RougeScorer
from eval_harness.templates import PromptTemplate
from eval_harness.types import EvalSample, ScorerOutput
from eval_harness.tasks import TaskDefinition
REPO_ROOT = Path(__file__).resolve().parents[2]
SUMMARY_PATTERN = re.compile(r"<summary>\s*(.*?)\s*</summary>", re.IGNORECASE | re.DOTALL)
STOPWORDS = {
"a",
"an",
"and",
"are",
"as",
"at",
"be",
"by",
"for",
"from",
"in",
"is",
"it",
"of",
"on",
"or",
"that",
"the",
"to",
"with",
}
TASK_SYSTEM_PROMPT = "You are a careful summarizer. Preserve the main claim, key evidence, and caveats without adding unsupported facts."
def _content_tokens(text: str) -> set[str]:
"""Extract non-trivial word tokens.
Args:
text: Input text.
Returns:
Set of lowercased content tokens.
Example:
>>> sorted(_content_tokens('The city approved the pilot.'))
['approved', 'city', 'pilot']
"""
return {
token.lower()
for token in re.findall(r"\w+", text)
if token.lower() not in STOPWORDS and len(token) > 2
}
class FaithfulnessScorer(Scorer):
"""Heuristic scorer that checks whether summary content is grounded in the source."""
async def score(self, sample: EvalSample, model_output: str) -> ScorerOutput:
"""Score how well summary claims are supported by the source document.
Args:
sample: Summarization sample where `sample.input` is the source document.
model_output: Summary produced by the model.
Returns:
Grounding score based on token coverage.
Example:
This heuristic is intentionally conservative and complements the LLM judge.
"""
source_tokens = _content_tokens(sample.input)
summary_sentences = [sentence.strip() for sentence in re.split(r"(?<=[.!?])\s+", model_output) if sentence.strip()]
if not summary_sentences:
return ScorerOutput(
scorer_name=self.name,
score=0.0,
reasoning="Empty summary cannot be faithful to the source.",
metadata={},
)
sentence_scores: list[float] = []
unsupported_tokens: list[str] = []
for sentence in summary_sentences:
tokens = _content_tokens(sentence)
if not tokens:
sentence_scores.append(1.0)
continue
grounded = tokens & source_tokens
sentence_scores.append(len(grounded) / len(tokens))
unsupported_tokens.extend(sorted(tokens - source_tokens))
score = sum(sentence_scores) / len(sentence_scores)
reasoning = (
f"Average sentence grounding score is {score:.3f}. Unsupported tokens include: "
f"{', '.join(unsupported_tokens[:10]) or 'none'}."
)
return ScorerOutput(
scorer_name=self.name,
score=score,
reasoning=reasoning,
metadata={"sentence_scores": [round(value, 3) for value in sentence_scores]},
)
class SummarizationTask:
"""Factory for the built-in summarization task."""
@staticmethod
def summary_extractor(raw_output: str, sample: EvalSample) -> str | None:
"""Extract the `<summary>` section when present.
Args:
raw_output: Raw model output.
sample: Sample associated with the output.
Returns:
Extracted summary or the raw output.
Example:
>>> SummarizationTask.summary_extractor('<summary>Hello.</summary>', EvalSample(id='1', input='x', expected_output='y', metadata={}, tags=[]))
'Hello.'
"""
del sample
match = SUMMARY_PATTERN.search(raw_output)
if match:
return match.group(1).strip()
return raw_output.strip() or None
@classmethod
def build(
cls,
judge_model_id: str | None = None,
judge_provider: ModelProvider | None = None,
) -> TaskDefinition:
"""Build the summarization task definition.
Args:
judge_model_id: Optional override for the judge model identifier.
judge_provider: Optional injected judge provider for testing.
Returns:
Fully configured task definition.
Example:
>>> SummarizationTask.build().name
'summarization'
"""
template = PromptTemplate.from_file(
REPO_ROOT / "templates" / "summarization.jinja2",
xml_output_tag="summary",
)
scorer = CompositeScorer(
scorers=[
RougeScorer(primary_metric="rougeL", weight=0.35),
FaithfulnessScorer(weight=0.25),
LLMJudgeScorer(
rubric=f"{COHERENCE}\n\n{FAITHFULNESS}",
judge_model_id=judge_model_id,
judge_provider=judge_provider,
weight=0.40,
),
],
mode="weighted_average",
)
return TaskDefinition(
name="summarization",
description="Document summarization with ROUGE-L, grounding, and LLM judge scoring.",
template=template,
scorer=scorer,
system_prompt=TASK_SYSTEM_PROMPT,
output_parser=cls.summary_extractor,
few_shot_examples=[],
)
__all__ = ["FaithfulnessScorer", "SummarizationTask"]