v1 - tool viewer in logs page

This commit is contained in:
Ishaan Jaffer 2026-01-30 16:25:32 -08:00
parent 437e9e23bd
commit 9e1c76e9d0
11 changed files with 921 additions and 0 deletions

View file

@ -27,6 +27,7 @@ import {
SPACING_XLARGE,
MESSAGE_REQUEST_ID_COPIED,
} from "./constants";
import { ToolsSection } from "../ToolsSection";
const { Text } = Typography;

View file

@ -0,0 +1,124 @@
/**
* Formatted view of tool definition with parameters table and call data
*/
import { Typography, Table } from "antd";
import { ParsedTool, ParameterRow } from "./types";
const { Text } = Typography;
interface FormattedToolViewProps {
tool: ParsedTool;
}
export function FormattedToolView({ tool }: FormattedToolViewProps) {
// Parse parameters for table display
const parameterRows: ParameterRow[] = Object.entries(
tool.parameters?.properties || {}
).map(([name, schema]: [string, any]) => ({
key: name,
name: name,
type: schema.type || "any",
description: schema.description || "-",
required: tool.parameters?.required?.includes(name) || false,
}));
const columns = [
{
title: "Parameter",
dataIndex: "name",
key: "name",
render: (name: string, record: ParameterRow) => (
<Text code>
{name}
{record.required && <Text type="danger">*</Text>}
</Text>
),
},
{
title: "Type",
dataIndex: "type",
key: "type",
render: (type: string) => (
<Text code style={{ color: "#1890ff" }}>
{type}
</Text>
),
},
{
title: "Description",
dataIndex: "description",
key: "description",
render: (desc: string) => <Text type="secondary">{desc}</Text>,
},
];
return (
<div>
{/* Description */}
{tool.description && (
<div style={{ marginBottom: 16 }}>
<Text style={{ lineHeight: 1.6 }}>{tool.description}</Text>
</div>
)}
{/* Parameters Table */}
{parameterRows.length > 0 && (
<div>
<Text
type="secondary"
style={{
fontSize: 12,
display: "block",
marginBottom: 8,
}}
>
Parameters
</Text>
<Table
dataSource={parameterRows}
columns={columns}
pagination={false}
size="small"
bordered
/>
</div>
)}
{/* If tool was called, show the arguments used */}
{tool.called && tool.callData && (
<div style={{ marginTop: 16 }}>
<Text
type="secondary"
style={{
fontSize: 12,
display: "block",
marginBottom: 8,
}}
>
Called With
</Text>
<div
style={{
background: "#f6ffed",
border: "1px solid #b7eb8f",
borderRadius: 4,
padding: 12,
}}
>
<pre
style={{
margin: 0,
fontSize: 12,
whiteSpace: "pre-wrap",
wordBreak: "break-word",
}}
>
{JSON.stringify(tool.callData.arguments, null, 2)}
</pre>
</div>
</div>
)}
</div>
);
}

View file

@ -0,0 +1,39 @@
/**
* JSON view of tool definition
*/
import { ParsedTool } from "./types";
interface JsonToolViewProps {
tool: ParsedTool;
}
export function JsonToolView({ tool }: JsonToolViewProps) {
// Reconstruct the original tool definition
const toolJson = {
type: "function",
function: {
name: tool.name,
description: tool.description,
parameters: tool.parameters,
},
};
return (
<pre
style={{
margin: 0,
whiteSpace: "pre-wrap",
wordBreak: "break-word",
fontSize: 12,
background: "#fafafa",
padding: 12,
borderRadius: 4,
maxHeight: 300,
overflow: "auto",
}}
>
{JSON.stringify(toolJson, null, 2)}
</pre>
);
}

View file

@ -0,0 +1,52 @@
/**
* Expanded content for a tool with view mode toggle
*/
import { useState } from "react";
import { Typography, Radio } from "antd";
import { ParsedTool } from "./types";
import { FormattedToolView } from "./FormattedToolView";
import { JsonToolView } from "./JsonToolView";
const { Text } = Typography;
type ViewMode = "formatted" | "json";
interface ToolExpandedContentProps {
tool: ParsedTool;
}
export function ToolExpandedContent({ tool }: ToolExpandedContentProps) {
const [viewMode, setViewMode] = useState<ViewMode>("formatted");
return (
<div>
{/* View Mode Toggle - Top Right */}
<div
style={{
display: "flex",
justifyContent: "space-between",
marginBottom: 12,
}}
>
<Text type="secondary" style={{ fontSize: 12 }}>
Description
</Text>
<Radio.Group
size="small"
value={viewMode}
onChange={(e) => setViewMode(e.target.value)}
>
<Radio.Button value="formatted">Formatted</Radio.Button>
<Radio.Button value="json">JSON</Radio.Button>
</Radio.Group>
</div>
{viewMode === "formatted" ? (
<FormattedToolView tool={tool} />
) : (
<JsonToolView tool={tool} />
)}
</div>
);
}

View file

@ -0,0 +1,74 @@
/**
* Individual tool item component with expandable details
*/
import { useState } from "react";
import { Typography, Tag } from "antd";
import { ToolOutlined, RightOutlined, DownOutlined } from "@ant-design/icons";
import { ParsedTool } from "./types";
import { ToolExpandedContent } from "./ToolExpandedContent";
const { Text } = Typography;
interface ToolItemProps {
tool: ParsedTool;
}
export function ToolItem({ tool }: ToolItemProps) {
const [expanded, setExpanded] = useState(false);
return (
<div
style={{
border: "1px solid #f0f0f0",
borderRadius: 8,
overflow: "hidden",
}}
>
{/* Header Row - Always Visible */}
<div
onClick={() => setExpanded(!expanded)}
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "12px 16px",
cursor: "pointer",
background: expanded ? "#fafafa" : "#fff",
transition: "background 0.2s",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: 10 }}>
<ToolOutlined style={{ color: "#8c8c8c", fontSize: 14 }} />
<Text style={{ fontSize: 14 }}>
{tool.index}. {tool.name}
</Text>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<Tag color={tool.called ? "blue" : "default"}>
{tool.called ? "called" : "not called"}
</Tag>
{expanded ? (
<DownOutlined style={{ fontSize: 12, color: "#8c8c8c" }} />
) : (
<RightOutlined style={{ fontSize: 12, color: "#8c8c8c" }} />
)}
</div>
</div>
{/* Expanded Content */}
{expanded && (
<div
style={{
padding: "16px",
borderTop: "1px solid #f0f0f0",
background: "#fff",
}}
>
<ToolExpandedContent tool={tool} />
</div>
)}
</div>
);
}

View file

