mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-23 00:41:40 +00:00
v1 - new pretty view
This commit is contained in:
parent
2156db9f06
commit
e1b1263da1
8 changed files with 681 additions and 43 deletions
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* HistorySection - Collapsible section for displaying message history
|
||||
* Shows a summary when collapsed, full messages when expanded
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Typography } from 'antd';
|
||||
import { UpOutlined, DownOutlined } from '@ant-design/icons';
|
||||
import { ParsedMessage } from './prettyMessagesTypes';
|
||||
import { MessageCard } from './MessageCard';
|
||||
import { ROLE_STYLES } from './prettyMessagesUtils';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface HistorySectionProps {
|
||||
messages: ParsedMessage[];
|
||||
}
|
||||
|
||||
export function HistorySection({ messages }: HistorySectionProps) {
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
if (messages.length === 0) return null;
|
||||
|
||||
// Build summary strip showing message flow
|
||||
const summary = messages
|
||||
.map((m) => {
|
||||
const style = ROLE_STYLES[m.role] || ROLE_STYLES.user;
|
||||
return style.label;
|
||||
})
|
||||
.join(' → ');
|
||||
|
||||
return (
|
||||
<div style={{ margin: '16px 0' }}>
|
||||
{/* Collapsed: Dashed line with label */}
|
||||
<div
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'pointer',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 1, borderTop: '1px dashed #d9d9d9' }} />
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
History ({messages.length} message{messages.length !== 1 ? 's' : ''})
|
||||
{!isExpanded && ` · ${summary}`}
|
||||
</Text>
|
||||
{isExpanded ? (
|
||||
<UpOutlined style={{ color: '#8c8c8c', fontSize: 10 }} />
|
||||
) : (
|
||||
<DownOutlined style={{ color: '#8c8c8c', fontSize: 10 }} />
|
||||
)}
|
||||
<div style={{ flex: 1, borderTop: '1px dashed #d9d9d9' }} />
|
||||
</div>
|
||||
|
||||
{/* Expanded View - Full Messages with subtle indent */}
|
||||
{isExpanded && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 16,
|
||||
paddingLeft: 16,
|
||||
borderLeft: '1px solid #f0f0f0',
|
||||
}}
|
||||
>
|
||||
{messages.map((msg, index) => (
|
||||
<MessageCard
|
||||
key={index}
|
||||
message={msg}
|
||||
defaultCollapsed={msg.content?.length > 500}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { useState } from "react";
|
||||
import { Drawer, Typography, Button, Descriptions, Card, Tag, Tabs, Alert, message, Collapse } from "antd";
|
||||
import { Drawer, Typography, Button, Descriptions, Card, Tag, Tabs, Alert, message, Collapse, Radio } from "antd";
|
||||
import { CopyOutlined } from "@ant-design/icons";
|
||||
import moment from "moment";
|
||||
import { LogEntry } from "../columns";
|
||||
|
|
@ -27,6 +27,7 @@ import {
|
|||
MESSAGE_REQUEST_ID_COPIED,
|
||||
} from "./constants";
|
||||
import { ToolsSection } from "../ToolsSection";
|
||||
import { PrettyMessagesView } from "./PrettyMessagesView";
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
|
|
@ -349,6 +350,7 @@ function RequestResponseSection({
|
|||
getFormattedResponse,
|
||||
}: RequestResponseSectionProps) {
|
||||
const [activeTab, setActiveTab] = useState<typeof TAB_REQUEST | typeof TAB_RESPONSE>(TAB_REQUEST);
|
||||
const [viewMode, setViewMode] = useState<'pretty' | 'json'>('pretty');
|
||||
|
||||
const handleCopy = () => {
|
||||
const data = activeTab === TAB_REQUEST ? getRawRequest() : getFormattedResponse();
|
||||
|
|
@ -367,47 +369,66 @@ function RequestResponseSection({
|
|||
label: <h3 className="text-lg font-medium text-gray-900">Request & Response</h3>,
|
||||
children: (
|
||||
<div style={{ padding: "0 24px" }}>
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={(key) => setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)}
|
||||
tabBarExtraContent={
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={handleCopy}
|
||||
disabled={activeTab === TAB_RESPONSE && !hasResponse}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
}
|
||||
items={[
|
||||
{
|
||||
key: TAB_REQUEST,
|
||||
label: "Request",
|
||||
children: (
|
||||
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
|
||||
<JsonViewer data={getRawRequest()} mode="formatted" />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: TAB_RESPONSE,
|
||||
label: "Response",
|
||||
children: (
|
||||
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
|
||||
{hasResponse ? (
|
||||
<JsonViewer data={getFormattedResponse()} mode="formatted" />
|
||||
) : (
|
||||
<div style={{ textAlign: "center", padding: 20, color: "#999", fontStyle: "italic" }}>
|
||||
Response data not available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{/* View Mode Toggle - Top Right */}
|
||||
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 16 }}>
|
||||
<Radio.Group
|
||||
size="small"
|
||||
value={viewMode}
|
||||
onChange={(e) => setViewMode(e.target.value)}
|
||||
>
|
||||
<Radio.Button value="pretty">Pretty</Radio.Button>
|
||||
<Radio.Button value="json">JSON</Radio.Button>
|
||||
</Radio.Group>
|
||||
</div>
|
||||
|
||||
{viewMode === 'pretty' ? (
|
||||
<PrettyMessagesView
|
||||
request={getRawRequest()}
|
||||
response={getFormattedResponse()}
|
||||
/>
|
||||
) : (
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={(key) => setActiveTab(key as typeof TAB_REQUEST | typeof TAB_RESPONSE)}
|
||||
tabBarExtraContent={
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={handleCopy}
|
||||
disabled={activeTab === TAB_RESPONSE && !hasResponse}
|
||||
>
|
||||
Copy
|
||||
</Button>
|
||||
}
|
||||
items={[
|
||||
{
|
||||
key: TAB_REQUEST,
|
||||
label: "Request",
|
||||
children: (
|
||||
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
|
||||
<JsonViewer data={getRawRequest()} mode="formatted" />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: TAB_RESPONSE,
|
||||
label: "Response",
|
||||
children: (
|
||||
<div style={{ paddingTop: SPACING_XLARGE, paddingBottom: SPACING_XLARGE }}>
|
||||
{hasResponse ? (
|
||||
<JsonViewer data={getFormattedResponse()} mode="formatted" />
|
||||
) : (
|
||||
<div style={{ textAlign: "center", padding: 20, color: "#999", fontStyle: "italic" }}>
|
||||
Response data not available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,199 @@
|
|||
/**
|
||||
* MessageCard - Display individual message with role-based styling
|
||||
* Features: collapsible long content, copy button, tool calls display
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button, Typography, message as antdMessage } from 'antd';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
import { ParsedMessage } from './prettyMessagesTypes';
|
||||
import { ROLE_STYLES } from './prettyMessagesUtils';
|
||||
import { ToolCallCard } from './ToolCallCard';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface MessageCardProps {
|
||||
message: ParsedMessage;
|
||||
defaultCollapsed?: boolean;
|
||||
showToolCalls?: boolean;
|
||||
}
|
||||
|
||||
const TRUNCATE_LENGTH = 500;
|
||||
|
||||
export function MessageCard({
|
||||
message,
|
||||
defaultCollapsed = false,
|
||||
showToolCalls = false,
|
||||
}: MessageCardProps) {
|
||||
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const style = ROLE_STYLES[message.role] || ROLE_STYLES.user;
|
||||
const content = message.content || '';
|
||||
const isLong = content.length > TRUNCATE_LENGTH;
|
||||
const shouldTruncate = isCollapsed && isLong;
|
||||
|
||||
// Don't show empty content for assistant messages with tool calls
|
||||
const hasContent = content.length > 0;
|
||||
const hasToolCalls = showToolCalls && message.toolCalls && message.toolCalls.length > 0;
|
||||
|
||||
// If assistant message with no content but has tool calls, skip null display
|
||||
if (message.role === 'assistant' && !hasContent && hasToolCalls) {
|
||||
return (
|
||||
<div
|
||||
style={{ marginBottom: 16 }}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{/* Role Label Row */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
strong
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: style.labelColor,
|
||||
letterSpacing: '0.5px',
|
||||
}}
|
||||
>
|
||||
{style.label}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/* Tool Calls with left border */}
|
||||
<div
|
||||
style={{
|
||||
borderLeft: `2px solid ${style.borderColor}`,
|
||||
paddingLeft: 12,
|
||||
}}
|
||||
>
|
||||
{message.toolCalls!.map((tool, index) => (
|
||||
<ToolCallCard key={tool.id || index} tool={tool} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(content);
|
||||
antdMessage.success('Message copied');
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{ marginBottom: 16 }}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{/* Role Label Row */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
strong
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: style.labelColor,
|
||||
letterSpacing: '0.5px',
|
||||
}}
|
||||
>
|
||||
{style.label}
|
||||
{isLong && (
|
||||
<Text type="secondary" style={{ marginLeft: 8, fontWeight: 'normal', fontSize: 11 }}>
|
||||
({content.length.toLocaleString()} chars)
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
|
||||
{/* Copy Button - Show on hover */}
|
||||
{hasContent && (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
style={{
|
||||
opacity: isHovered ? 1 : 0,
|
||||
transition: 'opacity 0.2s',
|
||||
}}
|
||||
onClick={handleCopy}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content with left border accent */}
|
||||
{hasContent && (
|
||||
<div
|
||||
style={{
|
||||
borderLeft: `2px solid ${style.borderColor}`,
|
||||
paddingLeft: 12,
|
||||
fontSize: 13,
|
||||
lineHeight: 1.6,
|
||||
color: '#262626',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{shouldTruncate ? (
|
||||
<>
|
||||
{content.slice(0, TRUNCATE_LENGTH)}...
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => setIsCollapsed(false)}
|
||||
style={{ padding: '0 4px', fontSize: 12 }}
|
||||
>
|
||||
Show more
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{content}
|
||||
{isLong && !isCollapsed && (
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
onClick={() => setIsCollapsed(true)}
|
||||
style={{
|
||||
padding: '0 4px',
|
||||
display: 'block',
|
||||
marginTop: 4,
|
||||
fontSize: 12,
|
||||
}}
|
||||
>
|
||||
Show less
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tool Calls (for assistant messages) */}
|
||||
{hasToolCalls && hasContent && (
|
||||
<div
|
||||
style={{
|
||||
borderLeft: `2px solid ${style.borderColor}`,
|
||||
paddingLeft: 12,
|
||||
marginTop: 8,
|
||||
}}
|
||||
>
|
||||
{message.toolCalls!.map((tool, index) => (
|
||||
<ToolCallCard key={tool.id || index} tool={tool} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
/**
|
||||
* PrettyMessagesView - Chat-style view of request and response messages
|
||||
* Replaces raw JSON with scannable, readable message cards
|
||||
*/
|
||||
|
||||
import { Typography } from 'antd';
|
||||
import { parseMessages } from './prettyMessagesUtils';
|
||||
import { MessageCard } from './MessageCard';
|
||||
import { HistorySection } from './HistorySection';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface PrettyMessagesViewProps {
|
||||
request: any;
|
||||
response: any;
|
||||
}
|
||||
|
||||
export function PrettyMessagesView({ request, response }: PrettyMessagesViewProps) {
|
||||
const { requestMessages, responseMessage } = parseMessages(request, response);
|
||||
|
||||
// Separate system, history, and last user message
|
||||
const systemMessage = requestMessages.find((m) => m.role === 'system');
|
||||
const nonSystemMessages = requestMessages.filter((m) => m.role !== 'system');
|
||||
const lastUserMessage =
|
||||
nonSystemMessages.length > 0 ? nonSystemMessages[nonSystemMessages.length - 1] : null;
|
||||
const historyMessages = nonSystemMessages.slice(0, -1);
|
||||
|
||||
return (
|
||||
<div style={{ paddingTop: 4, paddingBottom: 16 }}>
|
||||
{/* REQUEST SECTION */}
|
||||
<div>
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
marginBottom: 16,
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
Request ({requestMessages.length} message{requestMessages.length !== 1 ? 's' : ''})
|
||||
</Text>
|
||||
|
||||
{/* System Message - Collapsed by default */}
|
||||
{systemMessage && <MessageCard message={systemMessage} defaultCollapsed={true} />}
|
||||
|
||||
{/* History - Collapsed if > 0 messages */}
|
||||
{historyMessages.length > 0 && <HistorySection messages={historyMessages} />}
|
||||
|
||||
{/* Last User Message - Always expanded */}
|
||||
{lastUserMessage && lastUserMessage.role === 'user' && (
|
||||
<MessageCard message={lastUserMessage} defaultCollapsed={false} />
|
||||
)}
|
||||
|
||||
{/* Fallback if no messages */}
|
||||
{requestMessages.length === 0 && (
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: 20,
|
||||
color: '#8c8c8c',
|
||||
fontStyle: 'italic',
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
No request messages available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Section Divider */}
|
||||
<div
|
||||
style={{
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
margin: '20px 0',
|
||||
paddingTop: 16,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
type="secondary"
|
||||
style={{
|
||||
fontSize: 12,
|
||||
marginBottom: 16,
|
||||
display: 'block',
|
||||
}}
|
||||
>
|
||||
Response
|
||||
</Text>
|
||||
|
||||
{/* RESPONSE SECTION */}
|
||||
{responseMessage ? (
|
||||
<MessageCard message={responseMessage} defaultCollapsed={false} showToolCalls />
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
padding: 20,
|
||||
color: '#8c8c8c',
|
||||
fontStyle: 'italic',
|
||||
fontSize: 13,
|
||||
}}
|
||||
>
|
||||
Response data not available
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* ToolCallCard - Display tool call information inline in assistant messages
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button, Typography, message } from 'antd';
|
||||
import { CopyOutlined, ToolOutlined } from '@ant-design/icons';
|
||||
import { ToolCall } from './prettyMessagesTypes';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
interface ToolCallCardProps {
|
||||
tool: ToolCall;
|
||||
}
|
||||
|
||||
export function ToolCallCard({ tool }: ToolCallCardProps) {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(JSON.stringify(tool.arguments, null, 2));
|
||||
message.success('Tool arguments copied');
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
background: '#fafafa',
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: 4,
|
||||
padding: '8px 12px',
|
||||
marginBottom: 8,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
}}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{/* Tool Header */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: Object.keys(tool.arguments).length > 0 ? 6 : 0,
|
||||
}}
|
||||
>
|
||||
<Text strong style={{ fontSize: 12, color: '#262626' }}>
|
||||
{tool.name}
|
||||
</Text>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
style={{
|
||||
opacity: isHovered ? 1 : 0,
|
||||
transition: 'opacity 0.2s',
|
||||
}}
|
||||
onClick={handleCopy}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Tool Arguments - Simple key: value format */}
|
||||
{Object.keys(tool.arguments).length > 0 && (
|
||||
<div>
|
||||
{Object.entries(tool.arguments).map(([key, value]) => (
|
||||
<div key={key} style={{ marginBottom: 2 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{key}:
|
||||
</Text>{' '}
|
||||
<Text code style={{ background: 'transparent', fontSize: 12 }}>
|
||||
{JSON.stringify(value)}
|
||||
</Text>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/**
|
||||
* Type definitions for pretty messages view
|
||||
*/
|
||||
|
||||
export interface ParsedMessage {
|
||||
role: 'system' | 'user' | 'assistant' | 'tool';
|
||||
content: string;
|
||||
toolCalls?: ToolCall[];
|
||||
toolCallId?: string;
|
||||
}
|
||||
|
||||
export interface ToolCall {
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface ParsedMessages {
|
||||
requestMessages: ParsedMessage[];
|
||||
responseMessage: ParsedMessage | null;
|
||||
}
|
||||
|
||||
export interface RoleStyle {
|
||||
background: string;
|
||||
borderColor: string;
|
||||
label: string;
|
||||
labelColor: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* Utility functions for parsing and formatting messages for pretty view
|
||||
*/
|
||||
|
||||
import { ParsedMessage, ParsedMessages, RoleStyle } from './prettyMessagesTypes';
|
||||
|
||||
/**
|
||||
* Role color styles for message cards - minimal, professional design
|
||||
* Color only used for labels and left border accent
|
||||
*/
|
||||
export const ROLE_STYLES: Record<string, RoleStyle> = {
|
||||
system: {
|
||||
background: 'transparent',
|
||||
borderColor: '#8c8c8c',
|
||||
label: 'SYSTEM',
|
||||
labelColor: '#8c8c8c',
|
||||
},
|
||||
user: {
|
||||
background: 'transparent',
|
||||
borderColor: '#1677ff',
|
||||
label: 'USER',
|
||||
labelColor: '#1677ff',
|
||||
},
|
||||
assistant: {
|
||||
background: 'transparent',
|
||||
borderColor: '#52c41a',
|
||||
label: 'ASSISTANT',
|
||||
labelColor: '#52c41a',
|
||||
},
|
||||
tool: {
|
||||
background: 'transparent',
|
||||
borderColor: '#fa8c16',
|
||||
label: 'TOOL RESULT',
|
||||
labelColor: '#fa8c16',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse request messages and response message from log data
|
||||
*/
|
||||
export const parseMessages = (request: any, response: any): ParsedMessages => {
|
||||
// Parse request messages
|
||||
const requestMessages: ParsedMessage[] = [];
|
||||
|
||||
if (request?.messages && Array.isArray(request.messages)) {
|
||||
request.messages.forEach((msg: any) => {
|
||||
requestMessages.push({
|
||||
role: msg.role || 'user',
|
||||
content: parseMessageContent(msg.content),
|
||||
toolCallId: msg.tool_call_id,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Parse response message
|
||||
let responseMessage: ParsedMessage | null = null;
|
||||
const responseMsg = response?.choices?.[0]?.message;
|
||||
|
||||
if (responseMsg) {
|
||||
responseMessage = {
|
||||
role: responseMsg.role || 'assistant',
|
||||
content: responseMsg.content || '',
|
||||
toolCalls: parseToolCalls(responseMsg.tool_calls),
|
||||
};
|
||||
}
|
||||
|
||||
return { requestMessages, responseMessage };
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse message content - handle strings and content arrays (for vision, etc.)
|
||||
*/
|
||||
const parseMessageContent = (content: any): string => {
|
||||
if (typeof content === 'string') {
|
||||
return content;
|
||||
}
|
||||
|
||||
if (Array.isArray(content)) {
|
||||
// Handle content arrays (vision API format)
|
||||
return content
|
||||
.map((item) => {
|
||||
if (typeof item === 'string') return item;
|
||||
if (item.type === 'text') return item.text;
|
||||
if (item.type === 'image_url') return '[Image]';
|
||||
return JSON.stringify(item);
|
||||
})
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
// Fallback to JSON string for complex content
|
||||
return JSON.stringify(content);
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse tool calls from response message
|
||||
*/
|
||||
const parseToolCalls = (toolCalls: any[]): Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
arguments: Record<string, any>;
|
||||
}> | undefined => {
|
||||
if (!toolCalls || !Array.isArray(toolCalls)) return undefined;
|
||||
|
||||
return toolCalls.map((tc) => ({
|
||||
id: tc.id || '',
|
||||
name: tc.function?.name || 'unknown',
|
||||
arguments: parseToolArguments(tc.function?.arguments),
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* Parse tool arguments - handle both string and object formats
|
||||
*/
|
||||
const parseToolArguments = (args: any): Record<string, any> => {
|
||||
if (!args) return {};
|
||||
|
||||
if (typeof args === 'string') {
|
||||
try {
|
||||
return JSON.parse(args);
|
||||
} catch {
|
||||
return { raw: args };
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
};
|
||||
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue