1313
1414def read_json (path : str | Path ) -> dict [str , Any ]:
1515 resolved = Path (path )
16- with resolved .open ("r" , encoding = "utf-8" ) as file :
17- payload = json .load (file )
16+ try :
17+ with resolved .open ("r" , encoding = "utf-8" ) as file :
18+ payload = json .load (file , parse_constant = _reject_non_standard_json_constant )
19+ except (json .JSONDecodeError , ValueError ) as exc :
20+ raise ValueError (f"{ resolved } : invalid JSON: { exc } " ) from exc
1821 if not isinstance (payload , dict ):
1922 raise ValueError (f"expected JSON object in { resolved } " )
2023 return payload
2124
2225
26+ def _reject_non_standard_json_constant (constant : str ) -> None :
27+ raise ValueError (f"non-standard JSON constant { constant !r} " )
28+
29+
2330def load_eval_cases (path : str | Path , split : str | None = None ) -> list [EvalCase ]:
2431 payload = read_json (path )
2532 cases = payload .get ("cases" )
26- if not isinstance (cases , list ):
27- raise ValueError (f"evalset { path } must contain a cases list" )
2833 effective_split = split or payload .get ("split" ) or Path (path ).name .split ("." , 1 )[0 ]
29- return [EvalCase .from_dict (case , str (effective_split )) for case in cases ]
34+ if isinstance (cases , list ):
35+ return [EvalCase .from_dict (case , str (effective_split )) for case in cases ]
36+
37+ sdk_cases = payload .get ("eval_cases" ) or payload .get ("evalCases" )
38+ if isinstance (sdk_cases , list ):
39+ _validate_sdk_evalset (payload , path )
40+ return [_eval_case_from_sdk_dict (case , str (effective_split )) for case in sdk_cases ]
41+
42+ raise ValueError (f"evalset { path } must contain a cases or evalCases list" )
3043
3144
3245def load_optimizer_config (path : str | Path ) -> OptimizerConfig :
@@ -53,3 +66,84 @@ def sha256_file(path: str | Path) -> str:
5366 for chunk in iter (lambda : file .read (1024 * 1024 ), b"" ):
5467 digest .update (chunk )
5568 return digest .hexdigest ()
69+
70+
71+ def _validate_sdk_evalset (payload : dict [str , Any ], path : str | Path ) -> None :
72+ try :
73+ from trpc_agent_sdk .evaluation ._eval_set import EvalSet
74+
75+ EvalSet .model_validate (payload )
76+ except Exception as exc :
77+ raise ValueError (f"evalset { path } is not a valid SDK EvalSet: { exc } " ) from exc
78+
79+
80+ def _eval_case_from_sdk_dict (payload : dict [str , Any ], split : str ) -> EvalCase :
81+ case_id = payload .get ("eval_id" ) or payload .get ("evalId" )
82+ if not case_id :
83+ raise ValueError (f"SDK eval case is missing evalId/eval_id: { payload !r} " )
84+
85+ session_input = payload .get ("session_input" ) or payload .get ("sessionInput" ) or {}
86+ state = session_input .get ("state" ) if isinstance (session_input , dict ) else {}
87+ state = state if isinstance (state , dict ) else {}
88+ expectation = state .get ("eval_optimize_expectation" )
89+ if not isinstance (expectation , dict ):
90+ expectation = _infer_expectation_from_sdk_case (payload )
91+
92+ return EvalCase (
93+ case_id = str (case_id ),
94+ split = split ,
95+ input = _first_user_text (payload ),
96+ expectation = dict (expectation ),
97+ tags = [str (item ) for item in state .get ("eval_optimize_tags" , [])],
98+ protected = bool (state .get ("eval_optimize_protected" , False )),
99+ simulated_outputs = dict (state .get ("eval_optimize_simulated_outputs" ) or expectation .get ("simulated_outputs" ) or {}),
100+ expected_failure_category = state .get ("eval_optimize_expected_failure_category" )
101+ or expectation .get ("expected_failure_category" ),
102+ )
103+
104+
105+ def _infer_expectation_from_sdk_case (payload : dict [str , Any ]) -> dict [str , Any ]:
106+ expected = _first_final_response_text (payload )
107+ if expected :
108+ return {
109+ "type" : "exact" ,
110+ "expected" : expected ,
111+ "expected_failure_category" : "final_response_mismatch" ,
112+ }
113+ raise ValueError (
114+ "SDK eval case must put fake-mode metadata in sessionInput.state.eval_optimize_expectation "
115+ f"or provide a finalResponse that can be treated as an exact expectation: { payload !r} "
116+ )
117+
118+
119+ def _first_user_text (payload : dict [str , Any ]) -> str :
120+ for invocation in _conversation (payload ):
121+ content = invocation .get ("user_content" ) or invocation .get ("userContent" ) or {}
122+ text = _content_text (content )
123+ if text :
124+ return text
125+ return ""
126+
127+
128+ def _first_final_response_text (payload : dict [str , Any ]) -> str :
129+ for invocation in _conversation (payload ):
130+ content = invocation .get ("final_response" ) or invocation .get ("finalResponse" ) or {}
131+ text = _content_text (content )
132+ if text :
133+ return text
134+ return ""
135+
136+
137+ def _conversation (payload : dict [str , Any ]) -> list [dict [str , Any ]]:
138+ conversation = payload .get ("conversation" ) or []
139+ return [item for item in conversation if isinstance (item , dict )]
140+
141+
142+ def _content_text (content : Any ) -> str :
143+ if not isinstance (content , dict ):
144+ return ""
145+ texts = []
146+ for part in content .get ("parts" ) or []:
147+ if isinstance (part , dict ) and part .get ("text" ) is not None :
148+ texts .append (str (part ["text" ]))
149+ return "\n " .join (texts )
0 commit comments