55from typing import List , Dict , Any
66from pydantic import BaseModel
77from ..llm .client import GenericLLMClient
8- from .schema import AnalysisMetadata , AnnotationCriteriaConfig , AnnotationDecision
8+ from .schema import AnalysisMetadata , AnnotationCriteriaConfig , AnnotationDecision , StudyAnalysisGroup
99
1010logger = 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+
3556class 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 """
0 commit comments