Add refusal field to assistant conversations#243423
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds support for capturing and persisting LLM refusal reasons throughout the assistant conversation flow. When a model refuses to generate content (e.g., due to content filtering), the refusal reason is now captured from the LLM response, stored in the conversation data stream, and made searchable via the messages.refusal field.
Key changes:
- Extended type definitions across the inference stack to include optional
refusalfield in messages, events, and API responses - Modified chunk aggregation logic to preserve refusal values from streaming responses
- Updated conversation schema and field mappings to persist refusal data in Elasticsearch
- Added LangChain adapter support to extract refusal from
additional_kwargs
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/messages.ts | Added optional refusal field to AssistantMessage type definition |
| x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/events.ts | Added optional refusal field to ChatCompletionMessageEvent and ChatCompletionChunkEvent |
| x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/api.ts | Added optional refusal field to ChatCompleteResponse interface |
| x-pack/platform/plugins/shared/inference/server/chat_complete/adapters/openai/from_openai.ts | Map OpenAI delta.refusal to chunk event's refusal field |
| x-pack/platform/plugins/shared/inference/server/chat_complete/utils/merge_chunks.ts | Aggregate refusal values from chunks into final message |
| x-pack/platform/plugins/shared/inference/server/chat_complete/utils/chunks_into_message.ts | Extract and conditionally include refusal in completion message event |
| x-pack/platform/plugins/shared/inference/server/chat_complete/utils/stream_to_response.ts | Pass refusal field through to response object |
| x-pack/platform/plugins/shared/stack_connectors/server/connector_types/inference/helpers.ts | Aggregate refusal from chunks in inference connector with initial null value |
| x-pack/platform/packages/shared/ai-infra/inference-langchain/src/chat_model/from_inference/chunks.ts | Add refusal to AIMessageChunk's additional_kwargs when present in chunks |
| x-pack/platform/packages/shared/ai-infra/inference-langchain/src/chat_model/from_inference/messages.ts | Convert inference response refusal to LangChain AIMessage additional_kwargs |
| x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/executors/types.ts | Added optional refusal parameter to OnLlmResponse callback type |
| x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/helpers.ts | Extract refusal from AIMessage additional_kwargs in both streaming and invoke modes, pass to onLlmResponse |
| x-pack/solutions/security/plugins/elastic_assistant/server/routes/helpers.ts | Added messageRefusal parameter to appendAssistantMessageToConversation and getMessageFromRawResponse functions |
| x-pack/solutions/security/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts | Pass refusal from LLM response to message persistence logic |
| x-pack/solutions/security/plugins/elastic_assistant/server/routes/chat/chat_complete_route.ts | Pass refusal from LLM response to message persistence via onLlmResponse callback |
| x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/field_maps_configuration.ts | Added messages.refusal field mapping as non-required text field for searchability |
| x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/types.ts | Added optional refusal field to EsConversationSchema and CreateMessageSchema interfaces |
| x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/update_conversation.ts | Added optional refusal field to UpdateConversationSchema and conditionally include in transformed messages |
| x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/transforms.ts | Conditionally include refusal when transforming ES documents to conversation responses |
| x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/create_conversation.ts | Conditionally include refusal field when creating new conversation messages |
| x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/append_conversation_messages.ts | Conditionally include refusal field when appending messages to conversations |
| x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/append_conversation_messages.test.ts | Added test to verify refusal reason is preserved when present on messages |
| x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.schema.yaml | Added optional refusal field to Message schema with description |
| x-pack/platform/packages/shared/kbn-elastic-assistant-common/impl/schemas/conversations/common_attributes.gen.ts | Generated Zod schema with optional refusal string field for Message type |
| const additionalKwargs = response.refusal ? { refusal: response.refusal } : undefined; | ||
| return new AIMessage({ | ||
| content: response.content, | ||
| ...(additionalKwargs ? { additional_kwargs: additionalKwargs } : {}), |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
| 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 }); |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
| return { | ||
| role: 'assistant', | ||
| content: rawContent, | ||
| ...(refusal ? { refusal } : {}), |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
| return { | ||
| type: ChatCompletionEventType.ChatCompletionMessage, | ||
| content, | ||
| ...(refusal ? { refusal } : {}), |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
| const refusal = | ||
| typeof lastMessage?.additional_kwargs?.refusal === 'string' | ||
| ? (lastMessage.additional_kwargs.refusal as string) | ||
| : undefined; | ||
| if (onLlmResponse) { | ||
| await onLlmResponse({ | ||
| content: output, | ||
| traceData, | ||
| ...(refusal ? { refusal } : {}), |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
| if (chunk.refusal) { | ||
| prev.refusal = chunk.refusal; | ||
| } |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
| return { | ||
| type: ChatCompletionEventType.ChatCompletionChunk, | ||
| content: delta.content ?? '', | ||
| refusal: delta.refusal ?? undefined, |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
| if (chunk.choices[0].message.refusal) { | ||
| prev.choices[0].message.refusal = chunk.choices[0].message.refusal; | ||
| } |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
| const additionalKwargs = chunk.refusal ? { refusal: chunk.refusal } : {}; | ||
|
|
||
| return new AIMessageChunk({ | ||
| content: chunk.content, | ||
| tool_call_chunks: toolCallChunks, | ||
| additional_kwargs: {}, | ||
| additional_kwargs: additionalKwargs, |
This comment was marked as off-topic.
This comment was marked as off-topic.
Sorry, something went wrong.
|
Project deployed, see credentials at: https://buildkite.com/elastic/kibana-deploy-project-from-pr/builds/816 |
|
Cloud deployment initiated, see credentials at: https://buildkite.com/elastic/kibana-deploy-cloud-from-pr/builds/614 |
💔 Build Failed
Failed CI StepsTest FailuresMetrics [docs]Async chunks
History
|
| handleFinalContent({ finalResponse: data.output.content, isError: false }); | ||
| const refusal = | ||
| typeof data.output?.additional_kwargs?.refusal === 'string' | ||
| ? (data.output.additional_kwargs.refusal as string) |
|
Starting backport for target branches: 8.19, 9.1, 9.2, 9.3 |
(cherry picked from commit fe9c9fd)
💔 Some backports could not be created
Note: Successful backport PRs will be merged automatically after passing CI. Manual backportTo create the backport manually run: Questions ?Please refer to the Backport tool documentation |
(cherry picked from commit fe9c9fd) # Conflicts: # x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/executors/types.ts # x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/helpers.ts # x-pack/solutions/security/plugins/elastic_assistant/server/routes/chat/chat_complete_route.ts # x-pack/solutions/security/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts
(cherry picked from commit fe9c9fd) # Conflicts: # x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/events.ts # x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/messages.ts # x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/append_conversation_messages.test.ts # x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/executors/types.ts # x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/helpers.ts # x-pack/solutions/security/plugins/elastic_assistant/server/routes/chat/chat_complete_route.ts # x-pack/solutions/security/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts
💚 All backports created successfully
Note: Successful backport PRs will be merged automatically after passing CI. Questions ?Please refer to the Backport tool documentation |
(cherry picked from commit fe9c9fd) # Conflicts: # x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/events.ts # x-pack/platform/packages/shared/ai-infra/inference-common/src/chat_complete/messages.ts # x-pack/solutions/security/plugins/elastic_assistant/server/ai_assistant_data_clients/conversations/append_conversation_messages.test.ts # x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/executors/types.ts # x-pack/solutions/security/plugins/elastic_assistant/server/lib/langchain/graphs/default_assistant_graph/helpers.ts # x-pack/solutions/security/plugins/elastic_assistant/server/routes/chat/chat_complete_route.ts # x-pack/solutions/security/plugins/elastic_assistant/server/routes/post_actions_connector_execute.ts
# Backport This will backport the following commits from `main` to `8.19`: - [Add refusal field to assistant conversations (#243423)](#243423) <!--- Backport version: 10.2.0 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Jason Botzas-Coluni","email":"44372106+jaybcee@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-12-19T15:39:35Z","message":"Add refusal field to assistant conversations (#243423)","sha":"fe9c9fd75355732baf10cda2b70fe4bb9f36652b","branchLabelMapping":{"^v9.4.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","backport:all-open","ci:cloud-deploy","ci:cloud-redeploy","ci:project-deploy-security","v9.4.0"],"title":"Add refusal field to assistant conversations","number":243423,"url":"https://github.com/elastic/kibana/pull/243423","mergeCommit":{"message":"Add refusal field to assistant conversations (#243423)","sha":"fe9c9fd75355732baf10cda2b70fe4bb9f36652b"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.4.0","branchLabelMappingKey":"^v9.4.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/243423","number":243423,"mergeCommit":{"message":"Add refusal field to assistant conversations (#243423)","sha":"fe9c9fd75355732baf10cda2b70fe4bb9f36652b"}},{"url":"https://github.com/elastic/kibana/pull/247135","number":247135,"branch":"9.3","state":"OPEN"}]}] BACKPORT--> --------- Co-authored-by: Jason Botzas-Coluni <44372106+jaybcee@users.noreply.github.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
# Backport This will backport the following commits from `main` to `9.1`: - [Add refusal field to assistant conversations (#243423)](#243423) <!--- Backport version: 10.2.0 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Jason Botzas-Coluni","email":"44372106+jaybcee@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-12-19T15:39:35Z","message":"Add refusal field to assistant conversations (#243423)","sha":"fe9c9fd75355732baf10cda2b70fe4bb9f36652b","branchLabelMapping":{"^v9.4.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","backport:all-open","ci:cloud-deploy","ci:cloud-redeploy","ci:project-deploy-security","v9.4.0"],"title":"Add refusal field to assistant conversations","number":243423,"url":"https://github.com/elastic/kibana/pull/243423","mergeCommit":{"message":"Add refusal field to assistant conversations (#243423)","sha":"fe9c9fd75355732baf10cda2b70fe4bb9f36652b"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.4.0","branchLabelMappingKey":"^v9.4.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/243423","number":243423,"mergeCommit":{"message":"Add refusal field to assistant conversations (#243423)","sha":"fe9c9fd75355732baf10cda2b70fe4bb9f36652b"}},{"url":"https://github.com/elastic/kibana/pull/247135","number":247135,"branch":"9.3","state":"OPEN"}]}] BACKPORT--> --------- Co-authored-by: Jason Botzas-Coluni <44372106+jaybcee@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
# Backport This will backport the following commits from `main` to `9.2`: - [Add refusal field to assistant conversations (#243423)](#243423) <!--- Backport version: 10.2.0 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Jason Botzas-Coluni","email":"44372106+jaybcee@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-12-19T15:39:35Z","message":"Add refusal field to assistant conversations (#243423)","sha":"fe9c9fd75355732baf10cda2b70fe4bb9f36652b","branchLabelMapping":{"^v9.4.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","backport:all-open","ci:cloud-deploy","ci:cloud-redeploy","ci:project-deploy-security","v9.4.0"],"title":"Add refusal field to assistant conversations","number":243423,"url":"https://github.com/elastic/kibana/pull/243423","mergeCommit":{"message":"Add refusal field to assistant conversations (#243423)","sha":"fe9c9fd75355732baf10cda2b70fe4bb9f36652b"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.4.0","branchLabelMappingKey":"^v9.4.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/243423","number":243423,"mergeCommit":{"message":"Add refusal field to assistant conversations (#243423)","sha":"fe9c9fd75355732baf10cda2b70fe4bb9f36652b"}},{"url":"https://github.com/elastic/kibana/pull/247135","number":247135,"branch":"9.3","state":"OPEN"}]}] BACKPORT--> --------- Co-authored-by: Jason Botzas-Coluni <44372106+jaybcee@users.noreply.github.com> Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
# Backport This will backport the following commits from `main` to `9.3`: - [Add refusal field to assistant conversations (#243423)](#243423) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Jason Botzas-Coluni","email":"44372106+jaybcee@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-12-19T15:39:35Z","message":"Add refusal field to assistant conversations (#243423)","sha":"fe9c9fd75355732baf10cda2b70fe4bb9f36652b","branchLabelMapping":{"^v9.4.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:fix","backport:all-open","ci:cloud-deploy","ci:cloud-redeploy","ci:project-deploy-security","v9.4.0"],"title":"Add refusal field to assistant conversations","number":243423,"url":"https://github.com/elastic/kibana/pull/243423","mergeCommit":{"message":"Add refusal field to assistant conversations (#243423)","sha":"fe9c9fd75355732baf10cda2b70fe4bb9f36652b"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.4.0","branchLabelMappingKey":"^v9.4.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/243423","number":243423,"mergeCommit":{"message":"Add refusal field to assistant conversations (#243423)","sha":"fe9c9fd75355732baf10cda2b70fe4bb9f36652b"}}]}] BACKPORT--> Co-authored-by: Jason Botzas-Coluni <44372106+jaybcee@users.noreply.github.com> Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
Summary
messages.refusalin the assistant conversation schema/field map so it is searchable in the data streamRelated to https://github.com/elastic/search-team/issues/10868