Skip to content

Commit f883800

Browse files
authored
Merge pull request #102 from ag-ui-protocol/feat/langgraph-reasoning-support
Reasoning/thinking support
2 parents 1b645df + 01e638e commit f883800

File tree

9 files changed

+346
-24
lines changed

9 files changed

+346
-24
lines changed

python-sdk/ag_ui/core/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,15 @@
5151
"TextMessageContentEvent",
5252
"TextMessageEndEvent",
5353
"TextMessageChunkEvent",
54+
"ThinkingTextMessageStartEvent",
55+
"ThinkingTextMessageContentEvent",
56+
"ThinkingTextMessageEndEvent",
5457
"ToolCallStartEvent",
5558
"ToolCallArgsEvent",
5659
"ToolCallEndEvent",
5760
"ToolCallChunkEvent",
61+
"ThinkingStartEvent",
62+
"ThinkingEndEvent",
5863
"StateSnapshotEvent",
5964
"StateDeltaEvent",
6065
"MessagesSnapshotEvent",

python-sdk/ag_ui/core/events.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,15 @@ class EventType(str, Enum):
1717
TEXT_MESSAGE_CONTENT = "TEXT_MESSAGE_CONTENT"
1818
TEXT_MESSAGE_END = "TEXT_MESSAGE_END"
1919
TEXT_MESSAGE_CHUNK = "TEXT_MESSAGE_CHUNK"
20+
THINKING_TEXT_MESSAGE_START = "THINKING_TEXT_MESSAGE_START",
21+
THINKING_TEXT_MESSAGE_CONTENT = "THINKING_TEXT_MESSAGE_CONTENT",
22+
THINKING_TEXT_MESSAGE_END = "THINKING_TEXT_MESSAGE_END",
2023
TOOL_CALL_START = "TOOL_CALL_START"
2124
TOOL_CALL_ARGS = "TOOL_CALL_ARGS"
2225
TOOL_CALL_END = "TOOL_CALL_END"
2326
TOOL_CALL_CHUNK = "TOOL_CALL_CHUNK"
27+
THINKING_START = "THINKING_START",
28+
THINKING_END = "THINKING_END",
2429
STATE_SNAPSHOT = "STATE_SNAPSHOT"
2530
STATE_DELTA = "STATE_DELTA"
2631
MESSAGES_SNAPSHOT = "MESSAGES_SNAPSHOT"
@@ -80,6 +85,29 @@ class TextMessageChunkEvent(BaseEvent):
8085
role: Optional[Literal["assistant"]] = None
8186
delta: Optional[str] = None
8287

88+
class ThinkingTextMessageStartEvent(BaseEvent):
89+
"""
90+
Event indicating the start of a thinking text message.
91+
"""
92+
type: Literal[EventType.THINKING_TEXT_MESSAGE_START]
93+
94+
class ThinkingTextMessageContentEvent(BaseEvent):
95+
"""
96+
Event indicating a piece of a thinking text message.
97+
"""
98+
type: Literal[EventType.THINKING_TEXT_MESSAGE_CONTENT]
99+
delta: str # This should not be an empty string
100+
101+
def model_post_init(self, __context):
102+
if len(self.delta) == 0:
103+
raise ValueError("Delta must not be an empty string")
104+
105+
class ThinkingTextMessageEndEvent(BaseEvent):
106+
"""
107+
Event indicating the end of a thinking text message.
108+
"""
109+
type: Literal[EventType.THINKING_TEXT_MESSAGE_END]
110+
83111
class ToolCallStartEvent(BaseEvent):
84112
"""
85113
Event indicating the start of a tool call.
@@ -116,6 +144,19 @@ class ToolCallChunkEvent(BaseEvent):
116144
parent_message_id: Optional[str] = None
117145
delta: Optional[str] = None
118146

147+
class ThinkingStartEvent(BaseEvent):
148+
"""
149+
Event indicating the start of a thinking step event.
150+
"""
151+
type: Literal[EventType.THINKING_START]
152+
title: Optional[str] = 'Thinking...'
153+
154+
class ThinkingEndEvent(BaseEvent):
155+
"""
156+
Event indicating the end of a thinking step event.
157+
"""
158+
type: Literal[EventType.THINKING_END]
159+
119160
class StateSnapshotEvent(BaseEvent):
120161
"""
121162
Event containing a snapshot of the state.

typescript-sdk/integrations/langgraph/src/index.ts

