2024-07-05 17:42:00 +08:00
import io
import json
from abc import abstractmethod
from pathlib import Path
2024-11-25 16:27:34 +08:00
from typing import Tuple
2024-07-05 17:42:00 +08:00
import pandas as pd
from jinja2 import Environment , StrictUndefined
2024-11-25 16:27:34 +08:00
from rdagent.components.coder.factor_coder.config import FACTOR_COSTEER_SETTINGS
2024-07-05 17:42:00 +08:00
from rdagent.components.coder.factor_coder.factor import FactorTask
2024-07-17 15:00:13 +08:00
from rdagent.core.experiment import Task , Workspace
2024-07-05 17:42:00 +08:00
from rdagent.core.prompts import Prompts
2024-10-14 17:34:09 +08:00
from rdagent.oai.llm_conf import LLM_SETTINGS
2024-07-05 17:42:00 +08:00
from rdagent.oai.llm_utils import APIBackend
2024-11-25 16:27:34 +08:00
evaluate_prompts = Prompts ( file_path = Path ( __file__ ) . parent / "prompts.yaml" )
2024-07-05 17:42:00 +08:00
2024-11-25 16:27:34 +08:00
class FactorEvaluator :
"""Although the init method is same to Evaluator, but we want to emphasize they are different"""
def __init__ ( self , scen = None ) -> None :
self . scen = scen
2024-07-05 17:42:00 +08:00
@abstractmethod
def evaluate (
self ,
target_task : Task ,
2024-07-17 15:00:13 +08:00
implementation : Workspace ,
gt_implementation : Workspace ,
2024-07-05 17:42:00 +08:00
** kwargs ,
) -> Tuple [ str , object ]:
"""You can get the dataframe by
.. code-block:: python
_, gen_df = implementation.execute()
_, gt_df = gt_implementation.execute()
Returns
-------
Tuple[str, object]
- str: the text-based description of the evaluation result
- object: a comparable metric (bool, integer, float ...) None for evaluator with only text-based result
"""
raise NotImplementedError ( "Please implement the `evaluator` method" )
2024-07-17 15:00:13 +08:00
def _get_df ( self , gt_implementation : Workspace , implementation : Workspace ):
2024-07-05 17:42:00 +08:00
if gt_implementation is not None :
_ , gt_df = gt_implementation . execute ()
if isinstance ( gt_df , pd . Series ):
gt_df = gt_df . to_frame ( "gt_factor" )
if isinstance ( gt_df , pd . DataFrame ):
gt_df = gt_df . sort_index ()
else :
gt_df = None
_ , gen_df = implementation . execute ()
if isinstance ( gen_df , pd . Series ):
gen_df = gen_df . to_frame ( "source_factor" )
if isinstance ( gen_df , pd . DataFrame ):
gen_df = gen_df . sort_index ()
return gt_df , gen_df
def __str__ ( self ) -> str :
return self . __class__ . __name__
class FactorCodeEvaluator ( FactorEvaluator ):
def evaluate (
self ,
target_task : FactorTask ,
2024-07-17 15:00:13 +08:00
implementation : Workspace ,
2024-07-05 17:42:00 +08:00
execution_feedback : str ,
2024-11-25 16:27:34 +08:00
value_feedback : str = "" ,
2024-07-17 15:00:13 +08:00
gt_implementation : Workspace = None ,
2024-07-05 17:42:00 +08:00
** kwargs ,
):
2024-07-15 08:28:34 +00:00
factor_information = target_task . get_task_information ()
2025-01-17 22:53:05 +08:00
code = implementation . all_codes
2024-07-05 17:42:00 +08:00
system_prompt = (
Environment ( undefined = StrictUndefined )
. from_string ( evaluate_prompts [ "evaluator_code_feedback_v1_system" ])
2024-09-29 18:43:17 +08:00
. render (
2024-10-09 18:56:37 +08:00
scenario = (
2024-11-06 13:14:35 +08:00
self . scen . get_scenario_all_desc (
target_task ,
filtered_tag = "feature" ,
2024-11-25 16:27:34 +08:00
simple_background = FACTOR_COSTEER_SETTINGS . simple_background ,
2024-11-06 13:14:35 +08:00
)
2024-10-09 18:56:37 +08:00
if self . scen is not None
else "No scenario description."
)
2024-09-29 18:43:17 +08:00
)
2024-07-05 17:42:00 +08:00
)
execution_feedback_to_render = execution_feedback
for _ in range ( 10 ): # 10 times to split the content is enough
user_prompt = (
Environment ( undefined = StrictUndefined )
. from_string (
evaluate_prompts [ "evaluator_code_feedback_v1_user" ],
)
. render (
factor_information = factor_information ,
code = code ,
execution_feedback = execution_feedback_to_render ,
2024-11-25 16:27:34 +08:00
value_feedback = value_feedback ,
2024-07-05 17:42:00 +08:00
gt_code = gt_implementation . code if gt_implementation else None ,
)
)
if (
APIBackend () . build_messages_and_calculate_token (
user_prompt = user_prompt ,
system_prompt = system_prompt ,
)
2024-10-14 17:34:09 +08:00
> LLM_SETTINGS . chat_token_limit
2024-07-05 17:42:00 +08:00
):
execution_feedback_to_render = execution_feedback_to_render [ len ( execution_feedback_to_render ) // 2 :]
else :
break
critic_response = APIBackend () . build_messages_and_create_chat_completion (
user_prompt = user_prompt ,
system_prompt = system_prompt ,
json_mode = False ,
)
return critic_response , None
2024-10-16 16:00:55 +08:00
class FactorInfEvaluator ( FactorEvaluator ):
def evaluate (
self ,
implementation : Workspace ,
gt_implementation : Workspace ,
) -> Tuple [ str , object ]:
_ , gen_df = self . _get_df ( gt_implementation , implementation )
if gen_df is None :
return (
"The source dataframe is None. Please check the implementation." ,
False ,
)
INF_count = gen_df . isin ([ float ( "inf" ), - float ( "inf" )]) . sum () . sum ()
if INF_count == 0 :
return "The source dataframe does not have any infinite values." , True
else :
return (
f "The source dataframe has { INF_count } infinite values. Please check the implementation." ,
False ,
)
2024-07-05 17:42:00 +08:00
class FactorSingleColumnEvaluator ( FactorEvaluator ):
def evaluate (
self ,
2024-07-17 15:00:13 +08:00
implementation : Workspace ,
gt_implementation : Workspace ,
2024-07-05 17:42:00 +08:00
) -> Tuple [ str , object ]:
_ , gen_df = self . _get_df ( gt_implementation , implementation )
2024-08-20 12:27:13 +08:00
if gen_df is None :
return (
"The source dataframe is None. Please check the implementation." ,
False ,
)
2024-07-05 17:42:00 +08:00
if len ( gen_df . columns ) == 1 :
return "The source dataframe has only one column which is correct." , True
else :
return (
"The source dataframe has more than one column. Please check the implementation. We only evaluate the first column." ,
False ,
)
class FactorOutputFormatEvaluator ( FactorEvaluator ):
def evaluate (
self ,
2024-07-17 15:00:13 +08:00
implementation : Workspace ,
gt_implementation : Workspace ,
2024-07-05 17:42:00 +08:00
) -> Tuple [ str , object ]:
gt_df , gen_df = self . _get_df ( gt_implementation , implementation )
if gen_df is None :
return (
"The source dataframe is None. Skip the evaluation of the output format." ,
False ,
)
buffer = io . StringIO ()
gen_df . info ( buf = buffer )
2024-11-06 13:14:35 +08:00
gen_df_info_str = f "The user is currently working on a feature related task. \n The output dataframe info is: \n { buffer . getvalue () } "
2024-07-05 17:42:00 +08:00
system_prompt = (
Environment ( undefined = StrictUndefined )
. from_string (
evaluate_prompts [ "evaluator_output_format_system" ],
)
2024-09-29 18:43:17 +08:00
. render (
2024-10-09 18:56:37 +08:00
scenario = (
2024-11-06 13:14:35 +08:00
self . scen . get_scenario_all_desc ( implementation . target_task , filtered_tag = "feature" )
2024-10-09 18:56:37 +08:00
if self . scen is not None
else "No scenario description."
)
2024-09-29 18:43:17 +08:00
)
2024-07-05 17:42:00 +08:00
)
2024-08-02 15:04:49 +08:00
# TODO: with retry_context(retry_n=3, except_list=[KeyError]):
max_attempts = 3
attempts = 0
final_evaluation_dict = None
while attempts < max_attempts :
try :
2024-10-15 12:21:33 +08:00
api = APIBackend () if attempts == 0 else APIBackend ( use_chat_cache = False )
resp = api . build_messages_and_create_chat_completion (
2024-08-02 15:04:49 +08:00
user_prompt = gen_df_info_str , system_prompt = system_prompt , json_mode = True
)
resp_dict = json . loads ( resp )
2024-10-15 12:41:32 +08:00
resp_dict [ "output_format_decision" ] = str ( resp_dict [ "output_format_decision" ]) . lower () in [ "true" , "1" ]
2024-08-02 15:04:49 +08:00
return (
2024-10-23 16:47:36 +08:00
str ( resp_dict [ "output_format_feedback" ]),
2024-08-02 15:04:49 +08:00
resp_dict [ "output_format_decision" ],
)
2024-11-26 12:02:09 +08:00
except ( KeyError , json . JSONDecodeError ) as e :
2024-08-02 15:04:49 +08:00
attempts += 1
if attempts >= max_attempts :
raise KeyError (
2024-11-26 12:02:09 +08:00
"Wrong JSON Response or missing 'output_format_decision' or 'output_format_feedback' key after multiple attempts."
2024-08-02 15:04:49 +08:00
) from e
return "Failed to evaluate output format after multiple attempts." , False
2024-07-05 17:42:00 +08:00
2024-07-15 08:28:34 +00:00
class FactorDatetimeDailyEvaluator ( FactorEvaluator ):
def evaluate (
self ,
2024-07-17 15:00:13 +08:00
implementation : Workspace ,
gt_implementation : Workspace ,
2024-07-15 08:28:34 +00:00
) -> Tuple [ str | object ]:
_ , gen_df = self . _get_df ( gt_implementation , implementation )
if gen_df is None :
return "The source dataframe is None. Skip the evaluation of the datetime format." , False
if "datetime" not in gen_df . index . names :
return "The source dataframe does not have a datetime index. Please check the implementation." , False
2024-07-15 09:40:40 +00:00
try :
pd . to_datetime ( gen_df . index . get_level_values ( "datetime" ))
except Exception :
return (
2024-10-09 18:56:37 +08:00
f "The source dataframe has a datetime index but it is not in the correct format (maybe a regular string or other objects). Please check the implementation. \n The head of the output dataframe is: \n { gen_df . head () } " ,
2024-07-15 09:40:40 +00:00
False ,
)
2024-10-15 12:41:32 +08:00
time_diff = pd . to_datetime ( gen_df . index . get_level_values ( "datetime" )) . to_series () . diff () . dropna () . unique ()
2024-07-15 08:28:34 +00:00
if pd . Timedelta ( minutes = 1 ) in time_diff :
return (
"The generated dataframe is not daily. The implementation is definitely wrong. Please check the implementation." ,
False ,
)
return "The generated dataframe is daily." , True
2024-07-05 17:42:00 +08:00
class FactorRowCountEvaluator ( FactorEvaluator ):
def evaluate (
self ,
2024-07-17 15:00:13 +08:00
implementation : Workspace ,
gt_implementation : Workspace ,
2024-07-05 17:42:00 +08:00
) -> Tuple [ str , object ]:
gt_df , gen_df = self . _get_df ( gt_implementation , implementation )
2024-08-20 12:27:13 +08:00
if gen_df is None :
return (
"The source dataframe is None. Please check the implementation." ,
False ,
)
2024-09-29 09:59:54 +08:00
ratio = min ( len ( gen_df ), len ( gt_df )) / max ( len ( gen_df ), len ( gt_df ))
2024-09-27 09:34:55 +08:00
return (
2024-10-09 18:56:37 +08:00
(
f "The ratio of rows count in the source dataframe to the ground truth dataframe is { ratio : .2f } . "
+ "Please verify the implementation. "
if ratio <= 0.99
else ""
),
2024-09-27 09:34:55 +08:00
ratio ,
)
2024-07-05 17:42:00 +08:00
class FactorIndexEvaluator ( FactorEvaluator ):
def evaluate (
self ,
2024-07-17 15:00:13 +08:00
implementation : Workspace ,
gt_implementation : Workspace ,
2024-07-05 17:42:00 +08:00
) -> Tuple [ str , object ]:
gt_df , gen_df = self . _get_df ( gt_implementation , implementation )
2024-08-20 12:27:13 +08:00
if gen_df is None :
return (
"The source dataframe is None. Please check the implementation." ,
False ,
)
2024-09-27 09:34:55 +08:00
gen_index_set , gt_index_set = set ( gen_df . index ), set ( gt_df . index )
similarity = len ( gen_index_set . intersection ( gt_index_set )) / len ( gen_index_set . union ( gt_index_set ))
return (
2024-10-09 18:56:37 +08:00
(
f "The source dataframe and the ground truth dataframe have different index with a similarity of { similarity : .2% } . The similarity is calculated by the number of shared indices divided by the union indices. "
+ "Please check the implementation."
if similarity <= 0.99
else ""
),
2024-09-27 09:34:55 +08:00
similarity ,
)
2024-07-05 17:42:00 +08:00
class FactorMissingValuesEvaluator ( FactorEvaluator ):
def evaluate (
self ,
2024-07-17 15:00:13 +08:00
implementation : Workspace ,
gt_implementation : Workspace ,
2024-07-05 17:42:00 +08:00
) -> Tuple [ str , object ]:
gt_df , gen_df = self . _get_df ( gt_implementation , implementation )
2024-08-20 12:27:13 +08:00
if gen_df is None :
return (
"The source dataframe is None. Please check the implementation." ,
False ,
)
2024-07-05 17:42:00 +08:00
if gen_df . isna () . sum () . sum () == gt_df . isna () . sum () . sum ():
return "Both dataframes have the same missing values." , True
else :
return (
f "The dataframes do not have the same missing values. The source dataframe has { gen_df . isna () . sum () . sum () } missing values, while the ground truth dataframe has { gt_df . isna () . sum () . sum () } missing values. Please check the implementation." ,
False ,
)
2024-09-29 09:59:54 +08:00
class FactorEqualValueRatioEvaluator ( FactorEvaluator ):
2024-07-05 17:42:00 +08:00
def evaluate (
self ,
2024-07-17 15:00:13 +08:00
implementation : Workspace ,
gt_implementation : Workspace ,
2024-07-05 17:42:00 +08:00
) -> Tuple [ str , object ]:
gt_df , gen_df = self . _get_df ( gt_implementation , implementation )
2024-08-20 12:27:13 +08:00
if gen_df is None :
return (
"The source dataframe is None. Please check the implementation." ,
- 1 ,
)
2024-07-05 17:42:00 +08:00
try :
close_values = gen_df . sub ( gt_df ) . abs () . lt ( 1e-6 )
result_int = close_values . astype ( int )
pos_num = result_int . sum () . sum ()
acc_rate = pos_num / close_values . size
except :
close_values = gen_df
if close_values . all () . iloc [ 0 ]:
return (
"All values in the dataframes are equal within the tolerance of 1e-6." ,
acc_rate ,
)
else :
return (
"Some values differ by more than the tolerance of 1e-6. Check for rounding errors or differences in the calculation methods." ,
acc_rate ,
)
class FactorCorrelationEvaluator ( FactorEvaluator ):
def __init__ ( self , hard_check : bool , * args , ** kwargs ) -> None :
super () . __init__ ( * args , ** kwargs )
self . hard_check = hard_check
def evaluate (
self ,
2024-07-17 15:00:13 +08:00
implementation : Workspace ,
gt_implementation : Workspace ,
2024-07-05 17:42:00 +08:00
) -> Tuple [ str , object ]:
gt_df , gen_df = self . _get_df ( gt_implementation , implementation )
2024-08-20 12:27:13 +08:00
if gen_df is None :
return (
"The source dataframe is None. Please check the implementation." ,
False ,
)
2024-07-05 17:42:00 +08:00
concat_df = pd . concat ([ gen_df , gt_df ], axis = 1 )
concat_df . columns = [ "source" , "gt" ]
ic = concat_df . groupby ( "datetime" ) . apply ( lambda df : df [ "source" ] . corr ( df [ "gt" ])) . dropna () . mean ()
ric = (
concat_df . groupby ( "datetime" )
. apply ( lambda df : df [ "source" ] . corr ( df [ "gt" ], method = "spearman" ))
. dropna ()
. mean ()
)
if self . hard_check :
if ic > 0.99 and ric > 0.99 :
return (
f "The dataframes are highly correlated. The ic is { ic : .6f } and the rankic is { ric : .6f } ." ,
True ,
)
else :
return (
f "The dataframes are not sufficiently high correlated. The ic is { ic : .6f } and the rankic is { ric : .6f } . Investigate the factors that might be causing the discrepancies and ensure that the logic of the factor calculation is consistent." ,
False ,
)
else :
return f "The ic is ( { ic : .6f } ) and the rankic is ( { ric : .6f } )." , ic
class FactorValueEvaluator ( FactorEvaluator ):
def evaluate (
self ,
2024-07-17 15:00:13 +08:00
implementation : Workspace ,
gt_implementation : Workspace ,
2024-09-11 15:26:52 +08:00
version : int = 1 , # 1 for qlib factors and 2 for kaggle factors
2024-07-05 17:42:00 +08:00
** kwargs ,
) -> Tuple :
conclusions = []
2024-07-20 12:31:40 +08:00
# Initialize result variables
2024-09-27 10:42:12 +08:00
row_result = 0
index_result = 0
2024-07-20 12:31:40 +08:00
output_format_result = None
equal_value_ratio_result = 0
high_correlation_result = False
2024-09-29 09:59:54 +08:00
row_result = None
2024-07-20 12:31:40 +08:00
2024-09-11 15:26:52 +08:00
# Check if both dataframe has only one columns Mute this since factor task might generate more than one columns now
if version == 1 :
feedback_str , _ = FactorSingleColumnEvaluator ( self . scen ) . evaluate ( implementation , gt_implementation )
conclusions . append ( feedback_str )
2024-09-25 16:41:08 +08:00
elif version == 2 :
input_shape = self . scen . input_shape
_ , gen_df = self . _get_df ( gt_implementation , implementation )
if gen_df . shape [ - 1 ] > input_shape [ - 1 ]:
conclusions . append (
"Output dataframe has more columns than input feature which is not acceptable in feature processing tasks. Please check the implementation to avoid generating too many columns. Consider this implementation as a failure."
)
2024-07-05 17:42:00 +08:00
2024-10-16 16:00:55 +08:00
feedback_str , inf_evaluate_res = FactorInfEvaluator ( self . scen ) . evaluate ( implementation , gt_implementation )
conclusions . append ( feedback_str )
2024-07-05 17:42:00 +08:00
# Check if the index of the dataframe is ("datetime", "instrument")
feedback_str , _ = FactorOutputFormatEvaluator ( self . scen ) . evaluate ( implementation , gt_implementation )
conclusions . append ( feedback_str )
2024-09-11 15:26:52 +08:00
if version == 1 :
feedback_str , daily_check_result = FactorDatetimeDailyEvaluator ( self . scen ) . evaluate (
implementation , gt_implementation
)
conclusions . append ( feedback_str )
else :
daily_check_result = None
2024-07-15 08:28:34 +00:00
2024-09-27 09:34:55 +08:00
# Check dataframe format
2024-07-05 17:42:00 +08:00
if gt_implementation is not None :
2024-09-27 09:34:55 +08:00
feedback_str , row_result = FactorRowCountEvaluator ( self . scen ) . evaluate ( implementation , gt_implementation )
2024-07-05 17:42:00 +08:00
conclusions . append ( feedback_str )
2024-09-27 09:34:55 +08:00
feedback_str , index_result = FactorIndexEvaluator ( self . scen ) . evaluate ( implementation , gt_implementation )
2024-07-05 17:42:00 +08:00
conclusions . append ( feedback_str )
2024-07-26 12:12:16 +08:00
feedback_str , output_format_result = FactorMissingValuesEvaluator ( self . scen ) . evaluate (
implementation , gt_implementation
)
2024-07-05 17:42:00 +08:00
conclusions . append ( feedback_str )
2024-09-29 09:59:54 +08:00
feedback_str , equal_value_ratio_result = FactorEqualValueRatioEvaluator ( self . scen ) . evaluate (
2024-07-05 17:42:00 +08:00
implementation , gt_implementation
)
conclusions . append ( feedback_str )
2024-09-27 09:34:55 +08:00
if index_result > 0.99 :
2024-07-05 17:42:00 +08:00
feedback_str , high_correlation_result = FactorCorrelationEvaluator (
hard_check = True , scen = self . scen
) . evaluate ( implementation , gt_implementation )
else :
high_correlation_result = False
feedback_str = "The source dataframe and the ground truth dataframe have different index. Give up comparing the values and correlation because it's useless"
conclusions . append ( feedback_str )
# Combine all conclusions into a single string
conclusion_str = " \n " . join ( conclusions )
2024-07-16 11:37:38 +08:00
if gt_implementation is not None and ( equal_value_ratio_result > 0.99 ) or high_correlation_result :
decision_from_value_check = True
2024-09-29 09:59:54 +08:00
elif (
row_result is not None
and row_result <= 0.99
or output_format_result is False
or daily_check_result is False
2024-10-16 16:00:55 +08:00
or inf_evaluate_res is False
2024-09-29 09:59:54 +08:00
):
decision_from_value_check = False
else :
decision_from_value_check = None
2024-07-16 11:37:38 +08:00
return conclusion_str , decision_from_value_check
2024-07-05 17:42:00 +08:00
2024-11-25 16:27:34 +08:00
class FactorFinalDecisionEvaluator ( FactorEvaluator ):
2024-07-05 17:42:00 +08:00
def evaluate (
self ,
target_task : FactorTask ,
execution_feedback : str ,
value_feedback : str ,
code_feedback : str ,
** kwargs ,
) -> Tuple :
system_prompt = (
Environment ( undefined = StrictUndefined )
. from_string ( evaluate_prompts [ "evaluator_final_decision_v1_system" ])
2024-09-29 18:43:17 +08:00
. render (
2024-10-09 18:56:37 +08:00
scenario = (
2024-11-06 13:14:35 +08:00
self . scen . get_scenario_all_desc ( target_task , filtered_tag = "feature" )
2024-10-09 18:56:37 +08:00
if self . scen is not None
else "No scenario description."
)
2024-09-29 18:43:17 +08:00
)
2024-07-05 17:42:00 +08:00
)
execution_feedback_to_render = execution_feedback
for _ in range ( 10 ): # 10 times to split the content is enough
user_prompt = (
Environment ( undefined = StrictUndefined )
. from_string (
evaluate_prompts [ "evaluator_final_decision_v1_user" ],
)
. render (
2024-07-15 08:28:34 +00:00
factor_information = target_task . get_task_information (),
2024-07-05 17:42:00 +08:00
execution_feedback = execution_feedback_to_render ,
code_feedback = code_feedback ,
2024-11-25 16:27:34 +08:00
value_feedback = (
2024-07-05 17:42:00 +08:00
value_feedback
if value_feedback is not None
else "No Ground Truth Value provided, so no evaluation on value is performed."
),
)
)
if (
APIBackend () . build_messages_and_calculate_token (
user_prompt = user_prompt ,
system_prompt = system_prompt ,
)
2024-10-14 17:34:09 +08:00
> LLM_SETTINGS . chat_token_limit
2024-07-05 17:42:00 +08:00
):
execution_feedback_to_render = execution_feedback_to_render [ len ( execution_feedback_to_render ) // 2 :]
else :
break
2024-07-24 12:15:56 +08:00
# TODO: with retry_context(retry_n=3, except_list=[KeyError]):
final_evaluation_dict = None
attempts = 0
max_attempts = 3
while attempts < max_attempts :
try :
2024-10-15 12:21:33 +08:00
api = APIBackend () if attempts == 0 else APIBackend ( use_chat_cache = False )
2024-07-24 12:15:56 +08:00
final_evaluation_dict = json . loads (
2024-10-15 12:21:33 +08:00
api . build_messages_and_create_chat_completion (
2024-07-24 12:15:56 +08:00
user_prompt = user_prompt ,
system_prompt = system_prompt ,
json_mode = True ,
2024-09-19 22:03:33 +08:00
seed = attempts , # in case of useless retrying when cache enabled.
2024-07-24 12:15:56 +08:00
),
)
final_decision = final_evaluation_dict [ "final_decision" ]
final_feedback = final_evaluation_dict [ "final_feedback" ]
2024-10-15 12:41:32 +08:00
final_decision = str ( final_decision ) . lower () in [ "true" , "1" ]
2024-07-24 12:15:56 +08:00
return final_decision , final_feedback
except json . JSONDecodeError as e :
raise ValueError ( "Failed to decode JSON response from API." ) from e
except KeyError as e :
attempts += 1
if attempts >= max_attempts :
2024-07-26 12:12:16 +08:00
raise KeyError (
"Response from API is missing 'final_decision' or 'final_feedback' key after multiple attempts."
) from e
2024-07-24 12:15:56 +08:00
return None , None