@@ -1026,6 +1026,220 @@ def handler(request: httpx.Request) -> httpx.Response:
10261026 assert isinstance (nested_spans , list )
10271027 assert nested_spans [0 ]["name" ] == "nested"
10281028
1029+ @patch .dict (os .environ , LUNA_ENV )
1030+ @pytest .mark .asyncio
1031+ async def test_evaluator_posts_trace_with_raw_structured_selector_data (self ) -> None :
1032+ from agent_control_evaluator_galileo .luna import LunaEvaluator
1033+
1034+ captured : dict [str , object ] = {}
1035+
1036+ def handler (request : httpx .Request ) -> httpx .Response :
1037+ captured ["body" ] = json .loads (request .content .decode ())
1038+ return httpx .Response (200 , json = {"score" : 0.8 , "status" : "success" })
1039+
1040+ step = Step (
1041+ type = "trace" ,
1042+ name = "account_lookup_trace" ,
1043+ input = {"messages" : [{"role" : "user" , "content" : "Find my account information." }]},
1044+ output = {"content" : "Your account belongs to Jane Doe." },
1045+ children = [
1046+ Step (
1047+ type = "llm" ,
1048+ name = "account_agent" ,
1049+ input = [{"role" : "user" , "content" : "Find my account information." }],
1050+ output = {"content" : "Looking up the account." },
1051+ ),
1052+ Step (
1053+ type = "tool" ,
1054+ name = "lookup_account" ,
1055+ input = {"account_id" : "123" },
1056+ output = {"name" : "Jane Doe" },
1057+ children = [
1058+ Step (
1059+ type = "tool" ,
1060+ name = "account_service" ,
1061+ input = {"account_id" : "123" },
1062+ output = {"name" : "Jane Doe" },
1063+ )
1064+ ],
1065+ ),
1066+ Step (
1067+ type = "retriever" ,
1068+ name = "account_docs" ,
1069+ input = {"query" : "account 123" },
1070+ output = [{"content" : "Account owner: Jane Doe" , "metadata" : {"id" : "d1" }}],
1071+ ),
1072+ ],
1073+ tools = [{"name" : "lookup_account" , "description" : "Lookup" , "input_schema" : {}}],
1074+ ground_truth = {"owner" : "Jane Doe" },
1075+ )
1076+ selected = step .model_dump (mode = "json" )
1077+ evaluator = LunaEvaluator .from_dict (
1078+ {
1079+ "scorer_id" : SCORER_ID ,
1080+ "scorer_version_id" : SCORER_VERSION_ID ,
1081+ "scorer_label" : "account-check" ,
1082+ "threshold" : 0.5 ,
1083+ "config" : {"request_timeout_seconds" : 6 },
1084+ "timeout_ms" : 9000 ,
1085+ }
1086+ )
1087+ evaluator ._client ._client = httpx .AsyncClient (transport = httpx .MockTransport (handler ))
1088+
1089+ try :
1090+ result = await evaluator .evaluate_with_extensions (
1091+ selected ,
1092+ step ,
1093+ {
1094+ "namespace_key" : "org-1" ,
1095+ "caller_id" : "user-2" ,
1096+ "target_type" : "log_stream" ,
1097+ "target_id" : "run-3" ,
1098+ "metadata" : {"project_id" : "project-4" },
1099+ },
1100+ )
1101+ finally :
1102+ await evaluator ._client .close ()
1103+
1104+ assert result .error is None
1105+ body = captured ["body" ]
1106+ assert isinstance (body , dict )
1107+ assert body ["scorer_id" ] == SCORER_ID
1108+ assert body ["scorer_version_id" ] == SCORER_VERSION_ID
1109+ assert body ["scorer_label" ] == "account-check"
1110+ assert body ["execution_context" ] == {
1111+ "organization_id" : "org-1" ,
1112+ "user_id" : "user-2" ,
1113+ "project_id" : "project-4" ,
1114+ "run_id" : "run-3" ,
1115+ }
1116+ assert body ["inputs" ] == {
1117+ "query" : json .dumps (step .input , ensure_ascii = False , sort_keys = True ),
1118+ "response" : json .dumps (step .output , ensure_ascii = False , sort_keys = True ),
1119+ "ground_truth" : {"owner" : "Jane Doe" },
1120+ "tools" : step .tools ,
1121+ }
1122+ assert body ["config" ] == {"request_timeout_seconds" : 6.0 }
1123+ record = body ["record" ]
1124+ assert isinstance (record , dict )
1125+ assert record ["type" ] == "trace"
1126+ assert json .loads (record ["input" ]) == step .input
1127+ assert json .loads (record ["output" ]) == step .output
1128+ spans = record ["spans" ]
1129+ assert isinstance (spans , list )
1130+ assert [child ["type" ] for child in spans ] == ["llm" , "tool" , "retriever" ]
1131+ assert spans [0 ]["input" ][0 ]["content" ] == "Find my account information."
1132+ assert json .loads (spans [1 ]["input" ]) == {"account_id" : "123" }
1133+ assert spans [1 ]["spans" ][0 ]["name" ] == "account_service"
1134+ assert spans [2 ]["output" ][0 ]["content" ] == "Account owner: Jane Doe"
1135+
1136+ @patch .dict (os .environ , LUNA_ENV )
1137+ @pytest .mark .asyncio
1138+ async def test_evaluator_posts_session_with_raw_structured_selector_data (self ) -> None :
1139+ from agent_control_evaluator_galileo .luna import LunaEvaluator
1140+
1141+ captured : dict [str , object ] = {}
1142+
1143+ def handler (request : httpx .Request ) -> httpx .Response :
1144+ captured ["body" ] = json .loads (request .content .decode ())
1145+ return httpx .Response (200 , json = {"score" : 0.8 , "status" : "success" })
1146+
1147+ step = Step (
1148+ type = "session" ,
1149+ name = "support_session" ,
1150+ input = {"customer" : "Jane Doe" },
1151+ output = {"resolution" : "account found" },
1152+ children = [
1153+ Step (
1154+ type = "trace" ,
1155+ name = "account_lookup" ,
1156+ input = {"query" : "account info" },
1157+ output = {"owner" : "Jane Doe" },
1158+ children = [
1159+ Step (
1160+ type = "tool" ,
1161+ name = "lookup_account" ,
1162+ input = {"account_id" : "123" },
1163+ output = {"name" : "Jane Doe" },
1164+ )
1165+ ],
1166+ )
1167+ ],
1168+ )
1169+ evaluator = LunaEvaluator .from_dict (
1170+ {"scorer_id" : SCORER_ID , "scorer_version_id" : SCORER_VERSION_ID }
1171+ )
1172+ evaluator ._client ._client = httpx .AsyncClient (transport = httpx .MockTransport (handler ))
1173+
1174+ try :
1175+ result = await evaluator .evaluate_with_context (step .model_dump (mode = "json" ), step )
1176+ finally :
1177+ await evaluator ._client .close ()
1178+
1179+ assert result .error is None
1180+ body = captured ["body" ]
1181+ assert isinstance (body , dict )
1182+ assert body ["inputs" ]["query" ] == json .dumps (
1183+ step .input , ensure_ascii = False , sort_keys = True
1184+ )
1185+ assert body ["inputs" ]["response" ] == json .dumps (
1186+ step .output , ensure_ascii = False , sort_keys = True
1187+ )
1188+ record = body ["record" ]
1189+ assert isinstance (record , dict )
1190+ assert record ["type" ] == "session"
1191+ assert json .loads (record ["input" ]) == step .input
1192+ assert json .loads (record ["output" ]) == step .output
1193+ traces = record ["traces" ]
1194+ assert isinstance (traces , list )
1195+ assert traces [0 ]["type" ] == "trace"
1196+ assert traces [0 ]["spans" ][0 ]["type" ] == "tool"
1197+
1198+ @patch .dict (os .environ , LUNA_ENV )
1199+ @pytest .mark .asyncio
1200+ async def test_evaluator_does_not_post_trace_for_scalar_selector_or_invalid_hierarchy (
1201+ self ,
1202+ ) -> None :
1203+ from agent_control_evaluator_galileo .luna import LunaEvaluator
1204+
1205+ request_count = 0
1206+
1207+ def handler (_request : httpx .Request ) -> httpx .Response :
1208+ nonlocal request_count
1209+ request_count += 1
1210+ return httpx .Response (200 , json = {"score" : 0.8 , "status" : "success" })
1211+
1212+ evaluator = LunaEvaluator .from_dict (
1213+ {"scorer_id" : SCORER_ID , "scorer_version_id" : SCORER_VERSION_ID }
1214+ )
1215+ evaluator ._client ._client = httpx .AsyncClient (transport = httpx .MockTransport (handler ))
1216+ trace = Step (
1217+ type = "trace" ,
1218+ name = "trace" ,
1219+ input = {"question" : "q" },
1220+ children = [Step (type = "llm" , name = "answer" , input = "q" , output = "a" )],
1221+ )
1222+ invalid_session = Step (
1223+ type = "session" ,
1224+ name = "session" ,
1225+ input = "q" ,
1226+ children = [Step (type = "tool" , name = "invalid-child" , input = {"q" : "q" })],
1227+ )
1228+
1229+ try :
1230+ scalar_result = await evaluator .evaluate_with_context ("response text" , trace )
1231+ hierarchy_result = await evaluator .evaluate_with_context (
1232+ invalid_session .model_dump (mode = "json" ), invalid_session
1233+ )
1234+ finally :
1235+ await evaluator ._client .close ()
1236+
1237+ assert scalar_result .error is not None
1238+ assert "structured selector values" in scalar_result .error
1239+ assert hierarchy_result .error is not None
1240+ assert "cannot contain 'tool' child steps" in hierarchy_result .error
1241+ assert request_count == 0
1242+
10291243 @pytest .mark .parametrize ("field" , ["children" , "spans" , "traces" ])
10301244 @pytest .mark .asyncio
10311245 async def test_client_rejects_hierarchy_fields_from_step_context (self , field : str ) -> None :
@@ -1247,6 +1461,8 @@ async def test_evaluator_contextual_hook_forwards_complete_step(self) -> None:
12471461 mock_invoke .assert_awaited_once_with (
12481462 scorer_id = "scorer-123" ,
12491463 step = step ,
1464+ selected_data = "selected input" ,
1465+ selected_data_payload_field = "input" ,
12501466 input = "selected input" ,
12511467 output = None ,
12521468 config = None ,
@@ -1284,6 +1500,8 @@ async def test_evaluator_consumes_verified_opaque_extensions(self, target_type:
12841500 mock_invoke .assert_awaited_once_with (
12851501 scorer_id = "scorer-123" ,
12861502 step = Step (type = "llm" , name = "answer" , input = "prompt" ),
1503+ selected_data = "selected input" ,
1504+ selected_data_payload_field = "input" ,
12871505 execution_context = GalileoExecutionContext (
12881506 organization_id = "org-1" ,
12891507 user_id = "verified-user-2" ,
@@ -1323,6 +1541,8 @@ async def test_evaluator_allows_missing_caller_id(self) -> None:
13231541 mock_invoke .assert_awaited_once_with (
13241542 scorer_id = "scorer-123" ,
13251543 step = Step (type = "llm" , name = "answer" , input = "prompt" ),
1544+ selected_data = "selected input" ,
1545+ selected_data_payload_field = "input" ,
13261546 execution_context = GalileoExecutionContext (
13271547 organization_id = "org-1" ,
13281548 user_id = None ,
@@ -1363,6 +1583,8 @@ async def test_evaluator_omits_run_id_for_unsupported_target_type(self) -> None:
13631583 mock_invoke .assert_awaited_once_with (
13641584 scorer_id = "scorer-123" ,
13651585 step = Step (type = "llm" , name = "answer" , input = "prompt" ),
1586+ selected_data = "selected input" ,
1587+ selected_data_payload_field = "input" ,
13661588 execution_context = GalileoExecutionContext (
13671589 organization_id = "org-1" ,
13681590 user_id = "verified-user-2" ,
0 commit comments