Skip to content

Commit 2e5b3af

Browse files
authored
Add support to use thread history (#299)
* Add completed actions from llm response * Stop in case of UpdateTaskStatus action * Improve message content rendering * Include latest message when cloning thread history * Hackily fix asst message rendering * Auto-hide completed actions
1 parent 7dda45c commit 2e5b3af

File tree

8 files changed

+140
-29
lines changed

8 files changed

+140
-29
lines changed

web/src/components/common/ActionStack.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,9 @@ export const ActionStack: React.FC<{status: string, actions: Array<ActionStatusV
131131
setIsExpanded(!isExpanded)
132132
}
133133

134+
if (isEmpty(actions)) {
135+
return null;
136+
}
134137
return (
135138
<HStack aria-label="thinking-block" className={'action-stack'} justifyContent={'start'} maxWidth={"100%"} width={isExpanded ? "100%" : ""}>
136139
<Box
@@ -262,7 +265,7 @@ export const OngoingActionStack: React.FC = () => {
262265
const actions: ActionStatusView[] = []
263266
actionPlan.content.actionMessageIDs.forEach((messageID: string) => {
264267
const message = messages[messageID]
265-
if (message.role == 'tool') {
268+
if (message.role == 'tool' && !message.action.renderInfo?.hidden) {
266269
actions.push(message.action)
267270
}
268271
})

web/src/components/common/Chat.tsx

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Box, HStack, VStack, IconButton, Stack, Text } from '@chakra-ui/react'
33
import { BsFillHandThumbsUpFill, BsFillHandThumbsDownFill, BsDashCircle } from 'react-icons/bs';
44
import { dispatch } from '../../state/dispatch'
55
import { ChatMessage, addReaction, removeReaction, deleteUserMessage, ActionChatMessage } from '../../state/chat/reducer'
6-
import _, { isEmpty } from 'lodash'
6+
import _, { cloneDeep, isEmpty } from 'lodash'
77
import { useSelector } from 'react-redux';
88
import { RootState } from '../../state/store';
99
import { ActionStack, ActionStatusView, OngoingActionStack } from './ActionStack';
@@ -84,12 +84,14 @@ const Chat: React.FC<ReturnType<typeof addToolInfoToActionPlanMessages>[number]>
8484
}
8585
const actions: ActionStatusView[] = []
8686
content.toolCalls.forEach(toolCall => {
87-
actions.push({
88-
finished: true,
89-
function: toolCall.function,
90-
status: toolCall.status,
91-
renderInfo: toolCall.renderInfo
92-
})
87+
if (!toolCall.renderInfo.hidden) {
88+
actions.push({
89+
finished: true,
90+
function: toolCall.function,
91+
status: toolCall.status,
92+
renderInfo: toolCall.renderInfo
93+
})
94+
}
9395
})
9496
const latency = ('latency' in debug)? Math.round(debug.latency as number /100)/10 : 0
9597
return <ActionStack content={content.messageContent} actions={actions} status={'FINISHED'} index={index} latency={latency}/>
@@ -207,15 +209,20 @@ export const ChatSection = () => {
207209
// just create a map of all role='tool' messages by their id, and for each
208210
// tool call in each assistant message, add the status from the corresponding
209211
// tool message
210-
const messagesWithStatus = addToolInfoToActionPlanMessages(messages)
211-
messagesWithStatus.forEach(message => {
212-
if (message.role == 'assistant' && message.content.toolCalls.length == 0) {
213-
message.content = {
212+
const messagesWithStatusInfo = addToolInfoToActionPlanMessages(messages)
213+
const messagesWithStatus = messagesWithStatusInfo.flatMap(message => {
214+
// if (message.role == 'assistant' && message.content.toolCalls.length == 0) {
215+
const returnValue = [message]
216+
if (message.role == 'assistant' && message.content.messageContent && message.content.messageContent.length > 0) {
217+
const newMessage = cloneDeep(message)
218+
newMessage.content = {
214219
type: 'DEFAULT',
215220
text: message.content.messageContent,
216221
images: []
217222
}
223+
returnValue.push(newMessage)
218224
}
225+
return returnValue
219226
})
220227
const Chats = isEmpty(messagesWithStatus) ?
221228
(getDemoIDX(url) == -1 ? <HelperMessage /> : <DemoHelperMessage url={url}/>) :

web/src/components/devtools/ActionDebug.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export const ActionsView: React.FC<null> = () => {
4444
})
4545
const actionButtons = allActions.map((actions, index) => {
4646
const actionDisplay = actions.map((action, jindex) => {
47-
let jsonOutput = action.output.content
47+
let jsonOutput = action.output?.content
4848
try {
4949
jsonOutput = JSON.parse(action.output.content.content)
5050
} catch (e) {}

web/src/helpers/threadHistory.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,18 @@ export function scanThreadsForMBQL(
111111
}
112112

113113
if (normalizedExtractedMBQL === normalizedCurrentMBQL) {
114+
// Find the last message before the next user message
115+
let lastMessageIndex = messageIndex;
116+
for (let i = messageIndex + 1; i < thread.messages.length; i++) {
117+
if (thread.messages[i].role === 'user') {
118+
break; // Stop before user message
119+
}
120+
lastMessageIndex = i;
121+
}
122+
114123
return {
115124
threadIndex,
116-
messageIndex,
125+
messageIndex: lastMessageIndex,
117126
matchingSQL: extractedMBQL // Using matchingSQL field for consistency
118127
};
119128
}
@@ -166,9 +175,18 @@ export function scanThreadsForSQL(
166175
if (extractedSQL) {
167176
const normalizedExtractedSQL = normalizeSQL(extractedSQL);
168177
if (normalizedExtractedSQL === normalizedCurrentSQL) {
178+
// Find the last message before the next user message
179+
let lastMessageIndex = messageIndex;
180+
for (let i = messageIndex + 1; i < thread.messages.length; i++) {
181+
if (thread.messages[i].role === 'user') {
182+
break; // Stop before user message
183+
}
184+
lastMessageIndex = i;
185+
}
186+
169187
return {
170188
threadIndex,
171-
messageIndex,
189+
messageIndex: lastMessageIndex,
172190
matchingSQL: extractedSQL
173191
};
174192
}

web/src/planner/planner.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ function shouldContinue(getState: () => RootState) {
2222
return false
2323
}
2424
// check if the last tool was respondToUser and check what its params were
25-
if (lastMessage.role == 'tool' && lastMessage.action.function.name == 'markTaskDone') {
25+
if (lastMessage.role == 'tool' && (lastMessage.action.function.name == 'markTaskDone' || lastMessage.action.function.name == 'UpdateTaskStatus')) {
2626
return false;
2727
} else {
2828
// if last tool was not respondToUser, we continue anyway. not sure if we should keep it this way?

web/src/planner/plannerActions.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ export const performActions = async (signal: AbortSignal) => {
9696
if (message.role != 'tool')
9797
return
9898
const action = message.action
99+
// Skip actions that are already finished (completed tasks)
100+
if (action.finished)
101+
return
99102
actions.push({
100103
index: messageID,
101104
function: action.function.name,

web/src/state/chat/reducer.ts

Lines changed: 92 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,35 @@ const getActiveThread = (state: ChatState) => state.threads[state.activeThread]
215215

216216
const getMessages = (state: ChatState) => getActiveThread(state).messages
217217

218+
/**
219+
* Converts a Task to a ChatCompletionMessageToolCall for display purposes
220+
*/
221+
const taskToToolCall = (task: Task): ChatCompletionMessageToolCall => ({
222+
id: `task_${task.id}`,
223+
type: 'function',
224+
function: {
225+
name: task.agent,
226+
arguments: JSON.stringify(task.args)
227+
}
228+
})
229+
230+
/**
231+
* Converts a Task result to ActionChatMessageContent
232+
* Assumes task.result is always a string
233+
*/
234+
const taskResultToContent = (task: Task): ActionChatMessageContent => {
235+
return {
236+
type: 'BLANK',
237+
content: task.result as string,
238+
renderInfo: {
239+
text: '',
240+
code: undefined,
241+
oldCode: undefined,
242+
hidden: true
243+
}
244+
}
245+
}
246+
218247
export const chatSlice = createSlice({
219248
name: 'chat',
220249
initialState,
@@ -260,12 +289,32 @@ export const chatSlice = createSlice({
260289
state,
261290
action: PayloadAction<{llmResponse: LLMResponse, debug: any}>
262291
) => {
263-
// const actions: Array<OngoingAction> =
264292
const messages = getMessages(state)
265293
const latestMessageIndex = messages.length
266294
const toolCalls = action.payload.llmResponse.tool_calls
267295
const messageContent = action.payload.llmResponse.content
268-
const actionMessageIDs = [...Array(toolCalls.length).keys()].map((value) => value+1+latestMessageIndex)
296+
const newTasks = action.payload.llmResponse.tasks || []
297+
const currentTasks = getActiveThread(state).tasks || []
298+
299+
// Defensive programming: ensure we have valid arrays and lengths
300+
const currentTasksLength = Array.isArray(currentTasks) ? currentTasks.length : 0
301+
const newTasksLength = Array.isArray(newTasks) ? newTasks.length : 0
302+
303+
// Find completed tasks from task diff (excluding first task which is main agent)
304+
// Only proceed if we have new tasks beyond the current ones
305+
const completedTasks = newTasksLength > currentTasksLength
306+
? newTasks
307+
.slice(Math.max(currentTasksLength, 1)) // Get only new tasks, skip main agent
308+
.filter(task => task && task.result !== null && task.result !== undefined)
309+
: []
310+
311+
// Convert completed tasks to tool calls for display
312+
const completedToolCalls = completedTasks.map(taskToToolCall)
313+
314+
// Calculate total action message IDs for both completed and pending tool calls
315+
const totalToolCalls = completedToolCalls.length + toolCalls.length
316+
const actionMessageIDs = [...Array(totalToolCalls).keys()].map((value) => value+1+latestMessageIndex)
317+
269318
const timestamp = Date.now()
270319
const actionPlanMessage: ActionPlanChatMessage = {
271320
role: 'assistant',
@@ -276,18 +325,47 @@ export const chatSlice = createSlice({
276325
content: {
277326
type: 'ACTIONS',
278327
actionMessageIDs: actionMessageIDs,
279-
toolCalls,
328+
toolCalls: [...completedToolCalls, ...toolCalls], // Completed first, then pending
280329
messageContent,
281330
finishReason: action.payload.llmResponse.finish_reason,
282-
finished: false
331+
finished: completedTasks.length === totalToolCalls // Only finished if all are completed
283332
},
284333
createdAt: timestamp,
285334
updatedAt: timestamp,
286335
debug: action.payload.debug
287336
}
288337
messages.push(actionPlanMessage)
289-
toolCalls.forEach((toolCall, index) => {
290-
const actionMessageID = index+1+latestMessageIndex
338+
339+
let messageIndex = 1
340+
341+
// Add completed tasks as SUCCESS messages first
342+
completedTasks.forEach((task) => {
343+
const actionMessageID = messageIndex + latestMessageIndex
344+
const timestamp = Date.now()
345+
const actionMessage: ActionChatMessage = {
346+
role: 'tool',
347+
action: {
348+
...taskToToolCall(task),
349+
planID: latestMessageIndex,
350+
status: 'SUCCESS',
351+
finished: true,
352+
},
353+
feedback: {
354+
reaction: "unrated"
355+
},
356+
index: actionMessageID,
357+
content: taskResultToContent(task),
358+
createdAt: timestamp,
359+
updatedAt: timestamp,
360+
debug: {}
361+
}
362+
messages.push(actionMessage)
363+
messageIndex++
364+
})
365+
366+
// Add pending tool calls as TODO messages
367+
toolCalls.forEach((toolCall) => {
368+
const actionMessageID = messageIndex + latestMessageIndex
291369
const timestamp = Date.now()
292370
const actionMessage: ActionChatMessage = {
293371
role: 'tool',
@@ -304,20 +382,21 @@ export const chatSlice = createSlice({
304382
content: {
305383
type: 'BLANK',
306384
renderInfo: {
307-
text: null,
308-
code: null,
309-
oldCode: null
385+
text: undefined,
386+
code: undefined,
387+
oldCode: undefined
310388
}
311389
},
312390
createdAt: timestamp,
313391
updatedAt: timestamp,
314392
debug: {}
315393
}
316394
messages.push(actionMessage)
395+
messageIndex++
317396
})
318397

319-
const tasks = action.payload.llmResponse.tasks || []
320-
getActiveThread(state).tasks = tasks
398+
// Update tasks array with new tasks
399+
getActiveThread(state).tasks = newTasks
321400

322401
},
323402
startAction: (
@@ -576,8 +655,8 @@ export const chatSlice = createSlice({
576655
const previousID = state.threads[state.threads.length - 1].id
577656
const newID = generateNextThreadID(previousID)
578657

579-
// Clone messages up to and including the assistant response after the tool call
580-
const endIndex = Math.min(upToMessageIndex + 1, sourceThread.messages.length - 1);
658+
// Clone messages up to and including the specified index (threadHistory already calculated the correct endpoint)
659+
const endIndex = Math.min(upToMessageIndex, sourceThread.messages.length - 1);
581660
const clonedMessages = sourceThread.messages
582661
.slice(0, endIndex + 1)
583662
.map((message, index) => ({

web/src/state/chat/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,6 @@ export type ActionRenderInfo = {
3838
code?: string
3939
oldCode?: string
4040
language?: string
41+
hidden?: boolean
4142
extraArgs?: Record<string, any>
4243
}

0 commit comments

Comments
 (0)