truthbench 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
truthbench/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .truth_pipeline import truth_pipeline
2
+ from .pipeline import Pipeline, Step, Reader, LLM
3
+
4
+ __all__ = ["truth_pipeline", "Pipeline", "Step", "Reader", "LLM"]
truthbench/cli.py ADDED
@@ -0,0 +1,46 @@
1
+ import argparse
2
+ import pathlib
3
+
4
+ import truthbench
5
+ from truthbench.models import Report, Tracker, Sample
6
+ from truthbench.readers.json_reader import JsonReader
7
+
8
+
9
+ def main() -> None:
10
+ parser = argparse.ArgumentParser(description="Run truthbench pipeline")
11
+ parser.add_argument(
12
+ "--output-dir", "-o", required=True, type=pathlib.Path,
13
+ help="Directory where to place the output dataset and the execution report"
14
+ )
15
+ parser.add_argument(
16
+ "--input-file", "-i", required=True, type=pathlib.Path,
17
+ help="Input json dataset containing questions and ground truths"
18
+ )
19
+ parser.add_argument(
20
+ "--keep", "-k", default=.8, type=float,
21
+ help="Percentage of factual data to preserve"
22
+ )
23
+ parser.add_argument(
24
+ "--num-levels", "-l", default=5, type=int,
25
+ help="Number of perturbation levels to produce A0-AX"
26
+ )
27
+
28
+ args = parser.parse_args()
29
+
30
+ pipeline = truthbench.truth_pipeline(keep=args.keep, num_levels=args.num_levels)
31
+ samples, tracker = pipeline.run(JsonReader(args.input_file))
32
+
33
+ report = Report(report=Tracker(**tracker), questions=[Sample(**s) for s in samples])
34
+ dataset = report.to_dataset()
35
+
36
+ args.output_dir.mkdir(parents=True, exist_ok=True)
37
+
38
+ with open(args.output_dir / "report.json", "w", encoding="utf-8") as f:
39
+ f.write(report.model_dump_json(indent=4))
40
+
41
+ with open(args.output_dir / "dataset.json", "w", encoding="utf-8") as f:
42
+ f.write(dataset.model_dump_json(indent=4))
43
+
44
+
45
+ if __name__ == "__main__":
46
+ main()
File without changes
@@ -0,0 +1,16 @@
1
+ from typing import Dict, List
2
+
3
+ from openai import OpenAI
4
+
5
+ from truthbench.pipeline import LLM
6
+
7
+
8
+ class GPT(LLM):
9
+
10
+ def __init__(self, client: OpenAI, model: str = "gpt-4o"):
11
+ self._client = client
12
+ self._model = model
13
+
14
+ def query(self, messages: List[Dict[str, str]]) -> str:
15
+ completion = self._client.chat.completions.create(model=self._model, messages=messages)
16
+ return completion.choices[0].message.content.strip()
truthbench/models.py ADDED
@@ -0,0 +1,51 @@
1
+ from typing import Optional, List, Dict
2
+
3
+ import pydantic
4
+
5
+
6
+ class Tracker(pydantic.BaseModel):
7
+ input_samples: int = 0
8
+ find_factual_data_error: int = 0
9
+ json_parse_ranking_error: int = 0
10
+ index_ranking_error: int = 0
11
+ ranking_factual_data_error: int = 0
12
+ output_samples: int = 0
13
+
14
+
15
+ class Sample(pydantic.BaseModel):
16
+ question: Optional[str] = None
17
+ ground_truth: Optional[str] = None
18
+ raw_factual_data: Optional[List[str]] = None
19
+ with_brackets: Optional[Dict[str, str]] = None
20
+ thinking: Optional[Dict[str, str]] = None
21
+ blacklisted: Optional[List[str]] = None
22
+ factual_data: Optional[List[str]] = None
23
+ ranked_factual_data: Optional[List[str]] = None
24
+ answers: Optional[Dict[str, str]] = None
25
+
26
+ def is_valid(self) -> bool:
27
+ return len(self.answers.keys()) > 1
28
+
29
+
30
+ class Item(pydantic.BaseModel):
31
+ id: int
32
+ question: str
33
+ ground_truth: str
34
+ answers: Dict[str, str]
35
+
36
+ @classmethod
37
+ def from_sample(cls, id_: int, sample: Sample) -> 'Item':
38
+ return Item(id=id_, question=sample.question, ground_truth=sample.ground_truth, answers=sample.answers)
39
+
40
+
41
+ class Dataset(pydantic.BaseModel):
42
+ questions: List[Item]
43
+
44
+
45
+ class Report(pydantic.BaseModel):
46
+ report: Tracker
47
+ questions: List[Sample]
48
+
49
+ def to_dataset(self) -> Dataset:
50
+ items = [Item.from_sample(id_=i, sample=s) for i, s in enumerate(self.questions) if s.is_valid()]
51
+ return Dataset(questions=items)
truthbench/pipeline.py ADDED
@@ -0,0 +1,174 @@
1
+ import abc
2
+ from typing import List, Dict, Tuple, Any, Set
3
+
4
+ from tqdm import tqdm
5
+
6
+
7
+ class StrictTracker(dict):
8
+ """
9
+ A dictionary subclass that enforces allowed keys and initializes counters.
10
+
11
+ This tracker only allows keys declared in `allowed_keys`. Accessing or setting
12
+ a key not in `allowed_keys` raises a KeyError with a helpful message.
13
+ """
14
+
15
+ def __init__(self, allowed_keys: set[str]):
16
+ super().__init__()
17
+ self._allowed_keys = allowed_keys
18
+ self.update({k: 0 for k in self._allowed_keys})
19
+
20
+ def __getitem__(self, key):
21
+ if key not in self._allowed_keys:
22
+ raise KeyError(
23
+ f"Tracker counter '{key}' is being set but was not declared. "
24
+ f"Declare it at the Step constructor with: "
25
+ f" super().__init__(..., counters=frozenset({{{repr(key)}}}))"
26
+ )
27
+ return super().__getitem__(key)
28
+
29
+ def __setitem__(self, key, value):
30
+ if key not in self._allowed_keys:
31
+ raise KeyError(
32
+ f"Tracker counter '{key}' is being set but was not declared. "
33
+ f"Declare it at the Step constructor with: "
34
+ f" super().__init__(..., counters=frozenset({{{repr(key)}}}))"
35
+ )
36
+ return super().__setitem__(key, value)
37
+
38
+
39
+ class LLM(abc.ABC):
40
+ """
41
+ Abstract base class for Language Models.
42
+
43
+ Subclasses must implement the `query` method to send messages to the LLM
44
+ and return the response as a string.
45
+ """
46
+
47
+ @abc.abstractmethod
48
+ def query(self, messages: List[Dict[str, str]]) -> str:
49
+ """
50
+ Query the language model with a list of messages and get the output string.
51
+
52
+ Args:
53
+ messages (List[Dict[str, str]]): A list of message dicts with keys like 'role' and 'content'.
54
+
55
+ Returns:
56
+ str: The LLM's response as a string.
57
+ """
58
+ ...
59
+
60
+
61
+ class Step(abc.ABC):
62
+ """
63
+ Abstract base class representing a single processing step in the pipeline.
64
+
65
+ Args:
66
+ required_fields (Set[str]): Set of keys that must be present in each sample before running this step.
67
+ counters (Set[str]): Set of counter names that this step may increment in the tracker.
68
+ """
69
+
70
+ def __init__(self, required_fields: Set[str] = frozenset(), counters: Set[str] = frozenset()):
71
+ self.required_fields = required_fields
72
+ self.counters = counters
73
+
74
+ def validate(self, sample: Dict[str, Any]) -> None:
75
+ """
76
+ Validate that the sample contains all required fields for this step.
77
+
78
+ Raises:
79
+ ValueError: If any required field is missing in the sample.
80
+ """
81
+
82
+ current_fields = set(sample.keys())
83
+ if not self.required_fields.issubset(current_fields):
84
+ missing = self.required_fields.difference(current_fields)
85
+ raise ValueError(
86
+ f"{type(self).__name__} requires {sorted(self.required_fields)}, but some are missing from the sample: "
87
+ f"{sorted(missing)}. Check pipeline dependencies before proceeding."
88
+ )
89
+
90
+ @abc.abstractmethod
91
+ def step(self, sample: Dict[str, Any], tracker: Dict[str, int]) -> None:
92
+ """
93
+ Execute the step logic on the sample, possibly updating the tracker.
94
+
95
+ Args:
96
+ sample (Dict[str, Any]): The data sample to process.
97
+ tracker (Dict[str, int]): A dictionary tracking counters/errors during processing.
98
+ """
99
+ ...
100
+
101
+
102
+ class Reader(abc.ABC):
103
+ """
104
+ Abstract base class for data readers that provide samples to the pipeline.
105
+
106
+ Subclasses must implement the `samples` method that returns a list of validated samples.
107
+ """
108
+
109
+ @abc.abstractmethod
110
+ def samples(self) -> List[Dict[str, Any]]:
111
+ """
112
+ Load and return validated samples.
113
+
114
+ Returns:
115
+ List[Dict[str, Any]]: A list of dictionaries containing 'question' and 'ground_truth' keys.
116
+
117
+ Raises:
118
+ ValueError: If the source could not be read or has an invalid format.
119
+ """
120
+ ...
121
+
122
+
123
+ class Pipeline:
124
+ """
125
+ Orchestrates a sequence of Steps to process data samples.
126
+
127
+ Args:
128
+ with_progress (bool): Whether to display a progress bar during execution (tqdm).
129
+ """
130
+
131
+ def __init__(self, with_progress: bool = True):
132
+ self._steps: List[Step] = []
133
+ self._with_progress = with_progress
134
+
135
+ def with_step(self, step: Step) -> 'Pipeline':
136
+ """
137
+ Add a processing step to the pipeline.
138
+
139
+ Args:
140
+ step (Step): A step instance to add.
141
+
142
+ Returns:
143
+ Pipeline: Self, to allow method chaining.
144
+ """
145
+ self._steps.append(step)
146
+ return self
147
+
148
+ def run(self, reader: Reader) -> Tuple[List[Dict[str, Any]], Dict[str, int]]:
149
+ """
150
+ Execute all steps in sequence on each sample provided by the reader.
151
+
152
+ Args:
153
+ reader (Reader): Data reader yielding samples.
154
+
155
+ Returns:
156
+ Tuple[List[Dict[str, Any]], Dict[str, int]]:
157
+ - List of processed samples.
158
+ - Tracker dictionary with counters collected during processing.
159
+ """
160
+ allowed_keys = {"input_samples"} | frozenset.union(*(step.counters for step in self._steps))
161
+
162
+ tracker = StrictTracker(allowed_keys)
163
+
164
+ samples = reader.samples()
165
+
166
+ collected = []
167
+ for sample in tqdm(samples, desc="Samples:", disable=not self._with_progress):
168
+ tracker["input_samples"] += 1
169
+ for step in self._steps:
170
+ step.validate(sample)
171
+ step.step(sample, tracker)
172
+ collected.append(sample)
173
+
174
+ return collected, tracker
File without changes
@@ -0,0 +1,52 @@
1
+ import json
2
+ import pathlib
3
+ from typing import List, Dict, Any
4
+
5
+ from truthbench.pipeline import Reader
6
+
7
+
8
+ class JsonReader(Reader):
9
+ """
10
+ A reader that loads question-answer samples from a JSON file.
11
+
12
+ The JSON file must contain a list of dictionaries. Each dictionary must include
13
+ the following keys:
14
+ - "question" (str)
15
+ - "ground_truth" (str)
16
+
17
+ Example input JSON:
18
+ [
19
+ {"question": "What is Python?", "ground_truth": "A programming language."},
20
+ {"question": "What is 2+2?", "ground_truth": "4"}
21
+ ]
22
+
23
+ Parameters:
24
+ input_file (pathlib.Path): Path to the input JSON file.
25
+
26
+ Raises:
27
+ ValueError: If the JSON is invalid, not a list of objects, or missing required keys.
28
+ """
29
+
30
+ def __init__(self, input_file: pathlib.Path):
31
+ self._input_file = input_file
32
+
33
+ def samples(self) -> List[Dict[str, Any]]:
34
+ with open(self._input_file, "r") as f:
35
+ content = f.read()
36
+
37
+ gold_dataset = json.loads(content)
38
+
39
+ if not isinstance(gold_dataset, list):
40
+ raise ValueError("Expected top-level JSON array (list of samples)")
41
+
42
+ samples = []
43
+ for d in gold_dataset:
44
+ if not isinstance(d, dict):
45
+ raise ValueError(f"Samples must be JSON objects")
46
+ if "question" not in d or "ground_truth" not in d:
47
+ raise ValueError(
48
+ f"Missing required keys: 'question' and 'ground_truth'"
49
+ )
50
+ samples.append({"question": d["question"], "ground_truth": d["ground_truth"]})
51
+
52
+ return samples
File without changes
@@ -0,0 +1,60 @@
1
+ import re
2
+ from typing import Set, Dict, Any
3
+
4
+ from truthbench.pipeline import Step
5
+
6
+
7
+ class BlacklistItemsFromQuestionStep(Step):
8
+ """
9
+ Pipeline step that filters out factual items which contain words already present in the question.
10
+
11
+ For each item in `raw_factual_data`, this step checks whether any token in the item
12
+ (split by whitespace) overlaps with the question tokens. If so, the item is added to
13
+ the `blacklisted` list in the sample. Tokens are lowercased and stripped of punctuation.
14
+
15
+ Stop words can be provided to exclude common words from the question during matching.
16
+
17
+ Attributes:
18
+ - stop_words (Set[str]): A set of words to ignore when matching against the question.
19
+
20
+ Expected Sample Fields:
21
+ - question (str): The question text.
22
+ - raw_factual_data (List[str]): List of candidate factual items.
23
+
24
+ Modifies:
25
+ - sample["blacklisted"] (List[str] | None): List of lowercased blacklisted items,
26
+ or None if the question or factual data is empty.
27
+
28
+ Counter:
29
+ - Increments no counters
30
+
31
+ Notes:
32
+ - This step does not increment or modify the tracker.
33
+ - Matching is case-insensitive and ignores punctuation.
34
+ - If either `question` or `raw_factual_data` is empty, `blacklisted` is set to None.
35
+
36
+ Example:
37
+ question: "What is climate change?"
38
+ raw_factual_data: ["Climate models", "Carbon emissions", "Solar activity"]
39
+ -> blacklisted: ["climate models", "carbon emissions"]
40
+ """
41
+
42
+ def __init__(self, stop_words: Set[str]):
43
+ self._stop_words = stop_words
44
+ super().__init__(
45
+ required_fields=frozenset({"question", "raw_factual_data"})
46
+ )
47
+
48
+ def step(self, sample: Dict[str, Any], tracker: Dict[str, int]) -> None:
49
+ if not sample["question"] or not sample["raw_factual_data"]:
50
+ sample["blacklisted"] = None
51
+ return
52
+
53
+ # Simple tokenization of the question
54
+ # (strip punctuation, lowercase, then split on whitespace)
55
+ question_words = set(re.findall(r"\w+", sample["question"].lower()))
56
+ question_words = question_words - self._stop_words
57
+ sample["blacklisted"] = [
58
+ term.lower() for term in sample["raw_factual_data"]
59
+ if any(word.lower() in question_words for word in term.split())
60
+ ]
@@ -0,0 +1,46 @@
1
+ from typing import Dict, Any
2
+
3
+ from truthbench.pipeline import Step
4
+
5
+
6
+ class CounterStep(Step):
7
+ """
8
+ Pipeline step that counts samples with the expected number of answers.
9
+
10
+ This step checks whether the `answers` field exists and contains exactly the expected number
11
+ of answer levels. If so, it increments the `output_samples` counter in the tracker.
12
+
13
+ Attributes:
14
+ - expected_levels (int): The required number of answer levels for a valid sample.
15
+
16
+ Expected Sample Fields:
17
+ - answers (List[Any]): A list of answers to be validated.
18
+
19
+ Modifies:
20
+ - It does not modify any field.
21
+
22
+ Counter:
23
+ - tracker["output_samples"] (int): Incremented by 1 if the sample passes the check.
24
+
25
+ Notes:
26
+ - This step does not modify the sample.
27
+ - The sample is only counted if `answers` is present and its length matches the expected level.
28
+ - Does nothing if `answers` is None or its length does not match `expected_levels`.
29
+
30
+ Example:
31
+ expected_levels: 3
32
+ sample["answers"]: ["yes", "no", "maybe"]
33
+ -> tracker["output_samples"] += 1
34
+ """
35
+
36
+ def __init__(self, expected_levels: int):
37
+ self._expected_levels = expected_levels
38
+ super().__init__(
39
+ required_fields=frozenset({"answers"}),
40
+ counters=frozenset({"output_samples"})
41
+ )
42
+
43
+ def step(self, sample: Dict[str, Any], tracker: Dict[str, int]) -> None:
44
+ if sample["answers"] and len(sample["answers"]) == self._expected_levels:
45
+ tracker["output_samples"] += 1
46
+ return
@@ -0,0 +1,228 @@
1
+ import abc
2
+ import re
3
+ from typing import Union, Iterator, List, Tuple, Dict, Any
4
+
5
+ from spacy import Language, Errors
6
+ from spacy.symbols import NOUN, PROPN, ADV, ADJ, amod, NUM
7
+ from spacy.tokens import Doc, Span
8
+
9
+ from truthbench.pipeline import Step
10
+
11
+
12
+ class FactualChunker(abc.ABC):
13
+ """
14
+ Extract factual components of a sentence.
15
+ """
16
+
17
+ @abc.abstractmethod
18
+ def tag(self, sentence: str) -> str:
19
+ ...
20
+
21
+
22
+ class NounAdverbFactualChunker(FactualChunker):
23
+ """
24
+ Implements a rule-based factual chunker that identifies textual spans likely to carry factual content
25
+ within a sentence, focusing on noun phrases and adverbial modifiers.
26
+
27
+ This chunker uses syntactic dependency parsing to detect spans such as:
28
+ - Direct objects, attributes, and complements of the main verb.
29
+ - Adverbial modifiers and prepositional phrases expressing circumstantial details.
30
+ - Appositional phrases and descriptive noun modifiers.
31
+ - Numerical expressions excluding those in the subject.
32
+
33
+ To avoid altering the core meaning of sentences, it excludes spans that belong to grammatical subjects.
34
+ For noun phrase subjects containing embedded relative clauses, only the head noun is excluded,
35
+ allowing modifiers in relative clauses to be eligible.
36
+
37
+ Coordination is handled by propagating eligibility from a conjunct to its siblings if the head
38
+ satisfies the criteria. The chunker also suppresses nested or overlapping spans to produce a clean,
39
+ non-redundant set of factual candidates.
40
+
41
+ The output is a bracketed string marking identified factual spans, forming an intermediate
42
+ representation for downstream filtering, ranking, or perturbation.
43
+
44
+ Example:
45
+ Input: "The government announced the new policy in 2021 with confidence."
46
+ Output: "The government announced [the new policy] in [2021] with [confidence]."
47
+
48
+ Methods:
49
+ - tag(sentence: str) -> str:
50
+ Returns the input sentence with factual spans bracketed.
51
+
52
+ Notes:
53
+ - Requires a syntactic dependency parse (e.g., from spaCy).
54
+ - Focuses on spans relevant for factual content modification.
55
+ - Does not modify spans related to sentence subjects to prevent meaning distortion.
56
+ """
57
+
58
+ def __init__(self, nlp: Language):
59
+ self._nlp = nlp
60
+
61
+ def span_boxes(self, doclike: Union[Doc, Span]) -> Iterator[Span]:
62
+ """
63
+ Detect base noun phrases in the object and adverbs from a dependency parse.
64
+ """
65
+ labels = [
66
+ "oprd",
67
+ "dobj",
68
+ "advmod",
69
+ "amod",
70
+ "npadvmod",
71
+ "pcomp",
72
+ "pobj",
73
+ "dative",
74
+ "appos",
75
+ "attr",
76
+ "ROOT",
77
+ ]
78
+
79
+ doc = doclike.doc # Ensure works on both Doc and Span.
80
+
81
+ if not doc.has_annotation("DEP"):
82
+ raise ValueError(Errors.E029)
83
+
84
+ np_deps = [doc.vocab.strings.add(label) for label in labels]
85
+ conj = doc.vocab.strings.add("conj")
86
+ prev_end = -1
87
+
88
+ # Collect subject heads within the doclike (Span or Doc)
89
+ subject_heads = [token for token in doclike if token.dep_ in {'nsubj', 'nsubjpass'}]
90
+
91
+ # Collect indices of all tokens in their subtrees
92
+ subject_indices = set()
93
+ for head in subject_heads:
94
+ # Add all tokens in the subject head's subtree
95
+ head_subtree = {t.i for t in head.subtree}
96
+ subject_indices.update(head_subtree)
97
+
98
+ # Subtract tokens in relative clauses (relcl) attached to the subject head
99
+ for child in head.children:
100
+ if child.dep_ == "relcl":
101
+ relcl_subtree = {t.i for t in child.subtree}
102
+ subject_indices.difference_update(relcl_subtree)
103
+
104
+ for i, word in enumerate(doclike):
105
+ if word.pos not in (NOUN, PROPN, ADV, ADJ, NUM):
106
+ continue
107
+
108
+ # Skip if part of the subject
109
+ if word.i in subject_indices:
110
+ continue
111
+
112
+ if word.pos == ADJ and word.dep == amod and word.head.pos in (NOUN, PROPN):
113
+ continue
114
+
115
+ # Prevent nested chunks from being produced
116
+ if word.left_edge.i <= prev_end:
117
+ continue
118
+
119
+ if word.dep in np_deps or (word.pos == NUM and word.dep_ in ("nummod", "appos", "attr")):
120
+ prev_end = word.i
121
+ yield doc[word.left_edge.i:word.i + 1]
122
+ elif word.dep == conj:
123
+ head = word.head
124
+
125
+ while head.dep == conj and head.head.i < head.i:
126
+ head = head.head
127
+
128
+ # If the head is an NP, and we're coordinated to it, we're an NP
129
+ if head.dep in np_deps:
130
+ prev_end = word.i
131
+ yield doc[word.left_edge.i:word.i + 1]
132
+
133
+ def overlaps(self, idx: List[Tuple[int, int]]) -> bool:
134
+ sorted_intervals = sorted(idx)
135
+ return any(
136
+ current_end > next_start
137
+ for (_, current_end), (next_start, _) in zip(sorted_intervals, sorted_intervals[1:])
138
+ )
139
+
140
+ def tag(self, sentence: str) -> str:
141
+ doc = self._nlp(sentence)
142
+
143
+ idx = []
144
+ for box in self.span_boxes(doc):
145
+ idx.append((min(b.idx for b in box), max(b.idx + len(b) for b in box)))
146
+
147
+ idx.sort(reverse=True)
148
+
149
+ assert not self.overlaps(idx), \
150
+ f"Something went wrong... Overlapping indexes for `{sentence}`"
151
+
152
+ boxed_sentence = sentence
153
+ for start, end in idx:
154
+ boxed_sentence = boxed_sentence[:start] + "[" + boxed_sentence[start:end] + "]" + boxed_sentence[end:]
155
+
156
+ return boxed_sentence
157
+
158
+
159
+ class FactualDataStep(Step):
160
+ """
161
+ Step that identifies factual data spans within an answer text by leveraging a
162
+ provided FactualChunker implementation. It marks spans in the answer likely to
163
+ contain factual content, brackets them, and extracts these spans for downstream processing.
164
+
165
+ Attributes:
166
+ - chunker (FactualChunker): An instance responsible for tagging factual spans in sentences.
167
+
168
+ Expected Sample Fields:
169
+ - answers (Dict[str, str]): A dictionary containing answer texts keyed by identifiers
170
+ (e.g., "A0"). This step processes the text under the "A0" key.
171
+
172
+ Modifies:
173
+ - sample["with_brackets"] (Dict[str, str] or None): Adds a dictionary mapping answer keys
174
+ to bracketed strings marking factual spans. Set to None if input is missing or invalid.
175
+ - sample["raw_factual_data"] (List[str] or None): Extracted factual spans as a list of strings.
176
+ Set to None if no factual spans are found.
177
+
178
+ Counter:
179
+ - find_factual_data_error: Incremented when no factual spans are detected in the answer text.
180
+
181
+ Notes:
182
+ - Relies on the injected FactualChunker to perform the actual span identification and tagging.
183
+ - If "answers" is missing or does not contain the key "A0", no processing occurs and relevant
184
+ fields are set to None.
185
+ - Extracted spans are obtained by regex matching bracketed sections in the tagged text.
186
+
187
+ Example:
188
+ sample = {
189
+ "answers": {"A0": "The government announced the new policy in 2021 with confidence."}
190
+ }
191
+ tracker = {"find_factual_data_error": 0}
192
+
193
+ chunker = NounAdverbFactualChunker(nlp)
194
+ step = FactualDataStep(chunker)
195
+ step.step(sample, tracker)
196
+
197
+ # After processing:
198
+ # sample["with_brackets"]["A0"] will be:
199
+ # "The government announced [the new policy] in [2021] with [confidence]."
200
+ # sample["raw_factual_data"] will be:
201
+ # ["the new policy", "2021", "confidence"]
202
+ # tracker["find_factual_data_error"] remains 0 because factual spans were found.
203
+ """
204
+
205
+ def __init__(self, chunker: FactualChunker):
206
+ self._chunker = chunker
207
+ super().__init__(
208
+ required_fields=frozenset({"answers"}),
209
+ counters=frozenset({"find_factual_data_error"})
210
+ )
211
+
212
+ def step(self, sample: Dict[str, Any], tracker: Dict[str, int]) -> None:
213
+ if not sample["answers"] or "A0" not in sample["answers"].keys():
214
+ sample["with_brackets"] = None
215
+ sample["raw_factual_data"] = None
216
+ return
217
+
218
+ sample["with_brackets"] = {}
219
+ response_text = self._chunker.tag(sample["answers"]["A0"])
220
+ sample["with_brackets"]["A0"] = response_text
221
+
222
+ matches = re.findall(r"\[(.*?)]", response_text)
223
+ if not matches:
224
+ tracker["find_factual_data_error"] += 1
225
+ sample["raw_factual_data"] = None
226
+ return
227
+
228
+ sample["raw_factual_data"] = matches