1212# Initial GRADE points by study type (4=High, 3=Moderate, 2=Low, 1=Very Low)
1313_INITIAL_POINTS : Dict [str , int ] = {
1414 "RCT" : 4 ,
15- "SYSTEMATIC_REVIEW" : 4 , # Starts High (synthesizes RCTs or best available evidence)
16- "META_ANALYSIS" : 4 , # Starts High
17- "NMA" : 4 , # Network meta-analysis: starts High
15+ "SYSTEMATIC_REVIEW" : 4 , # Starts High (synthesizes RCTs or best available evidence)
16+ "META_ANALYSIS" : 4 , # Starts High
17+ "NMA" : 4 , # Network meta-analysis: starts High
1818 "COHORT" : 2 ,
1919 "CASE_CONTROL" : 2 ,
20- "CROSS_SECTIONAL" : 2 , # Observational: starts Low
21- "NARRATIVE_REVIEW" : 1 , # Expert synthesis without systematic search: Very Low
20+ "CROSS_SECTIONAL" : 2 , # Observational: starts Low
21+ "NARRATIVE_REVIEW" : 1 , # Expert synthesis without systematic search: Very Low
2222 "CASE_REPORT" : 1 ,
23- "GUIDELINE" : 3 , # Typically based on SR: starts Moderate
24- "EXPERT_OPINION" : 1 , # No systematic search: Very Low
23+ "GUIDELINE" : 3 , # Typically based on SR: starts Moderate
24+ "EXPERT_OPINION" : 1 , # No systematic search: Very Low
2525}
2626
2727# Mapping from GRADE codes to human-readable study type labels
@@ -123,7 +123,9 @@ def __init__(self, llm, tools: List[Any] = None):
123123 self .prompt_template = self ._load_prompt ()
124124
125125 def _load_prompt (self ) -> str :
126- prompt_path = Path (__file__ ).parent .parent / "config" / "prompts" / "appraise_agent.txt"
126+ prompt_path = (
127+ Path (__file__ ).parent .parent / "config" / "prompts" / "appraise_agent.txt"
128+ )
127129 with open (prompt_path , "r" , encoding = "utf-8" ) as f :
128130 return f .read ()
129131
@@ -137,7 +139,11 @@ def _format_evidence_list(self, evidence_list) -> str:
137139 for i , e in enumerate (evidence_list ):
138140 abstract_preview = (getattr (e , "abstract" , "" ) or "" )[:200 ]
139141 study_type_hint = getattr (e , "study_type" , "" ) or ""
140- hint_str = f"\n Source DB study_type hint: { study_type_hint } " if study_type_hint else ""
142+ hint_str = (
143+ f"\n Source DB study_type hint: { study_type_hint } "
144+ if study_type_hint
145+ else ""
146+ )
141147 parts .append (
142148 f"Evidence { i + 1 } :\n "
143149 f"Title: { e .title } \n "
@@ -157,7 +163,9 @@ def _appraise_batch(self, evidence_subset: list, backtrack_context: str) -> dict
157163 response = self .llm .invoke (prompt )
158164 return self ._parse_json (response .content )
159165
160- def _merge_appraisal_dicts (self , results : List [dict ], batch_sizes : List [int ]) -> dict :
166+ def _merge_appraisal_dicts (
167+ self , results : List [dict ], batch_sizes : List [int ]
168+ ) -> dict :
161169 """
162170 Merge multiple batch appraisal results into a single appraisal dict.
163171
@@ -175,8 +183,12 @@ def _merge_appraisal_dicts(self, results: List[dict], batch_sizes: List[int]) ->
175183 global_id += 1
176184
177185 # Conflict: pessimistic — YES wins
178- merged_conflict = {"conflicts_exist" : "NO" , "conflict_type" : "NA" ,
179- "conflict_severity" : "NA" , "conflict_description" : None }
186+ merged_conflict = {
187+ "conflicts_exist" : "NO" ,
188+ "conflict_type" : "NA" ,
189+ "conflict_severity" : "NA" ,
190+ "conflict_description" : None ,
191+ }
180192 for result in results :
181193 c = result .get ("conflict_assessment" , {})
182194 if c .get ("conflicts_exist" ) == "YES" :
@@ -194,7 +206,9 @@ def _merge_appraisal_dicts(self, results: List[dict], batch_sizes: List[int]) ->
194206 best_numerical = n
195207
196208 # Summary: join all
197- summaries = [r .get ("evidence_summary" , "" ) for r in results if r .get ("evidence_summary" )]
209+ summaries = [
210+ r .get ("evidence_summary" , "" ) for r in results if r .get ("evidence_summary" )
211+ ]
198212 merged_summary = " | " .join (summaries )
199213
200214 return {
@@ -235,8 +249,12 @@ def execute(self, state: WorkflowState) -> Dict[str, Any]:
235249 for batch in batches
236250 ]
237251 batch_results = [f .result () for f in futures ]
238- print (f"[TIMING] Appraise parallel batches ({ len (batches )} ×{ _BATCH_SIZE } ): { time .time ()- t0 :.1f} s" )
239- appraisal_dict = self ._merge_appraisal_dicts (batch_results , [len (b ) for b in batches ])
252+ print (
253+ f"[TIMING] Appraise parallel batches ({ len (batches )} ×{ _BATCH_SIZE } ): { time .time ()- t0 :.1f} s"
254+ )
255+ appraisal_dict = self ._merge_appraisal_dicts (
256+ batch_results , [len (b ) for b in batches ]
257+ )
240258 print (f"[PARALLEL-APPRAISE] { len (batches )} batches completed in parallel." )
241259 else :
242260 # Single batch (≤5 articles): no need for parallel overhead
@@ -260,27 +278,33 @@ def execute(self, state: WorkflowState) -> Dict[str, Any]:
260278 graded_evidence .append (evidence )
261279
262280 # Build rich rationale record for downstream consumers (including Judge)
263- initial_grade = _POINTS_TO_GRADE .get (_INITIAL_POINTS .get (study_type , 1 ), "Very Low" )
264- grade_rationales .append ({
265- "evidence_id" : i + 1 ,
266- "title" : evidence .title ,
267- "study_type" : study_type ,
268- "initial_grade" : initial_grade ,
269- "risk_of_bias" : appraisal .get ("risk_of_bias" , "NOT_SERIOUS" ),
270- "inconsistency" : appraisal .get ("inconsistency" , "NA" ),
271- "indirectness" : appraisal .get ("indirectness" , "NOT_SERIOUS" ),
272- "imprecision" : appraisal .get ("imprecision" , "NOT_SERIOUS" ),
273- "publication_bias" : appraisal .get ("publication_bias" , "UNDETECTED" ),
274- "large_effect" : appraisal .get ("large_effect" , "NA" ),
275- "dose_response" : appraisal .get ("dose_response" , "NA" ),
276- "computed_grade" : computed_grade ,
277- "rationale" : appraisal .get ("rationale" , "" ),
278- })
281+ initial_grade = _POINTS_TO_GRADE .get (
282+ _INITIAL_POINTS .get (study_type , 1 ), "Very Low"
283+ )
284+ grade_rationales .append (
285+ {
286+ "evidence_id" : i + 1 ,
287+ "title" : evidence .title ,
288+ "study_type" : study_type ,
289+ "initial_grade" : initial_grade ,
290+ "risk_of_bias" : appraisal .get ("risk_of_bias" , "NOT_SERIOUS" ),
291+ "inconsistency" : appraisal .get ("inconsistency" , "NA" ),
292+ "indirectness" : appraisal .get ("indirectness" , "NOT_SERIOUS" ),
293+ "imprecision" : appraisal .get ("imprecision" , "NOT_SERIOUS" ),
294+ "publication_bias" : appraisal .get ("publication_bias" , "UNDETECTED" ),
295+ "large_effect" : appraisal .get ("large_effect" , "NA" ),
296+ "dose_response" : appraisal .get ("dose_response" , "NA" ),
297+ "computed_grade" : computed_grade ,
298+ "rationale" : appraisal .get ("rationale" , "" ),
299+ }
300+ )
279301
280302 # --- Conflict assessment ---
281303 conflict = appraisal_dict .get ("conflict_assessment" , {})
282304 has_conflict = conflict .get ("conflicts_exist" ) == "YES"
283- conflict_description = conflict .get ("conflict_description" ) if has_conflict else None
305+ conflict_description = (
306+ conflict .get ("conflict_description" ) if has_conflict else None
307+ )
284308
285309 # --- Numerical assessment ---
286310 numerical = appraisal_dict .get ("numerical_assessment" , {})
@@ -304,5 +328,6 @@ def execute(self, state: WorkflowState) -> Dict[str, Any]:
304328 "note" : numerical .get ("note" , "" ),
305329 },
306330 # True only when there is a MAJOR conflict (influences scheduling)
307- "bias_inconsistency" : has_conflict and conflict .get ("conflict_severity" ) == "MAJOR" ,
331+ "bias_inconsistency" : has_conflict
332+ and conflict .get ("conflict_severity" ) == "MAJOR" ,
308333 }
0 commit comments