Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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 @@ -6,6 +6,7 @@
*/

import React, { lazy } from 'react';
import { isEmpty } from 'lodash';
import type {
ActionTypeModel as ConnectorTypeModel,
GenericValidationResult,
Expand All @@ -19,6 +20,7 @@ import {
CONNECTOR_REQUIRED,
CONNECTOR_TITLE,
MESSAGE_REQUIRED,
STATUS_REQUIRED,
} from './translations';

export function getConnectorType(
Expand All @@ -35,23 +37,26 @@ export function getConnectorType(
validateParams: async (
actionParams: ObsAIAssistantActionParams
): Promise<GenericValidationResult<ObsAIAssistantActionParams>> => {
const validationResult = {
errors: {
connector: [] as string[],
message: [] as string[],
prompts: [] as string[],
},
};
const validatePrompt = (prompt: { message: string; statuses: string[] }): string[] => {
const errors: string[] = [];

if (!actionParams.connector) {
validationResult.errors.connector.push(CONNECTOR_REQUIRED);
}
if (!prompt.message) {
errors.push(MESSAGE_REQUIRED);
}
if (isEmpty(prompt.statuses)) {
errors.push(STATUS_REQUIRED);
}

if (!actionParams.message) {
validationResult.errors.message.push(MESSAGE_REQUIRED);
}
return errors;
};

return validationResult;
return {
errors: {
connector: actionParams.connector ? [] : [CONNECTOR_REQUIRED],
message: actionParams.message && !actionParams.prompts ? [MESSAGE_REQUIRED] : [],
prompts: actionParams.prompts?.map(validatePrompt) || [],
},
};
},
actionParamsFields: lazy(() =>
import('./ai_assistant_params').then(({ default: ActionParamsFields }) => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,25 @@
import React, { useEffect } from 'react';
import type { ActionParamsProps } from '@kbn/triggers-actions-ui-plugin/public';
import { i18n } from '@kbn/i18n';
import { EuiFormRow, EuiFlexItem, EuiSelect, EuiSpacer, EuiTextArea } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n-react';
import {
EuiFormRow,
EuiFlexItem,
EuiSelect,
EuiSpacer,
EuiTextArea,
EuiComboBox,
EuiButton,
EuiFlexGroup,
} from '@elastic/eui';
import {
ObservabilityAIAssistantService,
useGenAIConnectorsWithoutContext,
} from '@kbn/observability-ai-assistant-plugin/public';
import { RuleFormParamsErrors } from '@kbn/alerts-ui-shared';
import { ObsAIAssistantActionParams } from './types';
import { ALERT_STATUSES } from '../../common/constants';
import { MESSAGE_REQUIRED, STATUS_REQUIRED } from './translations';

const ObsAIAssistantParamsFields: React.FunctionComponent<
ActionParamsProps<ObsAIAssistantActionParams> & { service: ObservabilityAIAssistantService }
Expand All @@ -22,9 +35,65 @@ const ObsAIAssistantParamsFields: React.FunctionComponent<
useGenAIConnectorsWithoutContext(service);

useEffect(() => {
editAction('connector', selectedConnector, index);
if (selectedConnector !== actionParams.connector) {
editAction('connector', selectedConnector, index);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedConnector, index]);
}, [actionParams, selectedConnector, index]);

useEffect(() => {
// Ensure backwards compatibility by using the message field as a prompt if prompts are missing
if (!actionParams.prompts) {
editAction(
'prompts',
[
{
statuses: ALERT_STATUSES,
message: actionParams.message || '',
},
],
index
);
}
// forward-compatible fallback.
if (actionParams.prompts && actionParams.prompts[0].message !== actionParams.message) {
editAction('message', actionParams.prompts[0].message, index);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pmuellr I’ve added forward-compatible fallback to handle the scenario when a new version of the connector runs in a new Kibana and the actions are later picked up by an old Kibana, it will degrade gracefully. we fall back to using just the first prompt and applying it across all statuses, ensuring older Kibana versions can still process the actions without breaking.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at the code, there's a new param prompts. Which is not in the "previous" version of the schema. So if a new Kibana creates an action with the prompts param, and an older Kibana runs the action, it will fail validation since it doesn't expect the prompts param. Or is this handled in some other way?

For this reason, our "intermediate release" doc suggests doing one release with just the schema change, but do not set the property values. That way, in the final release when the property IS set, the previous version of Kibana will at least pass the validation for the schema.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it, @pmuellr! I’ve just opened a PR to introduce the intermediate release, as suggested in the documentation. This includes the schema change along with the necessary updates to the action executor to handle both the old and new schemas.

Here’s the PR: #209221

Let me know if you have any feedback!

}
}, [actionParams, editAction, index]);

const handleOnChange = (
key: 'statuses' | 'message',
value: string | string[],
promptIndex: number
) => {
const prompts = actionParams.prompts ? [...actionParams.prompts] : [];
prompts[promptIndex] = { ...prompts[promptIndex], [key]: value };
editAction('prompts', prompts, index);
};

const handleAddPrompt = () => {
if (actionParams.prompts) {
const prompts = [
...actionParams.prompts,
{
statuses: ALERT_STATUSES,
message: '',
},
];
editAction('prompts', prompts, index);
}
};
const handleRemovePrompt = () => {
if (actionParams.prompts) {
const prompts = actionParams.prompts.slice(0, -1);
editAction('prompts', prompts, index);
}
};

const isValidField = (statusError: string, promptIndex: number) => {
const errorsList = ((errors.prompts as RuleFormParamsErrors)?.[promptIndex] as string[]) || [];
return errorsList.includes(statusError);
};

return (
<>
Expand All @@ -47,34 +116,99 @@ const ObsAIAssistantParamsFields: React.FunctionComponent<
selectConnector(event.target.value);
editAction('connector', event.target.value, index);
}}
value={selectedConnector}
value={actionParams.connector}
/>
</EuiFormRow>

{actionParams?.prompts?.map((prompt, promptIndex) => (
<div key={promptIndex}>
<EuiSpacer size="m" />
<EuiFormRow
fullWidth
label={i18n.translate(
'xpack.observabilityAiAssistant.alertConnector.messageTextAreaFieldLabel',
{
defaultMessage: 'On status changes',
}
)}
>
<EuiComboBox
fullWidth
id={`addNewActionConnectorActionGroup-${index}`}
data-test-subj={`addNewActionConnectorActionGroup-${index}`}
options={ALERT_STATUSES.map((id) => ({
label: id,
}))}
selectedOptions={prompt.statuses.map((id) => ({ label: id }))}
onChange={(statuses) => {
handleOnChange(
'statuses',
statuses.map((status) => status.label),
promptIndex
);
}}
isClearable={true}
isInvalid={isValidField(STATUS_REQUIRED, promptIndex)}
/>
</EuiFormRow>
<EuiSpacer size="m" />
<EuiFormRow
fullWidth
label={i18n.translate(
'xpack.observabilityAiAssistant.alertConnector.messageTextAreaFieldLabel',
{
defaultMessage: 'Message',
}
)}
>
<EuiFlexItem grow={false}>
<EuiTextArea
fullWidth
data-test-subj="observabilityAiAssistantAlertConnectorMessageTextArea"
value={prompt.message}
onChange={(event) => {
handleOnChange('message', event.target.value, promptIndex);
}}
isInvalid={isValidField(MESSAGE_REQUIRED, promptIndex)}
/>
</EuiFlexItem>
</EuiFormRow>
</div>
))}
<EuiSpacer size="m" />

<EuiFormRow
fullWidth
label={i18n.translate(
'xpack.observabilityAiAssistant.alertConnector.messageTextAreaFieldLabel',
{
defaultMessage: 'Message',
}
)}
>
<EuiFlexItem grow={false}>
<EuiTextArea
<EuiFlexGroup>
<EuiFlexItem grow>
<EuiButton
disabled={actionParams?.prompts?.length === 1}
size="m"
fullWidth
data-test-subj="observabilityAiAssistantAlertConnectorMessageTextArea"
value={actionParams.message}
onChange={(event) => {
editAction('message', event.target.value, index);
}}
// @ts-expect-error upgrade typescript v5.1.6
isInvalid={errors.message?.length > 0}
/>
color="danger"
iconType="minusInCircle"
data-test-subj="removePropmptButton"
onClick={handleRemovePrompt}
>
<FormattedMessage
id="xpack.observabilityAiAssistant.alertConnector.removePromptButtonLabel"
defaultMessage="Remove Prompt"
/>
</EuiButton>
</EuiFlexItem>
</EuiFormRow>
<EuiFlexItem grow>
<EuiButton
disabled={actionParams?.prompts?.length === ALERT_STATUSES.length}
size="m"
fullWidth
iconType="plusInCircle"
data-test-subj="addPrompButton"
onClick={handleAddPrompt}
>
<FormattedMessage
id="xpack.observabilityAiAssistant.alertConnector.addPromptButtonLabel"
defaultMessage="Add Prompt"
/>
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,10 @@ export const MESSAGE_REQUIRED = i18n.translate(
defaultMessage: 'Message is required.',
}
);

export const STATUS_REQUIRED = i18n.translate(
'xpack.observabilityAiAssistant.requiredStatusField',
{
defaultMessage: 'Status is required.',
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@
"@kbn/ai-assistant-icon",
"@kbn/product-doc-base-plugin",
"@kbn/rule-data-utils",
"@kbn/i18n-react",
"@kbn/utility-types",
"@kbn/alerts-ui-shared"
],
"exclude": ["target/**/*"]
}