Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
89 commits
Select commit Hold shift + click to select a range
8f8a494
anonyimization of direct user chat input
neptunian Mar 28, 2025
ab3ed11
fix to work with knowledge base
neptunian Mar 31, 2025
7872a85
change id to deterministic hash
neptunian Apr 1, 2025
37d1b4a
support regex detecting entities
neptunian Apr 1, 2025
f341873
update text
neptunian Apr 1, 2025
b729c86
remove highlighting in assistant responses to fix formatting
neptunian Apr 15, 2025
c11b1ed
cleanup
neptunian Apr 15, 2025
362ee97
don't save empty array
neptunian Apr 15, 2025
da30279
fix bug
neptunian Apr 15, 2025
665030a
change assistant messages to store entities instead of hash in messag…
neptunian Apr 17, 2025
cde550e
update comments
neptunian Apr 17, 2025
4e49fc0
improve comments
neptunian Apr 17, 2025
218c208
update mappings
neptunian Apr 18, 2025
fc435fb
move map to utility, remove unused map in ui
neptunian Apr 18, 2025
35ad504
move methods to utils and rename
neptunian Apr 18, 2025
0b3e59e
add api integration tests related to anonymization
neptunian Apr 22, 2025
38ec8be
change to anonymize
neptunian Apr 22, 2025
f563512
rename
neptunian Apr 22, 2025
20d2e5f
add enableAnonymization setting
neptunian Apr 22, 2025
d90964f
fix types
neptunian Apr 22, 2025
281ae9e
fix type
neptunian Apr 23, 2025
37184f7
fix kibana config in tests
neptunian Apr 23, 2025
2ce2cb0
change to snake case
neptunian Apr 23, 2025
fe7d7a2
fix test
neptunian Apr 23, 2025
d6e8bdf
merge fix
neptunian Apr 25, 2025
038788b
Merge branch 'main' into poc-ner-anonymization
neptunian Apr 29, 2025
2c3f8dd
don't index fields
neptunian Apr 30, 2025
1ec638b
remove unused file
neptunian Apr 30, 2025
e67f6e3
remove double check
neptunian Apr 30, 2025
c8e3e51
getHashedEntity function and tests
neptunian Apr 30, 2025
010d07a
remove default
neptunian Apr 30, 2025
e8db404
cleanup conditions
neptunian May 1, 2025
c5f70c5
remove anonymized property
neptunian May 1, 2025
abb7c9e
add upper limit to ml calls
neptunian May 1, 2025
41e70c1
fix type
neptunian May 1, 2025
aa4d225
remove unused kb in test
neptunian May 2, 2025
b47aeae
Merge branch 'main' into poc-ner-anonymization
neptunian May 2, 2025
954cf8b
merge fix
neptunian May 20, 2025
4e846ef
refactor into anonymization service
neptunian May 22, 2025
6ef30a9
fix type
neptunian May 23, 2025
312e2a8
Merge branch 'main' into poc-ner-anonymization
neptunian May 23, 2025
2496f54
Merge branch 'main' into poc-ner-anonymization
neptunian May 23, 2025
18de2e1
Merge branch 'main' into poc-ner-anonymization
neptunian May 23, 2025
3acf122
Merge branch 'poc-ner-anonymization' of https://github.com/neptunian/…
neptunian May 23, 2025
548dd89
Merge branch 'main' into poc-ner-anonymization
neptunian May 23, 2025
0bcc0a2
merge fix
neptunian May 27, 2025
4ac84ae
use eui code style
neptunian May 27, 2025
cdedd5b
update type
neptunian May 27, 2025
8a39047
batch chunks up to 10 for each infer request
neptunian May 27, 2025
332a553
fix failing tests
neptunian May 28, 2025
4ff5f10
Merge branch 'main' into poc-ner-anonymization
neptunian May 28, 2025
a13f690
fix type
neptunian May 29, 2025
3693cb5
Merge branch 'poc-ner-anonymization' of https://github.com/neptunian/…
neptunian May 29, 2025
c4a4e9a
update error
neptunian May 29, 2025
214af04
add back empty array
neptunian May 29, 2025
285dab2
remove enableAnonymization config, add enableAnonymizationRules confi…
neptunian May 30, 2025
7bf582d
Merge branch 'main' into poc-ner-anonymization
neptunian May 30, 2025
de51edf
remove unneeded config
neptunian May 30, 2025
08a4f17
Merge branch 'poc-ner-anonymization' of https://github.com/neptunian/…
neptunian May 30, 2025
2550f93
move rules into service
neptunian May 30, 2025
63b99d4
update tests
neptunian May 30, 2025
9d29193
Merge branch 'main' into poc-ner-anonymization
neptunian May 31, 2025
8e56ad9
fix lint
neptunian May 31, 2025
90ee2f1
Merge branch 'main' into poc-ner-anonymization
neptunian May 31, 2025
6f749ad
update import
neptunian May 31, 2025
ecf0bb3
unregister new setting
neptunian May 31, 2025
9f970c7
catch settings error
neptunian May 31, 2025
536c7fb
mutate messages in place
neptunian Jun 2, 2025
d07c4cd
add advanced setting schema
neptunian Jun 3, 2025
4f21a03
Merge branch 'main' into poc-ner-anonymization
neptunian Jun 3, 2025
11ee3e5
update tests
neptunian Jun 3, 2025
67efb52
Merge branch 'poc-ner-anonymization' of https://github.com/neptunian/…
neptunian Jun 3, 2025
15f5bc1
update type
neptunian Jun 3, 2025
152ddcb
inline regex
neptunian Jun 3, 2025
d48e1f3
update error handling
neptunian Jun 3, 2025
e4749e4
object hash util
neptunian Jun 3, 2025
7c6537d
update import
neptunian Jun 3, 2025
da9ce7b
refactor to not store detected entities for each message
neptunian Jun 5, 2025
8af4b8e
Merge branch 'main' into poc-ner-anonymization
neptunian Jun 6, 2025
4c50373
remove old logic
neptunian Jun 6, 2025
530c71e
Merge branch 'poc-ner-anonymization' of https://github.com/neptunian/…
neptunian Jun 6, 2025
e5a88f8
remove file
neptunian Jun 6, 2025
3b8b4e4
also detect assistant content
neptunian Jun 6, 2025
5f91f53
fix linting
neptunian Jun 6, 2025
12623a3
move unredactMessages to chat()
neptunian Jun 6, 2025
0598642
undo move unredact to chat
neptunian Jun 6, 2025
977d68b
undo concatenateChatCompletionChunks, update unredactChatCompletionEvent
neptunian Jun 6, 2025
32d3932
Merge branch 'main' of github.com:elastic/kibana into poc-ner-anonymi…
dgieselaar Jun 10, 2025
d07e8c8
Merge branch 'main' of github.com:elastic/kibana into assistant-anony…
dgieselaar Jun 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,16 @@ export type ChatCompletionMessageEvent<TToolOptions extends ToolOptions = ToolOp
toolCalls: ToolCallsOf<TToolOptions>['toolCalls'];
}
>;

