|
22 | 22 |
|
23 | 23 | from honeyhive import HoneyHive |
24 | 24 | from honeyhive.experiments import compare_runs, evaluate |
25 | | -from honeyhive.models import CreateDatapointRequest, CreateDatasetRequest |
| 25 | +from honeyhive.models import CreateDatapointRequest, CreateDatasetRequest, EventFilter |
26 | 26 |
|
27 | 27 |
|
28 | 28 | @pytest.mark.integration |
@@ -1034,6 +1034,283 @@ def accuracy_evaluator( |
1034 | 1034 |
|
1035 | 1035 | print(f"\n{'='*70}\nEVENT-LEVEL COMPARISON TEST COMPLETE\n{'='*70}\n") |
1036 | 1036 |
|
| 1037 | + @pytest.mark.slow |
| 1038 | + def test_evaluate_with_nested_enrich_span_backend_validation( |
| 1039 | + self, |
| 1040 | + real_api_key: str, |
| 1041 | + real_project: str, |
| 1042 | + integration_client: HoneyHive, |
| 1043 | + ) -> None: |
| 1044 | + """Test nested function calls with enrich_span() and validate enriched properties. |
| 1045 | +
|
| 1046 | + This test validates: |
| 1047 | + 1. Nested function calls (evaluation_function -> helper_function) |
| 1048 | + 2. enrich_span() calls in both parent and nested functions |
| 1049 | + 3. Backend verification that enriched properties show up on events |
| 1050 | + 4. Properties include metadata, metrics, inputs, outputs, config, feedback |
| 1051 | +
|
| 1052 | + Boss requirement: Validate that enriched properties are ACTUALLY SET, |
| 1053 | + not just that events exist. |
| 1054 | + """ |
| 1055 | + |
| 1056 | + # Track calls for debugging |
| 1057 | + calls: list = [] |
| 1058 | + |
| 1059 | + # Import trace decorator for creating spans |
| 1060 | + from honeyhive import enrich_span, trace |
| 1061 | + |
| 1062 | + # Nested helper function with enrich_span |
| 1063 | + @trace(event_type="tool", event_name="helper_function") |
| 1064 | + def helper_function(text: str, multiplier: int) -> str: |
| 1065 | + """Helper function that enriches span with nested context.""" |
| 1066 | + calls.append("helper_called") |
| 1067 | + |
| 1068 | + # Enrich nested span with detailed metadata and metrics |
| 1069 | + enrich_span( |
| 1070 | + metadata={ |
| 1071 | + "helper_function": "text_processor", |
| 1072 | + "text_length": len(text), |
| 1073 | + "multiplier": multiplier, |
| 1074 | + "nested_level": "1", |
| 1075 | + }, |
| 1076 | + metrics={ |
| 1077 | + "processing_complexity": len(text) * multiplier, |
| 1078 | + "helper_call_count": len( |
| 1079 | + [c for c in calls if c == "helper_called"] |
| 1080 | + ), |
| 1081 | + }, |
| 1082 | + ) |
| 1083 | + |
| 1084 | + return text.upper() * multiplier |
| 1085 | + |
| 1086 | + # Main evaluation function with enrich_span |
| 1087 | + @trace(event_type="chain", event_name="evaluation_function") |
| 1088 | + def evaluation_function(datapoint: Dict[str, Any]) -> Dict[str, Any]: |
| 1089 | + """Main function with nested call to helper_function.""" |
| 1090 | + calls.append("eval_called") |
| 1091 | + |
| 1092 | + inputs = datapoint.get("inputs", {}) |
| 1093 | + text = inputs.get("text", "") |
| 1094 | + multiplier = inputs.get("multiplier", 1) |
| 1095 | + |
| 1096 | + # Enrich parent span BEFORE calling nested function |
| 1097 | + enrich_span( |
| 1098 | + metadata={ |
| 1099 | + "evaluation_function": "text_evaluator", |
| 1100 | + "input_text": text, |
| 1101 | + "input_multiplier": multiplier, |
| 1102 | + }, |
| 1103 | + metrics={ |
| 1104 | + "eval_call_count": len([c for c in calls if c == "eval_called"]), |
| 1105 | + "total_call_count": len(calls), |
| 1106 | + }, |
| 1107 | + config={ |
| 1108 | + "model": "test-model-v1", |
| 1109 | + "temperature": 0.7, |
| 1110 | + "max_tokens": 100, |
| 1111 | + }, |
| 1112 | + ) |
| 1113 | + |
| 1114 | + # Call nested helper function (should create child span with enrichment) |
| 1115 | + processed_text = helper_function(text, multiplier) |
| 1116 | + |
| 1117 | + # Enrich parent span AFTER nested call |
| 1118 | + enrich_span( |
| 1119 | + metrics={ |
| 1120 | + "output_length": len(processed_text), |
| 1121 | + }, |
| 1122 | + feedback={ |
| 1123 | + "quality": "high", |
| 1124 | + "nested_processing": "successful", |
| 1125 | + }, |
| 1126 | + ) |
| 1127 | + |
| 1128 | + return { |
| 1129 | + "result": processed_text, |
| 1130 | + "status": "completed", |
| 1131 | + } |
| 1132 | + |
| 1133 | + # Create test dataset |
| 1134 | + dataset = [ |
| 1135 | + {"inputs": {"text": "hello", "multiplier": 2}}, |
| 1136 | + {"inputs": {"text": "world", "multiplier": 3}}, |
| 1137 | + ] |
| 1138 | + |
| 1139 | + run_name = f"nested-enrich-test-{int(time.time())}" |
| 1140 | + |
| 1141 | + print(f"\n{'='*70}") |
| 1142 | + print("TESTING NESTED ENRICH_SPAN() WITH BACKEND VALIDATION") |
| 1143 | + print(f"{'='*70}") |
| 1144 | + print(f"Run name: {run_name}") |
| 1145 | + print(f"Dataset: {len(dataset)} datapoints") |
| 1146 | + print("Pattern: evaluation_function -> helper_function") |
| 1147 | + print("Enrichment: Both parent and nested spans enriched") |
| 1148 | + |
| 1149 | + # Execute evaluate() |
| 1150 | + result = evaluate( |
| 1151 | + function=evaluation_function, |
| 1152 | + dataset=dataset, |
| 1153 | + api_key=real_api_key, |
| 1154 | + project=real_project, |
| 1155 | + name=run_name, |
| 1156 | + max_workers=1, # Serial execution for clearer trace hierarchy |
| 1157 | + verbose=True, |
| 1158 | + ) |
| 1159 | + |
| 1160 | + # Validate result |
| 1161 | + assert result is not None, "Result should not be None" |
| 1162 | + assert result.run_id, "Result should have run_id" |
| 1163 | + |
| 1164 | + print(f"\n✅ Evaluation completed: {result.run_id}") |
| 1165 | + print(f"✅ Status: {result.status}") |
| 1166 | + print(f"✅ Calls: {len(calls)} total") |
| 1167 | + |
| 1168 | + # CRITICAL: Wait for backend to process events |
| 1169 | + # Backend needs time to process OTLP spans into events |
| 1170 | + print("\n⏳ Waiting for backend to process events...") |
| 1171 | + time.sleep(5) # Backend processing time (reduced to minimize test contention) |
| 1172 | + |
| 1173 | + # Fetch events from backend to validate enrichment |
| 1174 | + print(f"\n{'='*70}") |
| 1175 | + print("BACKEND ENRICHMENT VALIDATION") |
| 1176 | + print(f"{'='*70}") |
| 1177 | + |
| 1178 | + try: |
| 1179 | + # Get the run from backend |
| 1180 | + backend_run = integration_client.evaluations.get_run(result.run_id) |
| 1181 | + |
| 1182 | + if not (hasattr(backend_run, "evaluation") and backend_run.evaluation): |
| 1183 | + raise ValueError("Backend response missing evaluation data") |
| 1184 | + |
| 1185 | + run_data = backend_run.evaluation |
| 1186 | + event_ids = getattr(run_data, "event_ids", []) |
| 1187 | + |
| 1188 | + assert len(event_ids) > 0, "Should have recorded events" |
| 1189 | + print(f"✅ Events in run: {len(event_ids)} session events") |
| 1190 | + print(f" Session IDs: {event_ids}") |
| 1191 | + |
| 1192 | + # Fetch ALL events in these sessions (including child spans from @trace) |
| 1193 | + # Query by session_id to get parent session + all child events |
| 1194 | + all_events = [] |
| 1195 | + for session_id in event_ids: |
| 1196 | + try: |
| 1197 | + # Convert UUID to string for EventFilter (backend returns UUIDType objects) |
| 1198 | + session_id_str = str(session_id) |
| 1199 | + events_response = integration_client.events.get_events( |
| 1200 | + project=real_project, |
| 1201 | + filters=[ |
| 1202 | + EventFilter( |
| 1203 | + field="session_id", value=session_id_str, operator="is" |
| 1204 | + ), |
| 1205 | + ], |
| 1206 | + ) |
| 1207 | + session_events = events_response.get("events", []) |
| 1208 | + all_events.extend(session_events) |
| 1209 | + print( |
| 1210 | + f" ✅ Session {session_id_str[:16]}... has {len(session_events)} events" |
| 1211 | + ) |
| 1212 | + except Exception as e: |
| 1213 | + print(f" ⚠️ Could not fetch events for session {session_id}: {e}") |
| 1214 | + |
| 1215 | + print(f"\n✅ Fetched {len(all_events)} total events for validation") |
| 1216 | + |
| 1217 | + # Validate enrichment on each event |
| 1218 | + found_eval_function_enrichment = False |
| 1219 | + found_helper_function_enrichment = False |
| 1220 | + |
| 1221 | + for event in all_events: |
| 1222 | + event_name = getattr(event, "event_name", "unknown") |
| 1223 | + event_type = getattr(event, "event_type", "unknown") |
| 1224 | + |
| 1225 | + print(f"\n📦 Event: {event_name} ({event_type})") |
| 1226 | + |
| 1227 | + # Check metadata |
| 1228 | + metadata = getattr(event, "metadata", {}) or {} |
| 1229 | + if metadata: |
| 1230 | + print(f" ✅ Metadata ({len(metadata)} fields):") |
| 1231 | + for key in list(metadata.keys())[:5]: # Show first 5 |
| 1232 | + print(f" - {key}: {metadata[key]}") |
| 1233 | + |
| 1234 | + # Validate evaluation_function enrichment |
| 1235 | + if "evaluation_function" in metadata: |
| 1236 | + assert ( |
| 1237 | + metadata["evaluation_function"] == "text_evaluator" |
| 1238 | + ), "evaluation_function metadata should match" |
| 1239 | + found_eval_function_enrichment = True |
| 1240 | + print(" ✅ Found evaluation_function enrichment") |
| 1241 | + |
| 1242 | + # Validate helper_function enrichment |
| 1243 | + if "helper_function" in metadata: |
| 1244 | + assert ( |
| 1245 | + metadata["helper_function"] == "text_processor" |
| 1246 | + ), "helper_function metadata should match" |
| 1247 | + found_helper_function_enrichment = True |
| 1248 | + print(" ✅ Found helper_function enrichment") |
| 1249 | + |
| 1250 | + # Check metrics |
| 1251 | + metrics = getattr(event, "metrics", {}) or {} |
| 1252 | + if metrics: |
| 1253 | + print(f" ✅ Metrics ({len(metrics)} fields):") |
| 1254 | + for key, value in list(metrics.items())[:5]: # Show first 5 |
| 1255 | + print(f" - {key}: {value}") |
| 1256 | + # Validate metrics are numeric |
| 1257 | + assert isinstance( |
| 1258 | + value, (int, float) |
| 1259 | + ), f"Metric {key} should be numeric, got {type(value)}" |
| 1260 | + |
| 1261 | + # Check config |
| 1262 | + config = getattr(event, "config", {}) or {} |
| 1263 | + if config: |
| 1264 | + print(f" ✅ Config ({len(config)} fields):") |
| 1265 | + for key, value in list(config.items())[:5]: |
| 1266 | + print(f" - {key}: {value}") |
| 1267 | + |
| 1268 | + # Validate config enrichment |
| 1269 | + if "model" in config: |
| 1270 | + assert ( |
| 1271 | + config["model"] == "test-model-v1" |
| 1272 | + ), "Model config should match" |
| 1273 | + print(" ✅ Found config enrichment") |
| 1274 | + |
| 1275 | + # Check feedback |
| 1276 | + feedback = getattr(event, "feedback", {}) or {} |
| 1277 | + if feedback: |
| 1278 | + print(f" ✅ Feedback ({len(feedback)} fields):") |
| 1279 | + for key, value in list(feedback.items())[:5]: |
| 1280 | + print(f" - {key}: {value}") |
| 1281 | + |
| 1282 | + # Validate feedback enrichment |
| 1283 | + if "quality" in feedback: |
| 1284 | + assert ( |
| 1285 | + feedback["quality"] == "high" |
| 1286 | + ), "Quality feedback should match" |
| 1287 | + print(" ✅ Found feedback enrichment") |
| 1288 | + |
| 1289 | + # CRITICAL ASSERTIONS: Verify enrichment was found |
| 1290 | + assert found_eval_function_enrichment, ( |
| 1291 | + "❌ CRITICAL: evaluation_function enrichment NOT FOUND in backend! " |
| 1292 | + "enrich_span() metadata not persisted." |
| 1293 | + ) |
| 1294 | + assert found_helper_function_enrichment, ( |
| 1295 | + "❌ CRITICAL: helper_function enrichment NOT FOUND in backend! " |
| 1296 | + "Nested enrich_span() not working." |
| 1297 | + ) |
| 1298 | + |
| 1299 | + print(f"\n{'='*70}") |
| 1300 | + print("✅ ALL ENRICHMENT VALIDATIONS PASSED") |
| 1301 | + print(f"{'='*70}") |
| 1302 | + print("✅ Parent function enrichment found") |
| 1303 | + print("✅ Nested function enrichment found") |
| 1304 | + print("✅ Metadata enrichment validated") |
| 1305 | + print("✅ Metrics enrichment validated") |
| 1306 | + print("✅ Config enrichment validated") |
| 1307 | + print("✅ Feedback enrichment validated") |
| 1308 | + print(f"{'='*70}\n") |
| 1309 | + |
| 1310 | + except Exception as e: |
| 1311 | + print(f"\n❌ Backend enrichment validation failed: {e}") |
| 1312 | + raise |
| 1313 | + |
1037 | 1314 |
|
1038 | 1315 | if __name__ == "__main__": |
1039 | 1316 | pytest.main([__file__, "-v", "-s", "--real-api"]) |
0 commit comments