mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-14 04:09:09 +00:00
fix(lens): retain paged content and recorded tool errors
This commit is contained in:
parent
a3ad12b259
commit
dae9bf0435
4 changed files with 73 additions and 5 deletions
|
|
@ -2,7 +2,7 @@ use std::collections::BTreeMap;
|
|||
|
||||
use litellm_http::Client;
|
||||
use litellm_traces::query::named::ReadAccessParams;
|
||||
use litellm_traces_cache::{ReadError, TraceReader};
|
||||
use litellm_traces_cache::{ReadError, TraceReader, TraceStore};
|
||||
use litellm_traces_clickhouse::{
|
||||
ClickHouseTraces, Connection, InsertTable, QueryScope, insert_rows,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -276,6 +276,30 @@ describe("TraceConversation", () => {
|
|||
).toBeVisible();
|
||||
});
|
||||
|
||||
it.each(["not_recorded", "conflicting"])(
|
||||
"preserves a recorded tool error with %s result evidence",
|
||||
async (status) => {
|
||||
vi.mocked(agentTraceSpanCall).mockResolvedValue({
|
||||
...toolDetail,
|
||||
output: "",
|
||||
attributes: {
|
||||
"lens.content.execution_status": "not_recorded",
|
||||
"lens.content.output_status": status,
|
||||
"lens.content.tool_result_is_error": "0",
|
||||
},
|
||||
});
|
||||
const failedTool = { ...tool, status: "error", error: "Permission denied" };
|
||||
renderWithProviders(
|
||||
<TraceConversation trace={{ ...trace, spans: [failedTool] }} accessToken="test" onOpenStep={vi.fn()} />,
|
||||
);
|
||||
expect(await screen.findByRole("button", { name: "Collapse read_file tool call" })).toHaveAttribute(
|
||||
"aria-expanded",
|
||||
"true",
|
||||
);
|
||||
expect(screen.getByText("Permission denied")).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
it("loads twenty span details initially and pages conversation entries on demand", async () => {
|
||||
const user = userEvent.setup();
|
||||
const spans = [
|
||||
|
|
@ -641,6 +665,36 @@ describe("TraceConversation", () => {
|
|||
expect(screen.getByText("The release is ready")).toBeVisible();
|
||||
});
|
||||
|
||||
it.each([false, true])(
|
||||
"retains loaded details when an extended batch fails (new span count: %s)",
|
||||
async (changedCount) => {
|
||||
const user = userEvent.setup();
|
||||
const later = { ...tool, span_id: "later", name: "later tool", start_offset_ms: 2 };
|
||||
vi.mocked(agentTraceSpansCall)
|
||||
.mockResolvedValueOnce([rootDetail, toolDetail])
|
||||
.mockRejectedValueOnce(new Error("next page unavailable"))
|
||||
.mockResolvedValueOnce([rootDetail, toolDetail, { ...toolDetail, span_id: "later", output: "Later result" }]);
|
||||
const { rerender } = renderWithProviders(
|
||||
<TraceConversation trace={trace} accessToken="test" onOpenStep={vi.fn()} />,
|
||||
);
|
||||
expect(await screen.findByText("Read the release notes")).toBeVisible();
|
||||
await user.click(screen.getByRole("button", { name: "Expand read_file tool call" }));
|
||||
const extended: Trace = {
|
||||
...trace,
|
||||
spans: [...trace.spans, later],
|
||||
summary: { ...trace.summary, span_count: trace.summary.span_count + Number(changedCount) },
|
||||
};
|
||||
rerender(<TraceConversation trace={extended} accessToken="test" onOpenStep={vi.fn()} />);
|
||||
expect(screen.getByText("All checks passed")).toBeVisible();
|
||||
expect(await screen.findByRole("alert")).toHaveTextContent("Retry this batch");
|
||||
expect(screen.getByText("Read the release notes")).toBeVisible();
|
||||
expect(screen.getByText("All checks passed")).toBeVisible();
|
||||
await user.click(screen.getByRole("button", { name: "Retry batch" }));
|
||||
await user.click(await screen.findByRole("button", { name: "Expand later tool tool call" }));
|
||||
expect(screen.getByText("Later result")).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["input", "output"])("distinguishes conflicting %s content from absent evidence", async (field) => {
|
||||
vi.mocked(agentTraceSpanCall).mockResolvedValue({
|
||||
...toolDetail,
|
||||
|
|
|
|||
|
|
@ -302,7 +302,7 @@ function ConversationStep({
|
|||
}
|
||||
|
||||
function ConversationTool({ item, onOpenStep }: { item: ConversationItem; onOpenStep: (id: string) => void }) {
|
||||
const failed = item.toolAttempt?.isError ?? item.span.status === "error";
|
||||
const failed = item.toolAttempt?.isError || item.span.status === "error";
|
||||
const executionStatus = failed ? "Failed" : "Completed";
|
||||
const [open, setOpen] = useState(failed);
|
||||
const name = item.toolCall?.name ?? item.span.name;
|
||||
|
|
|
|||
|
|
@ -33,6 +33,14 @@ export function useConversationDetails(trace: Trace, accessToken: string) {
|
|||
const steps = useMemo(() => conversationSteps(trace.spans), [trace.spans]);
|
||||
const visible = steps.slice(0, limit);
|
||||
const { trace_id: traceId, trace_ref: traceRef, span_count: spanCount } = trace.summary;
|
||||
const cachedDetails = useCallback(
|
||||
(ids: string[]) =>
|
||||
ids.flatMap((id) => {
|
||||
const detail = queryClient.getQueryData<SpanDetail>(["agentTraceSpan", traceId, traceRef, id, accessToken]);
|
||||
return detail ? [detail] : [];
|
||||
}),
|
||||
[traceId, traceRef, accessToken, queryClient],
|
||||
);
|
||||
const batchQuery = useCallback(
|
||||
(batch: Span[]) => {
|
||||
const ids = batch.map((span) => span.span_id);
|
||||
|
|
@ -45,7 +53,10 @@ export function useConversationDetails(trace: Trace, accessToken: string) {
|
|||
const result = await readBatch(traces, traceId, pending, traceRef);
|
||||
const details = [
|
||||
...new Map(
|
||||
[...(previous?.details ?? []), ...result.details].map((detail) => [detail.span_id, detail]),
|
||||
[...(previous?.details ?? []), ...cachedDetails(ids), ...result.details].map((detail) => [
|
||||
detail.span_id,
|
||||
detail,
|
||||
]),
|
||||
).values(),
|
||||
];
|
||||
for (const detail of result.details) {
|
||||
|
|
@ -58,7 +69,7 @@ export function useConversationDetails(trace: Trace, accessToken: string) {
|
|||
retryOnMount: false,
|
||||
};
|
||||
},
|
||||
[traceId, traceRef, spanCount, accessToken, traces, queryClient],
|
||||
[traceId, traceRef, spanCount, accessToken, traces, queryClient, cachedDetails],
|
||||
);
|
||||
const batches = Array.from({ length: Math.ceil(visible.length / CONVERSATION_PAGE_SIZE) }, (_, index) =>
|
||||
visible.slice(index * CONVERSATION_PAGE_SIZE, (index + 1) * CONVERSATION_PAGE_SIZE),
|
||||
|
|
@ -66,7 +77,10 @@ export function useConversationDetails(trace: Trace, accessToken: string) {
|
|||
const queries = useQueries({ queries: batches.map(batchQuery) });
|
||||
const complete = queries.every((query) => query.isSuccess && query.data.failedIds.length === 0);
|
||||
const details = new Map(
|
||||
queries.flatMap((query) => (query.data?.details ?? []).map((detail) => [detail.span_id, detail] as const)),
|
||||
[
|
||||
...cachedDetails(visible.map((span) => span.span_id)),
|
||||
...queries.flatMap((query) => query.data?.details ?? []),
|
||||
].map((detail) => [detail.span_id, detail]),
|
||||
);
|
||||
const loadMore = useCallback(
|
||||
async (signal: AbortSignal) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue