Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance: Rework debug view #523

Merged
merged 5 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions components/chat/messages/calls.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState } from 'react';
import StackTrace from './calls/stackTrace';
import CallFrames from './calls/callFrames';
import type { CallFrame } from '@gptscript-ai/gptscript';
import { IoCloseSharp } from 'react-icons/io5';
import { BsArrowsFullscreen } from 'react-icons/bs';
Expand Down Expand Up @@ -43,7 +43,7 @@ const Calls = ({ calls }: { calls: Record<string, CallFrame> }) => {
<ModalHeader className="flex justify-between">
<div>
<div className="my-2">
<h1 className="text-2xl inline mr-2">Stack Trace</h1>
<h1 className="text-2xl inline mr-2">Call Frames</h1>
<SaveFile content={calls} />
</div>
<h2 className="text-base text-zinc-500">
Expand Down Expand Up @@ -77,7 +77,7 @@ const Calls = ({ calls }: { calls: Record<string, CallFrame> }) => {
</div>
</ModalHeader>
<ModalBody className="mb-4 h-full overflow-y-scroll">
<StackTrace calls={calls} />
<CallFrames calls={calls} />
</ModalBody>
</ModalContent>
</Modal>
Expand Down
310 changes: 310 additions & 0 deletions components/chat/messages/calls/callFrames.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
import React, { useState, useRef } from 'react';
import { Button, Tooltip } from '@nextui-org/react';
import type { CallFrame } from '@gptscript-ai/gptscript';
import { FaChevronUp, FaChevronDown } from 'react-icons/fa';
import { AiOutlineLoading3Quarters } from 'react-icons/ai';
import dynamic from 'next/dynamic';
import { JSONTree } from 'react-json-tree';

const CallFrames = ({ calls }: { calls: Record<string, CallFrame> | null }) => {
if (!calls) return null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you move this line to be placed right before the final return statement?

or add a TODO to do so?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont follow. Isn't this a short-circuit, the point of which is to just quit this function early if calls is null/empty?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

spoke offline. i follow now


// eslint-disable-next-line react-hooks/rules-of-hooks
const logsContainerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line react-hooks/rules-of-hooks
const [allOpen, setAllOpen] = useState(false);

const EmptyLogs = () => {
return (
<div className="">
<p>Waiting for the first event from GPTScript...</p>
</div>
);
};

// Build tree structure
const buildTree = (calls: Record<string, CallFrame>) => {
const tree: Record<string, any> = {};
const rootNodes: string[] = [];

// Sort calls by start timestamp
const sortedCalls = Object.entries(calls).sort((a, b) =>
new Date(a[1].start).getTime() - new Date(b[1].start).getTime()
);

sortedCalls.forEach(([id, call]) => {
// Skip "GPTScript Gateway Provider" calls
if (call.tool?.name === "GPTScript Gateway Provider") {
return;
}

const parentId = call.parentID || '';
if (!parentId) {
rootNodes.push(id);
} else {
if (!tree[parentId]) {
tree[parentId] = [];
}
tree[parentId].push(id);
}
});

return { tree, rootNodes };
};

// Render input (JSON or text)
const renderInput = (input: any) => {
if (typeof input === 'string') {
try {
const jsonInput = JSON.parse(input);
return (
<JSONTree
data={jsonInput}
theme={{
base00: 'transparent', // Set the background to transparent
}}
invertTheme={false}
shouldExpandNode={() => !allOpen}
/>
);
} catch (e) {
// If parsing fails, render as text
return <p className="ml-5 whitespace-pre-wrap">{input}</p>;
}
}
// If input is already an object, render as JSON
return (
<JSONTree
data={input}
theme={{
base00: 'transparent', // Set the background to transparent
}}
invertTheme={false}
shouldExpandNode={() => !allOpen}
/>
);
};

// Helper function to truncate and stringify input
const truncateInput = (input: any): string => {
const stringified = typeof input === 'string' ? input : JSON.stringify(input);
return stringified.length > 100 ? stringified.slice(0, 100) + '...' : stringified;
};

// Render tree recursively
const renderTree = (nodeId: string, depth: number = 0) => {
const call = calls[nodeId];
const children = tree[nodeId] || [];

return (
<div key={nodeId} style={{ marginLeft: `${depth * 20}px` }}>
<details open={depth === 0 || allOpen}>
<Summary call={call} />
<div className="ml-5">
{call.tool?.source?.location && call.tool.source.location !== "inline" && (
<div className="mb-2 text-xs text-gray-400">
Source: <a href={call.tool.source.location} target="_blank" rel="noopener noreferrer" className="underline hover:text-gray-300">{call.tool.source.location}</a>
</div>
)}
<details open={allOpen}>
<summary className="cursor-pointer">
Input Message: {truncateInput(call?.input)}
</summary>
<div className="ml-5">{renderInput(call?.input)}</div>
</details>
<details open={allOpen}>
<summary className="cursor-pointer">Output Messages</summary>
<ul className="ml-5 list-none">
{call.output && call.output.length > 0 ? (
call.output.flatMap((output, key) => {
if (output.content) {
return [(
<li key={`content-${key}`} className="mb-2">
<details open={allOpen}>
<summary className="cursor-pointer">
{truncateInput(output.content)}
</summary>
<p className="ml-5 whitespace-pre-wrap">{output.content}</p>
</details>
</li>
)];
} else if (output.subCalls) {
return Object.entries(output.subCalls).map(([subCallKey, subCall]) => (
<li key={`subcall-${key}-${subCallKey}`} className="mb-2">
<details open={allOpen}>
<summary className="cursor-pointer">
Tool call: {truncateInput(subCallKey)}
</summary>
<p className="ml-5 whitespace-pre-wrap">Tool Call ID: {subCallKey}</p>
<p className="ml-5 whitespace-pre-wrap">Tool ID: {subCall.toolID}</p>
<p className="ml-5 whitespace-pre-wrap">Input: {subCall.input}</p>
</details>
</li>
));
}
return [];
})
) : (
<li>
<p className="ml-5">No output available</p>
</li>
)}
</ul>
</details>
{children.length > 0 && (
<details open={allOpen}>
<summary className="cursor-pointer">Subcalls</summary>
<div className="ml-5">
{children.map((childId: string) => renderTree(childId, depth + 1))}
</div>
</details>
)}
{(call.llmRequest || call.llmResponse) && (
<details open={allOpen}>
<summary className="cursor-pointer">
{call.llmRequest && 'messages' in call.llmRequest
? 'LLM Request & Response'
: 'Tool Command and Output'}
</summary>
<div className="ml-5">
{call.llmRequest && (
<details open={allOpen}>
<summary className="cursor-pointer">
{call.llmRequest && 'messages' in call.llmRequest ? 'Request' : 'Command'}
</summary>
<div className="ml-5">{renderInput(call.llmRequest)}</div>
</details>
)}
{call.llmResponse && (
<details open={allOpen}>
<summary className="cursor-pointer">
{call.llmRequest && 'messages' in call.llmRequest ? 'Response' : 'Output'}
</summary>
<div className="ml-5">{renderInput(call.llmResponse)}</div>
</details>
)}
</div>
</details>
)}
{call.tool?.toolMapping && (
<details open={allOpen}>
<summary className="cursor-pointer">Tools</summary>
<div className="ml-5">
{renderToolMapping(call.tool.toolMapping)}
</div>
</details>
)}
{call.tool?.export && (
<details open={allOpen}>
<summary className="cursor-pointer">Shared Tools</summary>
<div className="ml-5">
{renderExports(call.tool.export)}
</div>
</details>
)}
</div>
</details>
</div>
);
};

const renderToolMapping = (toolMapping: Record<string, any>) => {
return Object.entries(toolMapping).map(([key, value]) => (
<div key={key} className="mb-2">
{value.some((item: any) => item.toolID !== key) ? (
<>
{key}:
<ul className="list-none ml-5">
{value.map((item: any, index: number) => (
<li key={index} className="mb-2">
<p className="ml-5 whitespace-pre-wrap">{item.reference}</p>
<p className="ml-5 whitespace-pre-wrap">{item.toolID}</p>
</li>
))}
</ul>
</>
) : (
<p className="whitespace-pre-wrap">{key}</p>
)}
</div>
));
};

const renderExports = (exports: string[]) => {
return (
<ul className="list-none ml-5">
{exports.map((item, index) => (
<li key={index} className="mb-2">
<p className="whitespace-pre-wrap">{item}</p>
</li>
))}
</ul>
);
};

const { tree, rootNodes } = buildTree(calls);

return (
<div
className="h-full overflow-scroll p-4 rounded-2xl border-2 shadow-lg border-primary border-lg bg-black text-white"
ref={logsContainerRef}
>
<Tooltip content={allOpen ? 'Collapse all' : 'Expand all'} closeDelay={0}>
<Button
onPress={() => setAllOpen(!allOpen)}
className="absolute right-8"
isIconOnly
radius="full"
color="primary"
>
{allOpen ? <FaChevronUp /> : <FaChevronDown />}
</Button>
</Tooltip>
{rootNodes.length > 0 ? rootNodes.map((rootId) => renderTree(rootId)) : <EmptyLogs />}
</div>
);
};

const Summary = ({ call }: { call: CallFrame }) => {
const name =
call.tool?.name ||
call.tool?.source?.repo ||
call.tool?.source?.location ||
'main';
const category = call.toolCategory;

const startTime = new Date(call.start).toLocaleTimeString();
const endTime = call.end ? new Date(call.end).toLocaleTimeString() : 'In progress';
const duration = call.end
? `${((new Date(call.end).getTime() - new Date(call.start).getTime()) / 1000).toFixed(2)}s`
: 'N/A';

const timeInfo = `[ID: ${call.id}] [${startTime} - ${endTime}, ${duration}]`;

let summaryContent;
if (call.tool?.chat) {
summaryContent = call.type !== 'callFinish'
? `Chat open with ${name}`
: `Chatted with ${name}`;
} else {
summaryContent = call.type !== 'callFinish'
? category
? `Loading ${category} from ${name}` + '...'
: `Running ${name}`
: category
? `Loaded ${category} from ${name}`
: `Ran ${name}`;
}

return (
<summary className="cursor-pointer">
{summaryContent}{' '}
<span className="text-xs text-gray-400">{timeInfo}</span>
{call?.type !== 'callFinish' && (
<AiOutlineLoading3Quarters className="ml-2 animate-spin inline" />
)}
</summary>
);
};

export default CallFrames;
Loading
Loading