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.
- cotlab/__init__.py +3 -0
- cotlab/analyse_experiments.py +392 -0
- cotlab/analysis/__init__.py +11 -0
- cotlab/analysis/cot_parser.py +243 -0
- cotlab/analysis/faithfulness_metrics.py +192 -0
- cotlab/backends/__init__.py +16 -0
- cotlab/backends/base.py +78 -0
- cotlab/backends/transformers_backend.py +335 -0
- cotlab/backends/vllm_backend.py +227 -0
- cotlab/cli.py +83 -0
- cotlab/core/__init__.py +34 -0
- cotlab/core/base.py +749 -0
- cotlab/core/config.py +90 -0
- cotlab/core/registry.py +68 -0
- cotlab/datasets/__init__.py +45 -0
- cotlab/datasets/loaders.py +1889 -0
- cotlab/experiment/__init__.py +315 -0
- cotlab/experiments/__init__.py +43 -0
- cotlab/experiments/activation_compare.py +290 -0
- cotlab/experiments/activation_patching.py +1050 -0
- cotlab/experiments/attention_analysis.py +885 -0
- cotlab/experiments/classification.py +235 -0
- cotlab/experiments/composite_shift_detector.py +524 -0
- cotlab/experiments/cot_ablation.py +277 -0
- cotlab/experiments/cot_faithfulness.py +187 -0
- cotlab/experiments/cot_heads.py +208 -0
- cotlab/experiments/full_layer_cot.py +232 -0
- cotlab/experiments/full_layer_patching.py +225 -0
- cotlab/experiments/h_neuron_analysis.py +712 -0
- cotlab/experiments/logit_lens.py +439 -0
- cotlab/experiments/multi_head_cot.py +220 -0
- cotlab/experiments/multi_head_patching.py +229 -0
- cotlab/experiments/probing_classifier.py +402 -0
- cotlab/experiments/residual_norm_ood.py +413 -0
- cotlab/experiments/sae_feature_analysis.py +673 -0
- cotlab/experiments/steering_vectors.py +223 -0
- cotlab/experiments/sycophancy_heads.py +224 -0
- cotlab/logging/__init__.py +5 -0
- cotlab/logging/json_logger.py +161 -0
- cotlab/main.py +317 -0
- cotlab/patching/__init__.py +24 -0
- cotlab/patching/cache.py +141 -0
- cotlab/patching/hooks.py +558 -0
- cotlab/patching/interventions.py +86 -0
- cotlab/patching/patcher.py +439 -0
- cotlab/patching/sae.py +181 -0
- cotlab/prompts/__init__.py +43 -0
- cotlab/prompts/cardiology.py +378 -0
- cotlab/prompts/histopathology.py +265 -0
- cotlab/prompts/length_matched_strategies.py +157 -0
- cotlab/prompts/mcq.py +193 -0
- cotlab/prompts/neurology.py +353 -0
- cotlab/prompts/oncology.py +367 -0
- cotlab/prompts/plab.py +162 -0
- cotlab/prompts/pubhealthbench.py +82 -0
- cotlab/prompts/pubmedqa.py +173 -0
- cotlab/prompts/radiology.py +414 -0
- cotlab/prompts/strategies.py +939 -0
- cotlab/prompts/tcga.py +168 -0
- cotlab/runner.py +204 -0
- cotlab-0.8.0.dist-info/METADATA +166 -0
- cotlab-0.8.0.dist-info/RECORD +65 -0
- cotlab-0.8.0.dist-info/WHEEL +4 -0
- cotlab-0.8.0.dist-info/entry_points.txt +3 -0
- 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"
|