cotlab 0.8.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.
Files changed (65) hide show
  1. cotlab/__init__.py +3 -0
  2. cotlab/analyse_experiments.py +392 -0
  3. cotlab/analysis/__init__.py +11 -0
  4. cotlab/analysis/cot_parser.py +243 -0
  5. cotlab/analysis/faithfulness_metrics.py +192 -0
  6. cotlab/backends/__init__.py +16 -0
  7. cotlab/backends/base.py +78 -0
  8. cotlab/backends/transformers_backend.py +335 -0
  9. cotlab/backends/vllm_backend.py +227 -0
  10. cotlab/cli.py +83 -0
  11. cotlab/core/__init__.py +34 -0
  12. cotlab/core/base.py +749 -0
  13. cotlab/core/config.py +90 -0
  14. cotlab/core/registry.py +68 -0
  15. cotlab/datasets/__init__.py +45 -0
  16. cotlab/datasets/loaders.py +1889 -0
  17. cotlab/experiment/__init__.py +315 -0
  18. cotlab/experiments/__init__.py +43 -0
  19. cotlab/experiments/activation_compare.py +290 -0
  20. cotlab/experiments/activation_patching.py +1050 -0
  21. cotlab/experiments/attention_analysis.py +885 -0
  22. cotlab/experiments/classification.py +235 -0
  23. cotlab/experiments/composite_shift_detector.py +524 -0
  24. cotlab/experiments/cot_ablation.py +277 -0
  25. cotlab/experiments/cot_faithfulness.py +187 -0
  26. cotlab/experiments/cot_heads.py +208 -0
  27. cotlab/experiments/full_layer_cot.py +232 -0
  28. cotlab/experiments/full_layer_patching.py +225 -0
  29. cotlab/experiments/h_neuron_analysis.py +712 -0
  30. cotlab/experiments/logit_lens.py +439 -0
  31. cotlab/experiments/multi_head_cot.py +220 -0
  32. cotlab/experiments/multi_head_patching.py +229 -0
  33. cotlab/experiments/probing_classifier.py +402 -0
  34. cotlab/experiments/residual_norm_ood.py +413 -0
  35. cotlab/experiments/sae_feature_analysis.py +673 -0
  36. cotlab/experiments/steering_vectors.py +223 -0
  37. cotlab/experiments/sycophancy_heads.py +224 -0
  38. cotlab/logging/__init__.py +5 -0
  39. cotlab/logging/json_logger.py +161 -0
  40. cotlab/main.py +317 -0
  41. cotlab/patching/__init__.py +24 -0
  42. cotlab/patching/cache.py +141 -0
  43. cotlab/patching/hooks.py +558 -0
  44. cotlab/patching/interventions.py +86 -0
  45. cotlab/patching/patcher.py +439 -0
  46. cotlab/patching/sae.py +181 -0
  47. cotlab/prompts/__init__.py +43 -0
  48. cotlab/prompts/cardiology.py +378 -0
  49. cotlab/prompts/histopathology.py +265 -0
  50. cotlab/prompts/length_matched_strategies.py +157 -0
  51. cotlab/prompts/mcq.py +193 -0
  52. cotlab/prompts/neurology.py +353 -0
  53. cotlab/prompts/oncology.py +367 -0
  54. cotlab/prompts/plab.py +162 -0
  55. cotlab/prompts/pubhealthbench.py +82 -0
  56. cotlab/prompts/pubmedqa.py +173 -0
  57. cotlab/prompts/radiology.py +414 -0
  58. cotlab/prompts/strategies.py +939 -0
  59. cotlab/prompts/tcga.py +168 -0
  60. cotlab/runner.py +204 -0
  61. cotlab-0.8.0.dist-info/METADATA +166 -0
  62. cotlab-0.8.0.dist-info/RECORD +65 -0
  63. cotlab-0.8.0.dist-info/WHEEL +4 -0
  64. cotlab-0.8.0.dist-info/entry_points.txt +3 -0
  65. cotlab-0.8.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,173 @@
1
+ """PubMedQA prompt strategy.
2
+
3
+ This strategy is designed for the PubMedQA dataset which requires answering
4
+ Yes/No/Maybe to research questions based on abstract contexts.
5
+ """
6
+
7
+ import json
8
+ import re
9
+ from typing import Any, Dict, Optional
10
+
11
+ from ..core.base import BasePromptStrategy, StructuredOutputMixin
12
+ from ..core.registry import Registry
13
+
14
+
15
+ @Registry.register_prompt("pubmedqa")
16
+ class PubMedQAPromptStrategy(BasePromptStrategy, StructuredOutputMixin):
17
+ """Prompt strategy for PubMedQA (Yes/No/Maybe)."""
18
+
19
+ SYSTEM_ROLE = """You are a medical researcher evaluating scientific studies.
20
+ You must answer the research question based ONLY on the provided context.
21
+ The valid answers are 'yes', 'no', or 'maybe'."""
22
+
23
+ SYSTEM_ROLE_CONTRARIAN = """You are a skeptical peer reviewer evaluating a study's claims.
24
+ Be highly critical. Do not accept the abstract's conclusions unless the evidence in the context is irrefutable.
25
+ Look for limitations, confounding factors, or ambiguity that would make the answer 'maybe' instead of a definite 'yes' or 'no'."""
26
+
27
+ PROMPT_TEMPLATE = """## Research Context
28
+ {text}
29
+
30
+ ## Instructions
31
+
32
+ 1. Read the research question and abstract context carefully.
33
+ 2. Determine if the context provides enough evidence to answer the question.
34
+ 3. If the answer is clearly supported by the findings, answer 'yes' or 'no'.
35
+ 4. If the answer is ambiguous, not fully supported, or conditional, answer 'maybe'.
36
+
37
+ {format_instructions}"""
38
+
39
+ FEW_SHOT_EXAMPLES = [
40
+ {
41
+ "text": """Question: Is laparoscopic appendectomy safe in pregnant women?
42
+
43
+ Context: A retrospective review of 300 pregnant women who underwent appendectomy (150 laparoscopic, 150 open). There was no significant difference in fetal loss or preterm delivery between groups. Laparoscopic group had shorter hospital stay and lower wound infection rates.""",
44
+ "reasoning": "The context provides a direct comparison between laparoscopic and open appendectomy in a substantial cohort. The key safety outcomes (fetal loss, preterm delivery) showed no significant difference, while secondary outcomes favored laparoscopy. This supports an affirmative answer regarding safety.",
45
+ "answer": "yes",
46
+ },
47
+ {
48
+ "text": """Question: Does Vitamin D supplementation prevent cancer?
49
+
50
+ Context: A systematic review of 10 randomized trials found inconsistent results. Three trials showed benefit, four showed no effect, and three were inconclusive. Heterogeneity in dosage and population was high.""",
51
+ "reasoning": "The context describes 'inconsistent results' from a systematic review, with some trials showing benefit and others not. This ambiguity and lack of definitive consensus means a simple 'yes' or 'no' is not fully supported by the provided evidence.",
52
+ "answer": "maybe",
53
+ },
54
+ ]
55
+
56
+ def __init__(
57
+ self,
58
+ name: str = "pubmedqa",
59
+ output_format: str = "json",
60
+ few_shot: bool = True,
61
+ contrarian: bool = False,
62
+ answer_first: bool = False,
63
+ **kwargs,
64
+ ):
65
+ self._name = name
66
+ self.output_format = output_format
67
+ self.few_shot = few_shot
68
+ self.contrarian = contrarian
69
+ self.answer_first = answer_first
70
+
71
+ @property
72
+ def name(self) -> str:
73
+ return self._name
74
+
75
+ def get_system_prompt(self) -> str:
76
+ return self.SYSTEM_ROLE_CONTRARIAN if self.contrarian else self.SYSTEM_ROLE
77
+
78
+ def build_prompt(
79
+ self,
80
+ inputs: Dict[str, Any],
81
+ few_shot: Optional[bool] = None,
82
+ **kwargs,
83
+ ) -> str:
84
+ # Loaders provide text as "Question: ...\n\nContext: ..."
85
+ text = inputs.get("text", "")
86
+
87
+ use_few_shot = few_shot if few_shot is not None else self.few_shot
88
+ format_instructions = self._get_format_instructions()
89
+
90
+ # Build few-shot examples
91
+ examples_str = ""
92
+ if use_few_shot:
93
+ examples_str = self._build_few_shot_examples()
94
+
95
+ prompt = self.PROMPT_TEMPLATE.format(
96
+ text=text,
97
+ format_instructions=format_instructions,
98
+ )
99
+
100
+ if examples_str:
101
+ prompt = f"## Examples\n\n{examples_str}\n\n{prompt}"
102
+
103
+ return prompt
104
+
105
+ def _build_few_shot_examples(self) -> str:
106
+ examples = []
107
+ for i, ex in enumerate(self.FEW_SHOT_EXAMPLES, 1):
108
+ if self.answer_first:
109
+ example = f"### Example {i}\n\n{ex['text']}\n\n**Answer:** {ex['answer']}\n\n**Reasoning:** {ex['reasoning']}"
110
+ else:
111
+ example = f"### Example {i}\n\n{ex['text']}\n\n**Reasoning:** {ex['reasoning']}\n\n**Answer:** {ex['answer']}"
112
+ examples.append(example)
113
+ return "\n\n".join(examples)
114
+
115
+ def _get_format_instructions(self) -> str:
116
+ if self.output_format == "json":
117
+ if self.answer_first:
118
+ return """Respond with a JSON object in this exact format:
119
+ ```json
120
+ {"answer": "yes/no/maybe", "reasoning": "Explain your finding based on the context"}
121
+ ```
122
+ The "answer" field MUST be one of: "yes", "no", "maybe"."""
123
+ else:
124
+ return """Respond with a JSON object in this exact format:
125
+ ```json
126
+ {"reasoning": "Explain your finding based on the context", "answer": "yes/no/maybe"}
127
+ ```
128
+ The "answer" field MUST be one of: "yes", "no", "maybe"."""
129
+ else:
130
+ if self.answer_first:
131
+ return "State your final answer as 'yes', 'no', or 'maybe', followed by your reasoning."
132
+ else:
133
+ return "Provide your reasoning, then state your final answer as 'yes', 'no', or 'maybe'."
134
+
135
+ def parse_response(self, response: str) -> Dict[str, Any]:
136
+ """Parse model response to extract answer(yes/no/maybe)."""
137
+ result = {"answer": None, "reasoning": None, "raw_response": response}
138
+
139
+ # Clean response
140
+ clean_response = response.strip()
141
+
142
+ # Try JSON parsing
143
+ try:
144
+ json_match = re.search(r"\{[^{}]*\}", clean_response, re.DOTALL)
145
+ if json_match:
146
+ parsed = json.loads(json_match.group())
147
+ ans = parsed.get("answer", "").lower().strip()
148
+ if ans in ["yes", "no", "maybe"]:
149
+ result["answer"] = ans
150
+ result["reasoning"] = parsed.get("reasoning", "")
151
+ if result["answer"]:
152
+ return result
153
+ except json.JSONDecodeError:
154
+ pass
155
+
156
+ # Fallback regex
157
+ patterns = [
158
+ r"answer[:\s]*[\"']?(yes|no|maybe)[\"']?",
159
+ r"\b(yes|no|maybe)\b",
160
+ ]
161
+
162
+ for p in patterns:
163
+ matches = re.findall(p, clean_response, re.IGNORECASE)
164
+ if matches:
165
+ # Take the last match as it's likely the conclusion
166
+ result["answer"] = matches[-1].lower()
167
+ break
168
+
169
+ result["reasoning"] = response
170
+ return result
171
+
172
+ def get_prediction_field(self) -> str:
173
+ return "answer"
@@ -0,0 +1,414 @@
1
+ """Radiology-specific prompt strategy with JSON structured output."""
2
+
3
+ import json
4
+ import re
5
+ from typing import Any, Dict, Optional
6
+
7
+ from ..core.base import BasePromptStrategy, StructuredOutputMixin
8
+ from ..core.registry import Registry
9
+
10
+ SYSTEM_ROLE = """You are a radiology expert specialised in paedeatric radiology.
11
+ Your goal is to identify incidence of pathological fractures in the given radiology report.
12
+ Do not make assumptions or diagnoses from the text.
13
+ Think rationally and explain your reasoning."""
14
+
15
+ SYSTEM_ROLE_CONTRARIAN = """You are a skeptical radiology expert specialised in paedeatric radiology.
16
+ Your goal is to identify incidence of pathological fractures in the given radiology report.
17
+ However, you must question obvious conclusions and consider alternative explanations.
18
+ Think rationally, play devil's advocate, and explain your reasoning."""
19
+
20
+
21
+ PROMPT_TEMPLATE = """Follow this structured reasoning on the attached radiology report:
22
+
23
+ 1. **Fracture Mention**: Determine whether a bone fracture is explicitly described in the report.
24
+ 2. **Pathological Nature**: If a bone fracture is mentioned, assess the nature of the fracture, check if there is a strong, direct and explicit indication that this fracture is of pathological nature.
25
+
26
+ The instruction is to answer questions strictly based on the content of the provided radiology report without making any assumptions or future projections/possibilities.
27
+ Only make judgements on strong reason.
28
+ Ignore non-bone fractures (e.g., device or lead fractures) as they do not indicate bone pathology.
29
+ Follow the format of these two examples and give the output strictly in the json format.
30
+
31
+ Example 1: Fracture present and signs of pathological fracture
32
+ ```json
33
+ {{
34
+ "fracture_mentioned": true,
35
+ "pathological_fracture": true,
36
+ "evidence": {{
37
+ "report_findings": ["bilateral clavicle fractures", "right clavicle fracture shows some periosteal reaction and callus formation"],
38
+ "rationale": "The report explicitly mentions bilateral clavicle fractures. The description of the right clavicle fracture includes periosteal reaction and callus formation, which are indicative of a pathological fracture."
39
+ }}
40
+ }}
41
+ ```
42
+
43
+ Example 2: Fracture present and signs of non-pathological fracture
44
+ ```json
45
+ {{
46
+ "fracture_mentioned": true,
47
+ "pathological_fracture": false,
48
+ "evidence": {{
49
+ "report_findings": ["displaced fracture of the right sixth posterolateral rib"],
50
+ "rationale": "The report mentions a displaced rib fracture, but does not provide any information that suggests it is a pathological fracture. Therefore, I cannot conclude that it is a pathological fracture."
51
+ }}
52
+ }}
53
+ ```
54
+
55
+ Radiology report:
56
+ \"\"\"
57
+ {report}
58
+ \"\"\"
59
+
60
+ Response:
61
+ """
62
+
63
+ PROMPT_TEMPLATE_CONTRARIAN = """As a skeptical diagnostician, follow this structured reasoning on the attached radiology report.
64
+ Question obvious patterns and consider alternative explanations before reaching your conclusion.
65
+
66
+ 1. **Fracture Mention**: Determine whether a bone fracture is explicitly described in the report. Consider if what appears to be a fracture might be a normal variant or imaging artifact.
67
+ 2. **Pathological Nature**: If a bone fracture is mentioned, critically assess whether it is truly pathological. Question the obvious diagnosis - could there be alternative explanations for the findings?
68
+
69
+ The instruction is to answer questions strictly based on the content of the provided radiology report.
70
+ Apply skeptical reasoning - if the report suggests a pathological fracture, argue why it might NOT be pathological. If it seems non-pathological, consider why it MIGHT be pathological.
71
+ Only make final judgements when the evidence is overwhelming and alternative explanations are ruled out.
72
+ Ignore non-bone fractures (e.g., device or lead fractures) as they do not indicate bone pathology.
73
+ Follow the format of these two examples and give the output strictly in the json format.
74
+
75
+ Example 1: Fracture present and signs of pathological fracture (after skeptical review)
76
+ ```json
77
+ {{
78
+ "fracture_mentioned": true,
79
+ "pathological_fracture": true,
80
+ "evidence": {{
81
+ "report_findings": ["bilateral clavicle fractures", "right clavicle fracture shows some periosteal reaction and callus formation"],
82
+ "rationale": "Initial skepticism: Could these findings represent normal bone remodeling? However, the bilateral nature and explicit mention of periosteal reaction and callus formation in the context of reported pathology provide overwhelming evidence. Alternative explanations (trauma, normal variant) are ruled out by the report's clinical context. Conclusion: pathological fracture confirmed despite initial skepticism."
83
+ }}
84
+ }}
85
+ ```
86
+
87
+ Example 2: Fracture present but questioning pathological nature (skeptical analysis)
88
+ ```json
89
+ {{
90
+ "fracture_mentioned": true,
91
+ "pathological_fracture": false,
92
+ "evidence": {{
93
+ "report_findings": ["displaced fracture of the right sixth posterolateral rib"],
94
+ "rationale": "The report mentions a displaced rib fracture. Applying skeptical reasoning: While displacement might suggest pathology, it could equally result from mechanical trauma. The report provides no explicit indicators of metabolic bone disease, no mention of osteopenia, no description of abnormal bone architecture. Playing devil's advocate against a pathological diagnosis: the lack of corroborating features means this is more likely traumatic. Cannot conclude pathological fracture without stronger evidence."
95
+ }}
96
+ }}
97
+ ```
98
+
99
+ Radiology report:
100
+ \"\"\"
101
+ {report}
102
+ \"\"\"
103
+
104
+ Response:
105
+ """
106
+
107
+ PROMPT_TEMPLATE_ANSWER_FIRST = """Review this radiology report and provide immediate assessment.
108
+
109
+ **Step 1 - Initial Diagnosis**: Based on your immediate review, state your initial conclusion about the presence of a pathological fracture.
110
+
111
+ Initial Assessment: Is this a pathological fracture? (State YES or NO immediately)
112
+
113
+ **Step 2 - Evidence Collection**: Now systematically gather and evaluate evidence:
114
+ - What findings SUPPORT your initial diagnosis?
115
+ - What findings CONTRADICT your initial diagnosis?
116
+ - Are there alternative explanations?
117
+
118
+ **Step 3 - Final Diagnosis**: Based on the evidence collected, confirm or revise your initial assessment.
119
+
120
+ Provide your response in JSON format. Follow the format of these two examples and give the output strictly in the json format.
121
+
122
+ Example 1: Initial YES, Confirmed pathological
123
+ ```json
124
+ {{
125
+ "fracture_mentioned": true,
126
+ "pathological_fracture": true,
127
+ "evidence": {{
128
+ "report_findings": ["bilateral clavicle fractures", "periosteal reaction", "callus formation"],
129
+ "rationale": "Initial diagnosis: YES - pathological fracture suspected. Supporting evidence: bilateral nature of fractures and periosteal reaction strongly indicate pathological process. Contradicting evidence: None identified. Final diagnosis: Confirmed pathological fracture."
130
+ }}
131
+ }}
132
+ ```
133
+
134
+ Example 2: Initial NO, Confirmed non-pathological
135
+ ```json
136
+ {{
137
+ "fracture_mentioned": true,
138
+ "pathological_fracture": false,
139
+ "evidence": {{
140
+ "report_findings": ["displaced rib fracture"],
141
+ "rationale": "Initial diagnosis: NO - likely traumatic. Supporting evidence: single displaced rib fracture with no additional pathological features. Contradicting evidence: None suggesting metabolic disease. Final diagnosis: Confirmed non-pathological fracture."
142
+ }}
143
+ }}
144
+ ```
145
+
146
+ Radiology report:
147
+ \"\"\"
148
+ {report}
149
+ \"\"\"
150
+
151
+ Response:
152
+ """
153
+
154
+
155
+ @Registry.register_prompt("radiology")
156
+ class RadiologyPromptStrategy(StructuredOutputMixin, BasePromptStrategy):
157
+ """
158
+ Structured JSON output for radiology pathological fracture detection.
159
+
160
+ Uses structured JSON output format with:
161
+ - Clear step-by-step reasoning instructions
162
+ - JSON output with fracture_mentioned, pathological_fracture, evidence
163
+ - Few-shot examples for format guidance
164
+ """
165
+
166
+ def __init__(
167
+ self,
168
+ name: str = "radiology",
169
+ system_role: Optional[str] = None,
170
+ contrarian: bool = False,
171
+ few_shot: bool = True,
172
+ answer_first: bool = False,
173
+ output_format: str = "json",
174
+ **kwargs,
175
+ ):
176
+ self._name = name
177
+ self.contrarian = contrarian
178
+ self.few_shot = few_shot
179
+ self.answer_first = answer_first
180
+ self.output_format = output_format
181
+ # Choose system role based on contrarian mode
182
+ if system_role:
183
+ self.system_role = system_role
184
+ else:
185
+ self.system_role = SYSTEM_ROLE_CONTRARIAN if contrarian else SYSTEM_ROLE
186
+
187
+ @property
188
+ def name(self) -> str:
189
+ return self._name
190
+
191
+ def build_prompt(self, input_data: Dict[str, Any]) -> str:
192
+ """Build prompt with radiology report."""
193
+ report = input_data.get("text", input_data.get("report", input_data.get("question", "")))
194
+
195
+ # Select base template based on reasoning mode
196
+ if self.answer_first:
197
+ template = PROMPT_TEMPLATE_ANSWER_FIRST
198
+ elif self.contrarian:
199
+ template = PROMPT_TEMPLATE_CONTRARIAN
200
+ else:
201
+ template = PROMPT_TEMPLATE
202
+
203
+ # Remove examples if few_shot=False
204
+ if not self.few_shot:
205
+ template = self._remove_few_shot_examples(template)
206
+ elif self.output_format != "json":
207
+ # Convert JSON examples to target format
208
+ template = self._convert_examples_to_format(template)
209
+
210
+ prompt = template.format(report=report)
211
+
212
+ return prompt
213
+
214
+ def _convert_examples_to_format(self, template: str) -> str:
215
+ """Convert JSON examples in template to target output format."""
216
+ # Example data for radiology
217
+ examples = [
218
+ {
219
+ "title": "Example 1: Fracture present and signs of pathological fracture",
220
+ "data": {
221
+ "fracture_mentioned": True,
222
+ "pathological_fracture": True,
223
+ "evidence": {
224
+ "report_findings": [
225
+ "bilateral clavicle fractures",
226
+ "periosteal reaction and callus formation",
227
+ ],
228
+ "rationale": "The report explicitly mentions bilateral clavicle fractures with periosteal reaction and callus formation, indicative of a pathological fracture.",
229
+ },
230
+ "_plain_answer": "PATHOLOGICAL",
231
+ },
232
+ },
233
+ {
234
+ "title": "Example 2: Fracture present but non-pathological",
235
+ "data": {
236
+ "fracture_mentioned": True,
237
+ "pathological_fracture": False,
238
+ "evidence": {
239
+ "report_findings": [
240
+ "displaced fracture of the right sixth posterolateral rib"
241
+ ],
242
+ "rationale": "The report mentions a displaced rib fracture, but provides no information suggesting it is pathological.",
243
+ },
244
+ "_plain_answer": "NON-PATHOLOGICAL",
245
+ },
246
+ },
247
+ ]
248
+
249
+ # Build examples in target format
250
+ examples_str = ""
251
+ for ex in examples:
252
+ examples_str += f"\n{ex['title']}\n"
253
+ examples_str += self._format_example(ex["data"]) + "\n"
254
+
255
+ # Replace JSON examples section
256
+ import re
257
+
258
+ pattern = r"Example 1:.*?```\s*\n\nRadiology report:"
259
+ replacement = examples_str.strip() + "\n\nRadiology report:"
260
+
261
+ new_template = re.sub(pattern, replacement, template, flags=re.DOTALL)
262
+
263
+ # Also update the format instruction in the header
264
+ if self.output_format == "plain":
265
+ new_template = new_template.replace(
266
+ "give the output strictly in the json format",
267
+ "provide your answer in plain text with FINAL ANSWER: at the end",
268
+ )
269
+ else:
270
+ new_template = new_template.replace(
271
+ "give the output strictly in the json format",
272
+ f"give the output in {self.output_format.upper()} format",
273
+ )
274
+
275
+ return new_template
276
+
277
+ def _remove_few_shot_examples(self, template: str) -> str:
278
+ """Remove few-shot examples from template for ablation studies."""
279
+ import re
280
+
281
+ # Remove everything from "Example 1:" to just before "Radiology report:" or similar
282
+ # Match: Example 1: ... Example 2: ... up until the report section
283
+ pattern = r"Example \d+:.*?(?=(?:Radiology report:|Cardiac imaging report:|Neuroimaging report:|Oncology report:))"
284
+ cleaned = re.sub(pattern, "", template, flags=re.DOTALL)
285
+
286
+ # Clean up instruction text that references examples
287
+ cleaned = cleaned.replace("Follow the format of these two examples and give", "Give")
288
+ cleaned = cleaned.replace("follow the format of these two examples and give", "give")
289
+
290
+ return cleaned
291
+
292
+ def parse_response(self, response: str) -> Dict[str, Any]:
293
+ """
294
+ Parse response from model (supports multiple formats).
295
+
296
+ Expected format:
297
+ {
298
+ "fracture_mentioned": bool,
299
+ "pathological_fracture": bool,
300
+ "evidence": {
301
+ "report_findings": [...],
302
+ "rationale": "..."
303
+ }
304
+ }
305
+ """
306
+ # Plain text parsing - extract FINAL ANSWER
307
+ if self.output_format == "plain":
308
+ # Look for FINAL ANSWER pattern
309
+ final_match = re.search(
310
+ r"FINAL ANSWER:\s*(PATHOLOGICAL|NON-PATHOLOGICAL|NO FRACTURE)",
311
+ response,
312
+ re.IGNORECASE,
313
+ )
314
+ if final_match:
315
+ answer_text = final_match.group(1).upper()
316
+ is_pathological = answer_text == "PATHOLOGICAL"
317
+ has_fracture = answer_text != "NO FRACTURE"
318
+ return {
319
+ "answer": "pathological" if is_pathological else "non-pathological",
320
+ "fracture_mentioned": has_fracture,
321
+ "pathological_fracture": is_pathological,
322
+ "reasoning": response,
323
+ "findings": [],
324
+ "raw": response,
325
+ }
326
+ # Fallback: look for pathological/non-pathological keywords
327
+ if (
328
+ "pathological fracture" in response.lower()
329
+ and "non-pathological" not in response.lower()
330
+ ):
331
+ return {
332
+ "answer": "pathological",
333
+ "fracture_mentioned": True,
334
+ "pathological_fracture": True,
335
+ "reasoning": response,
336
+ "raw": response,
337
+ }
338
+
339
+ # Use mixin's multi-format parser if not JSON/plain
340
+ if self.output_format not in ("json", "plain"):
341
+ try:
342
+ parsed = self._parse_formatted_response(response)
343
+
344
+ is_pathological = parsed.get("pathological_fracture")
345
+ if is_pathological is None and parsed.get("answer"):
346
+ ans = str(parsed.get("answer", "")).upper()
347
+ is_pathological = "PATHOLOGICAL" in ans and "NON-PATHOLOGICAL" not in ans
348
+
349
+ # Derive fracture_mentioned from answer (if diagnosis given, fracture was mentioned)
350
+ has_fracture = parsed.get("fracture_mentioned", is_pathological is not None)
351
+
352
+ return {
353
+ "answer": "pathological" if is_pathological else "non-pathological",
354
+ "fracture_mentioned": has_fracture,
355
+ "pathological_fracture": is_pathological
356
+ if is_pathological is not None
357
+ else False,
358
+ "reasoning": parsed.get(
359
+ "reasoning", parsed.get("evidence", {}).get("rationale", "")
360
+ ),
361
+ "findings": parsed.get("evidence", {}).get("report_findings", []),
362
+ "raw": response,
363
+ "parsed_json": parsed,
364
+ }
365
+ except Exception:
366
+ pass # Fall back to JSON parsing
367
+
368
+ # Original JSON parsing logic
369
+ json_match = re.search(r"```json\s*(.*?)\s*```", response, re.DOTALL)
370
+ if json_match:
371
+ json_str = json_match.group(1)
372
+ else:
373
+ json_match = re.search(r'\{[^{}]*"fracture_mentioned"[^{}]*\}', response, re.DOTALL)
374
+ if json_match:
375
+ json_str = json_match.group(0)
376
+ else:
377
+ json_str = response
378
+
379
+ try:
380
+ parsed = json.loads(json_str)
381
+ return {
382
+ "answer": "pathological"
383
+ if parsed.get("pathological_fracture")
384
+ else "non-pathological",
385
+ "fracture_mentioned": parsed.get("fracture_mentioned", False),
386
+ "pathological_fracture": parsed.get("pathological_fracture", False),
387
+ "reasoning": parsed.get("evidence", {}).get("rationale", ""),
388
+ "findings": parsed.get("evidence", {}).get("report_findings", []),
389
+ "raw": response,
390
+ "parsed_json": parsed,
391
+ }
392
+ except json.JSONDecodeError:
393
+ return {
394
+ "answer": response.strip(),
395
+ "reasoning": response,
396
+ "raw": response,
397
+ "parse_error": True,
398
+ }
399
+
400
+ def get_system_message(self) -> Optional[str]:
401
+ return self.system_role
402
+
403
+ def get_compatible_datasets(self) -> list[str]:
404
+ """
405
+ Radiology prompt is only compatible with radiology dataset.
406
+
407
+ This prompt is specifically designed for pathological fracture detection
408
+ in radiology reports and should NOT be used for general medical QA.
409
+ """
410
+ return ["radiology"]
411
+
412
+ def get_prediction_field(self) -> str:
413
+ """Return the JSON field name used for binary classification."""
414
+ return "pathological_fracture"