""" LLM Fine-tuning Experiment Feedback Generation Provides feedback analysis for LLM fine-tuning experiments, including model performance evaluation, training metrics analysis, and improvement suggestions. """ import json from typing import Dict from rdagent.app.finetune.llm.conf import FT_RD_SETTING from rdagent.core.proposal import ( Experiment2Feedback, ExperimentFeedback, HypothesisFeedback, ) from rdagent.core.scenario import Scenario from rdagent.log import rdagent_logger as logger from rdagent.log.utils import dict_get_with_warning from rdagent.oai.llm_utils import APIBackend from rdagent.scenarios.finetune.experiment.experiment import FTExperiment from rdagent.scenarios.finetune.proposal.proposal import FTHypothesis from rdagent.scenarios.finetune.proposal.trace import FTTrace from rdagent.utils import convert2bool from rdagent.utils.agent.tpl import T class FTExperiment2Feedback(Experiment2Feedback): """Generate feedback for LLM fine-tuning experiments""" def __init__(self, scen: Scenario, version: str = "exp_feedback") -> None: super().__init__(scen) self.version = version def generate_feedback( self, exp: FTExperiment, trace: FTTrace | None = None, exception: Exception | None = None ) -> ExperimentFeedback: """ Generate comprehensive feedback for LLM fine-tuning experiment. Args: exp: The experiment to analyze trace: Experiment trace (optional) exception: If provided, indicates experiment failed and contains error details Note: If exception is None, it means training succeeded and we evaluate quality/effectiveness. If exception is provided, we analyze the failure cause. """ # Get task information task_desc = exp.sub_tasks[0].get_task_information() # Initialize for SOTA update logic later sota_benchmark = None if exception is not None: # Error case: use error analysis prompt version = "exp_feedback_error" error_info = str(exception) # Try to get FTRunnerEvaluator's analysis result from workspace # This contains structured feedback (execution, return_checking, code) instead of raw error string runner_feedback = None if exp.sub_workspace_list: for ws in exp.sub_workspace_list: if ws and hasattr(ws, "feedback") and ws.feedback: runner_feedback = ws.feedback break if runner_feedback: # Use FTRunnerEvaluator's structured analysis result error_info = f"""## Execution Analysis {runner_feedback.execution} ## Return Checking {runner_feedback.return_checking} ## Code Analysis {runner_feedback.code}""" system_prompt = T(f".prompts:{version}.system").r( scenario=self.scen.get_scenario_all_desc(), ) # Get workspace files safely workspace_files = {} if hasattr(exp, "experiment_workspace") and exp.experiment_workspace is not None: workspace_files = exp.experiment_workspace.file_dict user_prompt = T(f".prompts:{version}.user").r( hypothesis=exp.hypothesis, task_desc=task_desc, workspace_files=workspace_files, error_info=error_info, ) else: # Success case: use normal feedback prompt version = self.version # Process experiment result - handle both new and legacy formats exp_result = exp.experiment_workspace.running_info.result if isinstance(exp_result, dict) and "benchmark" in exp_result: # New format: contains benchmark and training_metrics benchmark = exp_result.get("benchmark", {}) raw_metrics = exp_result.get("training_metrics", {}) # Pass loss_history directly (simpler and preserves full information) loss_history = raw_metrics.get("loss_history", {"train": [], "eval": []}) # Sample train entries if too many to avoid token bloat if len(loss_history.get("train", [])) > 60: loss_history["train"] = loss_history["train"][:30] + loss_history["train"][-30:] training_metrics = ( {"loss_history": loss_history} if (loss_history.get("train") or loss_history.get("eval")) else {} ) else: # Legacy format: exp_result is directly the benchmark result (list of dicts) benchmark = {"accuracy_summary": exp_result, "error_samples": []} training_metrics = {} # Get SOTA experiment's benchmark results for comparison sota_benchmark = trace.sota_benchmark() if trace else None # Get baseline benchmark (always exists, computed at scenario init) baseline_benchmark = getattr(self.scen, "baseline_benchmark_score", None) system_prompt = T(f".prompts:{version}.system").r( scenario=self.scen.get_scenario_all_desc(), has_sota=sota_benchmark is not None, force_think_token=FT_RD_SETTING.force_think_token, ) user_prompt = T(f".prompts:{version}.user").r( hypothesis=exp.hypothesis, task_desc=task_desc, workspace_files=exp.experiment_workspace.file_dict, execution_time=exp.experiment_workspace.running_info.running_time, benchmark=benchmark, training_metrics=training_metrics, sota_benchmark=sota_benchmark, baseline_benchmark=baseline_benchmark, ) resp_dict = json.loads( APIBackend().build_messages_and_create_chat_completion( user_prompt=user_prompt, system_prompt=system_prompt, json_mode=True, json_target_type=Dict[str, str | bool | int], ) ) # Extract feedback components error_type = resp_dict.get("Error Type") if exception is not None else None hypothesis_feedback = HypothesisFeedback( code_change_summary=dict_get_with_warning(resp_dict, "Code Summary", "No code summary provided"), reason=dict_get_with_warning(resp_dict, "Reason", "No reasoning provided"), decision=convert2bool(dict_get_with_warning(resp_dict, "Decision", "no")), acceptable=exception is None, # Only acceptable if no error observations=error_type, # Store error type for history display ) return hypothesis_feedback