// with unredactions
export interface ChatCompletionUnredactedMessageEvent<
TToolOptions extends ToolOptions = ToolOptions
> extends ChatCompletionMessageEvent<TToolOptions> {
unredactions: Array<{
entity: string;
class_name: string;
hash: string;
}>;
}
/**
* Represent a partial tool call present in a chunk event.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,19 @@ import { ChatItemActions } from './chat_item_actions';
import { ChatItemAvatar } from './chat_item_avatar';
import { ChatItemContentInlinePromptEditor } from './chat_item_content_inline_prompt_editor';
import { ChatTimelineItem } from './chat_timeline';

// Helper function to extract plain text from a React node.
function extractTextFromReactNode(node: React.ReactNode): string {
if (typeof node === 'string' || typeof node === 'number') {
return node.toString();
}
if (Array.isArray(node)) {
return node.map(extractTextFromReactNode).join('');
}
if (React.isValidElement(node)) {
return extractTextFromReactNode(node.props.children);
}
return '';
}
export interface ChatItemProps extends Omit<ChatTimelineItem, 'message'> {
onActionClick: ChatActionClickHandler;
onEditSubmit: (message: Message) => void;
Expand All @@ -36,6 +48,7 @@ export interface ChatItemProps extends Omit<ChatTimelineItem, 'message'> {
onSendTelemetry: (eventWithPayload: TelemetryEventTypeWithPayload) => void;
onStopGeneratingClick: () => void;
isConversationOwnedByCurrentUser: boolean;
displayContent?: React.ReactNode;
}

const moreCompactHeaderClassName = css`
Expand Down Expand Up @@ -95,6 +108,7 @@ export function ChatItem({
onRegenerateClick,
onSendTelemetry,
onStopGeneratingClick,
anonymizedHighlightedContent,
}: ChatItemProps) {
const accordionId = useGeneratedHtmlId({ prefix: 'chat' });

Expand Down Expand Up @@ -135,17 +149,21 @@ export function ChatItem({
return onEditSubmit(newMessage);
};

// extract text if content is not a string.
const handleCopyToClipboard = () => {
navigator.clipboard.writeText(content || '');
const copyText = typeof content === 'string' ? content : extractTextFromReactNode(content);
navigator.clipboard.writeText(copyText || '');
};

let contentElement: React.ReactNode =
let contentElement: React.ReactNode;
contentElement =
content || loading || error ? (
<ChatItemContentInlinePromptEditor
editing={editing}
loading={loading}
functionCall={functionCall}
content={content}
anonymizedHighlightedContent={anonymizedHighlightedContent}
role={role}
onSubmit={handleInlineEditSubmit}
onActionClick={onActionClick}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ interface Props {
onActionClick: ChatActionClickHandler;
onSendTelemetry: (eventWithPayload: TelemetryEventTypeWithPayload) => void;
onSubmit: (message: Message) => void;
anonymizedHighlightedContent?: React.ReactNode;
}

const textContainerClassName = css`
Expand All @@ -48,6 +49,7 @@ export function ChatItemContentInlinePromptEditor({
onActionClick,
onSendTelemetry,
onSubmit,
anonymizedHighlightedContent,
}: Props) {
return !editing ? (
<EuiPanel
Expand All @@ -56,7 +58,12 @@ export function ChatItemContentInlinePromptEditor({
hasShadow={false}
className={textContainerClassName}
>
<MessageText content={content || ''} loading={loading} onActionClick={onActionClick} />
<MessageText
content={content || ''}
anonymizedHighlightedContent={anonymizedHighlightedContent}
loading={loading}
onActionClick={onActionClick}
/>
</EuiPanel>
) : (
<EuiPanel
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,24 @@

import React, { type ReactNode, useMemo } from 'react';
import { css } from '@emotion/css';
import { EuiCommentList } from '@elastic/eui';
import { EuiCode, EuiCommentList } from '@elastic/eui';
import type { AuthenticatedUser } from '@kbn/security-plugin/common';
import { omit } from 'lodash';
import type { Message } from '@kbn/observability-ai-assistant-plugin/common';
import {
ChatActionClickPayload,
ChatState,
type Feedback,
type Message,
type ObservabilityAIAssistantChatService,
type TelemetryEventTypeWithPayload,
aiAssistantAnonymizationRules,
} from '@kbn/observability-ai-assistant-plugin/public';
import { AnonymizationRule } from '@kbn/observability-ai-assistant-plugin/common';
import type { UseKnowledgeBaseResult } from '../hooks/use_knowledge_base';
import { ChatItem } from './chat_item';
import { ChatConsolidatedItems } from './chat_consolidated_items';
import { getTimelineItemsfromConversation } from '../utils/get_timeline_items_from_conversation';
import { useKibana } from '../hooks/use_kibana';
import { ElasticLlmConversationCallout } from './elastic_llm_conversation_callout';

export interface ChatTimelineItem
Expand All @@ -44,6 +47,7 @@ export interface ChatTimelineItem
error?: any;
message: Message;
functionCall?: Message['message']['function_call'];
anonymizedHighlightedContent?: React.ReactNode;
}

export interface ChatTimelineProps {
Expand Down Expand Up @@ -71,6 +75,34 @@ export interface ChatTimelineProps {
}) => void;
}

// helper using detected entity positions to transform user messages into react node to add text highlighting
function highlightContent(
content: string,
detectedEntities: Array<{ start_pos: number; end_pos: number; entity: string }>
): React.ReactNode {
// Sort the entities by start position
const sortedEntities = [...detectedEntities].sort((a, b) => a.start_pos - b.start_pos);
const parts: Array<string | React.ReactNode> = [];
let lastIndex = 0;
sortedEntities.forEach((entity, index) => {
// Add the text before the entity
if (entity.start_pos > lastIndex) {
parts.push(content.substring(lastIndex, entity.start_pos));
}
// Wrap the sensitive text in a span with highlight styles
parts.push(
<EuiCode key={`user-highlight-${index}`}>
{content.substring(entity.start_pos, entity.end_pos)}
</EuiCode>
);
lastIndex = entity.end_pos;
});
// Add any remaining text after the last entity
if (lastIndex < content.length) {
parts.push(content.substring(lastIndex));
}
return parts;
}
const euiCommentListClassName = css`
padding-bottom: 32px;
`;
Expand Down Expand Up @@ -98,6 +130,21 @@ export function ChatTimeline({
onActionClick,
chatState,
}: ChatTimelineProps) {
const {
services: { uiSettings },
} = useKibana();

const { anonymizationEnabled } = useMemo(() => {
try {
const rules = uiSettings?.get<AnonymizationRule[]>(aiAssistantAnonymizationRules);
return {
anonymizationEnabled: Array.isArray(rules) && rules.some((rule) => rule.enabled),
};
} catch (e) {
return { anonymizationEnabled: false };
}
}, [uiSettings]);

const items = useMemo(() => {
const timelineItems = getTimelineItemsfromConversation({
conversationId,
Expand All @@ -115,8 +162,13 @@ export function ChatTimeline({
let currentGroup: ChatTimelineItem[] | null = null;

for (const item of timelineItems) {
const { role, content, unredactions } = item.message.message;
if (item.display.hide || !item) continue;

if (anonymizationEnabled && role === 'user' && content && unredactions) {
item.anonymizedHighlightedContent = highlightContent(content, unredactions);
}

if (item.display.collapsed) {
if (currentGroup) {
currentGroup.push(item);
Expand All @@ -141,6 +193,7 @@ export function ChatTimeline({
isConversationOwnedByCurrentUser,
isArchived,
onActionClick,
anonymizationEnabled,
]);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type BuildMessageProps = DeepPartial<Message> & {
name: string;
trigger: MessageRole.Assistant | MessageRole.User | MessageRole.Elastic;
};
unredactions?: Message['message']['unredactions'];
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { i18n } from '@kbn/i18n';
import {
aiAssistantSimulatedFunctionCalling,
aiAssistantSearchConnectorIndexPattern,
aiAssistantAnonymizationRules,
} from '@kbn/observability-ai-assistant-plugin/common';

export const uiSettings: Record<string, UiSettingsParams> = {
Expand Down Expand Up @@ -57,4 +58,41 @@ export const uiSettings: Record<string, UiSettingsParams> = {
requiresPageReload: true,
solution: 'oblt',
},
[aiAssistantAnonymizationRules]: {
category: ['observability'],
name: i18n.translate(
'xpack.observabilityAiAssistantManagement.settingsTab.anonymizationRulesLabel',
{ defaultMessage: 'Anonymization Rules' }
),
value: [], // Default is an empty array, which disables all anonymization rules.
description: i18n.translate(
'xpack.observabilityAiAssistantManagement.settingsPage.anonymizationRulesDescription',
{
defaultMessage:
'JSON array of anonymization rules. Each rule is an object with properties:\n' +
'- id: unique string identifier\n' +
'- entityClass: class of entity (e.g., PER, ORG, EMAIL, URL)\n' +
'- type: "ner" or "regex"\n' +
'- pattern: (for regex rules) the regex string to match\n' +
'- enabled: boolean flag to turn the rule on or off\n' +
'- builtIn: boolean indicating this is a built‑in rule\n' +
'- description: optional human‑readable description\n' +
'Default is an empty array, which disables all anonymization rules.',
}
),
schema: schema.arrayOf(
schema.object({
id: schema.string(),
entityClass: schema.string(),
type: schema.oneOf([schema.literal('ner'), schema.literal('regex')]),
pattern: schema.string(),
enabled: schema.boolean(),
builtIn: schema.boolean(),
description: schema.maybe(schema.string()),
})
),
type: 'json',
requiresPageReload: true,
solution: 'oblt',
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ const useKnowledgeBaseMock = useKnowledgeBase as jest.Mock;

describe('Settings Page', () => {
const appContextValue = {
config: { spacesEnabled: true, visibilityEnabled: true, logSourcesEnabled: true },
config: {
spacesEnabled: true,
visibilityEnabled: true,
logSourcesEnabled: true,
},
setBreadcrumbs: () => {},
};
useKnowledgeBaseMock.mockReturnValue({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,9 @@ export type ObservabilityAIAssistantManagementConfig = TypeOf<typeof configSchem

export const config: PluginConfigDescriptor<ObservabilityAIAssistantManagementConfig> = {
schema: configSchema,
exposeToBrowser: { logSourcesEnabled: true, spacesEnabled: true, visibilityEnabled: true },
exposeToBrowser: {
logSourcesEnabled: true,
spacesEnabled: true,
visibilityEnabled: true,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { CoreSetup, CoreStart, Plugin } from '@kbn/core/server';
import { aiAssistantAnonymizationRules } from '@kbn/observability-ai-assistant-plugin/common';
import { uiSettings } from '../common/ui_settings';

export type ObservabilityPluginSetup = ReturnType<AiAssistantManagementPlugin['setup']>;
Expand All @@ -20,7 +21,8 @@ export class AiAssistantManagementPlugin implements Plugin<ObservabilityPluginSe
constructor() {}

public setup(core: CoreSetup<PluginStart>, plugins: PluginSetup) {
core.uiSettings.register(uiSettings);
const { [aiAssistantAnonymizationRules]: anonymizationRules, ...restSettings } = uiSettings;
core.uiSettings.register(restSettings);
return {};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@
* 2.0.
*/

export type { Message, Conversation, KnowledgeBaseEntry, ConversationCreateRequest } from './types';
export type {
Message,
Conversation,
KnowledgeBaseEntry,
ConversationCreateRequest,
AnonymizationRule,
} from './types';
export {
KnowledgeBaseEntryRole,
MessageRole,
Expand Down Expand Up @@ -47,6 +53,7 @@ export {
aiAssistantLogsIndexPattern,
aiAssistantSimulatedFunctionCalling,
aiAssistantSearchConnectorIndexPattern,
aiAssistantAnonymizationRules,
} from './ui_settings/settings_keys';

export { concatenateChatCompletionChunks } from './utils/concatenate_chat_completion_chunks';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,31 @@ export interface PendingMessage {
aborted?: boolean;
error?: any;
}
export interface DetectedEntity {
entity: string;
class_name: string;
start_pos: number;
end_pos: number;
hash: string;
type: 'ner' | 'regex';
}

export type DetectedEntityType = DetectedEntity['type'];
export interface Unredaction {
entity: string;
class_name: string;
start_pos: number;
end_pos: number;
type: 'ner' | 'regex';
}

export type UnredactionType = Unredaction['type'];

export interface Message {
'@timestamp': string;
message: {
content?: string;
unredactions?: Unredaction[];
name?: string;
role: MessageRole;
function_call?: {
Expand Down Expand Up @@ -160,3 +180,19 @@ export enum ConversationAccess {
SHARED = 'shared',
PRIVATE = 'private',
}

export interface InferenceChunk {
chunkText: string;
charStartOffset: number;
}

export interface AnonymizationRule {
id: string;
entityClass: string;
type: 'regex' | 'ner';
pattern?: string;
enabled: boolean;
builtIn: boolean;
description?: string;
normalize?: boolean;
}
Loading