Lines changed: 96 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ import {
1818
LangGraphEventTypes,
1919
State,
2020
MessagesInProgressRecord,
21+
ThinkingInProgress,
2122
SchemaKeys,
2223
MessageInProgress,
2324
RunMetadata,
2425
PredictStateTool,
26+
LangGraphReasoning
2527
} from "./types";
2628
import {
2729
AbstractAgent,
@@ -44,6 +46,11 @@ import {
4446
ToolCallArgsEvent,
4547
ToolCallEndEvent,
4648
ToolCallStartEvent,
49+
ThinkingTextMessageStartEvent,
50+
ThinkingTextMessageContentEvent,
51+
ThinkingTextMessageEndEvent,
52+
ThinkingStartEvent,
53+
ThinkingEndEvent,
4754
} from "@ag-ui/client";
4855
import { RunsStreamPayload } from "@langchain/langgraph-sdk/dist/types";
4956
import {
@@ -52,15 +59,22 @@ import {
5259
filterObjectBySchemaKeys,
5360
getStreamPayloadInput,
5461
langchainMessagesToAgui,
62+
resolveMessageContent,
63+
resolveReasoningContent
5564
} from "@/utils";
5665

5766
export type ProcessedEvents =
5867
| TextMessageStartEvent
5968
| TextMessageContentEvent
6069
| TextMessageEndEvent
70+
| ThinkingTextMessageStartEvent
71+
| ThinkingTextMessageContentEvent
72+
| ThinkingTextMessageEndEvent
6173
| ToolCallStartEvent
6274
| ToolCallArgsEvent
6375
| ToolCallEndEvent
76+
| ThinkingStartEvent
77+
| ThinkingEndEvent
6478
| StateSnapshotEvent
6579
| StateDeltaEvent
6680
| MessagesSnapshotEvent
@@ -98,6 +112,7 @@ export class LangGraphAgent extends AbstractAgent {
98112
graphId: string;
99113
assistant?: Assistant;
100114
messagesInProcess: MessagesInProgressRecord;
115+
thinkingProcess: null | ThinkingInProgress;
101116
activeRun?: RunMetadata;
102117
// @ts-expect-error no need to initialize subscriber right now
103118
subscriber: Subscriber<ProcessedEvents>;
@@ -108,6 +123,7 @@ export class LangGraphAgent extends AbstractAgent {
108123
this.agentName = config.agentName;
109124
this.graphId = config.graphId;
110125
this.assistantConfig = config.assistantConfig;
126+
this.thinkingProcess = null
111127
this.client =
112128
config?.client ??
113129
new LangGraphClient({
@@ -389,7 +405,7 @@ export class LangGraphAgent extends AbstractAgent {
389405
let shouldEmitToolCalls = event.metadata["emit-tool-calls"] ?? true;
390406

391407
if (event.data.chunk.response_metadata.finish_reason) return;
392-
const currentStream = this.getMessageInProgress(this.activeRun!.id);
408+
let currentStream = this.getMessageInProgress(this.activeRun!.id);
393409
const hasCurrentStream = Boolean(currentStream?.id);
394410
const toolCallData = event.data.chunk.tool_call_chunks?.[0];
395411
const toolCallUsedToPredictState = event.metadata["predict_state"]?.some(
@@ -401,11 +417,28 @@ export class LangGraphAgent extends AbstractAgent {
401417
hasCurrentStream && currentStream?.toolCallId && toolCallData.args;
402418
const isToolCallEndEvent = hasCurrentStream && currentStream?.toolCallId && !toolCallData;
403419

404-
const isMessageStartEvent = !hasCurrentStream && !toolCallData;
405-
const isMessageContentEvent = hasCurrentStream && !toolCallData;
420+
const reasoningData = resolveReasoningContent(event.data);
421+
const messageContent = resolveMessageContent(event.data.chunk.content);
422+
const isMessageContentEvent = Boolean(!toolCallData && messageContent);
423+
406424
const isMessageEndEvent =
407425
hasCurrentStream && !currentStream?.toolCallId && !isMessageContentEvent;
408426

427+
if (reasoningData) {
428+
this.handleThinkingEvent(reasoningData)
429+
break;
430+
}
431+
432+
if (!reasoningData && this.thinkingProcess) {
433+
this.dispatchEvent({
434+
type: EventType.THINKING_TEXT_MESSAGE_END,
435+
})
436+
this.dispatchEvent({
437+
type: EventType.THINKING_END,
438+
})
439+
this.thinkingProcess = null;
440+
}
441+
409442
if (toolCallUsedToPredictState) {
410443
this.dispatchEvent({
411444
type: EventType.CUSTOM,
@@ -417,7 +450,7 @@ export class LangGraphAgent extends AbstractAgent {
417450
if (isToolCallEndEvent) {
418451
const resolved = this.dispatchEvent({
419452
type: EventType.TOOL_CALL_END,
420-
toolCallId: currentStream.toolCallId!,
453+
toolCallId: currentStream?.toolCallId!,
421454
rawEvent: event,
422455
});
423456
if (resolved) {
@@ -460,36 +493,35 @@ export class LangGraphAgent extends AbstractAgent {
460493
if (isToolCallArgsEvent && shouldEmitToolCalls) {
461494
this.dispatchEvent({
462495
type: EventType.TOOL_CALL_ARGS,
463-
toolCallId: currentStream.toolCallId!,
496+
toolCallId: currentStream?.toolCallId!,
464497
delta: toolCallData.args,
465498
rawEvent: event,
466499
});
467500
break;
468501
}
469502

470-
// Message started: emit TextMessageStart
471-
if (isMessageStartEvent && shouldEmitMessages) {
472-
const resolved = this.dispatchEvent({
473-
type: EventType.TEXT_MESSAGE_START,
474-
role: "assistant",
475-
messageId: event.data.chunk.id,
476-
rawEvent: event,
477-
});
478-
if (resolved) {
503+
// Message content: emit TextMessageContent
504+
if (isMessageContentEvent && shouldEmitMessages) {
505+
// No existing message yet, also init the message
506+
if (!currentStream) {
507+
this.dispatchEvent({
508+
type: EventType.TEXT_MESSAGE_START,
509+
role: "assistant",
510+
messageId: event.data.chunk.id,
511+
rawEvent: event,
512+
});
479513
this.setMessageInProgress(this.activeRun!.id, {
480514
id: event.data.chunk.id,
481515
toolCallId: null,
482516
toolCallName: null,
483517
});
518+
currentStream = this.getMessageInProgress(this.activeRun!.id);
484519
}
485-
break;
486-
}
487-
// Message content: emit TextMessageContent
488-
if (isMessageContentEvent && shouldEmitMessages) {
520+
489521
this.dispatchEvent({
490522
type: EventType.TEXT_MESSAGE_CONTENT,
491523
messageId: currentStream!.id,
492-
delta: event.data.chunk.content,
524+
delta: messageContent!,
493525
rawEvent: event,
494526
});
495527
break;
@@ -583,6 +615,51 @@ export class LangGraphAgent extends AbstractAgent {
583615
}
584616
}
585617

618+
handleThinkingEvent(reasoningData: LangGraphReasoning) {
619+
if (!reasoningData || !reasoningData.type || !reasoningData.text) {
620+
return;
621+
}
622+
623+
const thinkingStepIndex = reasoningData.index;
624+
625+
if (this.thinkingProcess?.index && this.thinkingProcess.index !== thinkingStepIndex) {
626+
if (this.thinkingProcess.type) {
627+
this.dispatchEvent({
628+
type: EventType.THINKING_TEXT_MESSAGE_END,
629+
})
630+
}
631+
this.dispatchEvent({
632+
type: EventType.THINKING_END,
633+
})
634+
this.thinkingProcess = null;
635+
}
636+
637+
if (!this.thinkingProcess) {
638+
// No thinking step yet. Start a new one
639+
this.dispatchEvent({
640+
type: EventType.THINKING_START,
641+
})
642+
this.thinkingProcess = {
643+
index: thinkingStepIndex,
644+
};
645+
}
646+
647+
648+
if (this.thinkingProcess.type !== reasoningData.type) {
649+
this.dispatchEvent({
650+
type: EventType.THINKING_TEXT_MESSAGE_START,
651+
})
652+
this.thinkingProcess.type = reasoningData.type
653+
}
654+
655+
if (this.thinkingProcess.type) {
656+
this.dispatchEvent({
657+
type: EventType.THINKING_TEXT_MESSAGE_CONTENT,
658+
delta: reasoningData.text
659+
})
660+
}
661+
}
662+
586663
getStateSnapshot(state: State) {
587664
const schemaKeys = this.activeRun!.schemaKeys!;
588665
// Do not emit state keys that are not part of the output schema

typescript-sdk/integrations/langgraph/src/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ export type MessageInProgress = {
2828
toolCallName?: string | null;
2929
};
3030

31+
export type ThinkingInProgress = {
32+
index: number;
33+
type?: LangGraphReasoning['type'];
34+
}
35+
3136
export interface RunMetadata {
3237
id: string;
3338
schemaKeys?: SchemaKeys;
@@ -89,3 +94,9 @@ export interface PredictStateTool {
8994
state_key: string;
9095
tool_argument: string;
9196
}
97+
98+
export interface LangGraphReasoning {
99+
type: 'text';
100+
text: string;
101+
index: number
102+
}

0 commit comments

Comments
 (0)