Skip to content

Commit 52122b5

Browse files
New model tools (#334)
* remove unused controller fns and add v2 ones * misc changes * Hide renderInfo block for TalkToUser * Don't render empty content blocks in ChatContent * support v2 tools * misc * Limit streamed messages to current conversation * Selectively show continue analysis * Update thread ID on app start * Fix thread ID updation logic in App.tsx * style fixes * make markdown readable * Display completed tool calls --------- Co-authored-by: Sreejith <1743700+ppsreejith@users.noreply.github.com>
1 parent 8fbb439 commit 52122b5

File tree

9 files changed

+255
-650
lines changed

9 files changed

+255
-650
lines changed

apps/src/metabase/actionDescriptions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const COMMON_ACTION_DESCRIPTIONS: ActionDescription[] = [
2121

2222
const NEW_ACTION_DESCRIPTIONS: ActionDescription[] = [
2323
{
24-
name: 'ExecuteMBQLClient',
24+
name: 'ExecuteMBQLQuery',
2525
args: {
2626
mbql: {
2727
type: 'string',

apps/src/metabase/appController.ts

Lines changed: 46 additions & 504 deletions
Large diffs are not rendered by default.

web/src/components/common/ActionStack.tsx

Lines changed: 128 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ import { get, isEmpty, last } from 'lodash';
2525

2626
// Todo: Vivek: Hardcoding here, need to fix this later
2727
// This is a list of actions that are undo/redoable
28-
const UNDO_REDO_ACTIONS = ['ExecuteSQLClient', 'ExecuteQuery', 'EditAndExecuteQuery']
29-
const TABLE_OUTPUT_ACTIONS = ['ExecuteSQLClient', 'ExecuteQuery', 'EditAndExecuteQuery', 'ExecuteMBQLQuery']
28+
const UNDO_REDO_ACTIONS = ['ExecuteQuery', 'EditAndExecuteQuery', 'ExecuteQueryV2', 'EditAndExecuteQueryV2']
29+
const TABLE_OUTPUT_ACTIONS = ['ExecuteQuery', 'EditAndExecuteQuery', 'ExecuteMBQLQuery', 'ExecuteQueryV2', 'EditAndExecuteQueryV2', 'ExecuteMBQLQueryV2']
3030

3131

3232
function removeThinkingTags(input: string): string {
@@ -79,7 +79,14 @@ export const ActionStack: React.FC<{status: string, actions: Array<ActionStatusV
7979
let title: string = "";
8080
if (status == 'FINISHED') {
8181
let titles = actions.map(action => getActionLabels(action.function.name, 'labelDone'))
82-
title = [...new Set(titles)].join(', ')
82+
const titleCounts = titles.reduce((acc, title) => {
83+
acc[title] = (acc[title] || 0) + 1;
84+
return acc;
85+
}, {} as Record<string, number>);
86+
87+
title = Object.entries(titleCounts)
88+
.map(([label, count]) => count > 1 ? `${label} (${count})` : label)
89+
.join(', ')
8390

8491
} else {
8592
let titles = actions.map(action => getActionLabels(action.function.name, 'labelRunning'))
@@ -150,119 +157,119 @@ export const ActionStack: React.FC<{status: string, actions: Array<ActionStatusV
150157
return null;
151158
}
152159
return (
153-
<VStack maxWidth={"100%"} width={isExpanded ? "100%" : ""}>
154-
<HStack aria-label="thinking-block" className={'action-stack'} justifyContent={'start'} maxWidth={"100%"} width={isExpanded ? "100%" : ""}>
155-
<Box
156-
aria-label="thinking-block-container"
157-
// bg={'minusxGreen.800'}
158-
bg={'minusxBW.200'}
159-
// p={2}
160-
px={2}
161-
py={1}
162-
my={0}
163-
borderRadius={5}
164-
// color={'minusxBW.50'}
165-
color={'minusxGreen.800'}
166-
border={'1px'}
167-
width={'100%'}
168-
position="relative"
169-
>
170-
{content && <>
171-
<ChatContent content={{
172-
type: "DEFAULT",
173-
images: [],
174-
text: extractMessageContent(content)
175-
}} />
176-
<br />
177-
</>}
178-
<HStack
179-
// add border only if actions are present
180-
// paddingBottom={actions.length && isExpanded ? 1 : 0}
181-
p={0}
182-
>
183-
<VStack alignItems={"start"} flex={1} spacing={0}>
184-
<VStack>
185-
{preExpanderTextArr.length > 0 &&
186-
// <Text marginBottom={2} borderBottomWidth={1} borderBottomColor={'minusxGreen.800'} style={{ hyphens: 'auto' }} p={2} w={"100%"}>{"Thinking..."}<br/>{preExpanderText}</Text>
187-
preExpanderTextArr.map((text, i) => {
188-
return (
189-
<Box aria-label="thinking-content" borderBottomWidth={1} mb={1} borderBottomColor={'minusxGreen.800'} w={"100%"}>
190-
<Markdown content={text}></Markdown>
191-
{pageType && pageType == 'sql' && !taskInProgress && undoRedoArr[i]}
160+
<VStack maxWidth={"100%"} width={"100%"} alignItems={"flex-start"} spacing={2}>
161+
<HStack aria-label="thinking-block" className={'action-stack'} justifyContent={'start'} maxWidth={"100%"} width={isExpanded ? "100%" : ""}>
162+
<Box
163+
aria-label="thinking-block-container"
164+
// bg={'minusxGreen.800'}
165+
bg={'minusxBW.200'}
166+
// p={2}
167+
px={2}
168+
py={1}
169+
my={0}
170+
borderRadius={5}
171+
// color={'minusxBW.50'}
172+
color={'minusxGreen.800'}
173+
border={'1px'}
174+
width={'100%'}
175+
position="relative"
176+
>
177+
{content && <>
178+
<ChatContent content={{
179+
type: "DEFAULT",
180+
images: [],
181+
text: extractMessageContent(content)
182+
}} />
183+
<br />
184+
</>}
185+
<HStack
186+
// add border only if actions are present
187+
// paddingBottom={actions.length && isExpanded ? 1 : 0}
188+
p={0}
189+
>
190+
<VStack alignItems={"start"} flex={1} spacing={0}>
191+
<VStack>
192+
{preExpanderTextArr.length > 0 &&
193+
// <Text marginBottom={2} borderBottomWidth={1} borderBottomColor={'minusxGreen.800'} style={{ hyphens: 'auto' }} p={2} w={"100%"}>{"Thinking..."}<br/>{preExpanderText}</Text>
194+
preExpanderTextArr.map((text, i) => {
195+
return (
196+
<Box aria-label="thinking-content" borderBottomWidth={1} mb={1} borderBottomColor={'minusxGreen.800'} w={"100%"}>
197+
<Markdown content={text}></Markdown>
198+
{pageType && pageType == 'sql' && !taskInProgress && undoRedoArr[i]}
199+
</Box>
200+
)
201+
})}
202+
</VStack>
203+
<HStack
204+
aria-label="thinking-header"
205+
paddingBottom={actions.length && isExpanded ? 1 : 0}
206+
marginBottom={actions.length && isExpanded ? 1 : 0}
207+
borderBottomWidth={ actions.length && isExpanded ? '1px' : '0px'}
208+
// borderBottomColor={'minusxBW.50'}
209+
borderBottomColor={'minusxGreen.800'}
210+
justifyContent={'space-between'}
211+
onClick={toggleExpand} cursor={"pointer"}
212+
width={"100%"}
213+
>
214+
<HStack>
215+
{isExpanded ? <BsChevronDown strokeWidth={1}/> : <BsChevronRight strokeWidth={1}/>}
216+
<Box flex={5}>
217+
<Text>{title}</Text>
192218
</Box>
193-
)
194-
})}
219+
{ status != 'FINISHED' ? <Spinner size="xs" speed={'0.75s'} color="minusxBW.100" mx={3} /> : null }
220+
</HStack>
221+
{/* { isExpanded ? <Text fontSize={"12px"} flexDirection={"row"} display={"flex"} justifyContent={"center"} alignItems={"center"}><MdOutlineTimer/>{latency}{"s"}</Text> : null } */}
222+
</HStack>
195223
</VStack>
196-
<HStack
197-
aria-label="thinking-header"
198-
paddingBottom={actions.length && isExpanded ? 1 : 0}
199-
marginBottom={actions.length && isExpanded ? 1 : 0}
200-
borderBottomWidth={ actions.length && isExpanded ? '1px' : '0px'}
201-
// borderBottomColor={'minusxBW.50'}
202-
borderBottomColor={'minusxGreen.800'}
203-
justifyContent={'space-between'}
204-
onClick={toggleExpand} cursor={"pointer"}
205-
width={"100%"}
206-
>
207-
<HStack>
208-
{isExpanded ? <BsChevronDown strokeWidth={1}/> : <BsChevronRight strokeWidth={1}/>}
209-
<Box flex={5}>
210-
<Text>{title}</Text>
211-
</Box>
212-
{ status != 'FINISHED' ? <Spinner size="xs" speed={'0.75s'} color="minusxBW.100" mx={3} /> : null }
213224
</HStack>
214-
{/* { isExpanded ? <Text fontSize={"12px"} flexDirection={"row"} display={"flex"} justifyContent={"center"} alignItems={"center"}><MdOutlineTimer/>{latency}{"s"}</Text> : null } */}
215-
</HStack>
216-
</VStack>
217-
</HStack>
218-
{isExpanded && actions.map((action, index) => {
219-
const { text, code, oldCode, language, extraArgs } = action.renderInfo || {}
220-
return (
221-
<VStack className={'action'} padding={'2px'} key={index} alignItems={"start"}>
222-
<HStack>
223-
<Icon
224-
as={
225-
!action.finished
226-
? MdOutlineCheckBoxOutlineBlank
227-
: (action.status == 'SUCCESS' ? MdOutlineCheckBox : MdOutlineIndeterminateCheckBox)
225+
{isExpanded && actions.map((action, index) => {
226+
const { text, code, oldCode, language, extraArgs } = action.renderInfo || {}
227+
return (
228+
<VStack className={'action'} padding={'2px'} key={index} alignItems={"start"}>
229+
<HStack>
230+
<Icon
231+
as={
232+
!action.finished
233+
? MdOutlineCheckBoxOutlineBlank
234+
: (action.status == 'SUCCESS' ? MdOutlineCheckBox : MdOutlineIndeterminateCheckBox)
235+
}
236+
boxSize={5}
237+
/>
238+
{/* <Text>{action.function.name}{text ? " | " : ""}{text}</Text> */}
239+
<Text>{action.function.name}</Text>
240+
</HStack>
241+
<VStack width={"100%"} alignItems={"stretch"}>
242+
{ code && <Box width={"100%"} p={2} bg={"#1e1e1e"} borderRadius={5}>
243+
<CodeBlock code={code || ""} tool={currentTool} oldCode={oldCode} language={language} />
244+
</Box>
228245
}
229-
boxSize={5}
230-
/>
231-
{/* <Text>{action.function.name}{text ? " | " : ""}{text}</Text> */}
232-
<Text>{action.function.name}</Text>
233-
</HStack>
234-
<VStack width={"100%"} alignItems={"stretch"}>
235-
{ code && <Box width={"100%"} p={2} bg={"#1e1e1e"} borderRadius={5}>
236-
<CodeBlock code={code || ""} tool={currentTool} oldCode={oldCode} language={language} />
237-
</Box>
238-
}
239-
{extraArgs && <CodeBlock code={JSON.stringify(extraArgs, null, 2)} language='json' tool={currentTool}/>}
246+
{extraArgs && <CodeBlock code={JSON.stringify(extraArgs, null, 2)} language='json' tool={currentTool}/>}
247+
</VStack>
240248
</VStack>
241-
</VStack>
242-
)})}
243-
{/* {isHovered && isExpanded && configs.IS_DEV && index >= 0 && (
244-
<Box position="absolute" top={-1} right={0}>
245-
<IconButton
246-
aria-label="Debug Info"
247-
isRound={true}
248-
icon={<BsBugFill />}
249-
size="xs"
250-
colorScheme={"minusxBW"}
251-
mr={1}
252-
onClick={showDebugInfo}
253-
/>
249+
)})}
250+
{/* {isHovered && isExpanded && configs.IS_DEV && index >= 0 && (
251+
<Box position="absolute" top={-1} right={0}>
252+
<IconButton
253+
aria-label="Debug Info"
254+
isRound={true}
255+
icon={<BsBugFill />}
256+
size="xs"
257+
colorScheme={"minusxBW"}
258+
mr={1}
259+
onClick={showDebugInfo}
260+
/>
261+
</Box>
262+
)} */}
254263
</Box>
255-
)} */}
256-
</Box>
257-
</HStack>
258-
<VStack maxWidth={"100%"} width={"100%"} overflow={"auto"}>
259-
{tableOutputsArr.length > 0 &&
260-
// <Text marginBottom={2} borderBottomWidth={1} borderBottomColor={'minusxGreen.800'} style={{ hyphens: 'auto' }} p={2} w={"100%"}>{"Thinking..."}<br/>{preExpanderText}</Text>
261-
tableOutputsArr.map((text, i) => {
262-
return (<>{text}</>)
263-
})}
264+
</HStack>
265+
<VStack maxWidth={"100%"} width={"100%"} overflow={"auto"}>
266+
{tableOutputsArr.length > 0 &&
267+
// <Text marginBottom={2} borderBottomWidth={1} borderBottomColor={'minusxGreen.800'} style={{ hyphens: 'auto' }} p={2} w={"100%"}>{"Thinking..."}<br/>{preExpanderText}</Text>
268+
tableOutputsArr.map((text, i) => {
269+
return (<>{text}</>)
270+
})}
271+
</VStack>
264272
</VStack>
265-
</VStack>
266273
)
267274
}
268275

@@ -314,6 +321,7 @@ const PlanningActionStack: React.FC = () => {
314321
const isAnalystmode = useSelector((state: RootState) => state.settings.analystMode) || false;
315322
const dbMetadata = get(metadataProcessingCache, [dbId, 'result'], null);
316323
const thread = useSelector((state: RootState) => state.chat.activeThread)
324+
const activeThread = useSelector((state: RootState) => state.chat.threads[thread])
317325
const planningMessage = useSelector((state: RootState) => state.chat.threads[thread].planningMessage)
318326
const streamingContents = useSelector((state: RootState) => state.chat.threads[thread].streamingContents)
319327

@@ -328,8 +336,16 @@ const PlanningActionStack: React.FC = () => {
328336
return () => clearInterval(intervalId);
329337
}, []);
330338

331-
// Use socket planning message if available, otherwise cycle through default messages
332-
const displayMessage = planningMessage || planningActions[currentTitleIndex % planningActions.length]
339+
// Only display planning message if it's for the current thread
340+
const shouldShowPlanning = planningMessage?.conversationID === activeThread.id
341+
const displayMessage = shouldShowPlanning && planningMessage
342+
? planningMessage.message
343+
: planningActions[currentTitleIndex % planningActions.length]
344+
345+
// Filter streaming contents for current thread only
346+
const relevantStreamingContents = streamingContents?.filter(
347+
content => content.conversationID === activeThread.id
348+
) || []
333349

334350
return (
335351
<VStack aria-label={"planning"} className={'action-stack'} justifyContent={'start'} width={"100%"} spacing={2}>
@@ -350,7 +366,7 @@ const PlanningActionStack: React.FC = () => {
350366
<Spinner size="xs" speed={'0.75s'} color="minusxBW.100" aria-label={"planning-spinner"}/>
351367
</HStack>
352368
</Box>
353-
{streamingContents?.map((streamingContent) => (
369+
{relevantStreamingContents.map((streamingContent) => (
354370
<Box
355371
key={streamingContent.id}
356372
bg={'minusxGreen.800'}

web/src/components/common/App.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -190,20 +190,30 @@ const AppLoggedIn = forwardRef((_props, ref) => {
190190
console.warn('[minusx] Failed to load assets from Atlas API:', atlasError)
191191
}
192192
}, [atlasData, atlasLoading, atlasError])
193+
useEffect(() => {
194+
dispatch(updateThreadID())
195+
}, [])
193196

194197
// Disabling sockets for now
195198
useSocketIO({
196199
sessionToken: sessionJwt,
197200
onMessage: (message) => {
198201
console.log('Socket.io message received:', message);
199202
// Handle planning messages
200-
if (message?.type === 'message' && message?.content?.agent) {
203+
if (message?.type === 'message' && message?.content?.agent && message?.conversationID) {
201204
const agentName = message.content.agent;
202-
dispatch(setPlanningMessage(`Running ${agentName}`));
205+
dispatch(setPlanningMessage({
206+
message: `Running ${agentName}`,
207+
conversationID: message.conversationID
208+
}));
203209
}
204210
// Handle streaming content chunks
205-
if (message?.type === 'content' && message?.id && message?.content) {
206-
dispatch(appendStreamingContent({ id: message.id, chunk: message.content }));
211+
if (message?.type === 'content' && message?.id && message?.content && message?.conversationID) {
212+
dispatch(appendStreamingContent({
213+
id: message.id,
214+
chunk: message.content,
215+
conversationID: message.conversationID
216+
}));
207217
}
208218
},
209219
onConnect: () => {

web/src/components/common/ChatContent.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,33 @@ export const ChatContent: React.FC<{content: ChatMessageContent, messageIndex?:
2222
const origin = url ? new URL(url).origin : '';
2323
const pageType = toolContext?.pageType || ''
2424
const embedConfigs = useSelector((state: RootState) => state.configs.embed);
25-
25+
26+
// Get messages to check if next message exists
27+
const thread = useSelector((state: RootState) => state.chat.activeThread)
28+
const messages = useSelector((state: RootState) => state.chat.threads[thread].messages)
29+
2630
// Create mention items for parsing storage format mentions
2731
const mentionItems = useMemo(() => {
2832
if (!toolContext?.dbInfo) return []
2933
const tables = toolContext.dbInfo.tables || []
3034
const models = toolContext.dbInfo.models || []
3135
return createMentionItems(tables, models)
3236
}, [toolContext?.dbInfo]);
33-
37+
3438
if (content.type == 'DEFAULT') {
35-
const baseContentText = ((pageType === 'dashboard' || pageType === 'unknown') && role === 'assistant') ? `${content.text} {{MX_LAST_QUERY_URL}}` : content.text;
39+
// Check if next message exists and if it's a user message
40+
const nextMessage = messageIndex !== undefined ? messages[messageIndex + 1] : undefined
41+
const shouldAddQueryURL = (
42+
(pageType === 'dashboard' || pageType === 'unknown') &&
43+
role === 'assistant' &&
44+
(!nextMessage || nextMessage.role === 'user')
45+
)
46+
const baseContentText = shouldAddQueryURL ? `${content.text} {{MX_LAST_QUERY_URL}}` : content.text;
3647
// Convert storage format mentions (@{type:table,id:123}) to special code syntax ([mention:table:table_name])
3748
const contentTextWithMentionTags = convertMentionsToDisplay(baseContentText, mentionItems);
49+
if (contentTextWithMentionTags.trim() === '') {
50+
return null;
51+
}
3852
return (
3953
<div>
4054
{content.images.map(image => (

0 commit comments

Comments
 (0)