@ -0,0 +1,117 @@
/**
* Core tests for Tools section
*/
import { describe, it, expect } from "vitest";
import { parseToolsFromLog } from "./utils";
import { LogEntry } from "../columns";
describe("ToolsSection", () => {
it("should parse tools from request and match with response tool calls", () => {
const mockLog: LogEntry = {
request_id: "test-123",
api_key: "key",
team_id: "team",
model: "gpt-4",
model_id: "gpt-4",
call_type: "completion",
spend: 0.01,
total_tokens: 100,
prompt_tokens: 50,
completion_tokens: 50,
startTime: "2024-01-01T00:00:00Z",
endTime: "2024-01-01T00:00:01Z",
cache_hit: "none",
messages: JSON.stringify({
model: "gpt-4",
messages: [{ role: "user", content: "What's the weather?" }],
tools: [
{
type: "function",
function: {
name: "get_weather",
description: "Get the current weather",
parameters: {
type: "object",
required: ["location"],
properties: {
location: { type: "string", description: "City name" },
},
},
},
},
{
type: "function",
function: {
name: "search_web",
description: "Search the web",
parameters: {
type: "object",
required: ["query"],
properties: {
query: { type: "string", description: "Search query" },
},
},
},
},
],
}),
response: JSON.stringify({
choices: [
{
message: {
tool_calls: [
{
id: "call_123",
type: "function",
function: {
name: "get_weather",
arguments: '{"location": "San Francisco"}',
},
},
],
},
},
],
}),
};
const tools = parseToolsFromLog(mockLog);
expect(tools).toHaveLength(2);
expect(tools[0].name).toBe("get_weather");
expect(tools[0].called).toBe(true);
expect(tools[0].callData?.arguments).toEqual({ location: "San Francisco" });
expect(tools[1].name).toBe("search_web");
expect(tools[1].called).toBe(false);
});
it("should return empty array when no tools in request", () => {
const mockLog: LogEntry = {
request_id: "test-456",
api_key: "key",
team_id: "team",
model: "gpt-4",
model_id: "gpt-4",
call_type: "completion",
spend: 0.01,
total_tokens: 100,
prompt_tokens: 50,
completion_tokens: 50,
startTime: "2024-01-01T00:00:00Z",
endTime: "2024-01-01T00:00:01Z",
cache_hit: "none",
messages: JSON.stringify({
model: "gpt-4",
messages: [{ role: "user", content: "Hello" }],
}),
response: JSON.stringify({
choices: [{ message: { content: "Hi there!" } }],
}),
};
const tools = parseToolsFromLog(mockLog);
expect(tools).toHaveLength(0);
});
});

View file

@ -0,0 +1,42 @@
/**
* Tools section component that displays all available tools from the request
* and indicates which ones were actually called in the response
*/
import { Typography } from "antd";
import { LogEntry } from "../columns";
import { parseToolsFromLog } from "./utils";
import { ToolItem } from "./ToolItem";
const { Text } = Typography;
interface ToolsSectionProps {
log: LogEntry;
}
export function ToolsSection({ log }: ToolsSectionProps) {
const tools = parseToolsFromLog(log);
// Don't render if no tools
if (tools.length === 0) return null;
return (
<div className="bg-white rounded-lg shadow w-full max-w-full overflow-hidden p-4 mb-6">
<Text
strong
style={{
display: "block",
marginBottom: 12,
fontSize: 16,
}}
>
Tools
</Text>
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
{tools.map((tool) => (
<ToolItem key={tool.name} tool={tool} />
))}
</div>
</div>
);
}

View file

@ -0,0 +1,7 @@
/**
* Export main components and utilities for the Tools section
*/
export { ToolsSection } from "./ToolsSection";
export { parseToolsFromLog, hasTools } from "./utils";
export type { ParsedTool, ToolDefinition, ToolCall } from "./types";

View file

@ -0,0 +1,42 @@
/**
* Type definitions for the Tools section
*/
export interface ToolDefinition {
type: string;
function: {
name: string;
description?: string;
parameters?: Record<string, any>;
};
}
export interface ToolCall {
id: string;
type: string;
function: {
name: string;
arguments: string;
};
}
export interface ParsedTool {
index: number;
name: string;
description: string;
parameters: Record<string, any>;
called: boolean;
callData?: {
id: string;
name: string;
arguments: Record<string, any>;
};
}
export interface ParameterRow {
key: string;
name: string;
type: string;
description: string;
required: boolean;
}

View file

