diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 37d2770b1a9f8..fb066cea17672 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -83927,6 +83927,9 @@ components: reader: $ref: '#/components/schemas/Security_AI_Assistant_API_Reader' description: Message content. + refusal: + description: Refusal reason returned by the model when content is filtered. + type: string role: $ref: '#/components/schemas/Security_AI_Assistant_API_MessageRole' description: Message role. diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 3b32e565b0f8e..f2247047e3f4f 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -94445,6 +94445,9 @@ components: reader: $ref: '#/components/schemas/Security_AI_Assistant_API_Reader' description: Message content. + refusal: + description: Refusal reason returned by the model when content is filtered. + type: string role: $ref: '#/components/schemas/Security_AI_Assistant_API_MessageRole' description: Message role. diff --git a/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/api.ts b/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/api.ts index dff09b87feb04..6890ca3446a4a 100644 --- a/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/api.ts +++ b/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/api.ts @@ -197,6 +197,10 @@ export interface ChatCompleteResponse< * The text content of the LLM response. */ content: string; + /** + * Optional refusal reason returned by the model when content is filtered. + */ + refusal?: string; /** * The eventual tool calls performed by the LLM. */ diff --git a/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/events.ts b/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/events.ts index 765dbf7e9c3ba..5f507d0c65c2a 100644 --- a/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/events.ts +++ b/x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/events.ts @@ -32,6 +32,10 @@ export type ChatCompletionMessageEvent { + const additionalKwargs = response.refusal ? { refusal: response.refusal } : undefined; return new AIMessage({ content: response.content, + ...(additionalKwargs ? { additional_kwargs: additionalKwargs } : {}), tool_calls: response.toolCalls.map((toolCall) => { return { id: toolCall.toolCallId, diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml index ee247893af5c1..b39b711a8ebaf 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/ess/elastic_assistant_api_2023_10_31.bundled.schema.yaml @@ -2925,6 +2925,9 @@ components: reader: $ref: '#/components/schemas/Reader' description: Message content. + refusal: + description: Refusal reason returned by the model when content is filtered. + type: string role: $ref: '#/components/schemas/MessageRole' description: Message role. diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml index 4685dc7405e78..a662ea328da34 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/docs/openapi/serverless/elastic_assistant_api_2023_10_31.bundled.schema.yaml @@ -2925,6 +2925,9 @@ components: reader: $ref: '#/components/schemas/Reader' description: Message content. + refusal: + description: Refusal reason returned by the model when content is filtered. + type: string role: $ref: '#/components/schemas/MessageRole' description: Message role. diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts index 52fbe62d1e210..f0d8be8a94584 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts @@ -421,6 +421,10 @@ export const Message = z.object({ * Message content. */ content: z.string(), + /** + * Refusal reason returned by the model when content is filtered. + */ + refusal: z.string().optional(), /** * Message content. */ diff --git a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml index 3da5c54b05cb7..9a69c56ba7f3f 100644 --- a/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml +++ b/x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml @@ -443,6 +443,9 @@ components: type: string description: Message content. example: 'Hello, how can I assist you today?' + refusal: + type: string + description: Refusal reason returned by the model when content is filtered. reader: $ref: '#/components/schemas/Reader' description: Message content. diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/adapters/openai/from_openai.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/adapters/openai/from_openai.ts index 1743de8aae587..cc723baa39da0 100644 --- a/x-pack/platform/plugins/shared/inference/server/chat_complete/adapters/openai/from_openai.ts +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/adapters/openai/from_openai.ts @@ -18,6 +18,7 @@ export function chunkFromOpenAI(chunk: OpenAI.ChatCompletionChunk): ChatCompleti return { type: ChatCompletionEventType.ChatCompletionChunk, content: delta.content ?? '', + refusal: delta.refusal ?? undefined, tool_calls: delta.tool_calls?.map((toolCall) => { return { diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/chunks_into_message.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/chunks_into_message.ts index 06ccdba3c6338..545c224fc7d5f 100644 --- a/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/chunks_into_message.ts +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/chunks_into_message.ts @@ -45,7 +45,7 @@ export function chunksIntoMessage({ logger.debug(() => `Received completed message: ${JSON.stringify(concatenatedChunk)}`); - const { content, tool_calls: toolCalls } = concatenatedChunk; + const { content, refusal, tool_calls: toolCalls } = concatenatedChunk; const activeSpan = trace.getActiveSpan(); if (activeSpan) { setChoice(activeSpan, { content, toolCalls }); @@ -59,6 +59,7 @@ export function chunksIntoMessage({ return { type: ChatCompletionEventType.ChatCompletionMessage, content, + ...(refusal ? { refusal } : {}), toolCalls: validatedToolCalls, }; }) diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/merge_chunks.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/merge_chunks.ts index e3e1304d2247e..d2760616260a0 100644 --- a/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/merge_chunks.ts +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/merge_chunks.ts @@ -9,6 +9,7 @@ import type { ChatCompletionChunkEvent, UnvalidatedToolCall } from '@kbn/inferen interface UnvalidatedMessage { content: string; + refusal?: string; tool_calls: UnvalidatedToolCall[]; } @@ -19,6 +20,9 @@ export const mergeChunks = (chunks: ChatCompletionChunkEvent[]): UnvalidatedMess const message = chunks.reduce( (prev, chunk) => { prev.content += chunk.content ?? ''; + if (chunk.refusal) { + prev.refusal = chunk.refusal; + } chunk.tool_calls?.forEach((toolCall) => { let prevToolCall = prev.tool_calls[toolCall.index]; diff --git a/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/stream_to_response.ts b/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/stream_to_response.ts index ca414855d4cf3..15bf1d22f3390 100644 --- a/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/stream_to_response.ts +++ b/x-pack/platform/plugins/shared/inference/server/chat_complete/utils/stream_to_response.ts @@ -35,6 +35,7 @@ export const streamToResponse = return { content: messageEvent.content, + refusal: messageEvent.refusal, toolCalls: messageEvent.toolCalls, tokens: tokenEvent?.tokens, deanonymized_input: messageEvent.deanonymized_input, diff --git a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/inference/helpers.ts b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/inference/helpers.ts index f2acb676e4d05..767c67bbf31eb 100644 --- a/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/inference/helpers.ts +++ b/x-pack/platform/plugins/shared/stack_connectors/server/connector_types/inference/helpers.ts @@ -47,6 +47,9 @@ export function chunksIntoMessage(obs$: Observable) (prev, chunk) => { if (chunk.choices.length > 0 && !chunk.usage) { prev.choices[0].message.content += chunk.choices[0].message.content ?? ''; + if (chunk.choices[0].message.refusal) { + prev.choices[0].message.refusal = chunk.choices[0].message.refusal; + } chunk.choices[0].message.tool_calls?.forEach((toolCall) => { if (toolCall.index !== undefined) { @@ -89,6 +92,7 @@ export function chunksIntoMessage(obs$: Observable) { message: { content: '', + refusal: null, role: 'assistant', }, }, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/append_conversation_messages.test.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/append_conversation_messages.test.ts index d87fad534046b..bda2d9bf32e8d 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/append_conversation_messages.test.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/append_conversation_messages.test.ts @@ -326,6 +326,29 @@ describe('appendConversationMessages', () => { ); }); + it('preserves refusal reason when present on messages', async () => { + const messageWithRefusal = createMockMessage({ + refusal: 'Detected harmful input content: INSULTS', + }); + setupSuccessfulTest(); + + await callAppendConversationMessages([messageWithRefusal]); + + expect(dataWriter.bulk).toHaveBeenCalledWith( + expect.objectContaining({ + documentsToUpdate: expect.arrayContaining([ + expect.objectContaining({ + messages: expect.arrayContaining([ + expect.objectContaining({ + refusal: 'Detected harmful input content: INSULTS', + }), + ]), + }), + ]), + }) + ); + }); + it('generates UUID for messages without id', async () => { const messageWithoutId = createMockMessage({ id: undefined }); setupSuccessfulTest(); diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/append_conversation_messages.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/append_conversation_messages.ts index 754a344086480..c713c125cf896 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/append_conversation_messages.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/append_conversation_messages.ts @@ -106,6 +106,7 @@ export const transformToUpdateScheme = ( '@timestamp': message.timestamp, id: message.id ?? uuidv4(), content: message.content, + ...(message.refusal ? { refusal: message.refusal } : {}), is_error: message.isError, reader: message.reader, role: message.role, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/create_conversation.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/create_conversation.ts index c5759e78b36b6..5cfe8a6294b5f 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/create_conversation.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/create_conversation.ts @@ -96,6 +96,7 @@ export const transformToCreateScheme = ( '@timestamp': message.timestamp, id: message.id ?? uuidv4(), content: message.content, + ...(message.refusal ? { refusal: message.refusal } : {}), is_error: message.isError, reader: message.reader, role: message.role, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/field_maps_configuration.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/field_maps_configuration.ts index ea206348c9405..53349ba11a81d 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/field_maps_configuration.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/field_maps_configuration.ts @@ -92,6 +92,11 @@ export const conversationsFieldMap: FieldMap = { array: false, required: false, }, + 'messages.refusal': { + type: 'text', + array: false, + required: false, + }, 'messages.reader': { type: 'object', array: false, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/transforms.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/transforms.ts index 16d0d3150c580..3357d8e5db8f3 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/transforms.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/transforms.ts @@ -51,6 +51,7 @@ export const transformESToConversation = ( messageContent: message.content, replacements, }), + ...(message.refusal ? { refusal: message.refusal } : {}), ...(message.is_error ? { isError: message.is_error } : {}), ...(message.reader ? { reader: message.reader } : {}), ...(message.user ? { user: message.user } : {}), diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/types.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/types.ts index 101ee522b6d24..1f69c021aa4b5 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/types.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/types.ts @@ -29,6 +29,7 @@ export interface EsConversationSchema { '@timestamp': string; id?: string; content: string; + refusal?: string; reader?: Reader; role: MessageRole; is_error?: boolean; @@ -77,6 +78,7 @@ export interface CreateMessageSchema { '@timestamp': string; id: string; content: string; + refusal?: string; reader?: Reader; role: MessageRole; is_error?: boolean; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/update_conversation.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/update_conversation.ts index 8652487e7377e..86fa984a794d7 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/update_conversation.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/update_conversation.ts @@ -29,6 +29,7 @@ export interface UpdateConversationSchema { '@timestamp': string; id: string; content: string; + refusal?: string; reader?: Reader; role: MessageRole; is_error?: boolean; @@ -136,6 +137,7 @@ export const transformToUpdateScheme = ( '@timestamp': message.timestamp, id: message.id ?? uuidv4(), content: message.content, + ...(message.refusal ? { refusal: message.refusal } : {}), is_error: message.isError, reader: message.reader, role: message.role, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/executors/types.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/executors/types.ts index 3d06867f1ab61..48d65a8b9a8cf 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/executors/types.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/executors/types.ts @@ -35,6 +35,7 @@ import type { AIAssistantDataClient } from '../../../ai_assistant_data_clients'; export type OnLlmResponse = (args: { content: string; + refusal?: string; interruptValue?: InterruptValue; traceData?: Message['traceData']; isError?: boolean; diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/helpers.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/helpers.ts index 50a1d0150bf4b..10e19cd769daa 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/helpers.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/helpers.ts @@ -106,12 +106,14 @@ export const streamGraph = async ({ const handleFinalContent = (args: { finalResponse: string; + refusal?: string; isError: boolean; interruptValue?: InterruptValue; }) => { if (onLlmResponse) { onLlmResponse({ content: args.finalResponse, + refusal: args.refusal, interruptValue: args.interruptValue, traceData: { transactionId: streamingSpan?.transaction?.ids?.['transaction.id'], @@ -151,7 +153,11 @@ export const streamGraph = async ({ !data.output.lc_kwargs?.tool_calls?.length && !didEnd ) { - handleFinalContent({ finalResponse: data.output.content, isError: false }); + const refusal = + typeof data.output?.additional_kwargs?.refusal === 'string' + ? (data.output.additional_kwargs.refusal as string) + : undefined; + handleFinalContent({ finalResponse: data.output.content, refusal, isError: false }); } else if ( // This is the end of one model invocation but more message will follow as there are tool calls. If this chunk contains text content, add a newline separator to the stream to visually separate the chunks. event === 'on_chat_model_end' && @@ -234,10 +240,15 @@ export const invokeGraph = async ({ const lastMessage = result.messages[result.messages.length - 1]; const output = lastMessage.text; const conversationId = result.conversationId; + const refusal = + typeof lastMessage?.additional_kwargs?.refusal === 'string' + ? (lastMessage.additional_kwargs.refusal as string) + : undefined; if (onLlmResponse) { await onLlmResponse({ content: output, traceData, + ...(refusal ? { refusal } : {}), }); } diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/chat/chat_complete_route.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/chat/chat_complete_route.ts index 70aa8986e80ae..cc4ce72431130 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/chat/chat_complete_route.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/chat/chat_complete_route.ts @@ -212,6 +212,7 @@ export const chatCompleteRoute = ( const onLlmResponse: OnLlmResponse = async ({ content, + refusal, traceData = {}, isError = false, }): Promise => { @@ -225,6 +226,7 @@ export const chatCompleteRoute = ( conversationId, conversationsDataClient, messageContent: prunedContent, + messageRefusal: refusal, replacements: latestReplacements, isError, traceData, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/helpers.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/helpers.ts index 701a2c9e110c8..fad3daf22385b 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/helpers.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/helpers.ts @@ -109,11 +109,13 @@ export const getPluginNameFromRequest = ({ export const getMessageFromRawResponse = ({ rawContent, metadata, + refusal, isError, traceData, }: { rawContent?: string; metadata?: MessageMetadata; + refusal?: string; traceData?: TraceData; isError?: boolean; }): Message => { @@ -122,6 +124,7 @@ export const getMessageFromRawResponse = ({ return { role: 'assistant', content: rawContent, + ...(refusal ? { refusal } : {}), timestamp: dateTimeString, metadata, isError, @@ -179,6 +182,7 @@ export const getSystemPromptFromUserConversation = async ({ export interface AppendAssistantMessageToConversationParams { conversationsDataClient: AIAssistantConversationsDataClient; messageContent: string; + messageRefusal?: string; replacements: Replacements; conversationId: string; contentReferences: ContentReferences; @@ -189,6 +193,7 @@ export interface AppendAssistantMessageToConversationParams { export const appendAssistantMessageToConversation = async ({ conversationsDataClient, messageContent, + messageRefusal, replacements, conversationId, contentReferences, @@ -214,6 +219,7 @@ export const appendAssistantMessageToConversation = async ({ messageContent, replacements, }), + refusal: messageRefusal, metadata: !isEmpty(metadata) ? metadata : undefined, traceData, isError, diff --git a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts index cc2cda88089db..5ad0b199f8c08 100644 --- a/x-pack/solutions/security/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts +++ b/x-pack/solutions/security/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts @@ -171,6 +171,7 @@ export const postActionsConnectorExecuteRoute = ( onLlmResponse = async ({ content, + refusal, traceData, isError, interruptValue, @@ -185,6 +186,7 @@ export const postActionsConnectorExecuteRoute = ( conversationId, conversationsDataClient, messageContent: prunedContent, + messageRefusal: refusal, replacements: latestReplacements, isError, traceData,