mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
feat(lens): format review span previews as readable messages
This commit is contained in:
parent
616c59c8d0
commit
af1c2f1a95
2 changed files with 116 additions and 0 deletions
|
|
@ -0,0 +1,51 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import { spanPreviewLines } from "./spanPreview";
|
||||
|
||||
describe("span previews", () => {
|
||||
it("shows the last chat message of an agent span as role: content, not raw JSON", () => {
|
||||
const preview =
|
||||
'Input: [{"role": "system", "content": "You are a research assistant."}, {"role": "user", "content": "Find the median latency of the EU region."}]\nOutput: [{"role": "assistant", "content": "I could not find it."}]';
|
||||
expect(spanPreviewLines(preview)).toEqual([
|
||||
{ label: "in", text: "user: Find the median latency of the EU region.", error: false },
|
||||
{ label: "out", text: "assistant: I could not find it.", error: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("recovers the last message from truncated JSON with the omitted marker", () => {
|
||||
const preview =
|
||||
'Input: [{"role": "system", "content": "You are a bill\n[... preview omitted; read this span for evidence ...]\nn"}, {"role": "user", "content": "Cancel C2473\'s subscript';
|
||||
expect(spanPreviewLines(preview)).toEqual([
|
||||
{ label: "in", text: "user: Cancel C2473's subscript", error: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps tool payloads compact and drops an unset status", () => {
|
||||
const preview = 'Input: {"query":"latency"}\nOutput: {"results": []}\nStatus: STATUS_CODE_UNSET ';
|
||||
expect(spanPreviewLines(preview)).toEqual([
|
||||
{ label: "in", text: '{"query":"latency"}', error: false },
|
||||
{ label: "out", text: '{"results":[]}', error: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("marks a failed span's output and shows its error", () => {
|
||||
const preview =
|
||||
'Input: {"query":"q"}\nOutput: {"error": "500 search index unavailable"}\nStatus: STATUS_CODE_ERROR 500 search index unavailable';
|
||||
const lines = spanPreviewLines(preview);
|
||||
expect(lines.at(-1)).toEqual({ label: "error", text: "500 search index unavailable", error: true });
|
||||
expect(lines.find((l) => l.label === "out")?.error).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps multi-line payloads inside their section and collapses whitespace", () => {
|
||||
const preview = 'Output: {"content": "def total(items):\\n return 1"}\nmore text\nStatus: STATUS_CODE_OK';
|
||||
expect(spanPreviewLines(preview)).toEqual([
|
||||
{ label: "out", text: '{"content": "def total(items):\\n return 1"} more text', error: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("passes through plain text without sections", () => {
|
||||
expect(spanPreviewLines("500 Internal Server Error")).toEqual([
|
||||
{ label: "", text: "500 Internal Server Error", error: false },
|
||||
]);
|
||||
expect(spanPreviewLines("")).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
import { parseJson, parseMessages, previewText } from "@/components/view_logs/TraceView/traceUtils";
|
||||
|
||||
export interface PreviewLine {
|
||||
label: string;
|
||||
text: string;
|
||||
error: boolean;
|
||||
}
|
||||
|
||||
const SECTION = /^(Input|Output|Status): ?/;
|
||||
const OMITTED = /\n?\[\.\.\. preview omitted; read this span for evidence \.\.\.\]\n?/g;
|
||||
const OK_STATUS = /^STATUS_CODE_(UNSET|OK)\b/;
|
||||
const ROLE_CONTENT = /"role":\s*"(\w+)",\s*"content":\s*"((?:[^"\\]|\\.)*)/g;
|
||||
|
||||
function sections(preview: string): Readonly<Record<string, string>> {
|
||||
const lines = preview.split("\n");
|
||||
const starts = lines.flatMap((line, index) => (SECTION.test(line) ? [index] : []));
|
||||
if (!starts.length) return { Body: preview };
|
||||
return Object.fromEntries(
|
||||
starts.map((start, n) => {
|
||||
const body = lines.slice(start, starts[n + 1] ?? lines.length).join("\n");
|
||||
const name = SECTION.exec(body)?.[1] ?? "Body";
|
||||
return [name, body.replace(SECTION, "").trim()];
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function decode(escaped: string): string {
|
||||
const parsed = parseJson(`"${escaped.replace(/\\u[0-9a-fA-F]{0,3}$|\\$/, "")}"`);
|
||||
return typeof parsed === "string" ? parsed : escaped;
|
||||
}
|
||||
|
||||
function lastMessage(value: string): string | null {
|
||||
const messages = parseMessages(value);
|
||||
if (messages?.length) {
|
||||
const last = messages.at(-1);
|
||||
return last ? `${last.role}: ${last.content}` : null;
|
||||
}
|
||||
const matches = [...value.matchAll(ROLE_CONTENT)];
|
||||
const match = matches.at(-1);
|
||||
return match ? `${match[1]}: ${decode(match[2])}` : null;
|
||||
}
|
||||
|
||||
function compactJson(value: string): string {
|
||||
const parsed = parseJson(value);
|
||||
if (parsed === null || typeof parsed !== "object") return value;
|
||||
return JSON.stringify(parsed);
|
||||
}
|
||||
|
||||
function readable(value: string): string {
|
||||
const joined = value.replace(OMITTED, " … ");
|
||||
return (lastMessage(joined) ?? previewText(compactJson(joined))).replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
export function spanPreviewLines(preview: string): PreviewLine[] {
|
||||
const parts = sections(preview);
|
||||
const status = parts.Status ?? "";
|
||||
const failed = !!status && !OK_STATUS.test(status);
|
||||
const content = [
|
||||
...(parts.Body ? [{ label: "", text: readable(parts.Body), error: false }] : []),
|
||||
...(parts.Input ? [{ label: "in", text: readable(parts.Input), error: false }] : []),
|
||||
...(parts.Output ? [{ label: "out", text: readable(parts.Output), error: failed }] : []),
|
||||
...(failed ? [{ label: "error", text: status.replace(/^STATUS_CODE_ERROR\s*/, ""), error: true }] : []),
|
||||
];
|
||||
return content.filter((line) => line.text);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue