Skip to content

Commit dfd4dd0

Browse files
cezudasCopilotalexandrudanpop
authored
Display query router reasoning in the chat (#1541)
Part of OPS-2935. <img width="402" height="919" alt="Screenshot 2025-10-29 at 15 18 11" src="https://github.com/user-attachments/assets/95f9ba90-fe37-4aae-83c2-664ad2202381" /> <img width="403" height="920" alt="Screenshot 2025-10-29 at 15 18 35" src="https://github.com/user-attachments/assets/dbf2234a-8e64-44f0-a593-c60b65c627b4" /> --------- Co-authored-by: Copilot <[email protected]> Co-authored-by: Alexandru-Dan Pop <[email protected]>
1 parent 66aa6dd commit dfd4dd0

File tree

10 files changed

+90
-4
lines changed

10 files changed

+90
-4
lines changed

packages/react-ui/src/app/features/builder/step-settings/code-settings/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
ExpandableContent,
23
FormField,
34
FormItem,
45
FormLabel,
@@ -17,7 +18,6 @@ import { useTheme } from '@/app/common/providers/theme-provider';
1718
import { aiSettingsHooks } from '@/app/features/ai/lib/ai-settings-hooks';
1819
import { PropertyType } from '@openops/blocks-framework';
1920
import { useSafeBuilderStateContext } from '../../builder-hooks';
20-
import { ExpandableContent } from '../expandable-markdown';
2121
import { CodeEditior } from './code-editior';
2222

2323
const markdown = `

packages/react-ui/src/app/features/builder/step-settings/loops-settings.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
ExpandableContent,
23
FormField,
34
FormItem,
45
FormLabel,
@@ -11,7 +12,6 @@ import { useFormContext } from 'react-hook-form';
1112

1213
import { useTheme } from '@/app/common/providers/theme-provider';
1314
import { TextInputWithMentions } from '../block-properties/text-input-with-mentions';
14-
import { ExpandableContent } from './expandable-markdown';
1515

1616
const markdown = t(
1717
'Select the items to iterate over from the previous step by clicking on the **Items** input, which should be a **list** of items.\n\nThe loop will iterate over each item in the list and execute the next step for every item.',

packages/server/api/src/app/ai/chat/stream-message-builder.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,3 +90,35 @@ export function sendTextMessageToStream(
9090
responseStream.write(buildTextDeltaPart(message, messageId));
9191
responseStream.write(buildTextEndMessage(messageId));
9292
}
93+
94+
function buildReasoningStartPart(id: string): string {
95+
return createStreamMessage({
96+
type: 'reasoning-start',
97+
id,
98+
});
99+
}
100+
101+
function buildReasoningDeltaPart(delta: string, id: string): string {
102+
return createStreamMessage({
103+
type: 'reasoning-delta',
104+
id,
105+
delta,
106+
});
107+
}
108+
109+
function buildReasoningEndPart(id: string): string {
110+
return createStreamMessage({
111+
type: 'reasoning-end',
112+
id,
113+
});
114+
}
115+
116+
export function sendReasoningToStream(
117+
responseStream: NodeJS.WritableStream,
118+
reasoning: string,
119+
messageId: string,
120+
): void {
121+
responseStream.write(buildReasoningStartPart(messageId));
122+
responseStream.write(buildReasoningDeltaPart(reasoning, messageId));
123+
responseStream.write(buildReasoningEndPart(messageId));
124+
}

packages/server/api/src/app/ai/mcp/llm-query-router.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const MAX_SELECTED_TOOLS = 128;
1212
export type ToolsAndQueryResult = {
1313
tools?: ToolSet;
1414
queryClassification: QueryClassification[];
15+
reasoning?: string;
1516
};
1617

1718
const queryClassificationDescriptions: Record<QueryClassification, string> = {
@@ -44,6 +45,11 @@ const coreWithReasoningSchema = z.object({
4445
.describe(
4546
'The reasoning for the tool selection and classification. Fill this field first',
4647
),
48+
user_facing_reasoning: z
49+
.string()
50+
.describe(
51+
'Short non-technical description to show the next steps to the user',
52+
),
4753
tool_names: z.array(z.string()).describe('Array of selected tool names'),
4854
query_classification: z
4955
.array(queryClassificationUnionSchema)
@@ -69,6 +75,7 @@ export async function routeQuery({
6975
return {
7076
tools: undefined,
7177
queryClassification: [QueryClassification.general],
78+
reasoning: undefined,
7279
};
7380
}
7481

@@ -114,6 +121,7 @@ export async function routeQuery({
114121
return {
115122
queryClassification,
116123
tools: selectedTools,
124+
reasoning: selectionResult.user_facing_reasoning,
117125
};
118126
} catch (error) {
119127
const isAbortError = error instanceof Error && error.name === 'AbortError';
@@ -123,6 +131,7 @@ export async function routeQuery({
123131
return {
124132
tools: undefined,
125133
queryClassification: [QueryClassification.general],
134+
reasoning: undefined,
126135
};
127136
}
128137
}

packages/server/api/src/app/ai/mcp/tools-context-builder.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import { AiConfigParsed, ChatFlowContext } from '@openops/shared';
33
import { LanguageModel, ModelMessage, ToolSet } from 'ai';
44
import { FastifyInstance } from 'fastify';
55
import { MCPChatContext } from '../chat/ai-chat.service';
6+
import { generateMessageId } from '../chat/ai-id-generators';
67
import {
78
getBlockSystemPrompt,
89
getMcpSystemPrompt,
910
} from '../chat/prompts.service';
11+
import { sendReasoningToStream } from '../chat/stream-message-builder';
1012
import { routeQuery } from './llm-query-router';
1113
import {
1214
collectToolsByProvider,
@@ -65,7 +67,11 @@ export async function getMCPToolsContext({
6567
projectId,
6668
);
6769

68-
const { tools: filteredTools, queryClassification } = await routeQuery({
70+
const {
71+
tools: filteredTools,
72+
queryClassification,
73+
reasoning,
74+
} = await routeQuery({
6975
messages,
7076
tools,
7177
languageModel,
@@ -74,6 +80,11 @@ export async function getMCPToolsContext({
7480
abortSignal,
7581
});
7682

83+
if (reasoning && stream) {
84+
const messageId = generateMessageId();
85+
sendReasoningToStream(stream, reasoning, messageId);
86+
}
87+
7788
const systemPrompt = await getMcpSystemPrompt({
7889
queryClassification,
7990
selectedTools: filteredTools,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export * from './reasoning-part';
12
export * from './thread';
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { LoaderPinwheelIcon } from 'lucide-react';
2+
import { FC } from 'react';
3+
import { ExpandableContent } from '../../expandable-markdown';
4+
5+
type ReasoningPartProps = {
6+
text: string;
7+
};
8+
9+
export const ReasoningPart: FC<ReasoningPartProps> = ({ text }) => {
10+
return (
11+
<div className="my-2">
12+
<ExpandableContent
13+
fullContent={text}
14+
buttonClassName="text-sm ml-6 font-normal"
15+
>
16+
{(content) => (
17+
<div className="flex items-center gap-2 pt-2">
18+
<LoaderPinwheelIcon className="h-4 w-4 flex-shrink-0 text-border-300 dark:text-blue-400 mt-0.5" />
19+
<div className="flex-grow">
20+
<div className="text-sm text-primary-700 dark:text-blue-100 whitespace-pre-wrap">
21+
{content}
22+
</div>
23+
</div>
24+
</div>
25+
)}
26+
</ExpandableContent>
27+
</div>
28+
);
29+
};

packages/ui-components/src/components/assistant-ui/thread/thread.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { ToolFallback } from '../tool-fallback';
2727
import { TooltipIconButton } from '../tooltip-icon-button';
2828
import { ConnectionError } from './connection-error';
2929
import { ConnectionSlowWarning } from './connection-slow-warning';
30+
import { ReasoningPart } from './reasoning-part';
3031

3132
const MarkdownTextWrapper = memo(({ theme, ...props }: any) => {
3233
const { codeVariation, handleInject } = useThreadExtraContext();
@@ -234,6 +235,7 @@ const AssistantMessage: FC<{ theme: Theme }> = ({ theme }) => {
234235
const messageComponents = useMemo(
235236
() => ({
236237
Text: (props: any) => <MarkdownTextWrapper {...props} theme={theme} />,
238+
Reasoning: (props: any) => <ReasoningPart {...props} />,
237239
tools: {
238240
Fallback: (props: any) => <ToolFallback {...props} theme={theme} />,
239241
by_name: {

packages/react-ui/src/app/features/builder/step-settings/expandable-markdown.tsx renamed to packages/ui-components/src/components/expandable-markdown.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ChevronDown, ChevronUp } from 'lucide-react';
22
import { ReactNode, useState } from 'react';
3+
import { cn } from '../lib/cn';
34

45
type ExpandableContentProps = {
56
fullContent: string;
@@ -47,7 +48,7 @@ export const ExpandableContent = ({
4748
<button
4849
type="button"
4950
onClick={() => setIsExpanded(!isExpanded)}
50-
className={`${defaultButtonClass} ${buttonClassName}`}
51+
className={cn(defaultButtonClass, buttonClassName)}
5152
>
5253
{isExpanded ? (
5354
<>

packages/ui-components/src/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export * from './confirmation-dialog/confirmation-dialog';
1515
export * from './custom';
1616
export * from './dashboard-overview';
1717
export * from './dismissible-panel/dismissible-panel';
18+
export * from './expandable-markdown';
1819
export * from './explore-templates';
1920
export * from './flow-canvas/ai-widget';
2021
export * from './flow-canvas/canvas-context';

0 commit comments

Comments
 (0)