1616from .fake_judge import FakeJudge
1717from .fake_model import FakeModel
1818from .loader import load_eval_cases
19+ from .loader import read_json
1920from .optimizer import FakeOptimizer
2021from .schemas import CandidatePrompt
2122from .schemas import CaseResult
@@ -421,7 +422,7 @@ async def evaluate(
421422 target_paths ,
422423 context = f"cannot evaluate { prompt_id } " ,
423424 )
424- expected_cases = load_eval_cases (dataset_path , split = split )
425+ expected_cases = _load_sdk_expected_cases (dataset_path , split = split )
425426 snapshot = snapshot_prompt_files (target_paths )
426427 result : Any | None = None
427428 with temporary_prompt_bundle (snapshot , candidate_prompts ):
@@ -447,7 +448,7 @@ async def evaluate(
447448 result ,
448449 prompt_id = prompt_id ,
449450 split = split ,
450- expected_cases = expected_cases ,
451+ expected_cases = expected_cases . values () ,
451452 )
452453
453454 def _load_required_call_agent (self , * , for_evaluation : bool ):
@@ -721,6 +722,138 @@ def _safe_jsonable(value: Any) -> Any:
721722 return repr (value )
722723
723724
725+ def _load_sdk_expected_cases (
726+ dataset_path : str | Path ,
727+ * ,
728+ split : str ,
729+ ) -> dict [str , EvalCase ]:
730+ """Load wrapper metadata from an SDK EvalSet without changing the SDK file."""
731+
732+ payload = read_json (dataset_path )
733+ if "eval_cases" not in payload :
734+ if "cases" in payload :
735+ return _eval_cases_by_id (
736+ load_eval_cases (dataset_path , split = split ),
737+ context = f"legacy evalset { dataset_path } " ,
738+ )
739+ raise ValueError (f"SDK evalset { dataset_path } must contain an eval_cases list" )
740+
741+ eval_set_id = payload .get ("eval_set_id" )
742+ if not isinstance (eval_set_id , str ) or not eval_set_id .strip ():
743+ raise ValueError (f"SDK evalset { dataset_path } is missing non-empty eval_set_id" )
744+ raw_cases = payload ["eval_cases" ]
745+ if not isinstance (raw_cases , list ):
746+ raise ValueError (f"SDK evalset { dataset_path } eval_cases must be a list" )
747+
748+ expected_cases : dict [str , EvalCase ] = {}
749+ for index , raw_case in enumerate (raw_cases ):
750+ if not isinstance (raw_case , dict ):
751+ raise ValueError (
752+ f"SDK evalset { dataset_path } eval_cases[{ index } ] must be an object"
753+ )
754+ eval_id = raw_case .get ("eval_id" )
755+ if not isinstance (eval_id , str ) or not eval_id .strip ():
756+ raise ValueError (
757+ f"SDK evalset { dataset_path } eval_cases[{ index } ] is missing non-empty eval_id"
758+ )
759+ if eval_id in expected_cases :
760+ raise ValueError (f"SDK evalset { dataset_path } contains duplicate eval_id { eval_id !r} " )
761+ expected_cases [eval_id ] = _expected_case_from_sdk_case (
762+ raw_case ,
763+ eval_id = eval_id ,
764+ split = split ,
765+ dataset_path = dataset_path ,
766+ )
767+ return expected_cases
768+
769+
770+ def _expected_case_from_sdk_case (
771+ raw_case : dict [str , Any ],
772+ * ,
773+ eval_id : str ,
774+ split : str ,
775+ dataset_path : str | Path ,
776+ ) -> EvalCase :
777+ context = f"SDK evalset { dataset_path } case { eval_id !r} "
778+ conversation = raw_case .get ("conversation" )
779+ if not isinstance (conversation , list ) or not conversation :
780+ raise ValueError (f"{ context } must contain a non-empty conversation list" )
781+
782+ input_text = ""
783+ for turn_index , invocation in reversed (list (enumerate (conversation ))):
784+ if not isinstance (invocation , dict ):
785+ raise ValueError (f"{ context } conversation[{ turn_index } ] must be an object" )
786+ user_content = invocation .get ("user_content" )
787+ if user_content is None :
788+ continue
789+ if not isinstance (user_content , dict ):
790+ raise ValueError (
791+ f"{ context } conversation[{ turn_index } ].user_content must be an object"
792+ )
793+ candidate_text = _content_text (user_content )
794+ if candidate_text .strip ():
795+ input_text = candidate_text
796+ break
797+ if not input_text :
798+ raise ValueError (f"{ context } conversation has no user_content text" )
799+
800+ session_input = raw_case .get ("session_input" )
801+ if not isinstance (session_input , dict ):
802+ raise ValueError (f"{ context } must contain a session_input object" )
803+ state = session_input .get ("state" )
804+ if not isinstance (state , dict ):
805+ raise ValueError (f"{ context } session_input.state must be an object" )
806+ expectation = state .get ("eval_optimize_expectation" )
807+ if not isinstance (expectation , dict ):
808+ raise ValueError (
809+ f"{ context } session_input.state must contain eval_optimize_expectation object"
810+ )
811+
812+ tags = state .get ("eval_optimize_tags" , [])
813+ if not isinstance (tags , list ) or any (not isinstance (tag , str ) for tag in tags ):
814+ raise ValueError (f"{ context } eval_optimize_tags must be a list of strings" )
815+ protected = state .get ("eval_optimize_protected" , False )
816+ if not isinstance (protected , bool ):
817+ raise ValueError (f"{ context } eval_optimize_protected must be a boolean" )
818+
819+ expected_failure_category = state .get ("eval_optimize_expected_failure_category" )
820+ if expected_failure_category is None :
821+ expected_failure_category = state .get ("expected_failure_category" )
822+ if expected_failure_category is None :
823+ expected_failure_category = expectation .get ("expected_failure_category" )
824+ if (
825+ expected_failure_category is not None
826+ and (
827+ not isinstance (expected_failure_category , str )
828+ or not expected_failure_category .strip ()
829+ )
830+ ):
831+ raise ValueError (f"{ context } expected_failure_category must be a non-empty string" )
832+
833+ return EvalCase (
834+ case_id = eval_id ,
835+ split = split ,
836+ input = input_text ,
837+ expectation = dict (expectation ),
838+ tags = list (tags ),
839+ protected = protected ,
840+ expected_failure_category = expected_failure_category ,
841+ )
842+
843+
844+ def _eval_cases_by_id (
845+ cases : Iterable [EvalCase ],
846+ * ,
847+ context : str ,
848+ ) -> dict [str , EvalCase ]:
849+ by_id : dict [str , EvalCase ] = {}
850+ for case in cases :
851+ if case .case_id in by_id :
852+ raise ValueError (f"{ context } contains duplicate case id { case .case_id !r} " )
853+ by_id [case .case_id ] = case
854+ return by_id
855+
856+
724857def _eval_result_from_sdk_result (
725858 result : Any ,
726859 * ,
0 commit comments