Skip to content

Commit 52f9c5f

Browse files
stephmilovice40pud
andauthored
[9.1] [Security Assistant] Fixes conversation message not appended/title not updating (elastic#233219) (elastic#234966)
# Backport This will backport the following commits from `main` to `9.1`: - [[Security Assistant] Fixes conversation message not appended/title not updating (elastic#233219)](elastic#233219) <!--- Backport version: 10.0.2 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Steph Milovic","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-09-11T20:06:53Z","message":"[Security Assistant] Fixes conversation message not appended/title not updating (elastic#233219)","sha":"b81bbd005f94e6a333b04b06da3ac108e179fa6a","branchLabelMapping":{"^v9.2.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","Team: SecuritySolution","backport:version","v9.2.0","v9.1.3","v8.19.3"],"title":"[Security Assistant] Fixes conversation message not appended/title not updating","number":233219,"url":"https://github.com/elastic/kibana/pull/233219","mergeCommit":{"message":"[Security Assistant] Fixes conversation message not appended/title not updating (elastic#233219)","sha":"b81bbd005f94e6a333b04b06da3ac108e179fa6a"}},"sourceBranch":"main","suggestedTargetBranches":["9.1","8.19"],"targetPullRequestStates":[{"branch":"main","label":"v9.2.0","branchLabelMappingKey":"^v9.2.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/233219","number":233219,"mergeCommit":{"message":"[Security Assistant] Fixes conversation message not appended/title not updating (elastic#233219)","sha":"b81bbd005f94e6a333b04b06da3ac108e179fa6a"}},{"branch":"9.1","label":"v9.1.3","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"8.19","label":"v8.19.3","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT--> --------- Co-authored-by: Ievgen Sorokopud <[email protected]>
1 parent 7000b0a commit 52f9c5f

File tree

12 files changed

+676
-239
lines changed

12 files changed

+676
-239
lines changed

x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.test.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,34 @@ describe('use chat send', () => {
145145
});
146146
});
147147
});
148+
it('retries getConversation up to 5 times if title is empty, and stops when title is found', async () => {
149+
const promptText = 'test prompt';
150+
const getConversationMock = jest.fn();
151+
// First 3 calls return empty title, 4th returns non-empty
152+
getConversationMock
153+
.mockResolvedValueOnce({ title: '' })
154+
.mockResolvedValueOnce({ title: '' })
155+
.mockResolvedValueOnce({ title: '' })
156+
.mockResolvedValueOnce({ title: 'Final Title' });
157+
(useConversation as jest.Mock).mockReturnValue({
158+
removeLastMessage,
159+
clearConversation,
160+
getConversation: getConversationMock,
161+
createConversation: jest.fn(),
162+
});
163+
const { result } = renderHook(
164+
() =>
165+
useChatSend({
166+
...testProps,
167+
currentConversation: { ...emptyWelcomeConvo, id: 'convo-id', title: '' },
168+
}),
169+
{ wrapper: TestProviders }
170+
);
171+
await act(async () => {
172+
await result.current.handleChatSend(promptText);
173+
});
174+
// Should call getConversation 4 times (until non-empty title)
175+
expect(getConversationMock).toHaveBeenCalledTimes(4);
176+
expect(getConversationMock).toHaveBeenLastCalledWith('convo-id');
177+
});
148178
});

x-pack/platform/packages/shared/kbn-elastic-assistant/impl/assistant/chat_send/use_chat_send.tsx

