Skip to content

Commit 137197d

Browse files
committed
WIP: Multi analysis prompt
1 parent 7c9bd95 commit 137197d

12 files changed

Lines changed: 506 additions & 451 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,6 @@ poetry.toml
176176
pyrightconfig.json
177177

178178
# End of https://www.toptal.com/developers/gitignore/api/python
179+
# pixi environments
180+
.pixi/*
181+
!.pixi/config.toml

autonima/.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# SCM syntax highlighting & preventing 3-way merges
2+
pixi.lock merge=binary linguist-language=YAML linguist-generated=true

autonima/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# pixi environments
2+
.pixi/*
3+
!.pixi/config.toml

autonima/annotation/client.py

Lines changed: 97 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from typing import List, Dict, Any
66
from pydantic import BaseModel
77
from ..llm.client import GenericLLMClient
8-
from .schema import AnalysisMetadata, AnnotationCriteriaConfig, AnnotationDecision
8+
from .schema import AnalysisMetadata, AnnotationCriteriaConfig, AnnotationDecision, StudyAnalysisGroup
99

1010
logger = logging.getLogger(__name__)
1111

@@ -32,6 +32,27 @@ class MultiAnnotationDecisionOutputList(BaseModel):
3232
decisions: List[MultiAnnotationDecisionOutput]
3333

3434

35+
class AnalysisAnnotations(BaseModel):
36+
"""Annotations for a single analysis in study-level response."""
37+
annotation_name: str
38+
include: bool
39+
reasoning: str
40+
inclusion_criteria_applied: List[str] = []
41+
exclusion_criteria_applied: List[str] = []
42+
43+
44+
class StudyAnalysisDecision(BaseModel):
45+
"""Decision for a single analysis in study-level response."""
46+
analysis_id: str
47+
annotations: List[AnalysisAnnotations]
48+
49+
50+
class StudyMultiAnnotationOutput(BaseModel):
51+
"""Output schema for study-level multi-annotation decisions."""
52+
study_id: str
53+
decisions: List[StudyAnalysisDecision]
54+
55+
3556
class AnnotationClient:
3657
"""LLM client for making annotation decisions."""
3758

@@ -96,105 +117,23 @@ def _generate_function_schema(
96117
}
97118
}
98119

99-
def make_decision(
100-
self,
101-
metadata: AnalysisMetadata,
102-
criteria: AnnotationCriteriaConfig,
103-
model: str = "gpt-4o-mini"
104-
) -> AnnotationDecision:
105-
"""
106-
Make a decision about whether an analysis should be included in an annotation.
107-
108-
Args:
109-
metadata: Analysis metadata
110-
criteria: Annotation criteria configuration
111-
model: LLM model to use
112-
113-
Returns:
114-
Annotation decision with inclusion boolean and reasoning
115-
"""
116-
try:
117-
# Create the prompt
118-
from .prompts import create_annotation_prompt
119-
# Get metadata_fields from the criteria or use default
120-
metadata_fields = getattr(criteria, 'metadata_fields', None)
121-
prompt = create_annotation_prompt(metadata, criteria, metadata_fields)
122-
123-
# Generate function schema from Pydantic model
124-
func_name = "make_annotation_decision"
125-
function_schema = self._generate_function_schema(
126-
AnnotationDecisionOutput,
127-
func_name
128-
)
129-
130-
# Call the LLM API with function calling
131-
response = self._client.client.chat.completions.create(
132-
model=model,
133-
messages=[
134-
{
135-
"role": "system",
136-
"content": (
137-
"You are a neuroimaging meta-analysis expert. "
138-
"Respond using the make_annotation_decision function."
139-
)
140-
},
141-
{
142-
"role": "user",
143-
"content": prompt
144-
}
145-
],
146-
functions=[function_schema],
147-
function_call={"name": func_name}
148-
)
149-
150-
# Extract the function call result
151-
function_call = response.choices[0].message.function_call
152-
if not function_call:
153-
raise ValueError("No function call returned from API")
154-
155-
# Parse the result
156-
result_dict = json.loads(function_call.arguments)
157-
decision_output = AnnotationDecisionOutput(**result_dict)
158-
159-
# Create the annotation decision
160-
decision = AnnotationDecision(
161-
annotation_name=criteria.name,
162-
analysis_id=metadata.analysis_id,
163-
study_id=metadata.study_id,
164-
include=decision_output.include,
165-
reasoning=decision_output.reasoning,
166-
model_used=model,
167-
inclusion_criteria_applied=decision_output.inclusion_criteria_applied,
168-
exclusion_criteria_applied=decision_output.exclusion_criteria_applied
169-
)
170-
171-
return decision
172-
173-
except Exception as e:
174-
logger.error(f"Error making annotation decision: {e}")
175-
# Return a default decision (exclude) in case of error
176-
return AnnotationDecision(
177-
annotation_name=criteria.name,
178-
analysis_id=metadata.analysis_id,
179-
study_id=metadata.study_id,
180-
include=False,
181-
reasoning=f"Error in decision making: {str(e)}",
182-
model_used=model
183-
)
184-
185120
def make_multi_decision(
186121
self,
187122
metadata: AnalysisMetadata,
188123
criteria_list: List[AnnotationCriteriaConfig],
189-
model: str = "gpt-4o-mini"
124+
model: str = "gpt-4o-mini",
125+
prompt_type: str = "multi_analysis_table",
126+
study_group: StudyAnalysisGroup = None
190127
) -> List[AnnotationDecision]:
191128
"""
192129
Make decisions about whether an analysis should be included in multiple annotations.
193130
194131
Args:
195-
metadata: Analysis metadata
132+
metadata: Analysis metadata (used for multi_analysis_table)
196133
criteria_list: List of annotation criteria configurations
197134
model: LLM model to use
135+
prompt_type: Type of prompt ("single_analysis" or "multi_analysis")
136+
study_group: Study analysis group (required for multi_analysis)
198137
199138
Returns:
200139
List of annotation decisions
@@ -203,11 +142,27 @@ def make_multi_decision(
203142
return []
204143

205144
try:
206-
# Create the prompt for all annotations at once
207-
from .prompts import create_multi_annotation_prompt
208-
# Get metadata_fields from the first criteria or use default
209-
metadata_fields = getattr(criteria_list[0], 'metadata_fields', None)
210-
prompt = create_multi_annotation_prompt(metadata, criteria_list, metadata_fields)
145+
# Select the appropriate prompt based on prompt_type
146+
if prompt_type == "multi_analysis":
147+
if study_group is None:
148+
raise ValueError(
149+
"study_group is required for multi_analysis prompt type"
150+
)
151+
from .prompts import create_study_multi_annotation_prompt
152+
metadata_fields = getattr(
153+
criteria_list[0], 'metadata_fields', None
154+
)
155+
prompt = create_study_multi_annotation_prompt(
156+
study_group, criteria_list, metadata_fields
157+
)
158+
else: # Default to single_analysis
159+
from .prompts import create_multi_annotation_prompt
160+
metadata_fields = getattr(
161+
criteria_list[0], 'metadata_fields', None
162+
)
163+
prompt = create_multi_annotation_prompt(
164+
metadata, criteria_list, metadata_fields
165+
)
211166

212167
# Generate function schema from Pydantic model
213168
func_name = "make_multi_annotation_decisions"
@@ -243,38 +198,62 @@ def make_multi_decision(
243198

244199
# Parse the result
245200
result_dict = json.loads(function_call.arguments)
246-
decision_list_output = MultiAnnotationDecisionOutputList(**result_dict)
247-
decision_outputs = decision_list_output.decisions
248-
249-
# Create the annotation decisions
201+
202+
# Handle different response formats
250203
decisions = []
251-
for i, decision_output in enumerate(decision_outputs):
252-
if i < len(criteria_list):
253-
criteria = criteria_list[i]
254-
decision = AnnotationDecision(
255-
annotation_name=decision_output.annotation_name or criteria.name,
256-
analysis_id=metadata.analysis_id,
257-
study_id=metadata.study_id,
258-
include=decision_output.include,
259-
reasoning=decision_output.reasoning,
260-
model_used=model,
261-
inclusion_criteria_applied=decision_output.inclusion_criteria_applied,
262-
exclusion_criteria_applied=decision_output.exclusion_criteria_applied
263-
)
204+
if prompt_type == "multi_analysis":
205+
# Parse study-level response
206+
study_output = StudyMultiAnnotationOutput(**result_dict)
207+
for analysis_decision in study_output.decisions:
208+
for annotation_output in analysis_decision.annotations:
209+
decision = AnnotationDecision(
210+
annotation_name=annotation_output.annotation_name,
211+
analysis_id=analysis_decision.analysis_id,
212+
study_id=study_output.study_id,
213+
include=annotation_output.include,
214+
reasoning=annotation_output.reasoning,
215+
model_used=model,
216+
inclusion_criteria_applied=annotation_output.inclusion_criteria_applied,
217+
exclusion_criteria_applied=annotation_output.exclusion_criteria_applied
218+
)
219+
decisions.append(decision)
220+
else:
221+
# Parse table-level response
222+
decision_list_output = MultiAnnotationDecisionOutputList(
223+
**result_dict
224+
)
225+
decision_outputs = decision_list_output.decisions
226+
227+
for i, decision_output in enumerate(decision_outputs):
228+
if i < len(criteria_list):
229+
criteria = criteria_list[i]
230+
decision = AnnotationDecision(
231+
annotation_name=decision_output.annotation_name or criteria.name,
232+
analysis_id=metadata.analysis_id,
233+
study_id=metadata.study_id,
234+
include=decision_output.include,
235+
reasoning=decision_output.reasoning,
236+
model_used=model,
237+
inclusion_criteria_applied=decision_output.inclusion_criteria_applied,
238+
exclusion_criteria_applied=decision_output.exclusion_criteria_applied
239+
)
240+
decisions.append(decision)
241+
242+
# If we didn't get enough responses, fill in with fallback
243+
while len(decisions) < len(criteria_list):
244+
criteria = criteria_list[len(decisions)]
245+
decision = self.make_decision(metadata, criteria, model)
264246
decisions.append(decision)
265247

266-
# If we didn't get enough responses, fill in with individual decisions
267-
while len(decisions) < len(criteria_list):
268-
criteria = criteria_list[len(decisions)]
269-
decision = self.make_decision(metadata, criteria, model)
270-
decisions.append(decision)
271-
272248
return decisions
273249

274250
except Exception as e:
275251
logger.error(f"Error making multi annotation decisions: {e}")
276252
# Return individual decisions as fallback
277-
return [self.make_decision(metadata, criteria, model) for criteria in criteria_list]
253+
return [
254+
self.make_decision(metadata, criteria, model)
255+
for criteria in criteria_list
256+
]
278257

279258
def chat_completion(self, messages, model, response_format=None):
280259
"""

autonima/annotation/processor.py

Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -387,65 +387,6 @@ def _get_annotations_with_complete_results_for_study(
387387

388388
return annotations_with_complete_results
389389

390-
def _process_parallel(self, pairs: List[tuple], model: str, max_workers: int = 4) -> List[AnnotationDecision]:
391-
"""
392-
Process annotation decisions in parallel.
393-
394-
Args:
395-
pairs: List of (metadata, criteria) tuples
396-
model: LLM model to use
397-
max_workers: Maximum number of parallel workers
398-
399-
Returns:
400-
List of annotation decisions
401-
"""
402-
decisions = []
403-
404-
# Update each criteria with the top-level metadata_fields if not already set
405-
updated_pairs = []
406-
for metadata, criteria in pairs:
407-
# If criteria doesn't have metadata_fields set, use the top-level ones
408-
if not criteria.metadata_fields and self.config.metadata_fields:
409-
# Create a copy of the criteria with the top-level metadata_fields
410-
updated_criteria = AnnotationCriteriaConfig(
411-
name=criteria.name,
412-
description=criteria.description,
413-
inclusion_criteria=(self.config.inclusion_criteria + criteria.inclusion_criteria),
414-
exclusion_criteria=(self.config.exclusion_criteria + criteria.exclusion_criteria),
415-
metadata_fields=self.config.metadata_fields
416-
)
417-
updated_pairs.append((metadata, updated_criteria))
418-
else:
419-
updated_pairs.append((metadata, criteria))
420-
421-
with ThreadPoolExecutor(max_workers=max_workers) as executor:
422-
# Submit all tasks
423-
future_to_pair = {
424-
executor.submit(self._process_single_decision, metadata, criteria, model): (metadata, criteria)
425-
for metadata, criteria in updated_pairs
426-
}
427-
428-
# Collect results
429-
for future in tqdm(as_completed(future_to_pair), total=len(updated_pairs), desc="Processing annotations"):
430-
try:
431-
decision = future.result()
432-
decisions.append(decision)
433-
except Exception as e:
434-
metadata, criteria = future_to_pair[future]
435-
logger.error(f"Error processing annotation for {criteria.name}: {e}")
436-
# Create a default decision (exclude) in case of error
437-
decision = AnnotationDecision(
438-
annotation_name=criteria.name,
439-
analysis_id=metadata.analysis_id,
440-
study_id=metadata.study_id,
441-
include=False,
442-
reasoning=f"Error in processing: {str(e)}",
443-
model_used=model
444-
)
445-
decisions.append(decision)
446-
447-
return decisions
448-
449390
def _process_single_decision(self, metadata: AnalysisMetadata, criteria: AnnotationCriteriaConfig, model: str) -> AnnotationDecision:
450391
"""
451392
Process a single annotation decision.

0 commit comments

Comments
 (0)