Skip to content

Commit 576ef7a

Browse files
authored
Enhance: Rework debug view (#523)
Signed-off-by: Craig Jellick <[email protected]>
1 parent 1b56692 commit 576ef7a

File tree

7 files changed

+387
-144
lines changed

7 files changed

+387
-144
lines changed

components/chat/messages/calls.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useState } from 'react';
2-
import StackTrace from './calls/stackTrace';
2+
import CallFrames from './calls/callFrames';
33
import type { CallFrame } from '@gptscript-ai/gptscript';
44
import { IoCloseSharp } from 'react-icons/io5';
55
import { BsArrowsFullscreen } from 'react-icons/bs';
@@ -43,7 +43,7 @@ const Calls = ({ calls }: { calls: Record<string, CallFrame> }) => {
4343
<ModalHeader className="flex justify-between">
4444
<div>
4545
<div className="my-2">
46-
<h1 className="text-2xl inline mr-2">Stack Trace</h1>
46+
<h1 className="text-2xl inline mr-2">Call Frames</h1>
4747
<SaveFile content={calls} />
4848
</div>
4949
<h2 className="text-base text-zinc-500">
@@ -77,7 +77,7 @@ const Calls = ({ calls }: { calls: Record<string, CallFrame> }) => {
7777
</div>
7878
</ModalHeader>
7979
<ModalBody className="mb-4 h-full overflow-y-scroll">
80-
<StackTrace calls={calls} />
80+
<CallFrames calls={calls} />
8181
</ModalBody>
8282
</ModalContent>
8383
</Modal>
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
import React, { useState, useRef } from 'react';
2+
import { Button, Tooltip } from '@nextui-org/react';
3+
import type { CallFrame } from '@gptscript-ai/gptscript';
4+
import { FaChevronUp, FaChevronDown } from 'react-icons/fa';
5+
import { AiOutlineLoading3Quarters } from 'react-icons/ai';
6+
import { JSONTree } from 'react-json-tree';
7+
8+
const CallFrames = ({ calls }: { calls: Record<string, CallFrame> | null }) => {
9+
// TODO Move this to before the final return so that we aren't disobeying the rules of hooks
10+
if (!calls) return null;
11+
12+
// eslint-disable-next-line react-hooks/rules-of-hooks
13+
const logsContainerRef = useRef<HTMLDivElement>(null);
14+
// eslint-disable-next-line react-hooks/rules-of-hooks
15+
const [allOpen, setAllOpen] = useState(false);
16+
17+
const EmptyLogs = () => {
18+
return (
19+
<div className="">
20+
<p>Waiting for the first event from GPTScript...</p>
21+
</div>
22+
);
23+
};
24+
25+
// Build tree structure
26+
const buildTree = (calls: Record<string, CallFrame>) => {
27+
const tree: Record<string, any> = {};
28+
const rootNodes: string[] = [];
29+
30+
// Sort calls by start timestamp
31+
const sortedCalls = Object.entries(calls).sort(
32+
(a, b) => new Date(a[1].start).getTime() - new Date(b[1].start).getTime()
33+
);
34+
35+
sortedCalls.forEach(([id, call]) => {
36+
// Skip "GPTScript Gateway Provider" calls
37+
if (call.tool?.name === 'GPTScript Gateway Provider') {
38+
return;
39+
}
40+
41+
const parentId = call.parentID || '';
42+
if (!parentId) {
43+
rootNodes.push(id);
44+
} else {
45+
if (!tree[parentId]) {
46+
tree[parentId] = [];
47+
}
48+
tree[parentId].push(id);
49+
}
50+
});
51+
52+
return { tree, rootNodes };
53+
};
54+
55+
// Render input (JSON or text)
56+
const renderInput = (input: any) => {
57+
if (typeof input === 'string') {
58+
try {
59+
const jsonInput = JSON.parse(input);
60+
return (
61+
<JSONTree
62+
data={jsonInput}
63+
theme={{
64+
base00: 'transparent', // Set the background to transparent
65+
}}
66+
invertTheme={false}
67+
shouldExpandNodeInitially={() => !allOpen}
68+
/>
69+
);
70+
} catch (_) {
71+
// If parsing fails, render as text
72+
return <p className="ml-5 whitespace-pre-wrap">{input}</p>;
73+
}
74+
}
75+
// If input is already an object, render as JSON
76+
return (
77+
<JSONTree
78+
data={input}
79+
theme={{
80+
base00: 'transparent', // Set the background to transparent
81+
}}
82+
invertTheme={false}
83+
shouldExpandNodeInitially={() => !allOpen}
84+
/>
85+
);
86+
};
87+
88+
// Helper function to truncate and stringify input
89+
const truncateInput = (input: any): string => {
90+
const stringified =
91+
typeof input === 'string' ? input : JSON.stringify(input);
92+
return stringified.length > 100
93+
? stringified.slice(0, 100) + '...'
94+
: stringified;
95+
};
96+
97+
// Render tree recursively
98+
const renderTree = (nodeId: string, depth: number = 0) => {
99+
const call = calls[nodeId];
100+
const children = tree[nodeId] || [];
101+
102+
return (
103+
<div key={nodeId} style={{ marginLeft: `${depth * 20}px` }}>
104+
<details open={depth === 0 || allOpen}>
105+
<Summary call={call} />
106+
<div className="ml-5">
107+
{call.tool?.source?.location &&
108+
call.tool.source.location !== 'inline' && (
109+
<div className="mb-2 text-xs text-gray-400">
110+
Source:{' '}
111+
<a
112+
href={call.tool.source.location}
113+
target="_blank"
114+
rel="noopener noreferrer"
115+
className="underline hover:text-gray-300"
116+
>
117+
{call.tool.source.location}
118+
</a>
119+
</div>
120+
)}
121+
<details open={allOpen}>
122+
<summary className="cursor-pointer">
123+
Input Message: {truncateInput(call?.input)}
124+
</summary>
125+
<div className="ml-5">{renderInput(call?.input)}</div>
126+
</details>
127+
<details open={allOpen}>
128+
<summary className="cursor-pointer">Output Messages</summary>
129+
<ul className="ml-5 list-none">
130+
{call.output && call.output.length > 0 ? (
131+
call.output.flatMap((output, key) => {
132+
if (output.content) {
133+
return [
134+
<li key={`content-${key}`} className="mb-2">
135+
<details open={allOpen}>
136+
<summary className="cursor-pointer">
137+
{truncateInput(output.content)}
138+
</summary>
139+
<p className="ml-5 whitespace-pre-wrap">
140+
{output.content}
141+
</p>
142+
</details>
143+
</li>,
144+
];
145+
} else if (output.subCalls) {
146+
return Object.entries(output.subCalls).map(
147+
([subCallKey, subCall]) => (
148+
<li
149+
key={`subcall-${key}-${subCallKey}`}
150+
className="mb-2"
151+
>
152+
<details open={allOpen}>
153+
<summary className="cursor-pointer">
154+
Tool call: {truncateInput(subCallKey)}
155+
</summary>
156+
<p className="ml-5 whitespace-pre-wrap">
157+
Tool Call ID: {subCallKey}
158+
</p>
159+
<p className="ml-5 whitespace-pre-wrap">
160+
Tool ID: {subCall.toolID}
161+
</p>
162+
<p className="ml-5 whitespace-pre-wrap">
163+
Input: {subCall.input}
164+
</p>
165+
</details>
166+
</li>
167+
)
168+
);
169+
}
170+
return [];
171+
})
172+
) : (
173+
<li>
174+
<p className="ml-5">No output available</p>
175+
</li>
176+
)}
177+
</ul>
178+
</details>
179+
{children.length > 0 && (
180+
<details open={allOpen}>
181+
<summary className="cursor-pointer">Subcalls</summary>
182+
<div className="ml-5">
183+
{children.map((childId: string) =>
184+
renderTree(childId, depth + 1)
185+
)}
186+
</div>
187+
</details>
188+
)}
189+
{(call.llmRequest || call.llmResponse) && (
190+
<details open={allOpen}>
191+
<summary className="cursor-pointer">
192+
{call.llmRequest && 'messages' in call.llmRequest
193+
? 'LLM Request & Response'
194+
: 'Tool Command and Output'}
195+
</summary>
196+
<div className="ml-5">
197+
{call.llmRequest && (
198+
<details open={allOpen}>
199+
<summary className="cursor-pointer">
200+
{call.llmRequest && 'messages' in call.llmRequest
201+
? 'Request'
202+
: 'Command'}
203+
</summary>
204+
<div className="ml-5">{renderInput(call.llmRequest)}</div>
205+
</details>
206+
)}
207+
{call.llmResponse && (
208+
<details open={allOpen}>
209+
<summary className="cursor-pointer">
210+
{call.llmRequest && 'messages' in call.llmRequest
211+
? 'Response'
212+
: 'Output'}
213+
</summary>
214+
<div className="ml-5">
215+
{renderInput(call.llmResponse)}
216+
</div>
217+
</details>
218+
)}
219+
</div>
220+
</details>
221+
)}
222+
{call.tool?.toolMapping && (
223+
<details open={allOpen}>
224+
<summary className="cursor-pointer">Tools</summary>
225+
<div className="ml-5">
226+
{renderToolMapping(call.tool.toolMapping)}
227+
</div>
228+
</details>
229+
)}
230+
{call.tool?.export && (
231+
<details open={allOpen}>
232+
<summary className="cursor-pointer">Shared Tools</summary>
233+
<div className="ml-5">{renderExports(call.tool.export)}</div>
234+
</details>
235+
)}
236+
</div>
237+
</details>
238+
</div>
239+
);
240+
};
241+
242+
const renderToolMapping = (toolMapping: Record<string, any>) => {
243+
return Object.entries(toolMapping).map(([key, value]) => (
244+
<div key={key} className="mb-2">
245+
{value.some((item: any) => item.toolID !== key) ? (
246+
<>
247+
{key}:
248+
<ul className="list-none ml-5">
249+
{value.map((item: any, index: number) => (
250+
<li key={index} className="mb-2">
251+
<p className="ml-5 whitespace-pre-wrap">{item.reference}</p>
252+
<p className="ml-5 whitespace-pre-wrap">{item.toolID}</p>
253+
</li>
254+
))}
255+
</ul>
256+
</>
257+
) : (
258+
<p className="whitespace-pre-wrap">{key}</p>
259+
)}
260+
</div>
261+
));
262+
};
263+
264+
const renderExports = (exports: string[]) => {
265+
return (
266+
<ul className="list-none ml-5">
267+
{exports.map((item, index) => (
268+
<li key={index} className="mb-2">
269+
<p className="whitespace-pre-wrap">{item}</p>
270+
</li>
271+
))}
272+
</ul>
273+
);
274+
};
275+
276+
const { tree, rootNodes } = buildTree(calls);
277+
278+
return (
279+
<div
280+
className="h-full overflow-scroll p-4 rounded-2xl border-2 shadow-lg border-primary border-lg bg-black text-white"
281+
ref={logsContainerRef}
282+
>
283+
<Tooltip content={allOpen ? 'Collapse all' : 'Expand all'} closeDelay={0}>
284+
<Button
285+
onPress={() => setAllOpen(!allOpen)}
286+
className="absolute right-8"
287+
isIconOnly
288+
radius="full"
289+
color="primary"
290+
>
291+
{allOpen ? <FaChevronUp /> : <FaChevronDown />}
292+
</Button>
293+
</Tooltip>
294+
{rootNodes.length > 0 ? (
295+
rootNodes.map((rootId) => renderTree(rootId))
296+
) : (
297+
<EmptyLogs />
298+
)}
299+
</div>
300+
);
301+
};
302+
303+
const Summary = ({ call }: { call: CallFrame }) => {
304+
const name =
305+
call.tool?.name ||
306+
call.tool?.source?.repo ||
307+
call.tool?.source?.location ||
308+
'main';
309+
const category = call.toolCategory;
310+
311+
const startTime = new Date(call.start).toLocaleTimeString();
312+
const endTime = call.end
313+
? new Date(call.end).toLocaleTimeString()
314+
: 'In progress';
315+
const duration = call.end
316+
? `${((new Date(call.end).getTime() - new Date(call.start).getTime()) / 1000).toFixed(2)}s`
317+
: 'N/A';
318+
319+
const timeInfo = `[ID: ${call.id}] [${startTime} - ${endTime}, ${duration}]`;
320+
321+
let summaryContent;
322+
if (call.tool?.chat) {
323+
summaryContent =
324+
call.type !== 'callFinish'
325+
? `Chat open with ${name}`
326+
: `Chatted with ${name}`;
327+
} else {
328+
summaryContent =
329+
call.type !== 'callFinish'
330+
? category
331+
? `Loading ${category} from ${name}` + '...'
332+
: `Running ${name}`
333+
: category
334+
? `Loaded ${category} from ${name}`
335+
: `Ran ${name}`;
336+
}
337+
338+
return (
339+
<summary className="cursor-pointer">
340+
{summaryContent} <span className="text-xs text-gray-400">{timeInfo}</span>
341+
{call?.type !== 'callFinish' && (
342+
<AiOutlineLoading3Quarters className="ml-2 animate-spin inline" />
343+
)}
344+
</summary>
345+
);
346+
};
347+
348+
export default CallFrames;

0 commit comments

Comments
 (0)