Skip to content

Commit cc49c61

Browse files
Add an inspect section for node state (GH-17)
2 parents a69f79c + 33266fc commit cc49c61

File tree

8 files changed

+184
-79
lines changed

8 files changed

+184
-79
lines changed

langgraphics-web/src/components/InspectPanel.tsx

Lines changed: 101 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Tree from "antd/es/tree";
2+
import {Collapse, Space} from "antd";
23
import type {TreeDataNode} from "antd";
34
import ReactMarkdown from "react-markdown";
45
import type {ColorMode} from "@xyflow/react";
@@ -25,6 +26,11 @@ export function InspectPanel({colorMode, nodeEntries}: { colorMode: ColorMode, n
2526
}
2627
}, [])
2728

29+
const state = useMemo(() => {
30+
if (!selectedEntry?.state) return null;
31+
return safeParseJSON(selectedEntry?.state);
32+
}, [selectedEntry, safeParseJSON])
33+
2834
const system = useMemo(() => {
2935
const inputs = safeParseJSON(selectedEntry?.input);
3036
const system = inputs[0] || {};
@@ -41,6 +47,12 @@ export function InspectPanel({colorMode, nodeEntries}: { colorMode: ColorMode, n
4147
return output[output.length - 1] || null;
4248
}, [selectedEntry, safeParseJSON])
4349

50+
const sectionMaxHeight = useMemo(() => {
51+
const reducer: any = (acc: number, curr: boolean) => acc + Number(curr);
52+
const count = [state, system, input, output].map(Boolean).reduce(reducer, 0);
53+
return `calc((100vh - 240px) / ${count})`;
54+
}, [state, system, input, output])
55+
4456
const getChildren = useCallback((parent: NodeEntry) => {
4557
return nodeEntries.filter(({parent_run_id}) => parent_run_id === parent.run_id).map(child => {
4658
const children: TreeDataNode[] = getChildren(child);
@@ -108,45 +120,95 @@ export function InspectPanel({colorMode, nodeEntries}: { colorMode: ColorMode, n
108120
metrics={selectedEntry.metrics}
109121
/>
110122
)}
111-
{system && (
112-
<div className="inspect-detail-section">
113-
<span className={`inspect-section-label ${system.role ?? ""}`}>
114-
<span>System</span>
115-
<span className="tag">{system.role ?? "unknown"}</span>
116-
</span>
117-
<div className="inspect-detail-text">
118-
{system.role
119-
? <ReactMarkdown children={system.content.trim()}/>
120-
: <pre>{JSON.stringify(system, null, 4)}</pre>}
121-
</div>
122-
</div>
123-
)}
124-
{input && (
125-
<div className="inspect-detail-section">
126-
<span className={`inspect-section-label ${input.role ?? ""}`}>
127-
<span>Input</span>
128-
<span className="tag">{input.role ?? "unknown"}</span>
129-
</span>
130-
<div className="inspect-detail-text">
131-
{input.role
132-
? <ReactMarkdown children={input.content.trim()}/>
133-
: <pre>{JSON.stringify(input, null, 4)}</pre>}
134-
</div>
135-
</div>
136-
)}
137-
{output && (
138-
<div className={`inspect-detail-section ${selectedEntry.node_kind ?? ""}`}>
139-
<span className={`inspect-section-label ${output.role ?? ""}`}>
140-
<span>Output</span>
141-
<span className="tag">{output.role ?? "unknown"}</span>
142-
</span>
143-
<div className={`inspect-detail-text ${selectedEntry.status ?? ""}`}>
144-
{output.role
145-
? <ReactMarkdown children={output.content.trim()}/>
146-
: <pre>{JSON.stringify(output, null, 4)}</pre>}
147-
</div>
148-
</div>
149-
)}
123+
<Space vertical size={10}>
124+
{state && (
125+
<Collapse
126+
defaultActiveKey="state"
127+
styles={{body: {maxHeight: sectionMaxHeight}}}
128+
items={[
129+
{
130+
key: "state",
131+
label: "State",
132+
showArrow: false,
133+
children: <pre>{JSON.stringify(state, null, 2)}</pre>,
134+
},
135+
]}
136+
/>
137+
)}
138+
{system && (
139+
<Collapse
140+
defaultActiveKey="system"
141+
styles={{body: {maxHeight: sectionMaxHeight}}}
142+
items={[
143+
{
144+
key: "system",
145+
showArrow: false,
146+
label: (
147+
<>
148+
<span>System</span>
149+
<span className="tag">{system.role ?? "unknown"}</span>
150+
</>
151+
),
152+
children: (
153+
system.role
154+
? <ReactMarkdown children={system.content.trim()}/>
155+
: <pre>{JSON.stringify(system, null, 4)}</pre>
156+
),
157+
},
158+
]}
159+
/>
160+
)}
161+
{input && (
162+
<Collapse
163+
defaultActiveKey="input"
164+
styles={{body: {maxHeight: sectionMaxHeight}}}
165+
items={[
166+
{
167+
key: "input",
168+
showArrow: false,
169+
label: (
170+
<>
171+
<span>Input</span>
172+
<span className="tag">{input.role ?? "unknown"}</span>
173+
</>
174+
),
175+
children: (
176+
input.role
177+
? <ReactMarkdown children={input.content.trim()}/>
178+
: <pre>{JSON.stringify(input, null, 4)}</pre>
179+
),
180+
},
181+
]}
182+
/>
183+
)}
184+
{output && (
185+
<Collapse
186+
defaultActiveKey="output"
187+
styles={{body: {maxHeight: sectionMaxHeight}}}
188+
classNames={{body: `${selectedEntry.node_kind ?? ""} ${selectedEntry.status ?? ""}`}}
189+
items={[
190+
{
191+
key: "output",
192+
showArrow: false,
193+
label: (
194+
<>
195+
<span>Output</span>
196+
<span className="tag">{output.role ?? "unknown"}</span>
197+
</>
198+
),
199+
children: (
200+
output.role
201+
? (output.role === "error") ? (
202+
<pre>{output.content.trim().replace(/^\n+/mg, "\n")}</pre>
203+
)
204+
: <ReactMarkdown children={output.content.trim()}/>
205+
: <pre>{JSON.stringify(output, null, 4)}</pre>
206+
),
207+
},
208+
]}
209+
/>
210+
)}
211+
</Space>
150212
</>
151213
)}
152214
</div>

langgraphics-web/src/components/Metrics.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ export function Metrics({colorMode, metrics}: { colorMode: ColorMode, metrics: N
1010
}, [colorMode]);
1111

1212
const color = useMemo(() => {
13-
return theme === "dark" ? "#c8c8c8" : "#141414";
13+
return theme === "dark" ? "#ffffff" : "#141414";
1414
}, [theme]);
1515

1616
const background = useMemo(() => {
17-
return theme === "dark" ? "#141414" : "#c8c8c8";
17+
return theme === "dark" ? "#141414" : "#ffffff";
1818
}, [theme]);
1919

2020
const border = useMemo(() => {

langgraphics-web/src/index.css

Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ html, body, #root {
1515
--xy-node-color-default: #3c3c3c;
1616
--xy-background-color-default: #ffffff;
1717
--xy-node-border-default: 1px solid #c8c8c8;
18-
--xy-node-tag-background: #3c3c3c26;
18+
--xy-node-tag-background: #C8C8C81A;
1919
--xy-node-border-color: #C8C8C880;
2020
--xy-node-tag-color: #3c3c3ccc;
2121
}
@@ -30,7 +30,7 @@ html, body, #root {
3030
.react-flow.dark {
3131
--xy-node-color-default: #c8c8c8;
3232
--xy-node-border-default: 1px solid #3c3c3c;
33-
--xy-node-tag-background: #c8c8c826;
33+
--xy-node-tag-background: #3C3C3C1A;
3434
--xy-node-border-color: #3C3C3C80;
3535
--xy-node-tag-color: #c8c8c8cc;
3636
}
@@ -216,7 +216,7 @@ html, body, #root {
216216
display: flex;
217217
padding: 10px;
218218
flex: 1 1 auto;
219-
overflow-y: auto;
219+
overflow: hidden;
220220
flex-direction: column;
221221
}
222222

@@ -266,9 +266,9 @@ html, body, #root {
266266
text-overflow: ellipsis;
267267
}
268268

269+
.ant-collapse-body.error,
269270
.inspect-node-label.error,
270-
.inspect-step-label.error,
271-
.inspect-detail-text.error {
271+
.inspect-step-label.error {
272272
color: #ef4444;
273273
}
274274

@@ -302,80 +302,89 @@ html, body, #root {
302302
padding: 8px !important;
303303
}
304304

305-
.inspect-detail-section {
306-
display: flex;
307-
max-height: 50%;
308-
overflow: hidden;
305+
.ant-collapse {
309306
border-radius: 6px;
310-
flex-direction: column;
307+
background: unset !important;
311308
border: var(--xy-node-border-default);
312309
border-color: var(--xy-node-border-color);
313310
}
314311

315-
.inspect-section-label {
312+
.ant-collapse-header {
316313
display: flex;
317314
font-size: 10px;
318315
font-weight: 700;
319-
padding: 5px 10px;
320316
align-items: center;
321317
letter-spacing: 0.05em;
322318
text-transform: uppercase;
319+
padding: 5px 10px !important;
323320
background: var(--xy-node-border-default);
324-
border-bottom: var(--xy-node-border-default);
325-
border-color: var(--xy-node-border-color);
321+
color: var(--xy-node-tag-color) !important;
322+
}
323+
324+
.ant-collapse-title {
325+
display: flex;
326+
align-items: center;
327+
}
328+
329+
.ant-collapse-title .tag {
330+
font-size: 7px;
331+
padding: 1px 5px;
332+
margin-left: 8px;
333+
border-radius: 10px;
334+
color: var(--xy-node-tag-color);
335+
background: var(--xy-node-tag-background);
336+
border: 1px solid var(--xy-node-border-color);
326337
}
327338

328-
.inspect-detail-section > :not(.inspect-section-label) {
329-
padding: 8px 10px;
339+
.ant-collapse-panel {
340+
background: unset !important;
341+
border-top: var(--xy-node-border-default) !important;
342+
border-color: var(--xy-node-border-color) !important;
330343
}
331344

332-
.inspect-detail-text {
345+
.ant-collapse-body {
333346
width: 100%;
334347
font-size: 12px;
335348
overflow-y: auto;
349+
padding: 8px 10px !important;
336350
}
337351

338-
.inspect-detail-text > *:last-child,
339-
.inspect-detail-text > *:first-child {
352+
.ant-collapse-body * {
340353
margin: 0;
341354
}
342355

343-
.inspect-section-label .tag {
344-
border: 1px solid var(--xy-node-tag-color);
345-
background: var(--xy-node-tag-background);
346-
color: var(--xy-node-tag-color);
347-
border-radius: 10px;
348-
margin-left: 8px;
349-
padding: 1px 5px;
350-
font-size: 7px;
356+
.ant-collapse-body pre {
357+
overflow-y: auto;
358+
white-space: pre-wrap;
359+
word-break: break-all;
351360
}
352361

353-
.inspect-detail-section.tool .inspect-detail-text:not(.error) {
362+
.ant-collapse-body.tool:not(.error) {
354363
color: #ea580ccc;
355364
}
356365

357-
.inspect-detail-section.chain .inspect-detail-text:not(.error) {
366+
.ant-collapse-body.chain:not(.error) {
358367
color: #db2777cc;
359368
}
360369

361-
.inspect-detail-section.prompt .inspect-detail-text:not(.error) {
370+
.ant-collapse-body.prompt:not(.error) {
362371
color: #9332eacc;
363372
}
364373

365-
.inspect-detail-section.parser .inspect-detail-text:not(.error) {
374+
.ant-collapse-body.parser:not(.error) {
366375
color: #6467f2cc;
367376
}
368377

369-
.inspect-detail-section.embedding .inspect-detail-text:not(.error) {
378+
.ant-collapse-body.embedding:not(.error) {
370379
color: #d67506cc;
371380
}
372381

373-
.inspect-detail-section.retriever .inspect-detail-text:not(.error) {
382+
.ant-collapse-body.retriever:not(.error) {
374383
color: #0d9488cc;
375384
}
376385

377-
.inspect-detail-section.llm .inspect-detail-text:not(.error),
378-
.inspect-detail-section.chat_model .inspect-detail-text:not(.error) {
386+
.ant-collapse-body.llm:not(.error),
387+
.ant-collapse-body.chat_model:not(.error) {
379388
color: #4f46e5cc;
380389
}
381390

langgraphics-web/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ export interface NodeMessage {
9999
status?: "ok" | "error";
100100
input?: string | null;
101101
output?: string | null;
102+
state?: string | null;
102103
metrics?: NodeMetrics | null;
103104
}
104105

langgraphics/formatter.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,10 @@ def norm(msg: dict[str, Any]) -> dict[str, Any]:
8181
"type": msg["id"][-1].replace("Message", "").lower(),
8282
"data": msg["kwargs"],
8383
}
84+
tool_call = msg.get("tool_call", {})
85+
default_tool_calls = [tool_call] if tool_call else []
8486
data: dict[str, Any] = msg.get("data", {})
85-
if tool_calls := data.get("tool_calls", []):
87+
if tool_calls := data.get("tool_calls", default_tool_calls):
8688
fmt_tool = lambda tc: f"{tc.get('name', '?')}({tc.get('args', {})})"
8789
return {
8890
"role": msg.get("type", "unknown"),
@@ -107,8 +109,9 @@ def inputs(cls, run: Run) -> list[dict[str, Any]]:
107109
return [data]
108110
return list(map(cls.norm, messages))
109111
elif run.run_type == "chain":
112+
default = [cls.norm(data)] if "tool_call" in data else [data] if data else []
110113
messages = messages_to_dict(data.get("messages", []))
111-
return list(map(cls.norm, messages))
114+
return list(map(cls.norm, messages)) or default
112115
elif run.run_type == "tool":
113116
return [{"role": "input", "content": str(data.get("input", ""))}]
114117
elif run.run_type == "retriever":

0 commit comments

Comments
 (0)