@ -0,0 +1,293 @@
/**
* Tests for tool parsing utilities
*/
import { describe, it, expect } from "vitest";
import { parseToolsFromLog, hasTools } from "./utils";
import { LogEntry } from "../columns";
describe("ToolsSection utils", () => {
describe("parseToolsFromLog", () => {
it("should return empty array when no tools in request", () => {
const log: Partial<LogEntry> = {
request_id: "test-1",
messages: [],
response: {},
};
const result = parseToolsFromLog(log as LogEntry);
expect(result).toEqual([]);
});
it("should parse tools from proxy_server_request", () => {
const log: Partial<LogEntry> = {
request_id: "test-2",
proxy_server_request: {
tools: [
{
type: "function",
function: {
name: "get_weather",
description: "Get the current weather",
parameters: {
type: "object",
properties: {
location: { type: "string" },
},
required: ["location"],
},
},
},
],
},
response: {},
} as any;
const result = parseToolsFromLog(log as LogEntry);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
index: 1,
name: "get_weather",
description: "Get the current weather",
called: false,
});
});
it("should parse tools from messages object format", () => {
const log: Partial<LogEntry> = {
request_id: "test-3",
messages: {
tools: [
{
type: "function",
function: {
name: "search_web",
description: "Search the web",
},
},
],
},
response: {},
} as any;
const result = parseToolsFromLog(log as LogEntry);
expect(result).toHaveLength(1);
expect(result[0].name).toBe("search_web");
});
it("should mark tools as called when present in response", () => {
const log: Partial<LogEntry> = {
request_id: "test-4",
proxy_server_request: {
tools: [
{
type: "function",
function: {
name: "get_weather",
description: "Get weather",
},
},
{
type: "function",
function: {
name: "send_email",
description: "Send email",
},
},
],
},
response: {
choices: [
{
message: {
tool_calls: [
{
id: "call_123",
type: "function",
function: {
name: "get_weather",
arguments: '{"location": "San Francisco"}',
},
},
],
},
},
],
},
} as any;
const result = parseToolsFromLog(log as LogEntry);
expect(result).toHaveLength(2);
expect(result[0].called).toBe(true);
expect(result[0].callData).toBeDefined();
expect(result[0].callData?.arguments).toEqual({
location: "San Francisco",
});
expect(result[1].called).toBe(false);
expect(result[1].callData).toBeUndefined();
});
it("should handle string format request and response", () => {
const log: Partial<LogEntry> = {
request_id: "test-5",
proxy_server_request: JSON.stringify({
tools: [
{
type: "function",
function: {
name: "calculate",
},
},
],
}),
response: JSON.stringify({
choices: [
{
message: {
tool_calls: [
{
id: "call_456",
type: "function",
function: {
name: "calculate",
arguments: '{"x": 5}',
},
},
],
},
},
],
}),
} as any;
const result = parseToolsFromLog(log as LogEntry);
expect(result).toHaveLength(1);
expect(result[0].called).toBe(true);
});
it("should handle tools with no description or parameters", () => {
const log: Partial<LogEntry> = {
request_id: "test-6",
proxy_server_request: {
tools: [
{
type: "function",
function: {
name: "minimal_tool",
},
},
],
},
response: {},
} as any;
const result = parseToolsFromLog(log as LogEntry);
expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
index: 1,
name: "minimal_tool",
description: "",
parameters: {},
called: false,
});
});
it("should handle invalid JSON in tool call arguments gracefully", () => {
const log: Partial<LogEntry> = {
request_id: "test-7",
proxy_server_request: {
tools: [
{
type: "function",
function: {
name: "test_tool",
},
},
],
},
response: {
choices: [
{
message: {
tool_calls: [
{
id: "call_789",
type: "function",
function: {
name: "test_tool",
arguments: "invalid json",
},
},
],
},
},
],
},
} as any;
const result = parseToolsFromLog(log as LogEntry);
expect(result).toHaveLength(1);
expect(result[0].called).toBe(true);
expect(result[0].callData?.arguments).toEqual({});
});
it("should assign correct indices to multiple tools", () => {
const log: Partial<LogEntry> = {
request_id: "test-8",
proxy_server_request: {
tools: [
{ type: "function", function: { name: "tool1" } },
{ type: "function", function: { name: "tool2" } },
{ type: "function", function: { name: "tool3" } },
],
},
response: {},
} as any;
const result = parseToolsFromLog(log as LogEntry);
expect(result).toHaveLength(3);
expect(result[0].index).toBe(1);
expect(result[1].index).toBe(2);
expect(result[2].index).toBe(3);
});
});
describe("hasTools", () => {
it("should return false when no tools in request", () => {
const log: Partial<LogEntry> = {
request_id: "test-9",
messages: [],
response: {},
};
expect(hasTools(log as LogEntry)).toBe(false);
});
it("should return true when tools present in request", () => {
const log: Partial<LogEntry> = {
request_id: "test-10",
proxy_server_request: {
tools: [
{
type: "function",
function: {
name: "test_tool",
},
},
],
},
response: {},
} as any;
expect(hasTools(log as LogEntry)).toBe(true);
});
});
});

