Skip to content

Commit 19d116f

Browse files
address review comments, cleanup
1 parent 1d42985 commit 19d116f

3 files changed

Lines changed: 49 additions & 48 deletions

File tree

src/agentevals/api/routes.py

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -276,17 +276,24 @@ def _serialize_invocation(inv) -> dict[str, Any]:
276276
inv_dict: dict[str, Any] = {
277277
"invocation_id": inv.invocation_id,
278278
}
279-
if inv.user_content:
279+
if inv.user_content is not None:
280280
inv_dict["user_content"] = inv.user_content.model_dump(exclude_none=True)
281-
if inv.final_response:
281+
if inv.final_response is not None:
282282
inv_dict["final_response"] = inv.final_response.model_dump(exclude_none=True)
283-
if inv.intermediate_data:
283+
if inv.intermediate_data is not None:
284284
inv_dict["intermediate_data"] = inv.intermediate_data.model_dump(exclude_none=True)
285-
if inv.creation_timestamp:
285+
if inv.creation_timestamp is not None:
286286
inv_dict["creation_timestamp"] = inv.creation_timestamp
287287
return _camel_keys(inv_dict)
288288

289289

290+
def _get_format_for_file(path: str, explicit_format: str) -> str:
291+
"""Return the loader format for a single file, auto-detecting from extension."""
292+
if explicit_format:
293+
return explicit_format
294+
return "otlp-json" if path.lower().endswith(".jsonl") else "jaeger-json"
295+
296+
290297
@router.post("/convert", response_model=StandardResponse[ConvertTracesData])
291298
async def convert_trace_files(
292299
trace_files: list[UploadFile] = File(...),
@@ -295,62 +302,65 @@ async def convert_trace_files(
295302
"""Convert trace files to invocations and metadata without running evaluation."""
296303
temp_dir = tempfile.mkdtemp()
297304
try:
298-
trace_paths = []
299-
for trace_file in trace_files:
305+
saved_files: list[tuple[str, str]] = [] # (path, original_filename)
306+
for idx, trace_file in enumerate(trace_files):
300307
if not trace_file.filename:
301308
continue
302309

303-
if not (trace_file.filename.endswith(".json") or trace_file.filename.endswith(".jsonl")):
310+
original = trace_file.filename
311+
lower = original.lower()
312+
if not (lower.endswith(".json") or lower.endswith(".jsonl")):
304313
raise HTTPException(
305314
status_code=400,
306-
detail=f"Invalid file extension for {trace_file.filename}. Only .json and .jsonl files are allowed.",
315+
detail=f"Invalid file extension for {original}. Only .json and .jsonl files are allowed.",
307316
)
308317

309-
trace_path = os.path.join(temp_dir, trace_file.filename)
318+
safe_name = f"{idx}_{os.path.basename(original)}"
319+
trace_path = os.path.join(temp_dir, safe_name)
310320
with open(trace_path, "wb") as f: # noqa: ASYNC230
311321
content = await trace_file.read()
312322

313323
if len(content) > 10 * 1024 * 1024:
314324
raise HTTPException(
315325
status_code=400,
316-
detail=f"File {trace_file.filename} exceeds 10MB limit",
326+
detail=f"File {original} exceeds 10MB limit",
317327
)
318328

319329
f.write(content)
320-
trace_paths.append(trace_path)
330+
saved_files.append((trace_path, original))
321331

322-
if not trace_paths:
332+
if not saved_files:
323333
raise HTTPException(status_code=400, detail="No valid trace files provided")
324334

325-
fmt = trace_format
326-
if not fmt:
327-
if trace_paths[0].endswith(".jsonl"):
328-
fmt = "otlp-json"
329-
else:
330-
fmt = "jaeger-json"
331-
332-
loader = get_loader(fmt)
333335
all_traces = []
334336
trace_to_filename: dict[str, str] = {}
335-
for path in trace_paths:
337+
load_warnings: list[str] = []
338+
for path, original in saved_files:
339+
fmt = _get_format_for_file(path, trace_format)
340+
loader = get_loader(fmt)
336341
try:
337342
traces = loader.load(path)
338-
filename = os.path.basename(path)
339343
for t in traces:
340-
trace_to_filename[t.trace_id] = filename
344+
trace_to_filename[t.trace_id] = original
341345
all_traces.extend(traces)
342346
except Exception as exc:
343-
logger.warning(f"Failed to load trace file '{path}': {exc}")
347+
msg = f"Failed to load '{original}': {exc}"
348+
logger.warning(msg)
349+
load_warnings.append(msg)
344350

345351
if not all_traces:
346-
raise HTTPException(status_code=400, detail="No traces found in uploaded files")
352+
detail = "No traces found in uploaded files"
353+
if load_warnings:
354+
detail += ". Errors: " + "; ".join(load_warnings)
355+
raise HTTPException(status_code=400, detail=detail)
347356

348357
conversion_results = convert_traces(all_traces)
349358
trace_map = {t.trace_id: t for t in all_traces}
350359

351360
entries: list[TraceConversionEntry] = []
352361
for conv_result in conversion_results:
353362
invocations = [_serialize_invocation(inv) for inv in conv_result.invocations]
363+
warnings = list(conv_result.warnings)
354364

355365
trace = trace_map.get(conv_result.trace_id)
356366
meta = TraceConversionMetadata()
@@ -371,7 +381,7 @@ async def convert_trace_files(
371381
TraceConversionEntry(
372382
trace_id=conv_result.trace_id,
373383
invocations=invocations,
374-
warnings=conv_result.warnings,
384+
warnings=warnings,
375385
metadata=meta,
376386
)
377387
)

ui/src/lib/evalset-builder.ts

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,5 @@
11
import type { Invocation, EvalSet, EvalCase } from './types';
2-
3-
/**
4-
* Convert camelCase keys to snake_case recursively
5-
*/
6-
function convertCamelToSnake(obj: any): any {
7-
if (Array.isArray(obj)) {
8-
return obj.map(convertCamelToSnake);
9-
}
10-
11-
if (obj !== null && typeof obj === 'object') {
12-
const converted: any = {};
13-
for (const [key, value] of Object.entries(obj)) {
14-
const snakeKey = key.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
15-
converted[snakeKey] = convertCamelToSnake(value);
16-
}
17-
return converted;
18-
}
19-
20-
return obj;
21-
}
2+
import { convertCamelToSnake } from './utils';
223

234
/**
245
* Generate an EvalSet from pre-converted invocations (backend is source of truth).

ui/src/lib/trace-helpers.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,24 @@ export const ADK_SCOPE = 'gcp.vertex.agent';
55
export const USER_ROLES = ['user', 'human'];
66
export const ASSISTANT_ROLES = ['assistant', 'model', 'ai'];
77

8+
function isGenAISpan(span: Span): boolean {
9+
return !!(
10+
span.tags['gen_ai.request.model'] ||
11+
span.tags['gen_ai.system'] ||
12+
span.tags['gen_ai.input.messages'] ||
13+
span.tags['gen_ai.prompt'] ||
14+
span.tags['gen_ai.request.messages']
15+
);
16+
}
17+
818
export function detectTraceFormat(trace: Trace): 'adk' | 'genai' {
919
const check = (spans: Span[]): 'adk' | 'genai' | null => {
1020
let hasGenai = false;
1121
for (const span of spans) {
1222
if (span.tags['otel.scope.name'] === ADK_SCOPE) {
1323
return 'adk';
1424
}
15-
if (!hasGenai && (span.tags['gen_ai.request.model'] || span.tags['gen_ai.input.messages'])) {
25+
if (!hasGenai && isGenAISpan(span)) {
1626
hasGenai = true;
1727
}
1828
}
@@ -52,7 +62,7 @@ export function findDescendantLLMSpans(root: Span): Span[] {
5262

5363
while (queue.length > 0) {
5464
const span = queue.shift()!;
55-
if (span.tags['gen_ai.request.model'] || span.tags['gen_ai.input.messages']) {
65+
if (isGenAISpan(span)) {
5666
results.push(span);
5767
}
5868
queue.push(...span.children);

0 commit comments

Comments
 (0)