Lines changed: 99 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,15 @@ export const useChatSend = ({
6161
const { setLastConversation } = useAssistantLastConversation({ spaceId });
6262
const [userPrompt, setUserPrompt] = useState<string | null>(null);
6363

64-
const { isLoading, sendMessage, abortStream } = useSendMessage();
64+
const { sendMessage, abortStream } = useSendMessage();
6565
const { clearConversation, createConversation, getConversation, removeLastMessage } =
6666
useConversation();
6767
const { data: kbStatus } = useKnowledgeBaseStatus({ http, enabled: isAssistantEnabled });
6868
const isSetupComplete = kbStatus?.elser_exists && kbStatus?.security_labs_exists;
6969

70+
// Local loading state that persists until the entire message flow is complete
71+
const [isLoadingChatSend, setIsLoadingChatSend] = useState(false);
72+
7073
// Handles sending latest user prompt to API
7174
const handleSendMessage = useCallback(
7275
async (promptText: string) => {
@@ -81,79 +84,102 @@ export const useChatSend = ({
8184
);
8285
return;
8386
}
84-
const apiConfig = currentConversation.apiConfig;
85-
let newConvo;
86-
if (currentConversation.id === '') {
87-
// create conversation with empty title, GENERATE_CHAT_TITLE graph step will properly title
88-
newConvo = await createConversation(currentConversation);
89-
if (newConvo?.id) {
90-
setLastConversation({
91-
id: newConvo.id,
92-
});
87+
88+
setIsLoadingChatSend(true);
89+
90+
try {
91+
const apiConfig = currentConversation.apiConfig;
92+
let newConvo;
93+
if (currentConversation.id === '') {
94+
// create conversation with empty title, GENERATE_CHAT_TITLE graph step will properly title
95+
newConvo = await createConversation(currentConversation);
96+
if (newConvo?.id) {
97+
setLastConversation({
98+
id: newConvo.id,
99+
});
100+
}
93101
}
102+
const convo: Conversation = { ...currentConversation, ...(newConvo ?? {}) };
103+
const userMessage = getCombinedMessage({
104+
currentReplacements: convo.replacements,
105+
promptText,
106+
selectedPromptContexts,
107+
});
108+
109+
const baseReplacements: Replacements = userMessage.replacements ?? convo.replacements;
110+
111+
const selectedPromptContextsReplacements = Object.values(
112+
selectedPromptContexts
113+
).reduce<Replacements>((acc, context) => ({ ...acc, ...context.replacements }), {});
114+
115+
const replacements: Replacements = {
116+
...baseReplacements,
117+
...selectedPromptContextsReplacements,
118+
};
119+
const updatedMessages = [...convo.messages, userMessage].map((m) => ({
120+
...m,
121+
content: m.content ?? '',
122+
}));
123+
setCurrentConversation({
124+
...convo,
125+
replacements,
126+
messages: updatedMessages,
127+
});
128+
129+
// Reset prompt context selection and preview before sending:
130+
setSelectedPromptContexts({});
131+
132+
const rawResponse = await sendMessage({
133+
apiConfig,
134+
http,
135+
message: userMessage.content ?? '',
136+
conversationId: convo.id,
137+
replacements,
138+
});
139+
140+
assistantTelemetry?.reportAssistantMessageSent({
141+
role: userMessage.role,
142+
actionTypeId: apiConfig.actionTypeId,
143+
model: apiConfig.model,
144+
provider: apiConfig.provider,
145+
isEnabledKnowledgeBase: isSetupComplete ?? false,
146+
});
147+
148+
const responseMessage: ClientMessage = getMessageFromRawResponse(rawResponse);
149+
if (convo.title === '') {
150+
// Retry getConversation up to 5 times if title is empty
151+
let retryCount = 0;
152+
const maxRetries = 5;
153+
while (retryCount < maxRetries) {
154+
const conversation = await getConversation(convo.id);
155+
convo.title = conversation?.title ?? '';
156+
157+
if (convo.title !== '') {
158+
break; // Title found, exit retry loop
159+
}
160+
161+
retryCount++;
162+
if (retryCount < maxRetries) {
163+
// Wait 1 second before next retry
164+
await new Promise((resolve) => setTimeout(resolve, 1000));
165+
}
166+
}
167+
}
168+
setCurrentConversation({
169+
...convo,
170+
replacements,
171+
messages: [...updatedMessages, responseMessage],
172+
});
173+
assistantTelemetry?.reportAssistantMessageSent({
174+
role: responseMessage.role,
175+
actionTypeId: apiConfig.actionTypeId,
176+
model: apiConfig.model,
177+
provider: apiConfig.provider,
178+
isEnabledKnowledgeBase: isSetupComplete ?? false,
179+
});
180+
} finally {
181+
setIsLoadingChatSend(false);
94182
}
95-
const convo: Conversation = { ...currentConversation, ...(newConvo ?? {}) };
96-
const userMessage = getCombinedMessage({
97-
currentReplacements: convo.replacements,
98-
promptText,
99-
selectedPromptContexts,
100-
});
101-
102-
const baseReplacements: Replacements = userMessage.replacements ?? convo.replacements;
103-
104-
const selectedPromptContextsReplacements = Object.values(
105-
selectedPromptContexts
106-
).reduce<Replacements>((acc, context) => ({ ...acc, ...context.replacements }), {});
107-
108-
const replacements: Replacements = {
109-
...baseReplacements,
110-
...selectedPromptContextsReplacements,
111-
};
112-
const updatedMessages = [...convo.messages, userMessage].map((m) => ({
113-
...m,
114-
content: m.content ?? '',
115-
}));
116-
setCurrentConversation({
117-
...convo,
118-
replacements,
119-
messages: updatedMessages,
120-
});
121-
122-
// Reset prompt context selection and preview before sending:
123-
setSelectedPromptContexts({});
124-
125-
const rawResponse = await sendMessage({
126-
apiConfig,
127-
http,
128-
message: userMessage.content ?? '',
129-
conversationId: convo.id,
130-
replacements,
131-
});
132-
133-
assistantTelemetry?.reportAssistantMessageSent({
134-
role: userMessage.role,
135-
actionTypeId: apiConfig.actionTypeId,
136-
model: apiConfig.model,
137-
provider: apiConfig.provider,
138-
isEnabledKnowledgeBase: isSetupComplete ?? false,
139-
});
140-
141-
const responseMessage: ClientMessage = getMessageFromRawResponse(rawResponse);
142-
if (convo.title === '') {
143-
convo.title = (await getConversation(convo.id))?.title ?? '';
144-
}
145-
setCurrentConversation({
146-
...convo,
147-
replacements,
148-
messages: [...updatedMessages, responseMessage],
149-
});
150-
assistantTelemetry?.reportAssistantMessageSent({
151-
role: responseMessage.role,
152-
actionTypeId: apiConfig.actionTypeId,
153-
model: apiConfig.model,
154-
provider: apiConfig.provider,
155-
isEnabledKnowledgeBase: isSetupComplete ?? false,
156-
});
157183
},
158184
[
159185
assistantTelemetry,
@@ -241,7 +267,7 @@ export const useChatSend = ({
241267
handleChatSend,
242268
abortStream,
243269
handleRegenerateResponse,
244-
isLoading,
270+
isLoading: isLoadingChatSend,
245271
userPrompt,
246272
setUserPrompt,
247273
};

x-pack/solutions/security/plugins/elastic_assistant/server/__mocks__/conversations_schema.mock.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,3 +244,35 @@ export const getEsCreateConversationSchemaMock = (
244244
namespace: 'default',
245245
...rest,
246246
});
247+
248+
export const getEsConversationSchemaMock = (
249+
rest?: Partial<EsConversationSchema>
250+
): EsConversationSchema => ({
251+
'@timestamp': '2020-04-20T15:25:31.830Z',
252+
created_at: '2020-04-20T15:25:31.830Z',
253+
title: 'title-1',
254+
updated_at: '2020-04-20T15:25:31.830Z',
255+
messages: [],
256+
id: '1',
257+
namespace: 'default',
258+
exclude_from_last_conversation_storage: false,
259+
api_config: {
260+
action_type_id: '.gen-ai',
261+
connector_id: 'c1',
262+
default_system_prompt_id: 'prompt-1',
263+
model: 'test',
264+
provider: 'Azure OpenAI',
265+
},
266+
summary: {
267+
content: 'test',
268+
},
269+
category: 'assistant',
270+
users: [
271+
{
272+
id: '1111',
273+
name: 'elastic',
274+
},
275+
],
276+
replacements: undefined,
277+
...rest,
278+
});

0 commit comments

Comments
 (0)