View file

@ -0,0 +1,130 @@
/**
* Utility functions for parsing and processing tool data from log entries
*/
import { LogEntry } from "../columns";
import { ParsedTool, ToolDefinition, ToolCall } from "./types";
/**
* Parse raw data that might be a string or object
*/
function parseData(input: any): any {
if (typeof input === "string") {
try {
return JSON.parse(input);
} catch {
return input;
}
}
return input;
}
/**
* Extract tools array from request data
*/
function extractToolsFromRequest(log: LogEntry): ToolDefinition[] {
// Check proxy_server_request first (most complete), then messages
const requestData = parseData(log.proxy_server_request || log.messages);
if (!requestData) return [];
// Handle array format (messages array)
if (Array.isArray(requestData)) {
// Tools are not typically in messages array, return empty
return [];
}
// Handle object format (request body)
if (typeof requestData === "object" && requestData.tools) {
return Array.isArray(requestData.tools) ? requestData.tools : [];
}
return [];
}
/**
* Extract tool calls from response data
*/
function extractToolCallsFromResponse(log: LogEntry): ToolCall[] {
const responseData = parseData(log.response);
if (!responseData || typeof responseData !== "object") return [];
// OpenAI format: response.choices[0].message.tool_calls
const choices = responseData.choices;
if (Array.isArray(choices) && choices.length > 0) {
const firstChoice = choices[0];
const message = firstChoice.message;
if (message && Array.isArray(message.tool_calls)) {
return message.tool_calls;
}
}
return [];
}
/**
* Parse safe JSON with fallback
*/
function parseSafeJson(jsonString: string): Record<string, any> {
try {
return JSON.parse(jsonString);
} catch {
return {};
}
}
/**
* Main function to parse tools from a log entry
* Returns an array of tools with their definition and call status
*/
export function parseToolsFromLog(log: LogEntry): ParsedTool[] {
// Get tools from request
const requestTools = extractToolsFromRequest(log);
if (requestTools.length === 0) {
return [];
}
// Get tool calls from response
const toolCalls = extractToolCallsFromResponse(log);
const calledToolNames = new Set(
toolCalls.map((tc: ToolCall) => tc.function?.name).filter(Boolean)
);
// Map tool calls by name for quick lookup
const toolCallMap = new Map<string, any>();
toolCalls.forEach((tc: ToolCall) => {
const name = tc.function?.name;
if (name) {
toolCallMap.set(name, {
id: tc.id,
name: name,
arguments: parseSafeJson(tc.function?.arguments || "{}"),
});
}
});
// Parse each tool definition
return requestTools.map((tool: ToolDefinition, index: number) => {
const func = tool.function || { name: `Tool ${index + 1}` };
const name = func.name || `Tool ${index + 1}`;
return {
index: index + 1,
name: name,
description: func.description || "",
parameters: func.parameters || {},
called: calledToolNames.has(name),
callData: toolCallMap.get(name),
};
});
}
/**
* Check if a log entry has any tools
*/
export function hasTools(log: LogEntry): boolean {
const requestTools = extractToolsFromRequest(log);
return requestTools.length > 0;
}