mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Obs AI Assistant] Add KB user instructions (#187607)
Related to: https://github.com/elastic/obs-ai-assistant-team/issues/157 This adds a top-level property `type: 'user_instruction' | 'contextual'` to the knowledge base. The description given to the LLM: > The type can be: "user_instruction" or "contextual". A "user_instruction" entry will be included in the system message if it fits within the token budget and it will be marked as a user instruction. A "contextual" entry will be included as part of the context function response (and not the system message) if it semantically matches the user's prompt. # Screenshots **"Edit system prompt" button**  **"Edit system prompt" flyout** cc781d46
-3966-4530-8036-dd44506ec771
This commit is contained in:
parent
3ff9f920cc
commit
e9f23aa98e
54 changed files with 1244 additions and 525 deletions
|
@ -51,3 +51,5 @@ export { DEFAULT_LANGUAGE_OPTION, LANGUAGE_OPTIONS } from './ui_settings/languag
|
|||
export { isSupportedConnectorType } from './connectors';
|
||||
|
||||
export { ShortIdTable } from './utils/short_id_table';
|
||||
|
||||
export { KnowledgeBaseType } from './types';
|
||||
|
|
|
@ -84,6 +84,7 @@ export interface KnowledgeBaseEntry {
|
|||
doc_id: string;
|
||||
confidence: 'low' | 'medium' | 'high';
|
||||
is_correction: boolean;
|
||||
type?: 'user_instruction' | 'contextual';
|
||||
public: boolean;
|
||||
labels?: Record<string, string>;
|
||||
role: KnowledgeBaseEntryRole;
|
||||
|
@ -92,13 +93,26 @@ export interface KnowledgeBaseEntry {
|
|||
};
|
||||
}
|
||||
|
||||
export interface UserInstruction {
|
||||
export interface Instruction {
|
||||
doc_id: string;
|
||||
text: string;
|
||||
system?: boolean;
|
||||
}
|
||||
|
||||
export type UserInstructionOrPlainText = string | UserInstruction;
|
||||
export interface AdHocInstruction {
|
||||
doc_id?: string;
|
||||
text: string;
|
||||
instruction_type: 'user_instruction' | 'application_instruction';
|
||||
}
|
||||
|
||||
export type InstructionOrPlainText = string | Instruction;
|
||||
|
||||
export enum KnowledgeBaseType {
|
||||
// user instructions are included in the system prompt regardless of the user's input
|
||||
UserInstruction = 'user_instruction',
|
||||
|
||||
// contextual entries are only included in the system prompt if the user's input matches the context
|
||||
Contextual = 'contextual',
|
||||
}
|
||||
|
||||
export interface ObservabilityAIAssistantScreenContextRequest {
|
||||
screenDescription?: string;
|
||||
|
|
|
@ -78,6 +78,8 @@ export type {
|
|||
ShortIdTable,
|
||||
} from '../common';
|
||||
|
||||
export { KnowledgeBaseType } from '../common';
|
||||
|
||||
export type { TelemetryEventTypeWithPayload } from './analytics';
|
||||
export { ObservabilityAIAssistantTelemetryEventType } from './analytics/telemetry_event_type';
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@ import type {
|
|||
Message,
|
||||
ObservabilityAIAssistantScreenContext,
|
||||
PendingMessage,
|
||||
UserInstructionOrPlainText,
|
||||
AdHocInstruction,
|
||||
} from '../common/types';
|
||||
import type { TelemetryEventTypeWithPayload } from './analytics';
|
||||
import type { ObservabilityAIAssistantAPIClient } from './api';
|
||||
|
@ -68,7 +68,7 @@ export interface ObservabilityAIAssistantChatService {
|
|||
};
|
||||
signal: AbortSignal;
|
||||
responseLanguage?: string;
|
||||
instructions?: UserInstructionOrPlainText[];
|
||||
instructions?: AdHocInstruction[];
|
||||
}) => Observable<StreamingChatResponseEventWithoutError>;
|
||||
getFunctions: (options?: { contexts?: string[]; filter?: string }) => FunctionDefinition[];
|
||||
hasFunction: (name: string) => boolean;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { KnowledgeBaseType } from '../../common/types';
|
||||
import type { FunctionRegistrationParameters } from '.';
|
||||
import { KnowledgeBaseEntryRole } from '../../common';
|
||||
|
||||
|
@ -66,13 +67,14 @@ export function registerSummarizationFunction({
|
|||
signal
|
||||
) => {
|
||||
return client
|
||||
.createKnowledgeBaseEntry({
|
||||
.addKnowledgeBaseEntry({
|
||||
entry: {
|
||||
doc_id: id,
|
||||
role: KnowledgeBaseEntryRole.AssistantSummarization,
|
||||
id,
|
||||
text,
|
||||
is_correction: isCorrection,
|
||||
type: KnowledgeBaseType.Contextual,
|
||||
confidence,
|
||||
public: isPublic,
|
||||
labels: {},
|
||||
|
|
|
@ -41,17 +41,15 @@ const chatCompleteBaseRt = t.type({
|
|||
}),
|
||||
]),
|
||||
instructions: t.array(
|
||||
t.union([
|
||||
t.string,
|
||||
t.intersection([
|
||||
t.type({
|
||||
doc_id: t.string,
|
||||
text: t.string,
|
||||
}),
|
||||
t.partial({
|
||||
system: t.boolean,
|
||||
}),
|
||||
]),
|
||||
t.intersection([
|
||||
t.partial({ doc_id: t.string }),
|
||||
t.type({
|
||||
text: t.string,
|
||||
instruction_type: t.union([
|
||||
t.literal('user_instruction'),
|
||||
t.literal('application_instruction'),
|
||||
]),
|
||||
}),
|
||||
])
|
||||
),
|
||||
}),
|
||||
|
|
|
@ -41,7 +41,7 @@ const getFunctionsRoute = createObservabilityAIAssistantServerRoute({
|
|||
screenContexts: [],
|
||||
}),
|
||||
// error is caught in client
|
||||
client.fetchUserInstructions(),
|
||||
client.getKnowledgeBaseUserInstructions(),
|
||||
]);
|
||||
|
||||
const functionDefinitions = functionClient.getFunctions().map((fn) => fn.definition);
|
||||
|
@ -51,9 +51,9 @@ const getFunctionsRoute = createObservabilityAIAssistantServerRoute({
|
|||
return {
|
||||
functionDefinitions: functionClient.getFunctions().map((fn) => fn.definition),
|
||||
systemMessage: getSystemMessageFromInstructions({
|
||||
registeredInstructions: functionClient.getInstructions(),
|
||||
applicationInstructions: functionClient.getInstructions(),
|
||||
userInstructions,
|
||||
requestInstructions: [],
|
||||
adHocInstructions: [],
|
||||
availableFunctionNames,
|
||||
}),
|
||||
};
|
||||
|
@ -111,6 +111,7 @@ const functionSummariseRoute = createObservabilityAIAssistantServerRoute({
|
|||
text: nonEmptyStringRt,
|
||||
confidence: t.union([t.literal('low'), t.literal('medium'), t.literal('high')]),
|
||||
is_correction: toBooleanRt,
|
||||
type: t.union([t.literal('user_instruction'), t.literal('contextual')]),
|
||||
public: toBooleanRt,
|
||||
labels: t.record(t.string, t.string),
|
||||
}),
|
||||
|
@ -129,17 +130,19 @@ const functionSummariseRoute = createObservabilityAIAssistantServerRoute({
|
|||
confidence,
|
||||
id,
|
||||
is_correction: isCorrection,
|
||||
type,
|
||||
text,
|
||||
public: isPublic,
|
||||
labels,
|
||||
} = resources.params.body;
|
||||
|
||||
return client.createKnowledgeBaseEntry({
|
||||
return client.addKnowledgeBaseEntry({
|
||||
entry: {
|
||||
confidence,
|
||||
id,
|
||||
doc_id: id,
|
||||
is_correction: isCorrection,
|
||||
type,
|
||||
text,
|
||||
public: isPublic,
|
||||
labels,
|
||||
|
|
|
@ -13,7 +13,12 @@ import { notImplemented } from '@hapi/boom';
|
|||
import { nonEmptyStringRt, toBooleanRt } from '@kbn/io-ts-utils';
|
||||
import * as t from 'io-ts';
|
||||
import { createObservabilityAIAssistantServerRoute } from '../create_observability_ai_assistant_server_route';
|
||||
import { KnowledgeBaseEntry, KnowledgeBaseEntryRole } from '../../../common/types';
|
||||
import {
|
||||
Instruction,
|
||||
KnowledgeBaseEntry,
|
||||
KnowledgeBaseEntryRole,
|
||||
KnowledgeBaseType,
|
||||
} from '../../../common/types';
|
||||
|
||||
const getKnowledgeBaseStatus = createObservabilityAIAssistantServerRoute({
|
||||
endpoint: 'GET /internal/observability_ai_assistant/kb/status',
|
||||
|
@ -60,6 +65,64 @@ const setupKnowledgeBase = createObservabilityAIAssistantServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const getKnowledgeBaseUserInstructions = createObservabilityAIAssistantServerRoute({
|
||||
endpoint: 'GET /internal/observability_ai_assistant/kb/user_instructions',
|
||||
options: {
|
||||
tags: ['access:ai_assistant'],
|
||||
},
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<{
|
||||
userInstructions: Array<Instruction & { public?: boolean }>;
|
||||
}> => {
|
||||
const client = await resources.service.getClient({ request: resources.request });
|
||||
|
||||
if (!client) {
|
||||
throw notImplemented();
|
||||
}
|
||||
|
||||
return {
|
||||
userInstructions: await client.getKnowledgeBaseUserInstructions(),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const saveKnowledgeBaseUserInstruction = createObservabilityAIAssistantServerRoute({
|
||||
endpoint: 'PUT /internal/observability_ai_assistant/kb/user_instructions',
|
||||
params: t.type({
|
||||
body: t.type({
|
||||
id: t.string,
|
||||
text: nonEmptyStringRt,
|
||||
public: toBooleanRt,
|
||||
}),
|
||||
}),
|
||||
options: {
|
||||
tags: ['access:ai_assistant'],
|
||||
},
|
||||
handler: async (resources): Promise<void> => {
|
||||
const client = await resources.service.getClient({ request: resources.request });
|
||||
|
||||
if (!client) {
|
||||
throw notImplemented();
|
||||
}
|
||||
|
||||
const { id, text, public: isPublic } = resources.params.body;
|
||||
return client.addKnowledgeBaseEntry({
|
||||
entry: {
|
||||
id,
|
||||
doc_id: id,
|
||||
text,
|
||||
public: isPublic,
|
||||
confidence: 'high',
|
||||
is_correction: false,
|
||||
type: KnowledgeBaseType.UserInstruction,
|
||||
labels: {},
|
||||
role: KnowledgeBaseEntryRole.UserEntry,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const getKnowledgeBaseEntries = createObservabilityAIAssistantServerRoute({
|
||||
endpoint: 'GET /internal/observability_ai_assistant/kb/entries',
|
||||
options: {
|
||||
|
@ -130,13 +193,14 @@ const saveKnowledgeBaseEntry = createObservabilityAIAssistantServerRoute({
|
|||
role,
|
||||
} = resources.params.body;
|
||||
|
||||
return client.createKnowledgeBaseEntry({
|
||||
return client.addKnowledgeBaseEntry({
|
||||
entry: {
|
||||
id,
|
||||
text,
|
||||
doc_id: id,
|
||||
confidence: confidence ?? 'high',
|
||||
is_correction: isCorrection ?? false,
|
||||
type: 'contextual',
|
||||
public: isPublic ?? true,
|
||||
labels: labels ?? {},
|
||||
role: (role as KnowledgeBaseEntryRole) ?? KnowledgeBaseEntryRole.UserEntry,
|
||||
|
@ -192,6 +256,7 @@ const importKnowledgeBaseEntries = createObservabilityAIAssistantServerRoute({
|
|||
doc_id: entry.id,
|
||||
confidence: 'high' as KnowledgeBaseEntry['confidence'],
|
||||
is_correction: false,
|
||||
type: 'contextual' as const,
|
||||
public: true,
|
||||
labels: {},
|
||||
role: KnowledgeBaseEntryRole.UserEntry,
|
||||
|
@ -206,6 +271,8 @@ export const knowledgeBaseRoutes = {
|
|||
...setupKnowledgeBase,
|
||||
...getKnowledgeBaseStatus,
|
||||
...getKnowledgeBaseEntries,
|
||||
...saveKnowledgeBaseUserInstruction,
|
||||
...getKnowledgeBaseUserInstructions,
|
||||
...importKnowledgeBaseEntries,
|
||||
...saveKnowledgeBaseEntry,
|
||||
...deleteKnowledgeBaseEntry,
|
||||
|
|
|
@ -16,7 +16,7 @@ import type {
|
|||
FunctionCallChatFunction,
|
||||
FunctionHandler,
|
||||
FunctionHandlerRegistry,
|
||||
RegisteredInstruction,
|
||||
InstructionOrCallback,
|
||||
RegisterFunction,
|
||||
RegisterInstruction,
|
||||
} from '../types';
|
||||
|
@ -34,7 +34,7 @@ const ajv = new Ajv({
|
|||
export const GET_DATA_ON_SCREEN_FUNCTION_NAME = 'get_data_on_screen';
|
||||
|
||||
export class ChatFunctionClient {
|
||||
private readonly instructions: RegisteredInstruction[] = [];
|
||||
private readonly instructions: InstructionOrCallback[] = [];
|
||||
private readonly functionRegistry: FunctionHandlerRegistry = new Map();
|
||||
private readonly validators: Map<string, ValidateFunction> = new Map();
|
||||
|
||||
|
@ -107,7 +107,7 @@ export class ChatFunctionClient {
|
|||
}
|
||||
}
|
||||
|
||||
getInstructions(): RegisteredInstruction[] {
|
||||
getInstructions(): InstructionOrCallback[] {
|
||||
return this.instructions;
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ import { createFunctionResponseMessage } from '../../../common/utils/create_func
|
|||
import { CONTEXT_FUNCTION_NAME } from '../../functions/context';
|
||||
import { ChatFunctionClient } from '../chat_function_client';
|
||||
import type { KnowledgeBaseService } from '../knowledge_base_service';
|
||||
import { USER_INSTRUCTIONS_HEADER } from '../util/get_system_message_from_instructions';
|
||||
import { observableIntoStream } from '../util/observable_into_stream';
|
||||
import { CreateChatCompletionResponseChunk } from './adapters/process_openai_stream';
|
||||
|
||||
|
@ -34,7 +33,7 @@ type ChunkDelta = CreateChatCompletionResponseChunk['choices'][number]['delta'];
|
|||
|
||||
type LlmSimulator = ReturnType<typeof createLlmSimulator>;
|
||||
|
||||
const EXPECTED_STORED_SYSTEM_MESSAGE = `system\n\n${USER_INSTRUCTIONS_HEADER}\n\nYou MUST respond in the users preferred language which is: English.`;
|
||||
const EXPECTED_STORED_SYSTEM_MESSAGE = `system\n\nYou MUST respond in the users preferred language which is: English.`;
|
||||
|
||||
const nextTick = () => {
|
||||
return new Promise(process.nextTick);
|
||||
|
@ -367,8 +366,8 @@ describe('Observability AI Assistant client', () => {
|
|||
last_updated: expect.any(String),
|
||||
token_count: {
|
||||
completion: 1,
|
||||
prompt: 84,
|
||||
total: 85,
|
||||
prompt: 46,
|
||||
total: 47,
|
||||
},
|
||||
},
|
||||
type: StreamingChatResponseEventType.ConversationCreate,
|
||||
|
@ -424,8 +423,8 @@ describe('Observability AI Assistant client', () => {
|
|||
last_updated: expect.any(String),
|
||||
token_count: {
|
||||
completion: 6,
|
||||
prompt: 268,
|
||||
total: 274,
|
||||
prompt: 230,
|
||||
total: 236,
|
||||
},
|
||||
},
|
||||
type: StreamingChatResponseEventType.ConversationCreate,
|
||||
|
@ -442,8 +441,8 @@ describe('Observability AI Assistant client', () => {
|
|||
title: 'An auto-generated title',
|
||||
token_count: {
|
||||
completion: 6,
|
||||
prompt: 268,
|
||||
total: 274,
|
||||
prompt: 230,
|
||||
total: 236,
|
||||
},
|
||||
},
|
||||
labels: {},
|
||||
|
@ -573,8 +572,8 @@ describe('Observability AI Assistant client', () => {
|
|||
last_updated: expect.any(String),
|
||||
token_count: {
|
||||
completion: 2,
|
||||
prompt: 162,
|
||||
total: 164,
|
||||
prompt: 124,
|
||||
total: 126,
|
||||
},
|
||||
},
|
||||
type: StreamingChatResponseEventType.ConversationUpdate,
|
||||
|
@ -592,8 +591,8 @@ describe('Observability AI Assistant client', () => {
|
|||
title: 'My stored conversation',
|
||||
token_count: {
|
||||
completion: 2,
|
||||
prompt: 162,
|
||||
total: 164,
|
||||
prompt: 124,
|
||||
total: 126,
|
||||
},
|
||||
},
|
||||
labels: {},
|
||||
|
@ -1609,7 +1608,10 @@ describe('Observability AI Assistant client', () => {
|
|||
.subscribe(() => {}); // To trigger call to chat
|
||||
await nextTick();
|
||||
|
||||
expect(chatSpy.mock.calls[0][1].messages[0].message.content).toEqual(
|
||||
const systemMessage = chatSpy.mock.calls[0][1].messages[0];
|
||||
|
||||
expect(systemMessage.message.role).toEqual(MessageRole.System);
|
||||
expect(systemMessage.message.content).toEqual(
|
||||
EXPECTED_STORED_SYSTEM_MESSAGE.replace('English', 'Orcish')
|
||||
);
|
||||
});
|
||||
|
|
|
@ -46,12 +46,12 @@ import {
|
|||
} from '../../../common/conversation_complete';
|
||||
import { CompatibleJSONSchema } from '../../../common/functions/types';
|
||||
import {
|
||||
UserInstructionOrPlainText,
|
||||
type Conversation,
|
||||
type ConversationCreateRequest,
|
||||
type ConversationUpdateRequest,
|
||||
type KnowledgeBaseEntry,
|
||||
type Message,
|
||||
type AdHocInstruction,
|
||||
} from '../../../common/types';
|
||||
import { withoutTokenCountEvents } from '../../../common/utils/without_token_count_events';
|
||||
import { CONTEXT_FUNCTION_NAME } from '../../functions/context';
|
||||
|
@ -159,7 +159,21 @@ export class ObservabilityAIAssistantClient {
|
|||
});
|
||||
};
|
||||
|
||||
complete = (params: {
|
||||
complete = ({
|
||||
functionClient,
|
||||
connectorId,
|
||||
simulateFunctionCalling,
|
||||
instructions: adHocInstructions = [],
|
||||
messages: initialMessages,
|
||||
signal,
|
||||
responseLanguage = 'English',
|
||||
persist,
|
||||
kibanaPublicUrl,
|
||||
isPublic,
|
||||
title: predefinedTitle,
|
||||
conversationId: predefinedConversationId,
|
||||
disableFunctions = false,
|
||||
}: {
|
||||
messages: Message[];
|
||||
connectorId: string;
|
||||
signal: AbortSignal;
|
||||
|
@ -170,7 +184,7 @@ export class ObservabilityAIAssistantClient {
|
|||
title?: string;
|
||||
isPublic?: boolean;
|
||||
kibanaPublicUrl?: string;
|
||||
instructions?: UserInstructionOrPlainText[];
|
||||
instructions?: AdHocInstruction[];
|
||||
simulateFunctionCalling?: boolean;
|
||||
disableFunctions?:
|
||||
| boolean
|
||||
|
@ -181,26 +195,11 @@ export class ObservabilityAIAssistantClient {
|
|||
return new LangTracer(context.active()).startActiveSpan(
|
||||
'complete',
|
||||
({ tracer: completeTracer }) => {
|
||||
const {
|
||||
functionClient,
|
||||
connectorId,
|
||||
simulateFunctionCalling,
|
||||
instructions: requestInstructions = [],
|
||||
messages: initialMessages,
|
||||
signal,
|
||||
responseLanguage = 'English',
|
||||
persist,
|
||||
kibanaPublicUrl,
|
||||
isPublic,
|
||||
title: predefinedTitle,
|
||||
conversationId: predefinedConversationId,
|
||||
disableFunctions = false,
|
||||
} = params;
|
||||
|
||||
if (responseLanguage) {
|
||||
requestInstructions.push(
|
||||
`You MUST respond in the users preferred language which is: ${responseLanguage}.`
|
||||
);
|
||||
adHocInstructions.push({
|
||||
instruction_type: 'application_instruction',
|
||||
text: `You MUST respond in the users preferred language which is: ${responseLanguage}.`,
|
||||
});
|
||||
}
|
||||
|
||||
const isConversationUpdate = persist && !!predefinedConversationId;
|
||||
|
@ -208,14 +207,15 @@ export class ObservabilityAIAssistantClient {
|
|||
const conversationId = persist ? predefinedConversationId || v4() : '';
|
||||
|
||||
if (persist && !isConversationUpdate && kibanaPublicUrl) {
|
||||
requestInstructions.push(
|
||||
`This conversation will be persisted in Kibana and available at this url: ${
|
||||
adHocInstructions.push({
|
||||
instruction_type: 'application_instruction',
|
||||
text: `This conversation will be persisted in Kibana and available at this url: ${
|
||||
kibanaPublicUrl + `/app/observabilityAIAssistant/conversations/${conversationId}`
|
||||
}.`
|
||||
);
|
||||
}.`,
|
||||
});
|
||||
}
|
||||
|
||||
const userInstructions$ = from(this.fetchUserInstructions()).pipe(shareReplay());
|
||||
const userInstructions$ = from(this.getKnowledgeBaseUserInstructions()).pipe(shareReplay());
|
||||
|
||||
// from the initial messages, override any system message with
|
||||
// the one that is based on the instructions (registered, request, kb)
|
||||
|
@ -224,9 +224,9 @@ export class ObservabilityAIAssistantClient {
|
|||
// this is what we eventually store in the conversation
|
||||
const messagesWithUpdatedSystemMessage = replaceSystemMessage(
|
||||
getSystemMessageFromInstructions({
|
||||
registeredInstructions: functionClient.getInstructions(),
|
||||
applicationInstructions: functionClient.getInstructions(),
|
||||
userInstructions,
|
||||
requestInstructions,
|
||||
adHocInstructions,
|
||||
availableFunctionNames: functionClient
|
||||
.getFunctions()
|
||||
.map((fn) => fn.definition.name),
|
||||
|
@ -303,7 +303,7 @@ export class ObservabilityAIAssistantClient {
|
|||
functionCallsLeft: MAX_FUNCTION_CALLS,
|
||||
functionClient,
|
||||
userInstructions,
|
||||
requestInstructions,
|
||||
adHocInstructions,
|
||||
signal,
|
||||
logger: this.dependencies.logger,
|
||||
disableFunctions,
|
||||
|
@ -731,7 +731,7 @@ export class ObservabilityAIAssistantClient {
|
|||
return this.dependencies.knowledgeBaseService.setup();
|
||||
};
|
||||
|
||||
createKnowledgeBaseEntry = async ({
|
||||
addKnowledgeBaseEntry = async ({
|
||||
entry,
|
||||
}: {
|
||||
entry: Omit<KnowledgeBaseEntry, '@timestamp'>;
|
||||
|
@ -772,7 +772,7 @@ export class ObservabilityAIAssistantClient {
|
|||
return this.dependencies.knowledgeBaseService.deleteEntry({ id });
|
||||
};
|
||||
|
||||
fetchUserInstructions = async () => {
|
||||
getKnowledgeBaseUserInstructions = async () => {
|
||||
return this.dependencies.knowledgeBaseService.getUserInstructions(
|
||||
this.dependencies.namespace,
|
||||
this.dependencies.user
|
||||
|
|
|
@ -28,7 +28,7 @@ import {
|
|||
MessageOrChatEvent,
|
||||
} from '../../../../common/conversation_complete';
|
||||
import { FunctionVisibility } from '../../../../common/functions/types';
|
||||
import { UserInstruction } from '../../../../common/types';
|
||||
import { AdHocInstruction, Instruction } from '../../../../common/types';
|
||||
import { createFunctionResponseMessage } from '../../../../common/utils/create_function_response_message';
|
||||
import { emitWithConcatenatedMessage } from '../../../../common/utils/emit_with_concatenated_message';
|
||||
import { withoutTokenCountEvents } from '../../../../common/utils/without_token_count_events';
|
||||
|
@ -171,7 +171,7 @@ export function continueConversation({
|
|||
chat,
|
||||
signal,
|
||||
functionCallsLeft,
|
||||
requestInstructions,
|
||||
adHocInstructions,
|
||||
userInstructions,
|
||||
logger,
|
||||
disableFunctions,
|
||||
|
@ -182,8 +182,8 @@ export function continueConversation({
|
|||
chat: ChatFunctionWithoutConnector;
|
||||
signal: AbortSignal;
|
||||
functionCallsLeft: number;
|
||||
requestInstructions: Array<string | UserInstruction>;
|
||||
userInstructions: UserInstruction[];
|
||||
adHocInstructions: AdHocInstruction[];
|
||||
userInstructions: Instruction[];
|
||||
logger: Logger;
|
||||
disableFunctions:
|
||||
| boolean
|
||||
|
@ -204,9 +204,9 @@ export function continueConversation({
|
|||
|
||||
const messagesWithUpdatedSystemMessage = replaceSystemMessage(
|
||||
getSystemMessageFromInstructions({
|
||||
registeredInstructions: functionClient.getInstructions(),
|
||||
applicationInstructions: functionClient.getInstructions(),
|
||||
userInstructions,
|
||||
requestInstructions,
|
||||
adHocInstructions,
|
||||
availableFunctionNames: definitions.map((def) => def.name),
|
||||
}),
|
||||
initialMessages
|
||||
|
@ -325,7 +325,7 @@ export function continueConversation({
|
|||
functionClient,
|
||||
signal,
|
||||
userInstructions,
|
||||
requestInstructions,
|
||||
adHocInstructions,
|
||||
logger,
|
||||
disableFunctions,
|
||||
tracer,
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
} from '../../../../common';
|
||||
import { ChatEvent } from '../../../../common/conversation_complete';
|
||||
import { LangTracer } from '../instrumentation/lang_tracer';
|
||||
import { getGeneratedTitle } from './get_generated_title';
|
||||
import { TITLE_CONVERSATION_FUNCTION_NAME, getGeneratedTitle } from './get_generated_title';
|
||||
|
||||
describe('getGeneratedTitle', () => {
|
||||
const messages: Message[] = [
|
||||
|
@ -83,7 +83,7 @@ describe('getGeneratedTitle', () => {
|
|||
const { chatSpy, title$ } = callGenerateTitle([
|
||||
createChatCompletionChunk({
|
||||
function_call: {
|
||||
name: 'title_conversation',
|
||||
name: TITLE_CONVERSATION_FUNCTION_NAME,
|
||||
arguments: JSON.stringify({ title: 'My title' }),
|
||||
},
|
||||
}),
|
||||
|
|
|
@ -14,6 +14,8 @@ import { hideTokenCountEvents } from './hide_token_count_events';
|
|||
import { ChatEvent, TokenCountEvent } from '../../../../common/conversation_complete';
|
||||
import { LangTracer } from '../instrumentation/lang_tracer';
|
||||
|
||||
export const TITLE_CONVERSATION_FUNCTION_NAME = 'title_conversation';
|
||||
|
||||
type ChatFunctionWithoutConnectorAndTokenCount = (
|
||||
name: string,
|
||||
params: Omit<
|
||||
|
@ -59,7 +61,7 @@ export function getGeneratedTitle({
|
|||
],
|
||||
functions: [
|
||||
{
|
||||
name: 'title_conversation',
|
||||
name: TITLE_CONVERSATION_FUNCTION_NAME,
|
||||
description:
|
||||
'Use this function to title the conversation. Do not wrap the title in quotes',
|
||||
parameters: {
|
||||
|
@ -73,7 +75,7 @@ export function getGeneratedTitle({
|
|||
},
|
||||
},
|
||||
],
|
||||
functionCall: 'title_conversation',
|
||||
functionCall: TITLE_CONVERSATION_FUNCTION_NAME,
|
||||
tracer,
|
||||
}).pipe(
|
||||
hide(),
|
||||
|
|
|
@ -323,7 +323,7 @@ export class ObservabilityAIAssistantService {
|
|||
return fnClient;
|
||||
}
|
||||
|
||||
addToKnowledgeBase(entries: KnowledgeBaseEntryRequest[]): void {
|
||||
addToKnowledgeBaseQueue(entries: KnowledgeBaseEntryRequest[]): void {
|
||||
this.init()
|
||||
.then(() => {
|
||||
this.kbService!.queue(
|
||||
|
@ -334,6 +334,7 @@ export class ObservabilityAIAssistantService {
|
|||
doc_id: entry.id,
|
||||
public: true,
|
||||
confidence: 'high' as const,
|
||||
type: 'contextual' as const,
|
||||
is_correction: false,
|
||||
labels: {
|
||||
...entry.labels,
|
||||
|
@ -364,7 +365,7 @@ export class ObservabilityAIAssistantService {
|
|||
}
|
||||
|
||||
addCategoryToKnowledgeBase(categoryId: string, entries: KnowledgeBaseEntryRequest[]) {
|
||||
this.addToKnowledgeBase(
|
||||
this.addToKnowledgeBaseQueue(
|
||||
entries.map((entry) => {
|
||||
return {
|
||||
...entry,
|
||||
|
|
|
@ -38,6 +38,7 @@ export const kbComponentTemplate: ClusterComponentTemplate['component_template']
|
|||
name: keyword,
|
||||
},
|
||||
},
|
||||
type: keyword,
|
||||
labels: dynamic,
|
||||
conversation: {
|
||||
properties: {
|
||||
|
|
|
@ -19,7 +19,12 @@ import {
|
|||
INDEX_QUEUED_DOCUMENTS_TASK_TYPE,
|
||||
resourceNames,
|
||||
} from '..';
|
||||
import { KnowledgeBaseEntry, KnowledgeBaseEntryRole, UserInstruction } from '../../../common/types';
|
||||
import {
|
||||
Instruction,
|
||||
KnowledgeBaseEntry,
|
||||
KnowledgeBaseEntryRole,
|
||||
KnowledgeBaseType,
|
||||
} from '../../../common/types';
|
||||
import { getAccessQuery } from '../util/get_access_query';
|
||||
import { getCategoryQuery } from '../util/get_category_query';
|
||||
import { recallFromConnectors } from './recall_from_connectors';
|
||||
|
@ -355,6 +360,9 @@ export class KnowledgeBaseService {
|
|||
namespace,
|
||||
}),
|
||||
...getCategoryQuery({ categories }),
|
||||
|
||||
// exclude user instructions
|
||||
{ bool: { must_not: { term: { type: KnowledgeBaseType.UserInstruction } } } },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
@ -425,6 +433,13 @@ export class KnowledgeBaseService {
|
|||
}),
|
||||
]);
|
||||
|
||||
this.dependencies.logger.debug(
|
||||
`documentsFromKb: ${JSON.stringify(documentsFromKb.slice(0, 5), null, 2)}`
|
||||
);
|
||||
this.dependencies.logger.debug(
|
||||
`documentsFromConnectors: ${JSON.stringify(documentsFromConnectors.slice(0, 5), null, 2)}`
|
||||
);
|
||||
|
||||
const sortedEntries = orderBy(
|
||||
documentsFromKb.concat(documentsFromConnectors),
|
||||
'score',
|
||||
|
@ -458,34 +473,30 @@ export class KnowledgeBaseService {
|
|||
getUserInstructions = async (
|
||||
namespace: string,
|
||||
user?: { name: string }
|
||||
): Promise<UserInstruction[]> => {
|
||||
): Promise<Array<Instruction & { public?: boolean }>> => {
|
||||
try {
|
||||
const response = await this.dependencies.esClient.asInternalUser.search<KnowledgeBaseEntry>({
|
||||
index: resourceNames.aliases.kb,
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
filter: [
|
||||
{
|
||||
term: {
|
||||
'labels.category.keyword': {
|
||||
value: 'instruction',
|
||||
},
|
||||
type: KnowledgeBaseType.UserInstruction,
|
||||
},
|
||||
},
|
||||
...getAccessQuery({ user, namespace }),
|
||||
],
|
||||
filter: getAccessQuery({
|
||||
user,
|
||||
namespace,
|
||||
}),
|
||||
},
|
||||
},
|
||||
size: 500,
|
||||
_source: ['doc_id', 'text'],
|
||||
_source: ['doc_id', 'text', 'public'],
|
||||
});
|
||||
|
||||
return response.hits.hits.map((hit) => ({
|
||||
doc_id: hit._source?.doc_id ?? '',
|
||||
text: hit._source?.text ?? '',
|
||||
public: hit._source?.public,
|
||||
}));
|
||||
} catch (error) {
|
||||
this.dependencies.logger.error('Failed to load instructions from knowledge base');
|
||||
|
@ -506,17 +517,18 @@ export class KnowledgeBaseService {
|
|||
try {
|
||||
const response = await this.dependencies.esClient.asInternalUser.search<KnowledgeBaseEntry>({
|
||||
index: resourceNames.aliases.kb,
|
||||
...(query
|
||||
? {
|
||||
query: {
|
||||
wildcard: {
|
||||
doc_id: {
|
||||
value: `${query}*`,
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
// filter title by query
|
||||
...(query ? [{ wildcard: { doc_id: { value: `${query}*` } } }] : []),
|
||||
{
|
||||
// exclude user instructions
|
||||
bool: { must_not: { term: { type: KnowledgeBaseType.UserInstruction } } },
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
],
|
||||
},
|
||||
},
|
||||
sort: [
|
||||
{
|
||||
[String(sortBy)]: {
|
||||
|
@ -536,6 +548,7 @@ export class KnowledgeBaseService {
|
|||
'@timestamp',
|
||||
'role',
|
||||
'user.name',
|
||||
'type',
|
||||
],
|
||||
},
|
||||
});
|
||||
|
@ -556,6 +569,35 @@ export class KnowledgeBaseService {
|
|||
}
|
||||
};
|
||||
|
||||
getExistingUserInstructionId = async ({
|
||||
isPublic,
|
||||
user,
|
||||
namespace,
|
||||
}: {
|
||||
isPublic: boolean;
|
||||
user?: { name: string; id?: string };
|
||||
namespace?: string;
|
||||
}) => {
|
||||
const res = await this.dependencies.esClient.asInternalUser.search<
|
||||
Pick<KnowledgeBaseEntry, 'doc_id'>
|
||||
>({
|
||||
index: resourceNames.aliases.kb,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { type: KnowledgeBaseType.UserInstruction } },
|
||||
{ term: { public: isPublic } },
|
||||
...getAccessQuery({ user, namespace }),
|
||||
],
|
||||
},
|
||||
},
|
||||
size: 1,
|
||||
_source: ['doc_id'],
|
||||
});
|
||||
|
||||
return res.hits.hits[0]?._source?.doc_id;
|
||||
};
|
||||
|
||||
addEntry = async ({
|
||||
entry: { id, ...document },
|
||||
user,
|
||||
|
@ -565,6 +607,20 @@ export class KnowledgeBaseService {
|
|||
user?: { name: string; id?: string };
|
||||
namespace?: string;
|
||||
}): Promise<void> => {
|
||||
// for now we want to limit the number of user instructions to 1 per user
|
||||
if (document.type === KnowledgeBaseType.UserInstruction) {
|
||||
const existingId = await this.getExistingUserInstructionId({
|
||||
isPublic: document.public,
|
||||
user,
|
||||
namespace,
|
||||
});
|
||||
|
||||
if (existingId) {
|
||||
id = existingId;
|
||||
document.doc_id = existingId;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.dependencies.esClient.asInternalUser.index({
|
||||
index: resourceNames.aliases.kb,
|
||||
|
|
|
@ -16,7 +16,7 @@ import type {
|
|||
import type {
|
||||
Message,
|
||||
ObservabilityAIAssistantScreenContextRequest,
|
||||
UserInstructionOrPlainText,
|
||||
InstructionOrPlainText,
|
||||
} from '../../common/types';
|
||||
import type { ObservabilityAIAssistantRouteHandlerResources } from '../routes/types';
|
||||
import { ChatFunctionClient } from './chat_function_client';
|
||||
|
@ -63,17 +63,15 @@ export interface FunctionHandler {
|
|||
respond: RespondFunction<any, FunctionResponse>;
|
||||
}
|
||||
|
||||
export type RegisteredInstruction = UserInstructionOrPlainText | RegisterInstructionCallback;
|
||||
export type InstructionOrCallback = InstructionOrPlainText | RegisterInstructionCallback;
|
||||
|
||||
type RegisterInstructionCallback = ({
|
||||
availableFunctionNames,
|
||||
}: {
|
||||
availableFunctionNames: string[];
|
||||
}) => UserInstructionOrPlainText | UserInstructionOrPlainText[] | undefined;
|
||||
}) => InstructionOrPlainText | InstructionOrPlainText[] | undefined;
|
||||
|
||||
export type RegisterInstruction = (
|
||||
...instructions: Array<UserInstructionOrPlainText | RegisterInstructionCallback>
|
||||
) => void;
|
||||
export type RegisterInstruction = (...instructions: InstructionOrCallback[]) => void;
|
||||
|
||||
export type RegisterFunction = <
|
||||
TParameters extends CompatibleJSONSchema = any,
|
||||
|
|
|
@ -13,9 +13,9 @@ describe('getSystemMessageFromInstructions', () => {
|
|||
it('handles plain instructions', () => {
|
||||
expect(
|
||||
getSystemMessageFromInstructions({
|
||||
registeredInstructions: ['first', 'second'],
|
||||
applicationInstructions: ['first', 'second'],
|
||||
userInstructions: [],
|
||||
requestInstructions: [],
|
||||
adHocInstructions: [],
|
||||
availableFunctionNames: [],
|
||||
})
|
||||
).toEqual(`first\n\nsecond`);
|
||||
|
@ -24,36 +24,42 @@ describe('getSystemMessageFromInstructions', () => {
|
|||
it('handles callbacks', () => {
|
||||
expect(
|
||||
getSystemMessageFromInstructions({
|
||||
registeredInstructions: [
|
||||
applicationInstructions: [
|
||||
'first',
|
||||
({ availableFunctionNames }) => {
|
||||
return availableFunctionNames[0];
|
||||
},
|
||||
],
|
||||
userInstructions: [],
|
||||
requestInstructions: [],
|
||||
adHocInstructions: [],
|
||||
availableFunctionNames: ['myFunction'],
|
||||
})
|
||||
).toEqual(`first\n\nmyFunction`);
|
||||
});
|
||||
|
||||
it('overrides kb instructions with request instructions', () => {
|
||||
it('overrides kb instructions with adhoc instructions', () => {
|
||||
expect(
|
||||
getSystemMessageFromInstructions({
|
||||
registeredInstructions: ['first'],
|
||||
userInstructions: [{ doc_id: 'second', text: 'second_kb' }],
|
||||
requestInstructions: [{ doc_id: 'second', text: 'second_request' }],
|
||||
applicationInstructions: ['first'],
|
||||
userInstructions: [{ doc_id: 'second', text: 'second from kb' }],
|
||||
adHocInstructions: [
|
||||
{
|
||||
doc_id: 'second',
|
||||
text: 'second from adhoc instruction',
|
||||
instruction_type: 'user_instruction',
|
||||
},
|
||||
],
|
||||
availableFunctionNames: [],
|
||||
})
|
||||
).toEqual(`first\n\n${USER_INSTRUCTIONS_HEADER}\n\nsecond_request`);
|
||||
).toEqual(`first\n\n${USER_INSTRUCTIONS_HEADER}\n\nsecond from adhoc instruction`);
|
||||
});
|
||||
|
||||
it('includes kb instructions if there is no request instruction', () => {
|
||||
expect(
|
||||
getSystemMessageFromInstructions({
|
||||
registeredInstructions: ['first'],
|
||||
applicationInstructions: ['first'],
|
||||
userInstructions: [{ doc_id: 'second', text: 'second_kb' }],
|
||||
requestInstructions: [],
|
||||
adHocInstructions: [],
|
||||
availableFunctionNames: [],
|
||||
})
|
||||
).toEqual(`first\n\n${USER_INSTRUCTIONS_HEADER}\n\nsecond_kb`);
|
||||
|
@ -62,14 +68,14 @@ describe('getSystemMessageFromInstructions', () => {
|
|||
it('handles undefined values', () => {
|
||||
expect(
|
||||
getSystemMessageFromInstructions({
|
||||
registeredInstructions: [
|
||||
applicationInstructions: [
|
||||
'first',
|
||||
({ availableFunctionNames }) => {
|
||||
return undefined;
|
||||
},
|
||||
],
|
||||
userInstructions: [],
|
||||
requestInstructions: [],
|
||||
adHocInstructions: [],
|
||||
availableFunctionNames: [],
|
||||
})
|
||||
).toEqual(`first`);
|
||||
|
|
|
@ -5,11 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { compact, partition } from 'lodash';
|
||||
import { compact, partition, uniqBy } from 'lodash';
|
||||
import { v4 } from 'uuid';
|
||||
import { UserInstruction, UserInstructionOrPlainText } from '../../../common/types';
|
||||
import { AdHocInstruction, Instruction } from '../../../common/types';
|
||||
import { withTokenBudget } from '../../../common/utils/with_token_budget';
|
||||
import { RegisteredInstruction } from '../types';
|
||||
import { InstructionOrCallback } from '../types';
|
||||
|
||||
export const USER_INSTRUCTIONS_HEADER = `## User instructions
|
||||
|
||||
|
@ -19,18 +19,23 @@ as long as they don't conflict with anything you've been told so far:
|
|||
`;
|
||||
|
||||
export function getSystemMessageFromInstructions({
|
||||
registeredInstructions,
|
||||
userInstructions,
|
||||
requestInstructions,
|
||||
// application instructions registered by the functions. These will be displayed first
|
||||
applicationInstructions,
|
||||
|
||||
// instructions provided by the user. These will be displayed after the application instructions and only if they fit within the token budget
|
||||
userInstructions: kbUserInstructions,
|
||||
|
||||
// ad-hoc instruction. Can be either user or application instruction
|
||||
adHocInstructions,
|
||||
availableFunctionNames,
|
||||
}: {
|
||||
registeredInstructions: RegisteredInstruction[];
|
||||
userInstructions: UserInstruction[];
|
||||
requestInstructions: UserInstructionOrPlainText[];
|
||||
applicationInstructions: InstructionOrCallback[];
|
||||
userInstructions: Instruction[];
|
||||
adHocInstructions: AdHocInstruction[];
|
||||
availableFunctionNames: string[];
|
||||
}): string {
|
||||
const allRegisteredInstructions = compact(
|
||||
registeredInstructions.flatMap((instruction) => {
|
||||
const allApplicationInstructions = compact(
|
||||
applicationInstructions.flatMap((instruction) => {
|
||||
if (typeof instruction === 'function') {
|
||||
return instruction({ availableFunctionNames });
|
||||
}
|
||||
|
@ -38,31 +43,30 @@ export function getSystemMessageFromInstructions({
|
|||
})
|
||||
);
|
||||
|
||||
const requestInstructionsWithId = requestInstructions.map((instruction) =>
|
||||
typeof instruction === 'string'
|
||||
? { doc_id: v4(), text: instruction, system: false }
|
||||
: instruction
|
||||
const adHocInstructionsWithId = adHocInstructions.map((adHocInstruction) => ({
|
||||
...adHocInstruction,
|
||||
doc_id: adHocInstruction.doc_id ?? v4(),
|
||||
}));
|
||||
|
||||
// split ad hoc instructions into user instructions and application instructions
|
||||
const [adHocUserInstructions, adHocApplicationInstructions] = partition(
|
||||
adHocInstructionsWithId,
|
||||
(instruction) => instruction.instruction_type === 'user_instruction'
|
||||
);
|
||||
|
||||
const [requestSystemInstructions, requestUserInstructionsWithId] = partition(
|
||||
requestInstructionsWithId,
|
||||
(instruction) => instruction.system === true
|
||||
// all adhoc instructions and KB instructions.
|
||||
// adhoc instructions will be prioritized over Knowledge Base instructions if the doc_id is the same
|
||||
const allUserInstructions = withTokenBudget(
|
||||
uniqBy([...adHocUserInstructions, ...kbUserInstructions], (i) => i.doc_id),
|
||||
1000
|
||||
);
|
||||
|
||||
const requestOverrideIds = requestUserInstructionsWithId.map((instruction) => instruction.doc_id);
|
||||
|
||||
// all request instructions, and those from the KB that are not defined as a request instruction
|
||||
const allUserInstructions = requestInstructionsWithId.concat(
|
||||
userInstructions.filter((instruction) => !requestOverrideIds.includes(instruction.doc_id))
|
||||
);
|
||||
|
||||
const instructionsWithinBudget = withTokenBudget(allUserInstructions, 1000);
|
||||
|
||||
return [
|
||||
...allRegisteredInstructions.concat(requestSystemInstructions),
|
||||
...(instructionsWithinBudget.length
|
||||
? [USER_INSTRUCTIONS_HEADER, ...instructionsWithinBudget]
|
||||
: []),
|
||||
// application instructions
|
||||
...allApplicationInstructions.concat(adHocApplicationInstructions),
|
||||
|
||||
// user instructions
|
||||
...(allUserInstructions.length ? [USER_INSTRUCTIONS_HEADER, ...allUserInstructions] : []),
|
||||
]
|
||||
.map((instruction) => {
|
||||
return typeof instruction === 'string' ? instruction : instruction.text;
|
||||
|
|
|
@ -20,11 +20,10 @@
|
|||
"uiActions",
|
||||
"triggersActionsUi",
|
||||
"share",
|
||||
"security",
|
||||
"licensing",
|
||||
"ml",
|
||||
"alerting",
|
||||
"features",
|
||||
"features"
|
||||
],
|
||||
"requiredBundles": ["kibanaReact", "esqlDataGrid"],
|
||||
"optionalPlugins": ["cloud"],
|
||||
|
|
|
@ -11,11 +11,7 @@ import { useKibana } from './use_kibana';
|
|||
|
||||
export function useCurrentUser() {
|
||||
const {
|
||||
services: {
|
||||
plugins: {
|
||||
start: { security },
|
||||
},
|
||||
},
|
||||
services: { security },
|
||||
} = useKibana();
|
||||
|
||||
const [user, setUser] = useState<AuthenticatedUser>();
|
||||
|
|
|
@ -16,7 +16,6 @@ import type {
|
|||
ObservabilityAIAssistantPublicStart,
|
||||
} from '@kbn/observability-ai-assistant-plugin/public';
|
||||
import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public';
|
||||
import type { SecurityPluginStart, SecurityPluginSetup } from '@kbn/security-plugin/public';
|
||||
import type { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugin/public';
|
||||
import type {
|
||||
ObservabilitySharedPluginSetup,
|
||||
|
@ -41,7 +40,6 @@ export interface ObservabilityAIAssistantAppPublicSetup {}
|
|||
export interface ObservabilityAIAssistantAppPluginStartDependencies {
|
||||
licensing: LicensingPluginStart;
|
||||
share: SharePluginStart;
|
||||
security: SecurityPluginStart;
|
||||
lens: LensPublicStart;
|
||||
dataViews: DataViewsPublicPluginStart;
|
||||
uiActions: UiActionsStart;
|
||||
|
@ -56,7 +54,6 @@ export interface ObservabilityAIAssistantAppPluginStartDependencies {
|
|||
export interface ObservabilityAIAssistantAppPluginSetupDependencies {
|
||||
licensing: LicensingPluginSetup;
|
||||
share: SharePluginSetup;
|
||||
security: SecurityPluginSetup;
|
||||
lens: LensPublicSetup;
|
||||
dataViews: DataViewsPublicPluginSetup;
|
||||
uiActions: UiActionsSetup;
|
||||
|
|
|
@ -36,6 +36,7 @@ import { concatenateChatCompletionChunks } from '@kbn/observability-ai-assistant
|
|||
import { CompatibleJSONSchema } from '@kbn/observability-ai-assistant-plugin/common/functions/types';
|
||||
import { AlertDetailsContextualInsightsService } from '@kbn/observability-plugin/server/services';
|
||||
import { getSystemMessageFromInstructions } from '@kbn/observability-ai-assistant-plugin/server/service/util/get_system_message_from_instructions';
|
||||
import { AdHocInstruction } from '@kbn/observability-ai-assistant-plugin/common/types';
|
||||
import { convertSchemaToOpenApi } from './convert_schema_to_open_api';
|
||||
import { OBSERVABILITY_AI_ASSISTANT_CONNECTOR_ID } from '../../common/rule_connector';
|
||||
|
||||
|
@ -177,12 +178,15 @@ async function executor(
|
|||
});
|
||||
});
|
||||
|
||||
const backgroundInstruction = dedent(
|
||||
`You are called as a background process because alerts have changed state.
|
||||
As a background process you are not interacting with a user. Because of that DO NOT ask for user
|
||||
input if tasked to execute actions. You can generate multiple responses in a row.
|
||||
If available, include the link of the conversation at the end of your answer.`
|
||||
);
|
||||
const backgroundInstruction: AdHocInstruction = {
|
||||
instruction_type: 'application_instruction',
|
||||
text: dedent(
|
||||
`You are called as a background process because alerts have changed state.
|
||||
As a background process you are not interacting with a user. Because of that DO NOT ask for user
|
||||
input if tasked to execute actions. You can generate multiple responses in a row.
|
||||
If available, include the link of the conversation at the end of your answer.`
|
||||
),
|
||||
};
|
||||
|
||||
const alertsContext = await getAlertsContext(
|
||||
execOptions.params.rule,
|
||||
|
@ -223,9 +227,9 @@ async function executor(
|
|||
role: MessageRole.System,
|
||||
content: getSystemMessageFromInstructions({
|
||||
availableFunctionNames: functionClient.getFunctions().map((fn) => fn.definition.name),
|
||||
registeredInstructions: functionClient.getInstructions(),
|
||||
applicationInstructions: functionClient.getInstructions(),
|
||||
userInstructions: [],
|
||||
requestInstructions: [],
|
||||
adHocInstructions: [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
export const REACT_QUERY_KEYS = {
|
||||
GET_GENAI_CONNECTORS: 'get_genai_connectors',
|
||||
GET_KB_ENTRIES: 'get_kb_entries',
|
||||
GET_KB_USER_INSTRUCTIONS: 'get_kb_user_instructions',
|
||||
CREATE_KB_ENTRIES: 'create_kb_entry',
|
||||
IMPORT_KB_ENTRIES: 'import_kb_entry',
|
||||
};
|
||||
|
|
|
@ -22,7 +22,7 @@ export function useCreateKnowledgeBaseEntry() {
|
|||
} = useKibana().services;
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const observabilityAIAssistantApi = observabilityAIAssistant?.service.callApi;
|
||||
const observabilityAIAssistantApi = observabilityAIAssistant.service.callApi;
|
||||
|
||||
return useMutation<
|
||||
void,
|
||||
|
@ -36,11 +36,7 @@ export function useCreateKnowledgeBaseEntry() {
|
|||
>(
|
||||
[REACT_QUERY_KEYS.CREATE_KB_ENTRIES],
|
||||
({ entry }) => {
|
||||
if (!observabilityAIAssistantApi) {
|
||||
return Promise.reject('Error with observabilityAIAssistantApi: API not found.');
|
||||
}
|
||||
|
||||
return observabilityAIAssistantApi?.(
|
||||
return observabilityAIAssistantApi(
|
||||
'POST /internal/observability_ai_assistant/kb/entries/save',
|
||||
{
|
||||
signal: null,
|
||||
|
@ -59,8 +55,7 @@ export function useCreateKnowledgeBaseEntry() {
|
|||
i18n.translate(
|
||||
'xpack.observabilityAiAssistantManagement.kb.addManualEntry.successNotification',
|
||||
{
|
||||
defaultMessage: 'Successfully created {name}',
|
||||
values: { name: entry.id },
|
||||
defaultMessage: 'Entry saved',
|
||||
}
|
||||
)
|
||||
);
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { type Instruction } from '@kbn/observability-ai-assistant-plugin/common/types';
|
||||
import { REACT_QUERY_KEYS } from '../constants';
|
||||
import { useKibana } from './use_kibana';
|
||||
|
||||
type ServerError = IHttpFetchError<ResponseErrorBody>;
|
||||
|
||||
export function useCreateKnowledgeBaseUserInstruction() {
|
||||
const {
|
||||
observabilityAIAssistant,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const observabilityAIAssistantApi = observabilityAIAssistant.service.callApi;
|
||||
|
||||
return useMutation<void, ServerError, { entry: Instruction & { public: boolean } }>(
|
||||
[REACT_QUERY_KEYS.CREATE_KB_ENTRIES],
|
||||
({ entry }) => {
|
||||
return observabilityAIAssistantApi(
|
||||
'PUT /internal/observability_ai_assistant/kb/user_instructions',
|
||||
{
|
||||
signal: null,
|
||||
params: {
|
||||
body: {
|
||||
id: entry.doc_id,
|
||||
text: entry.text,
|
||||
public: entry.public,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
{
|
||||
onSuccess: (_data, { entry }) => {
|
||||
toasts.addSuccess(
|
||||
i18n.translate(
|
||||
'xpack.observabilityAiAssistantManagement.kb.addUserInstruction.successNotification',
|
||||
{
|
||||
defaultMessage: 'User instruction saved',
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [REACT_QUERY_KEYS.GET_KB_USER_INSTRUCTIONS],
|
||||
refetchType: 'all',
|
||||
});
|
||||
},
|
||||
onError: (error, { entry }) => {
|
||||
toasts.addError(new Error(error.body?.message ?? error.message), {
|
||||
title: i18n.translate(
|
||||
'xpack.observabilityAiAssistantManagement.kb.addUserInstruction.errorNotification',
|
||||
{
|
||||
defaultMessage: 'Something went wrong while creating {name}',
|
||||
values: { name: entry.doc_id },
|
||||
}
|
||||
),
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -20,16 +20,12 @@ export function useDeleteKnowledgeBaseEntry() {
|
|||
} = useKibana().services;
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const observabilityAIAssistantApi = observabilityAIAssistant?.service.callApi;
|
||||
const observabilityAIAssistantApi = observabilityAIAssistant.service.callApi;
|
||||
|
||||
return useMutation<unknown, ServerError, { id: string }>(
|
||||
[REACT_QUERY_KEYS.CREATE_KB_ENTRIES],
|
||||
({ id: entryId }) => {
|
||||
if (!observabilityAIAssistantApi) {
|
||||
return Promise.reject('Error with observabilityAIAssistantApi: API not found.');
|
||||
}
|
||||
|
||||
return observabilityAIAssistantApi?.(
|
||||
return observabilityAIAssistantApi(
|
||||
'DELETE /internal/observability_ai_assistant/kb/entries/{entryId}',
|
||||
{
|
||||
signal: null,
|
||||
|
|
|
@ -20,13 +20,13 @@ export function useGetKnowledgeBaseEntries({
|
|||
}) {
|
||||
const { observabilityAIAssistant } = useKibana().services;
|
||||
|
||||
const observabilityAIAssistantApi = observabilityAIAssistant?.service.callApi;
|
||||
const observabilityAIAssistantApi = observabilityAIAssistant.service.callApi;
|
||||
|
||||
const { isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery({
|
||||
queryKey: [REACT_QUERY_KEYS.GET_KB_ENTRIES, query, sortBy, sortDirection],
|
||||
queryFn: async ({ signal }) => {
|
||||
if (!observabilityAIAssistantApi || !signal) {
|
||||
return Promise.reject('Error with observabilityAIAssistantApi: API not found.');
|
||||
if (!signal) {
|
||||
throw new Error('Abort signal missing');
|
||||
}
|
||||
|
||||
return observabilityAIAssistantApi(`GET /internal/observability_ai_assistant/kb/entries`, {
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { REACT_QUERY_KEYS } from '../constants';
|
||||
import { useKibana } from './use_kibana';
|
||||
|
||||
export function useGetUserInstructions() {
|
||||
const { observabilityAIAssistant } = useKibana().services;
|
||||
const observabilityAIAssistantApi = observabilityAIAssistant.service.callApi;
|
||||
|
||||
const { isLoading, isError, isSuccess, isRefetching, data, refetch } = useQuery({
|
||||
queryKey: [REACT_QUERY_KEYS.GET_KB_USER_INSTRUCTIONS],
|
||||
queryFn: async ({ signal }) => {
|
||||
if (!signal) {
|
||||
throw new Error('Abort signal missing');
|
||||
}
|
||||
|
||||
return observabilityAIAssistantApi(
|
||||
`GET /internal/observability_ai_assistant/kb/user_instructions`,
|
||||
{ signal }
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
userInstructions: data?.userInstructions,
|
||||
refetch,
|
||||
isLoading,
|
||||
isRefetching,
|
||||
isSuccess,
|
||||
isError,
|
||||
};
|
||||
}
|
|
@ -20,7 +20,7 @@ export function useImportKnowledgeBaseEntries() {
|
|||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
const queryClient = useQueryClient();
|
||||
const observabilityAIAssistantApi = observabilityAIAssistant?.service.callApi;
|
||||
const observabilityAIAssistantApi = observabilityAIAssistant.service.callApi;
|
||||
|
||||
return useMutation<
|
||||
void,
|
||||
|
@ -36,11 +36,7 @@ export function useImportKnowledgeBaseEntries() {
|
|||
>(
|
||||
[REACT_QUERY_KEYS.IMPORT_KB_ENTRIES],
|
||||
({ entries }) => {
|
||||
if (!observabilityAIAssistantApi) {
|
||||
return Promise.reject('Error with observabilityAIAssistantApi: API not found.');
|
||||
}
|
||||
|
||||
return observabilityAIAssistantApi?.(
|
||||
return observabilityAIAssistantApi(
|
||||
'POST /internal/observability_ai_assistant/kb/entries/import',
|
||||
{
|
||||
signal: null,
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiFormRow,
|
||||
EuiMarkdownEditor,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { useGetUserInstructions } from '../../hooks/use_get_user_instructions';
|
||||
import { useCreateKnowledgeBaseUserInstruction } from '../../hooks/use_create_knowledge_base_user_instruction';
|
||||
|
||||
export function KnowledgeBaseEditUserInstructionFlyout({ onClose }: { onClose: () => void }) {
|
||||
const { userInstructions, isLoading: isFetching } = useGetUserInstructions();
|
||||
const { mutateAsync: createEntry, isLoading: isSaving } = useCreateKnowledgeBaseUserInstruction();
|
||||
const [newEntryText, setNewEntryText] = useState('');
|
||||
const [newEntryDocId, setNewEntryDocId] = useState<string>();
|
||||
const isSubmitDisabled = newEntryText.trim() === '';
|
||||
|
||||
useEffect(() => {
|
||||
const userInstruction = userInstructions?.find((entry) => !entry.public);
|
||||
setNewEntryDocId(userInstruction?.doc_id);
|
||||
setNewEntryText(userInstruction?.text ?? '');
|
||||
}, [userInstructions]);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
await createEntry({
|
||||
entry: {
|
||||
doc_id: newEntryDocId ?? uuidv4(),
|
||||
text: newEntryText,
|
||||
public: false, // limit user instructions to private (for now)
|
||||
},
|
||||
});
|
||||
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlyout onClose={onClose}>
|
||||
<EuiFlyoutHeader hasBorder data-test-subj="knowledgeBaseManualEntryFlyout">
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.observabilityAiAssistantManagement.knowledgeBaseEditSystemPrompt.h2.editEntryLabel',
|
||||
{ defaultMessage: 'AI User Profile' }
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
|
||||
<EuiFlyoutBody>
|
||||
<EuiText>
|
||||
{i18n.translate(
|
||||
'xpack.observabilityAiAssistantManagement.knowledgeBaseEditSystemPromptFlyout.personalPromptTextLabel',
|
||||
{
|
||||
defaultMessage:
|
||||
'The AI User Profile will be appended to the system prompt. It is space-aware and will only be used for your prompts - not shared with other users.',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiFormRow fullWidth>
|
||||
<EuiMarkdownEditor
|
||||
editorId="knowledgeBaseEditManualEntryFlyoutMarkdownEditor"
|
||||
aria-label={i18n.translate(
|
||||
'xpack.observabilityAiAssistantManagement.knowledgeBaseNewManualEntryFlyout.euiMarkdownEditor.observabilityAiAssistantKnowledgeBaseViewMarkdownEditorLabel',
|
||||
{ defaultMessage: 'observabilityAiAssistantKnowledgeBaseViewMarkdownEditor' }
|
||||
)}
|
||||
height={300}
|
||||
initialViewMode="editing"
|
||||
readOnly={isFetching}
|
||||
placeholder={i18n.translate(
|
||||
'xpack.observabilityAiAssistantManagement.knowledgeBaseEditManualEntryFlyout.euiMarkdownEditor.enterContentsLabel',
|
||||
{ defaultMessage: 'Enter contents' }
|
||||
)}
|
||||
value={newEntryText}
|
||||
onChange={(text) => setNewEntryText(text)}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer size="m" />
|
||||
</EuiFlyoutBody>
|
||||
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="knowledgeBaseEditManualEntryFlyoutCancelButton"
|
||||
disabled={isSaving}
|
||||
onClick={onClose}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.observabilityAiAssistantManagement.knowledgeBaseNewManualEntryFlyout.cancelButtonEmptyLabel',
|
||||
{ defaultMessage: 'Cancel' }
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="knowledgeBaseEditManualEntryFlyoutSaveButton"
|
||||
fill
|
||||
isLoading={isSaving}
|
||||
onClick={handleSubmit}
|
||||
isDisabled={isSubmitDisabled}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.observabilityAiAssistantManagement.knowledgeBaseNewManualEntryFlyout.saveButtonLabel',
|
||||
{ defaultMessage: 'Save' }
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
);
|
||||
}
|
|
@ -24,13 +24,14 @@ import {
|
|||
EuiScreenReaderOnly,
|
||||
} from '@elastic/eui';
|
||||
import moment from 'moment';
|
||||
import type { KnowledgeBaseEntry } from '@kbn/observability-ai-assistant-plugin/common/types';
|
||||
import { KnowledgeBaseEntry } from '@kbn/observability-ai-assistant-plugin/public';
|
||||
import { useGetKnowledgeBaseEntries } from '../../hooks/use_get_knowledge_base_entries';
|
||||
import { categorizeEntries, KnowledgeBaseEntryCategory } from '../../helpers/categorize_entries';
|
||||
import { KnowledgeBaseEditManualEntryFlyout } from './knowledge_base_edit_manual_entry_flyout';
|
||||
import { KnowledgeBaseCategoryFlyout } from './knowledge_base_category_flyout';
|
||||
import { KnowledgeBaseBulkImportFlyout } from './knowledge_base_bulk_import_flyout';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
import { KnowledgeBaseEditUserInstructionFlyout } from './knowledge_base_edit_user_instruction_flyout';
|
||||
|
||||
export function KnowledgeBaseTab() {
|
||||
const { uiSettings } = useKibana().services;
|
||||
|
@ -175,11 +176,12 @@ export function KnowledgeBaseTab() {
|
|||
KnowledgeBaseEntryCategory | undefined
|
||||
>();
|
||||
|
||||
const [flyoutOpenType, setFlyoutOpenType] = useState<
|
||||
'singleEntry' | 'bulkImport' | 'category' | undefined
|
||||
const [newEntryFlyoutType, setNewEntryFlyoutType] = useState<
|
||||
'singleEntry' | 'bulkImport' | undefined
|
||||
>();
|
||||
|
||||
const [newEntryPopoverOpen, setNewEntryPopoverOpen] = useState(false);
|
||||
const [isNewEntryPopoverOpen, setIsNewEntryPopoverOpen] = useState(false);
|
||||
const [isEditUserInstructionFlyoutOpen, setIsEditUserInstructionFlyoutOpen] = useState(false);
|
||||
const [query, setQuery] = useState('');
|
||||
const [sortBy, setSortBy] = useState<'doc_id' | '@timestamp'>('doc_id');
|
||||
const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('asc');
|
||||
|
@ -206,10 +208,6 @@ export function KnowledgeBaseTab() {
|
|||
}
|
||||
};
|
||||
|
||||
const handleClickNewEntry = () => {
|
||||
setNewEntryPopoverOpen(true);
|
||||
};
|
||||
|
||||
const handleChangeQuery = (e: React.ChangeEvent<HTMLInputElement> | undefined) => {
|
||||
setQuery(e?.currentTarget.value || '');
|
||||
};
|
||||
|
@ -236,6 +234,7 @@ export function KnowledgeBaseTab() {
|
|||
)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="knowledgeBaseTabReloadButton"
|
||||
|
@ -249,17 +248,30 @@ export function KnowledgeBaseTab() {
|
|||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
data-test-subj="observabilityAiAssistantManagementKnowledgeBaseTabEditInstructionsButton"
|
||||
color="text"
|
||||
onClick={() => setIsEditUserInstructionFlyoutOpen(true)}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.observabilityAiAssistantManagement.knowledgeBaseTab.editInstructionsButtonLabel',
|
||||
{ defaultMessage: 'Edit AI User Profile' }
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
isOpen={newEntryPopoverOpen}
|
||||
closePopover={() => setNewEntryPopoverOpen(false)}
|
||||
isOpen={isNewEntryPopoverOpen}
|
||||
closePopover={() => setIsNewEntryPopoverOpen(false)}
|
||||
button={
|
||||
<EuiButton
|
||||
fill
|
||||
data-test-subj="knowledgeBaseNewEntryButton"
|
||||
iconSide="right"
|
||||
iconType="arrowDown"
|
||||
onClick={handleClickNewEntry}
|
||||
onClick={() => setIsNewEntryPopoverOpen((prevValue) => !prevValue)}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.observabilityAiAssistantManagement.knowledgeBaseTab.newEntryButtonLabel',
|
||||
|
@ -278,8 +290,8 @@ export function KnowledgeBaseTab() {
|
|||
icon="document"
|
||||
data-test-subj="knowledgeBaseSingleEntryContextMenuItem"
|
||||
onClick={() => {
|
||||
setNewEntryPopoverOpen(false);
|
||||
setFlyoutOpenType('singleEntry');
|
||||
setIsNewEntryPopoverOpen(false);
|
||||
setNewEntryFlyoutType('singleEntry');
|
||||
}}
|
||||
size="s"
|
||||
>
|
||||
|
@ -293,8 +305,8 @@ export function KnowledgeBaseTab() {
|
|||
icon="documents"
|
||||
data-test-subj="knowledgeBaseBulkImportContextMenuItem"
|
||||
onClick={() => {
|
||||
setNewEntryPopoverOpen(false);
|
||||
setFlyoutOpenType('bulkImport');
|
||||
setIsNewEntryPopoverOpen(false);
|
||||
setNewEntryFlyoutType('bulkImport');
|
||||
}}
|
||||
>
|
||||
{i18n.translate(
|
||||
|
@ -329,12 +341,18 @@ export function KnowledgeBaseTab() {
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
{flyoutOpenType === 'singleEntry' ? (
|
||||
<KnowledgeBaseEditManualEntryFlyout onClose={() => setFlyoutOpenType(undefined)} />
|
||||
{isEditUserInstructionFlyoutOpen ? (
|
||||
<KnowledgeBaseEditUserInstructionFlyout
|
||||
onClose={() => setIsEditUserInstructionFlyoutOpen(false)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{flyoutOpenType === 'bulkImport' ? (
|
||||
<KnowledgeBaseBulkImportFlyout onClose={() => setFlyoutOpenType(undefined)} />
|
||||
{newEntryFlyoutType === 'singleEntry' ? (
|
||||
<KnowledgeBaseEditManualEntryFlyout onClose={() => setNewEntryFlyoutType(undefined)} />
|
||||
) : null}
|
||||
|
||||
{newEntryFlyoutType === 'bulkImport' ? (
|
||||
<KnowledgeBaseBulkImportFlyout onClose={() => setNewEntryFlyoutType(undefined)} />
|
||||
) : null}
|
||||
|
||||
{selectedCategory ? (
|
||||
|
|
|
@ -11131,8 +11131,8 @@
|
|||
"xpack.apm.serviceIcons.service": "Service",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.architecture": "Architecture",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.availabilityZoneLabel": "{zones, plural, =0 {Zone de disponibilité} one {Zone de disponibilité} other {Zones de disponibilité}} ",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.faasTriggerTypeLabel": "{triggerTypes, plural, =0 {Type de déclencheur} one {Type de déclencheur} other {Types de déclencheurs}} ",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.functionNameLabel": "{functionNames, plural, =0 {Nom de fonction} one {Nom de fonction} other {Noms de fonction}} ",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.faasTriggerTypeLabel": "{triggerTypes, plural, =0 {Type de déclencheur} one {Type de déclencheur} other {Types de déclencheurs}} ",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.machineTypesLabel": "{machineTypes, plural, =0{Type de machine} one {Type de machine} other {Types de machines}} ",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.projectIdLabel": "ID de projet",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.providerLabel": "Fournisseur cloud",
|
||||
|
@ -27088,8 +27088,8 @@
|
|||
"xpack.maps.source.esSearch.descendingLabel": "décroissant",
|
||||
"xpack.maps.source.esSearch.extentFilterLabel": "Filtre dynamique pour les données de la zone de carte visible",
|
||||
"xpack.maps.source.esSearch.fieldNotFoundMsg": "Impossible de trouver \"{fieldName}\" dans le modèle d'indexation \"{indexPatternName}\".",
|
||||
"xpack.maps.source.esSearch.geofieldLabel": "Champ géospatial",
|
||||
"xpack.maps.source.esSearch.geoFieldLabel": "Champ géospatial",
|
||||
"xpack.maps.source.esSearch.geofieldLabel": "Champ géospatial",
|
||||
"xpack.maps.source.esSearch.geoFieldTypeLabel": "Type de champ géospatial",
|
||||
"xpack.maps.source.esSearch.indexOverOneLengthEditError": "Votre vue de données pointe vers plusieurs index. Un seul index est autorisé par vue de données.",
|
||||
"xpack.maps.source.esSearch.indexZeroLengthEditError": "Votre vue de données ne pointe vers aucun index.",
|
||||
|
@ -36554,8 +36554,8 @@
|
|||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldLessThanWarning": "Kibana ne permet qu'un maximum de {maxNumber} {maxNumber, plural, =1 {alerte} other {alertes}} par exécution de règle.",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "Nom obligatoire.",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "Ajouter un guide d'investigation sur les règles...",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText": "Ajouter le guide de configuration de règle...",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupHelpText": "Fournissez des instructions sur les conditions préalables à la règle, telles que les intégrations requises, les étapes de configuration et tout ce qui est nécessaire au bon fonctionnement de la règle.",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText": "Ajouter le guide de configuration de règle...",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupLabel": "Guide de configuration",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.tagFieldEmptyError": "Une balise ne doit pas être vide",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.threatIndicatorPathFieldEmptyError": "Le remplacement du préfixe d'indicateur ne peut pas être vide.",
|
||||
|
@ -42193,8 +42193,8 @@
|
|||
"xpack.slo.sloEmbeddable.config.sloSelector.placeholder": "Sélectionner un SLO",
|
||||
"xpack.slo.sloEmbeddable.displayName": "Aperçu du SLO",
|
||||
"xpack.slo.sloEmbeddable.overview.sloNotFoundText": "Le SLO a été supprimé. Vous pouvez supprimer sans risque le widget du tableau de bord.",
|
||||
"xpack.slo.sloGridItem.targetFlexItemLabel": "Cible {target}",
|
||||
"xpack.slo.sLOGridItem.targetFlexItemLabel": "Cible {target}",
|
||||
"xpack.slo.sloGridItem.targetFlexItemLabel": "Cible {target}",
|
||||
"xpack.slo.sloGroupConfiguration.customFiltersLabel": "Personnaliser le filtre",
|
||||
"xpack.slo.sloGroupConfiguration.customFiltersOptional": "Facultatif",
|
||||
"xpack.slo.sloGroupConfiguration.customFilterText": "Personnaliser le filtre",
|
||||
|
@ -43640,8 +43640,8 @@
|
|||
"xpack.stackConnectors.components.casesWebhookxpack.stackConnectors.components.casesWebhook.connectorTypeTitle": "Webhook - Données de gestion des cas",
|
||||
"xpack.stackConnectors.components.d3security.bodyCodeEditorAriaLabel": "Éditeur de code",
|
||||
"xpack.stackConnectors.components.d3security.bodyFieldLabel": "Corps",
|
||||
"xpack.stackConnectors.components.d3security.connectorTypeTitle": "Données D3",
|
||||
"xpack.stackConnectors.components.d3Security.connectorTypeTitle": "D3 Security",
|
||||
"xpack.stackConnectors.components.d3security.connectorTypeTitle": "Données D3",
|
||||
"xpack.stackConnectors.components.d3security.eventTypeFieldLabel": "Type d'événement",
|
||||
"xpack.stackConnectors.components.d3security.invalidActionText": "Nom d'action non valide.",
|
||||
"xpack.stackConnectors.components.d3security.requiredActionText": "L'action est requise.",
|
||||
|
|
|
@ -11120,8 +11120,8 @@
|
|||
"xpack.apm.serviceIcons.service": "サービス",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.architecture": "アーキテクチャー",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.availabilityZoneLabel": "{zones, plural, other {可用性ゾーン}} ",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.faasTriggerTypeLabel": "{triggerTypes, plural, other {トリガータイプ}} ",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.functionNameLabel": "{functionNames, plural, other {関数名}} ",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.faasTriggerTypeLabel": "{triggerTypes, plural, other {トリガータイプ}} ",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.machineTypesLabel": "{machineTypes, plural, other {コンピュータータイプ} }\n ",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.projectIdLabel": "プロジェクト ID",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.providerLabel": "クラウドプロバイダー",
|
||||
|
@ -27076,8 +27076,8 @@
|
|||
"xpack.maps.source.esSearch.descendingLabel": "降順",
|
||||
"xpack.maps.source.esSearch.extentFilterLabel": "マップの表示範囲でデータを動的にフィルタリング",
|
||||
"xpack.maps.source.esSearch.fieldNotFoundMsg": "インデックスパターン''{indexPatternName}''に''{fieldName}''が見つかりません。",
|
||||
"xpack.maps.source.esSearch.geofieldLabel": "地理空間フィールド",
|
||||
"xpack.maps.source.esSearch.geoFieldLabel": "地理空間フィールド",
|
||||
"xpack.maps.source.esSearch.geofieldLabel": "地理空間フィールド",
|
||||
"xpack.maps.source.esSearch.geoFieldTypeLabel": "地理空間フィールドタイプ",
|
||||
"xpack.maps.source.esSearch.indexOverOneLengthEditError": "データビューは複数のインデックスを参照しています。データビューごとに1つのインデックスのみが許可されています。",
|
||||
"xpack.maps.source.esSearch.indexZeroLengthEditError": "データビューはどのインデックスも参照していません。",
|
||||
|
@ -36537,8 +36537,8 @@
|
|||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldLessThanWarning": "Kibanaで許可される最大数は、1回の実行につき、{maxNumber} {maxNumber, plural, other {アラート}}です。",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "名前が必要です。",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "ルール調査ガイドを追加...",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText": "ルールセットアップガイドを追加...",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupHelpText": "必要な統合、構成ステップ、ルールが正常に動作するために必要な他のすべての項目といった、ルール前提条件に関する指示を入力します。",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText": "ルールセットアップガイドを追加...",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupLabel": "セットアップガイド",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.tagFieldEmptyError": "タグを空にすることはできません",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.threatIndicatorPathFieldEmptyError": "インジケータープレフィックスの無効化を空にすることはできません",
|
||||
|
@ -42176,8 +42176,8 @@
|
|||
"xpack.slo.sloEmbeddable.config.sloSelector.placeholder": "SLOを選択",
|
||||
"xpack.slo.sloEmbeddable.displayName": "SLO概要",
|
||||
"xpack.slo.sloEmbeddable.overview.sloNotFoundText": "SLOが削除されました。ウィジェットをダッシュボードから安全に削除できます。",
|
||||
"xpack.slo.sloGridItem.targetFlexItemLabel": "目標{target}",
|
||||
"xpack.slo.sLOGridItem.targetFlexItemLabel": "目標{target}",
|
||||
"xpack.slo.sloGridItem.targetFlexItemLabel": "目標{target}",
|
||||
"xpack.slo.sloGroupConfiguration.customFiltersLabel": "カスタムフィルター",
|
||||
"xpack.slo.sloGroupConfiguration.customFiltersOptional": "オプション",
|
||||
"xpack.slo.sloGroupConfiguration.customFilterText": "カスタムフィルター",
|
||||
|
@ -43619,8 +43619,8 @@
|
|||
"xpack.stackConnectors.components.casesWebhookxpack.stackConnectors.components.casesWebhook.connectorTypeTitle": "Webフック - ケース管理データ",
|
||||
"xpack.stackConnectors.components.d3security.bodyCodeEditorAriaLabel": "コードエディター",
|
||||
"xpack.stackConnectors.components.d3security.bodyFieldLabel": "本文",
|
||||
"xpack.stackConnectors.components.d3security.connectorTypeTitle": "D3データ",
|
||||
"xpack.stackConnectors.components.d3Security.connectorTypeTitle": "D3セキュリティ",
|
||||
"xpack.stackConnectors.components.d3security.connectorTypeTitle": "D3データ",
|
||||
"xpack.stackConnectors.components.d3security.eventTypeFieldLabel": "イベントタイプ",
|
||||
"xpack.stackConnectors.components.d3security.invalidActionText": "無効なアクション名です。",
|
||||
"xpack.stackConnectors.components.d3security.requiredActionText": "アクションが必要です。",
|
||||
|
|
|
@ -11139,8 +11139,8 @@
|
|||
"xpack.apm.serviceIcons.service": "服务",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.architecture": "架构",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.availabilityZoneLabel": "{zones, plural, other {可用性区域}} ",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.faasTriggerTypeLabel": "{triggerTypes, plural, other {触发类型}} ",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.functionNameLabel": "{functionNames, plural, other {功能名称}} ",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.faasTriggerTypeLabel": "{triggerTypes, plural, other {触发类型}} ",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.machineTypesLabel": "{machineTypes, plural, other {机器类型}} ",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.projectIdLabel": "项目 ID",
|
||||
"xpack.apm.serviceIcons.serviceDetails.cloud.providerLabel": "云服务提供商",
|
||||
|
@ -27108,8 +27108,8 @@
|
|||
"xpack.maps.source.esSearch.descendingLabel": "降序",
|
||||
"xpack.maps.source.esSearch.extentFilterLabel": "在可见地图区域中动态筛留数据",
|
||||
"xpack.maps.source.esSearch.fieldNotFoundMsg": "在索引模式“{indexPatternName}”中找不到“{fieldName}”。",
|
||||
"xpack.maps.source.esSearch.geofieldLabel": "地理空间字段",
|
||||
"xpack.maps.source.esSearch.geoFieldLabel": "地理空间字段",
|
||||
"xpack.maps.source.esSearch.geofieldLabel": "地理空间字段",
|
||||
"xpack.maps.source.esSearch.geoFieldTypeLabel": "地理空间字段类型",
|
||||
"xpack.maps.source.esSearch.indexOverOneLengthEditError": "您的数据视图指向多个索引。每个数据视图只允许一个索引。",
|
||||
"xpack.maps.source.esSearch.indexZeroLengthEditError": "您的数据视图未指向任何索引。",
|
||||
|
@ -36579,8 +36579,8 @@
|
|||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldLessThanWarning": "每次规则运行时,Kibana 最多只允许 {maxNumber} 个{maxNumber, plural, other {告警}}。",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "名称必填。",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "添加规则调查指南......",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText": "添加规则设置指南......",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupHelpText": "提供有关规则先决条件的说明,如所需集成、配置步骤,以及规则正常运行所需的任何其他内容。",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText": "添加规则设置指南......",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupLabel": "设置指南",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.tagFieldEmptyError": "标签不得为空",
|
||||
"xpack.securitySolution.detectionEngine.createRule.stepAboutRule.threatIndicatorPathFieldEmptyError": "指标前缀覆盖不得为空",
|
||||
|
@ -42220,8 +42220,8 @@
|
|||
"xpack.slo.sloEmbeddable.config.sloSelector.placeholder": "选择 SLO",
|
||||
"xpack.slo.sloEmbeddable.displayName": "SLO 概览",
|
||||
"xpack.slo.sloEmbeddable.overview.sloNotFoundText": "SLO 已删除。您可以放心从仪表板中删除小组件。",
|
||||
"xpack.slo.sloGridItem.targetFlexItemLabel": "目标 {target}",
|
||||
"xpack.slo.sLOGridItem.targetFlexItemLabel": "目标 {target}",
|
||||
"xpack.slo.sloGridItem.targetFlexItemLabel": "目标 {target}",
|
||||
"xpack.slo.sloGroupConfiguration.customFiltersLabel": "定制筛选",
|
||||
"xpack.slo.sloGroupConfiguration.customFiltersOptional": "可选",
|
||||
"xpack.slo.sloGroupConfiguration.customFilterText": "定制筛选",
|
||||
|
@ -43667,8 +43667,8 @@
|
|||
"xpack.stackConnectors.components.casesWebhookxpack.stackConnectors.components.casesWebhook.connectorTypeTitle": "Webhook - 案例管理数据",
|
||||
"xpack.stackConnectors.components.d3security.bodyCodeEditorAriaLabel": "代码编辑器",
|
||||
"xpack.stackConnectors.components.d3security.bodyFieldLabel": "正文",
|
||||
"xpack.stackConnectors.components.d3security.connectorTypeTitle": "D3 数据",
|
||||
"xpack.stackConnectors.components.d3Security.connectorTypeTitle": "D3 Security",
|
||||
"xpack.stackConnectors.components.d3security.connectorTypeTitle": "D3 数据",
|
||||
"xpack.stackConnectors.components.d3security.eventTypeFieldLabel": "事件类型",
|
||||
"xpack.stackConnectors.components.d3security.invalidActionText": "操作名称无效。",
|
||||
"xpack.stackConnectors.components.d3security.requiredActionText": "“操作”必填。",
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type OpenAI from 'openai';
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
import { createOpenAIConnector } from './utils/create_openai_connector';
|
||||
import { MachineLearningCommonAPIProvider } from '../../services/ml/common_api';
|
||||
|
@ -142,9 +141,7 @@ export default function (ftrContext: FtrProviderContext) {
|
|||
const conversationInterceptor = proxy.intercept(
|
||||
'conversation',
|
||||
(body) =>
|
||||
(JSON.parse(body) as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming).tools?.find(
|
||||
(fn) => fn.function.name === 'title_conversation'
|
||||
) === undefined
|
||||
body.tools?.find((fn) => fn.function.name === 'title_conversation') === undefined
|
||||
);
|
||||
|
||||
await pageObjects.searchPlayground.PlaygroundChatPage.sendQuestion();
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ToolingLog } from '@kbn/tooling-log';
|
||||
import { Agent } from 'supertest';
|
||||
|
||||
export async function deleteActionConnector({
|
||||
supertest,
|
||||
connectorId,
|
||||
log,
|
||||
}: {
|
||||
supertest: Agent;
|
||||
connectorId: string;
|
||||
log: ToolingLog;
|
||||
}) {
|
||||
try {
|
||||
await supertest
|
||||
.delete(`/api/actions/connector/${connectorId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204);
|
||||
} catch (e) {
|
||||
log.error(`Failed to delete action connector with id ${connectorId} due to: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createProxyActionConnector({
|
||||
log,
|
||||
supertest,
|
||||
port,
|
||||
}: {
|
||||
log: ToolingLog;
|
||||
supertest: Agent;
|
||||
port: number;
|
||||
}) {
|
||||
try {
|
||||
const res = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'OpenAI Proxy',
|
||||
connector_type_id: '.gen-ai',
|
||||
config: {
|
||||
apiProvider: 'OpenAI',
|
||||
apiUrl: `http://localhost:${port}`,
|
||||
},
|
||||
secrets: {
|
||||
apiKey: 'my-api-key',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const connectorId = res.body.id as string;
|
||||
return connectorId;
|
||||
} catch (e) {
|
||||
log.error(`Failed to create action connector due to: ${e}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
|
@ -51,6 +51,9 @@ export function createObservabilityAIAssistantAPIConfig({
|
|||
servers,
|
||||
services: {
|
||||
...services,
|
||||
getScopedApiClientForUsername: () => {
|
||||
return (username: string) => getScopedApiClient(kibanaServer, username);
|
||||
},
|
||||
apmSynthtraceEsClient: (context: InheritedFtrProviderContext) =>
|
||||
getApmSynthtraceEsClient(context, apmSynthtraceKibanaClient),
|
||||
observabilityAIAssistantAPIClient: async () => {
|
||||
|
|
|
@ -9,20 +9,26 @@ import { ToolingLog } from '@kbn/tooling-log';
|
|||
import getPort from 'get-port';
|
||||
import http, { type Server } from 'http';
|
||||
import { once, pull } from 'lodash';
|
||||
import OpenAI from 'openai';
|
||||
import { TITLE_CONVERSATION_FUNCTION_NAME } from '@kbn/observability-ai-assistant-plugin/server/service/client/operators/get_generated_title';
|
||||
import { createOpenAiChunk } from './create_openai_chunk';
|
||||
|
||||
type Request = http.IncomingMessage;
|
||||
type Response = http.ServerResponse<http.IncomingMessage> & { req: http.IncomingMessage };
|
||||
|
||||
type RequestHandler = (request: Request, response: Response, body: string) => void;
|
||||
type RequestHandler = (
|
||||
request: Request,
|
||||
response: Response,
|
||||
body: OpenAI.Chat.ChatCompletionCreateParamsNonStreaming
|
||||
) => void;
|
||||
|
||||
interface RequestInterceptor {
|
||||
name: string;
|
||||
when: (body: string) => boolean;
|
||||
when: (body: OpenAI.Chat.ChatCompletionCreateParamsNonStreaming) => boolean;
|
||||
}
|
||||
|
||||
export interface LlmResponseSimulator {
|
||||
body: string;
|
||||
body: OpenAI.Chat.ChatCompletionCreateParamsNonStreaming;
|
||||
status: (code: number) => Promise<void>;
|
||||
next: (
|
||||
msg:
|
||||
|
@ -40,10 +46,13 @@ export interface LlmResponseSimulator {
|
|||
|
||||
export class LlmProxy {
|
||||
server: Server;
|
||||
interval: NodeJS.Timeout;
|
||||
|
||||
interceptors: Array<RequestInterceptor & { handle: RequestHandler }> = [];
|
||||
|
||||
constructor(private readonly port: number, private readonly log: ToolingLog) {
|
||||
this.interval = setInterval(() => this.log.debug(`LLM proxy listening on port ${port}`), 1000);
|
||||
|
||||
this.server = http
|
||||
.createServer()
|
||||
.on('request', async (request, response) => {
|
||||
|
@ -62,7 +71,9 @@ export class LlmProxy {
|
|||
}
|
||||
}
|
||||
|
||||
response.writeHead(500, 'No interceptors found to handle request: ' + request.url);
|
||||
const errorMessage = `No interceptors found to handle request: ${request.method} ${request.url}`;
|
||||
this.log.error(`${errorMessage}. Messages: ${JSON.stringify(body.messages, null, 2)}`);
|
||||
response.writeHead(500, { errorMessage, messages: JSON.stringify(body.messages) });
|
||||
response.end();
|
||||
})
|
||||
.on('error', (error) => {
|
||||
|
@ -80,6 +91,8 @@ export class LlmProxy {
|
|||
}
|
||||
|
||||
close() {
|
||||
this.log.debug(`Closing LLM Proxy on port ${this.port}`);
|
||||
clearInterval(this.interval);
|
||||
this.server.close();
|
||||
}
|
||||
|
||||
|
@ -87,6 +100,27 @@ export class LlmProxy {
|
|||
return Promise.all(this.interceptors);
|
||||
}
|
||||
|
||||
interceptConversation({
|
||||
name = 'default_interceptor_conversation_name',
|
||||
response,
|
||||
}: {
|
||||
name?: string;
|
||||
response: string;
|
||||
}) {
|
||||
return this.intercept(name, (body) => !isFunctionTitleRequest(body), response);
|
||||
}
|
||||
|
||||
interceptConversationTitle(title: string) {
|
||||
return this.intercept('conversation_title', (body) => isFunctionTitleRequest(body), [
|
||||
{
|
||||
function_call: {
|
||||
name: TITLE_CONVERSATION_FUNCTION_NAME,
|
||||
arguments: JSON.stringify({ title }),
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
intercept<
|
||||
TResponseChunks extends Array<Record<string, unknown>> | string | undefined = undefined
|
||||
>(
|
||||
|
@ -175,11 +209,13 @@ export class LlmProxy {
|
|||
|
||||
export async function createLlmProxy(log: ToolingLog) {
|
||||
const port = await getPort({ port: getPort.makeRange(9000, 9100) });
|
||||
|
||||
log.debug(`Starting LLM Proxy on port ${port}`);
|
||||
return new LlmProxy(port, log);
|
||||
}
|
||||
|
||||
async function getRequestBody(request: http.IncomingMessage): Promise<string> {
|
||||
async function getRequestBody(
|
||||
request: http.IncomingMessage
|
||||
): Promise<OpenAI.Chat.ChatCompletionCreateParamsNonStreaming> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = '';
|
||||
|
||||
|
@ -188,7 +224,7 @@ async function getRequestBody(request: http.IncomingMessage): Promise<string> {
|
|||
});
|
||||
|
||||
request.on('close', () => {
|
||||
resolve(data);
|
||||
resolve(JSON.parse(data));
|
||||
});
|
||||
|
||||
request.on('error', (error) => {
|
||||
|
@ -196,3 +232,9 @@ async function getRequestBody(request: http.IncomingMessage): Promise<string> {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function isFunctionTitleRequest(body: OpenAI.Chat.ChatCompletionCreateParamsNonStreaming) {
|
||||
return (
|
||||
body.tools?.find((fn) => fn.function.name === TITLE_CONVERSATION_FUNCTION_NAME) !== undefined
|
||||
);
|
||||
}
|
||||
|
|
|
@ -15,9 +15,8 @@ import supertest from 'supertest';
|
|||
import { Subtract } from 'utility-types';
|
||||
import { format, UrlObject } from 'url';
|
||||
import { kbnTestConfig } from '@kbn/test';
|
||||
import { User } from './users/users';
|
||||
|
||||
export async function getScopedApiClient(kibanaServer: UrlObject, username: User['username']) {
|
||||
export function getScopedApiClient(kibanaServer: UrlObject, username: string) {
|
||||
const { password } = kbnTestConfig.getUrlParts();
|
||||
const baseUrlWithAuth = format({
|
||||
...kibanaServer,
|
||||
|
@ -27,6 +26,9 @@ export async function getScopedApiClient(kibanaServer: UrlObject, username: User
|
|||
return createObservabilityAIAssistantApiClient(supertest(baseUrlWithAuth));
|
||||
}
|
||||
|
||||
export type ObservabilityAIAssistantApiClient = ReturnType<
|
||||
typeof createObservabilityAIAssistantApiClient
|
||||
>;
|
||||
export function createObservabilityAIAssistantApiClient(st: supertest.Agent) {
|
||||
return <TEndpoint extends ObservabilityAIAssistantAPIEndpoint>(
|
||||
options: {
|
||||
|
|
|
@ -10,6 +10,7 @@ import { MessageRole, type Message } from '@kbn/observability-ai-assistant-plugi
|
|||
import { PassThrough } from 'stream';
|
||||
import { createLlmProxy, LlmProxy } from '../../common/create_llm_proxy';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { createProxyActionConnector, deleteActionConnector } from '../../common/action_connectors';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
@ -41,28 +42,12 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
|
||||
before(async () => {
|
||||
proxy = await createLlmProxy(log);
|
||||
|
||||
const response = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'OpenAI',
|
||||
connector_type_id: '.gen-ai',
|
||||
config: {
|
||||
apiProvider: 'OpenAI',
|
||||
apiUrl: `http://localhost:${proxy.getPort()}`,
|
||||
},
|
||||
secrets: {
|
||||
apiKey: 'my-api-key',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
connectorId = response.body.id;
|
||||
connectorId = await createProxyActionConnector({ supertest, log, port: proxy.getPort() });
|
||||
});
|
||||
|
||||
after(() => {
|
||||
after(async () => {
|
||||
proxy.close();
|
||||
await deleteActionConnector({ supertest, connectorId, log });
|
||||
});
|
||||
|
||||
it("returns a 4xx if the connector doesn't exist", async () => {
|
||||
|
@ -195,12 +180,5 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
`Token limit reached. Token limit is 8192, but the current conversation has 11036 tokens.`
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await supertest
|
||||
.delete(`/api/actions/connector/${connectorId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { Response } from 'supertest';
|
||||
import { MessageRole, type Message } from '@kbn/observability-ai-assistant-plugin/common';
|
||||
import { omit, pick } from 'lodash';
|
||||
import { PassThrough, Readable } from 'stream';
|
||||
import { PassThrough } from 'stream';
|
||||
import expect from '@kbn/expect';
|
||||
import {
|
||||
ChatCompletionChunkEvent,
|
||||
|
@ -17,11 +17,21 @@ import {
|
|||
StreamingChatResponseEvent,
|
||||
StreamingChatResponseEventType,
|
||||
} from '@kbn/observability-ai-assistant-plugin/common/conversation_complete';
|
||||
import type OpenAI from 'openai';
|
||||
import { ObservabilityAIAssistantScreenContextRequest } from '@kbn/observability-ai-assistant-plugin/common/types';
|
||||
import { createLlmProxy, LlmProxy, LlmResponseSimulator } from '../../common/create_llm_proxy';
|
||||
import {
|
||||
createLlmProxy,
|
||||
isFunctionTitleRequest,
|
||||
LlmProxy,
|
||||
LlmResponseSimulator,
|
||||
} from '../../common/create_llm_proxy';
|
||||
import { createOpenAiChunk } from '../../common/create_openai_chunk';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
decodeEvents,
|
||||
getConversationCreatedEvent,
|
||||
getConversationUpdatedEvent,
|
||||
} from '../conversations/helpers';
|
||||
import { createProxyActionConnector, deleteActionConnector } from '../../common/action_connectors';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
@ -105,33 +115,12 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
|
||||
before(async () => {
|
||||
proxy = await createLlmProxy(log);
|
||||
|
||||
const response = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'OpenAI Proxy',
|
||||
connector_type_id: '.gen-ai',
|
||||
config: {
|
||||
apiProvider: 'OpenAI',
|
||||
apiUrl: `http://localhost:${proxy.getPort()}`,
|
||||
},
|
||||
secrets: {
|
||||
apiKey: 'my-api-key',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
connectorId = response.body.id;
|
||||
connectorId = await createProxyActionConnector({ supertest, log, port: proxy.getPort() });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await supertest
|
||||
.delete(`/api/actions/connector/${connectorId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204);
|
||||
|
||||
proxy.close();
|
||||
await deleteActionConnector({ supertest, connectorId, log });
|
||||
});
|
||||
|
||||
it('returns a streaming response from the server', async () => {
|
||||
|
@ -390,20 +379,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
let conversationCreatedEvent: ConversationCreateEvent;
|
||||
let conversationUpdatedEvent: ConversationUpdateEvent;
|
||||
|
||||
function getConversationCreatedEvent(body: Readable | string) {
|
||||
const decodedEvents = decodeEvents(body);
|
||||
return decodedEvents.find(
|
||||
(event) => event.type === StreamingChatResponseEventType.ConversationCreate
|
||||
) as ConversationCreateEvent;
|
||||
}
|
||||
|
||||
function getConversationUpdatedEvent(body: Readable | string) {
|
||||
const decodedEvents = decodeEvents(body);
|
||||
return decodedEvents.find(
|
||||
(event) => event.type === StreamingChatResponseEventType.ConversationUpdate
|
||||
) as ConversationUpdateEvent;
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
proxy
|
||||
.intercept('conversation_title', (body) => isFunctionTitleRequest(body), [
|
||||
|
@ -511,16 +486,3 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
it.skip('executes a function', async () => {});
|
||||
});
|
||||
}
|
||||
|
||||
function decodeEvents(body: Readable | string) {
|
||||
return String(body)
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line) as StreamingChatResponseEvent);
|
||||
}
|
||||
|
||||
function isFunctionTitleRequest(body: string) {
|
||||
const parsedBody = JSON.parse(body) as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming;
|
||||
return parsedBody.tools?.find((fn) => fn.function.name === 'title_conversation') !== undefined;
|
||||
}
|
||||
|
|
|
@ -10,14 +10,13 @@ import expect from '@kbn/expect';
|
|||
import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
import { ELASTICSEARCH_FUNCTION_NAME } from '@kbn/observability-ai-assistant-plugin/server/functions/elasticsearch';
|
||||
import { LlmProxy } from '../../../common/create_llm_proxy';
|
||||
import { LlmProxy, createLlmProxy } from '../../../common/create_llm_proxy';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { getMessageAddedEvents, invokeChatCompleteWithFunctionRequest } from './helpers';
|
||||
import {
|
||||
createLLMProxyConnector,
|
||||
deleteLLMProxyConnector,
|
||||
getMessageAddedEvents,
|
||||
invokeChatCompleteWithFunctionRequest,
|
||||
} from './helpers';
|
||||
createProxyActionConnector,
|
||||
deleteActionConnector,
|
||||
} from '../../../common/action_connectors';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
@ -31,7 +30,12 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
let events: MessageAddEvent[];
|
||||
|
||||
before(async () => {
|
||||
({ connectorId, proxy } = await createLLMProxyConnector({ log, supertest }));
|
||||
proxy = await createLlmProxy(log);
|
||||
connectorId = await createProxyActionConnector({ supertest, log, port: proxy.getPort() });
|
||||
|
||||
// intercept the LLM request and return a fixed response
|
||||
proxy.intercept('conversation', () => true, 'Hello from LLM Proxy').completeAfterIntercept();
|
||||
|
||||
await generateApmData(apmSynthtraceEsClient);
|
||||
|
||||
const responseBody = await invokeChatCompleteWithFunctionRequest({
|
||||
|
@ -63,7 +67,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
after(async () => {
|
||||
await deleteLLMProxyConnector({ supertest, connectorId, proxy });
|
||||
proxy.close();
|
||||
await deleteActionConnector({ supertest, connectorId, log });
|
||||
await apmSynthtraceEsClient.clean();
|
||||
});
|
||||
|
||||
|
|
|
@ -11,11 +11,8 @@ import {
|
|||
MessageRole,
|
||||
StreamingChatResponseEvent,
|
||||
} from '@kbn/observability-ai-assistant-plugin/common';
|
||||
import { ToolingLog } from '@kbn/tooling-log';
|
||||
import { Agent } from 'supertest';
|
||||
import { Readable } from 'stream';
|
||||
import { CreateTest } from '../../../common/config';
|
||||
import { createLlmProxy, LlmProxy } from '../../../common/create_llm_proxy';
|
||||
|
||||
function decodeEvents(body: Readable | string) {
|
||||
return String(body)
|
||||
|
@ -31,57 +28,6 @@ export function getMessageAddedEvents(body: Readable | string) {
|
|||
);
|
||||
}
|
||||
|
||||
export async function createLLMProxyConnector({
|
||||
log,
|
||||
supertest,
|
||||
}: {
|
||||
log: ToolingLog;
|
||||
supertest: Agent;
|
||||
}) {
|
||||
const proxy = await createLlmProxy(log);
|
||||
|
||||
// intercept the LLM request and return a fixed response
|
||||
proxy.intercept('conversation', () => true, 'Hello from LLM Proxy').completeAfterIntercept();
|
||||
|
||||
const response = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'OpenAI Proxy',
|
||||
connector_type_id: '.gen-ai',
|
||||
config: {
|
||||
apiProvider: 'OpenAI',
|
||||
apiUrl: `http://localhost:${proxy.getPort()}`,
|
||||
},
|
||||
secrets: {
|
||||
apiKey: 'my-api-key',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
return {
|
||||
proxy,
|
||||
connectorId: response.body.id,
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteLLMProxyConnector({
|
||||
supertest,
|
||||
connectorId,
|
||||
proxy,
|
||||
}: {
|
||||
supertest: Agent;
|
||||
connectorId: string;
|
||||
proxy: LlmProxy;
|
||||
}) {
|
||||
await supertest
|
||||
.delete(`/api/actions/connector/${connectorId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204);
|
||||
|
||||
proxy.close();
|
||||
}
|
||||
|
||||
export async function invokeChatCompleteWithFunctionRequest({
|
||||
connectorId,
|
||||
observabilityAIAssistantAPIClient,
|
||||
|
|
|
@ -7,13 +7,13 @@
|
|||
|
||||
import { MessageRole } from '@kbn/observability-ai-assistant-plugin/common';
|
||||
import expect from '@kbn/expect';
|
||||
import { LlmProxy } from '../../../common/create_llm_proxy';
|
||||
import { LlmProxy, createLlmProxy } from '../../../common/create_llm_proxy';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { invokeChatCompleteWithFunctionRequest } from './helpers';
|
||||
import {
|
||||
createLLMProxyConnector,
|
||||
deleteLLMProxyConnector,
|
||||
invokeChatCompleteWithFunctionRequest,
|
||||
} from './helpers';
|
||||
createProxyActionConnector,
|
||||
deleteActionConnector,
|
||||
} from '../../../common/action_connectors';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
@ -26,7 +26,11 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
let connectorId: string;
|
||||
|
||||
before(async () => {
|
||||
({ connectorId, proxy } = await createLLMProxyConnector({ log, supertest }));
|
||||
proxy = await createLlmProxy(log);
|
||||
connectorId = await createProxyActionConnector({ supertest, log, port: proxy.getPort() });
|
||||
|
||||
// intercept the LLM request and return a fixed response
|
||||
proxy.intercept('conversation', () => true, 'Hello from LLM Proxy').completeAfterIntercept();
|
||||
|
||||
await invokeChatCompleteWithFunctionRequest({
|
||||
connectorId,
|
||||
|
@ -48,7 +52,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
after(async () => {
|
||||
await deleteLLMProxyConnector({ supertest, connectorId, proxy });
|
||||
proxy.close();
|
||||
await deleteActionConnector({ supertest, connectorId, log });
|
||||
});
|
||||
|
||||
it('persists entry in knowledge base', async () => {
|
||||
|
|
|
@ -8,10 +8,12 @@
|
|||
import expect from '@kbn/expect';
|
||||
import type { Agent as SuperTestAgent } from 'supertest';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { createProxyActionConnector, deleteActionConnector } from '../../common/action_connectors';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient');
|
||||
const supertest = getService('supertest');
|
||||
const log = getService('log');
|
||||
|
||||
describe('List connectors', () => {
|
||||
before(async () => {
|
||||
|
@ -39,21 +41,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it("returns the gen ai connector if it's been created", async () => {
|
||||
const connectorCreateResponse = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'OpenAI',
|
||||
connector_type_id: '.gen-ai',
|
||||
config: {
|
||||
apiProvider: 'OpenAI',
|
||||
apiUrl: 'http://localhost:9200',
|
||||
},
|
||||
secrets: {
|
||||
apiKey: 'my-api-key',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
const connectorId = await createProxyActionConnector({ supertest, log, port: 1234 });
|
||||
|
||||
const res = await observabilityAIAssistantAPIClient.editorUser({
|
||||
endpoint: 'GET /internal/observability_ai_assistant/connectors',
|
||||
|
@ -61,12 +49,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
|
||||
expect(res.body.length).to.be(1);
|
||||
|
||||
const connectorId = connectorCreateResponse.body.id;
|
||||
|
||||
await supertest
|
||||
.delete(`/api/actions/connector/${connectorId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204);
|
||||
await deleteActionConnector({ supertest, connectorId, log });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Readable } from 'stream';
|
||||
import {
|
||||
ConversationCreateEvent,
|
||||
ConversationUpdateEvent,
|
||||
StreamingChatResponseEvent,
|
||||
StreamingChatResponseEventType,
|
||||
} from '@kbn/observability-ai-assistant-plugin/common/conversation_complete';
|
||||
|
||||
export function decodeEvents(body: Readable | string) {
|
||||
return String(body)
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean)
|
||||
.map((line) => JSON.parse(line) as StreamingChatResponseEvent);
|
||||
}
|
||||
|
||||
export function getConversationCreatedEvent(body: Readable | string) {
|
||||
const decodedEvents = decodeEvents(body);
|
||||
const conversationCreatedEvent = decodedEvents.find(
|
||||
(event) => event.type === StreamingChatResponseEventType.ConversationCreate
|
||||
) as ConversationCreateEvent;
|
||||
|
||||
if (!conversationCreatedEvent) {
|
||||
throw new Error(
|
||||
`No conversation created event found: ${JSON.stringify(decodedEvents, null, 2)}`
|
||||
);
|
||||
}
|
||||
|
||||
return conversationCreatedEvent;
|
||||
}
|
||||
|
||||
export function getConversationUpdatedEvent(body: Readable | string) {
|
||||
const decodedEvents = decodeEvents(body);
|
||||
const conversationUpdatedEvent = decodedEvents.find(
|
||||
(event) => event.type === StreamingChatResponseEventType.ConversationUpdate
|
||||
) as ConversationUpdateEvent;
|
||||
|
||||
if (!conversationUpdatedEvent) {
|
||||
throw new Error(
|
||||
`No conversation created event found: ${JSON.stringify(decodedEvents, null, 2)}`
|
||||
);
|
||||
}
|
||||
|
||||
return conversationUpdatedEvent;
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Client } from '@elastic/elasticsearch';
|
||||
import { MachineLearningProvider } from '../../../api_integration/services/ml';
|
||||
import { SUPPORTED_TRAINED_MODELS } from '../../../functional/services/ml/api';
|
||||
|
||||
|
@ -23,9 +24,32 @@ export async function createKnowledgeBaseModel(ml: ReturnType<typeof MachineLear
|
|||
await ml.api.importTrainedModel(TINY_ELSER.name, TINY_ELSER.id, config);
|
||||
await ml.api.assureMlStatsIndexExists();
|
||||
}
|
||||
|
||||
export async function deleteKnowledgeBaseModel(ml: ReturnType<typeof MachineLearningProvider>) {
|
||||
await ml.api.stopTrainedModelDeploymentES(TINY_ELSER.id, true);
|
||||
await ml.api.deleteTrainedModelES(TINY_ELSER.id);
|
||||
await ml.api.cleanMlIndices();
|
||||
await ml.testResources.cleanMLSavedObjects();
|
||||
}
|
||||
|
||||
export async function clearKnowledgeBase(es: Client) {
|
||||
const KB_INDEX = '.kibana-observability-ai-assistant-kb-*';
|
||||
|
||||
return es.deleteByQuery({
|
||||
index: KB_INDEX,
|
||||
conflicts: 'proceed',
|
||||
query: { match_all: {} },
|
||||
refresh: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function clearConversations(es: Client) {
|
||||
const KB_INDEX = '.kibana-observability-ai-assistant-conversations-*';
|
||||
|
||||
return es.deleteByQuery({
|
||||
index: KB_INDEX,
|
||||
conflicts: 'proceed',
|
||||
query: { match_all: {} },
|
||||
refresh: true,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -7,19 +7,13 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { createKnowledgeBaseModel, deleteKnowledgeBaseModel } from './helpers';
|
||||
|
||||
interface KnowledgeBaseEntry {
|
||||
id: string;
|
||||
text: string;
|
||||
}
|
||||
import { clearKnowledgeBase, createKnowledgeBaseModel, deleteKnowledgeBaseModel } from './helpers';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const ml = getService('ml');
|
||||
const es = getService('es');
|
||||
|
||||
const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient');
|
||||
const KB_INDEX = '.kibana-observability-ai-assistant-kb-*';
|
||||
|
||||
describe('Knowledge base', () => {
|
||||
before(async () => {
|
||||
|
@ -106,10 +100,9 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
},
|
||||
})
|
||||
.expect(200);
|
||||
expect(
|
||||
res.body.entries.filter((entry: KnowledgeBaseEntry) => entry.id.startsWith('my-doc-id'))
|
||||
.length
|
||||
).to.eql(0);
|
||||
expect(res.body.entries.filter((entry) => entry.id.startsWith('my-doc-id')).length).to.eql(
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
it('returns 500 on delete not found', async () => {
|
||||
|
@ -126,20 +119,12 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
describe('when managing multiple entries', () => {
|
||||
before(async () => {
|
||||
es.deleteByQuery({
|
||||
index: KB_INDEX,
|
||||
conflicts: 'proceed',
|
||||
query: { match_all: {} },
|
||||
});
|
||||
await clearKnowledgeBase(es);
|
||||
});
|
||||
afterEach(async () => {
|
||||
es.deleteByQuery({
|
||||
index: KB_INDEX,
|
||||
conflicts: 'proceed',
|
||||
query: { match_all: {} },
|
||||
});
|
||||
await clearKnowledgeBase(es);
|
||||
});
|
||||
const knowledgeBaseEntries: KnowledgeBaseEntry[] = [
|
||||
const knowledgeBaseEntries = [
|
||||
{
|
||||
id: 'my_doc_a',
|
||||
text: 'My content a',
|
||||
|
@ -173,10 +158,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
},
|
||||
})
|
||||
.expect(200);
|
||||
expect(
|
||||
res.body.entries.filter((entry: KnowledgeBaseEntry) => entry.id.startsWith('my_doc'))
|
||||
.length
|
||||
).to.eql(3);
|
||||
expect(res.body.entries.filter((entry) => entry.id.startsWith('my_doc')).length).to.eql(3);
|
||||
});
|
||||
|
||||
it('allows sorting', async () => {
|
||||
|
@ -200,9 +182,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
})
|
||||
.expect(200);
|
||||
|
||||
const entries = res.body.entries.filter((entry: KnowledgeBaseEntry) =>
|
||||
entry.id.startsWith('my_doc')
|
||||
);
|
||||
const entries = res.body.entries.filter((entry) => entry.id.startsWith('my_doc'));
|
||||
expect(entries[0].id).to.eql('my_doc_c');
|
||||
expect(entries[1].id).to.eql('my_doc_b');
|
||||
expect(entries[2].id).to.eql('my_doc_a');
|
||||
|
@ -221,9 +201,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
})
|
||||
.expect(200);
|
||||
|
||||
const entriesAsc = resAsc.body.entries.filter((entry: KnowledgeBaseEntry) =>
|
||||
entry.id.startsWith('my_doc')
|
||||
);
|
||||
const entriesAsc = resAsc.body.entries.filter((entry) => entry.id.startsWith('my_doc'));
|
||||
expect(entriesAsc[0].id).to.eql('my_doc_a');
|
||||
expect(entriesAsc[1].id).to.eql('my_doc_b');
|
||||
expect(entriesAsc[2].id).to.eql('my_doc_c');
|
||||
|
|
|
@ -0,0 +1,319 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { kbnTestConfig } from '@kbn/test';
|
||||
import { sortBy } from 'lodash';
|
||||
import { Message, MessageRole } from '@kbn/observability-ai-assistant-plugin/common';
|
||||
import { CONTEXT_FUNCTION_NAME } from '@kbn/observability-ai-assistant-plugin/server/functions/context';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import {
|
||||
clearConversations,
|
||||
clearKnowledgeBase,
|
||||
createKnowledgeBaseModel,
|
||||
deleteKnowledgeBaseModel,
|
||||
} from './helpers';
|
||||
import { getConversationCreatedEvent } from '../conversations/helpers';
|
||||
import { LlmProxy, createLlmProxy } from '../../common/create_llm_proxy';
|
||||
import { createProxyActionConnector, deleteActionConnector } from '../../common/action_connectors';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient');
|
||||
const getScopedApiClientForUsername = getService('getScopedApiClientForUsername');
|
||||
const security = getService('security');
|
||||
const supertest = getService('supertest');
|
||||
const es = getService('es');
|
||||
const ml = getService('ml');
|
||||
const log = getService('log');
|
||||
|
||||
describe('Knowledge base user instructions', () => {
|
||||
const userJohn = 'john';
|
||||
|
||||
before(async () => {
|
||||
// create user
|
||||
const password = kbnTestConfig.getUrlParts().password!;
|
||||
await security.user.create(userJohn, { password, roles: ['editor'] });
|
||||
await createKnowledgeBaseModel(ml);
|
||||
|
||||
await observabilityAIAssistantAPIClient
|
||||
.editorUser({ endpoint: 'POST /internal/observability_ai_assistant/kb/setup' })
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await deleteKnowledgeBaseModel(ml);
|
||||
await security.user.delete(userJohn);
|
||||
await clearKnowledgeBase(es);
|
||||
await clearConversations(es);
|
||||
});
|
||||
|
||||
describe('when creating private and public user instructions', () => {
|
||||
before(async () => {
|
||||
await clearKnowledgeBase(es);
|
||||
|
||||
const promises = [
|
||||
{
|
||||
username: 'editor',
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
username: 'editor',
|
||||
isPublic: false,
|
||||
},
|
||||
{
|
||||
username: userJohn,
|
||||
isPublic: true,
|
||||
},
|
||||
{
|
||||
username: userJohn,
|
||||
isPublic: false,
|
||||
},
|
||||
].map(async ({ username, isPublic }) => {
|
||||
const visibility = isPublic ? 'Public' : 'Private';
|
||||
await getScopedApiClientForUsername(username)({
|
||||
endpoint: 'PUT /internal/observability_ai_assistant/kb/user_instructions',
|
||||
params: {
|
||||
body: {
|
||||
id: `${visibility.toLowerCase()}-doc-from-${username}`,
|
||||
text: `${visibility} user instruction from "${username}"`,
|
||||
public: isPublic,
|
||||
},
|
||||
},
|
||||
}).expect(200);
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
});
|
||||
|
||||
it('"editor" can retrieve their own private instructions and the public instruction', async () => {
|
||||
const res = await observabilityAIAssistantAPIClient.editorUser({
|
||||
endpoint: 'GET /internal/observability_ai_assistant/kb/user_instructions',
|
||||
});
|
||||
const instructions = res.body.userInstructions;
|
||||
|
||||
const sortByDocId = (data: any) => sortBy(data, 'doc_id');
|
||||
expect(sortByDocId(instructions)).to.eql(
|
||||
sortByDocId([
|
||||
{
|
||||
doc_id: 'private-doc-from-editor',
|
||||
public: false,
|
||||
text: 'Private user instruction from "editor"',
|
||||
},
|
||||
{
|
||||
doc_id: 'public-doc-from-editor',
|
||||
public: true,
|
||||
text: 'Public user instruction from "editor"',
|
||||
},
|
||||
{
|
||||
doc_id: 'public-doc-from-john',
|
||||
public: true,
|
||||
text: 'Public user instruction from "john"',
|
||||
},
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('"john" can retrieve their own private instructions and the public instruction', async () => {
|
||||
const res = await getScopedApiClientForUsername(userJohn)({
|
||||
endpoint: 'GET /internal/observability_ai_assistant/kb/user_instructions',
|
||||
});
|
||||
const instructions = res.body.userInstructions;
|
||||
|
||||
const sortByDocId = (data: any) => sortBy(data, 'doc_id');
|
||||
expect(sortByDocId(instructions)).to.eql(
|
||||
sortByDocId([
|
||||
{
|
||||
doc_id: 'public-doc-from-editor',
|
||||
public: true,
|
||||
text: 'Public user instruction from "editor"',
|
||||
},
|
||||
{
|
||||
doc_id: 'public-doc-from-john',
|
||||
public: true,
|
||||
text: 'Public user instruction from "john"',
|
||||
},
|
||||
{
|
||||
doc_id: 'private-doc-from-john',
|
||||
public: false,
|
||||
text: 'Private user instruction from "john"',
|
||||
},
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updating an existing user instructions', () => {
|
||||
before(async () => {
|
||||
await clearKnowledgeBase(es);
|
||||
|
||||
await observabilityAIAssistantAPIClient
|
||||
.editorUser({
|
||||
endpoint: 'PUT /internal/observability_ai_assistant/kb/user_instructions',
|
||||
params: {
|
||||
body: {
|
||||
id: 'doc-to-update',
|
||||
text: 'Initial text',
|
||||
public: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
await observabilityAIAssistantAPIClient
|
||||
.editorUser({
|
||||
endpoint: 'PUT /internal/observability_ai_assistant/kb/user_instructions',
|
||||
params: {
|
||||
body: {
|
||||
id: 'doc-to-update',
|
||||
text: 'Updated text',
|
||||
public: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it('updates the user instruction', async () => {
|
||||
const res = await observabilityAIAssistantAPIClient.editorUser({
|
||||
endpoint: 'GET /internal/observability_ai_assistant/kb/user_instructions',
|
||||
});
|
||||
const instructions = res.body.userInstructions;
|
||||
|
||||
expect(instructions).to.eql([
|
||||
{
|
||||
doc_id: 'doc-to-update',
|
||||
text: 'Updated text',
|
||||
public: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a user instruction exist and a conversation is created', () => {
|
||||
let proxy: LlmProxy;
|
||||
let connectorId: string;
|
||||
|
||||
const userInstructionText =
|
||||
'Be polite and use language that is easy to understand. Never disagree with the user.';
|
||||
|
||||
async function getConversationForUser(username: string) {
|
||||
const apiClient = getScopedApiClientForUsername(username);
|
||||
|
||||
// the user instruction is always created by "editor" user
|
||||
await observabilityAIAssistantAPIClient
|
||||
.editorUser({
|
||||
endpoint: 'PUT /internal/observability_ai_assistant/kb/user_instructions',
|
||||
params: {
|
||||
body: {
|
||||
id: 'private-instruction-about-language',
|
||||
text: userInstructionText,
|
||||
public: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const interceptPromises = [
|
||||
proxy.interceptConversationTitle('LLM-generated title').completeAfterIntercept(),
|
||||
proxy
|
||||
.interceptConversation({ name: 'conversation', response: 'I, the LLM, hear you!' })
|
||||
.completeAfterIntercept(),
|
||||
];
|
||||
|
||||
const messages: Message[] = [
|
||||
{
|
||||
'@timestamp': new Date().toISOString(),
|
||||
message: {
|
||||
role: MessageRole.System,
|
||||
content: 'You are a helpful assistant',
|
||||
},
|
||||
},
|
||||
{
|
||||
'@timestamp': new Date().toISOString(),
|
||||
message: {
|
||||
role: MessageRole.User,
|
||||
content: 'Today we will be testing user instructions!',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const createResponse = await apiClient({
|
||||
endpoint: 'POST /internal/observability_ai_assistant/chat/complete',
|
||||
params: {
|
||||
body: {
|
||||
messages,
|
||||
connectorId,
|
||||
persist: true,
|
||||
screenContexts: [],
|
||||
},
|
||||
},
|
||||
}).expect(200);
|
||||
|
||||
await proxy.waitForAllInterceptorsSettled();
|
||||
const conversationCreatedEvent = getConversationCreatedEvent(createResponse.body);
|
||||
const conversationId = conversationCreatedEvent.conversation.id;
|
||||
|
||||
const res = await apiClient({
|
||||
endpoint: 'GET /internal/observability_ai_assistant/conversation/{conversationId}',
|
||||
params: {
|
||||
path: {
|
||||
conversationId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// wait for all interceptors to be settled
|
||||
await Promise.all(interceptPromises);
|
||||
|
||||
const conversation = res.body;
|
||||
return conversation;
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
proxy = await createLlmProxy(log);
|
||||
connectorId = await createProxyActionConnector({ supertest, log, port: proxy.getPort() });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
proxy.close();
|
||||
await deleteActionConnector({ supertest, connectorId, log });
|
||||
});
|
||||
|
||||
it('adds the instruction to the system prompt', async () => {
|
||||
const conversation = await getConversationForUser('editor');
|
||||
const systemMessage = conversation.messages.find(
|
||||
(message) => message.message.role === MessageRole.System
|
||||
)!;
|
||||
expect(systemMessage.message.content).to.contain(userInstructionText);
|
||||
});
|
||||
|
||||
it('does not add the instruction to the context', async () => {
|
||||
const conversation = await getConversationForUser('editor');
|
||||
const contextMessage = conversation.messages.find(
|
||||
(message) => message.message.name === CONTEXT_FUNCTION_NAME
|
||||
);
|
||||
|
||||
// there should be no suggestions with the user instruction
|
||||
expect(contextMessage?.message.content).to.not.contain(userInstructionText);
|
||||
expect(contextMessage?.message.data).to.not.contain(userInstructionText);
|
||||
|
||||
// there should be no suggestions at all
|
||||
expect(JSON.parse(contextMessage?.message.data!).suggestions.length).to.be(0);
|
||||
});
|
||||
|
||||
it('does not add the instruction conversation for other users', async () => {
|
||||
const conversation = await getConversationForUser('john');
|
||||
const systemMessage = conversation.messages.find(
|
||||
(message) => message.message.role === MessageRole.System
|
||||
)!;
|
||||
|
||||
expect(systemMessage.message.content).to.not.contain(userInstructionText);
|
||||
expect(conversation.messages.length).to.be(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
|
@ -10,18 +10,23 @@ import {
|
|||
MessageRole,
|
||||
type Message,
|
||||
} from '@kbn/observability-ai-assistant-plugin/common';
|
||||
import { StreamingChatResponseEvent } from '@kbn/observability-ai-assistant-plugin/common/conversation_complete';
|
||||
import { type StreamingChatResponseEvent } from '@kbn/observability-ai-assistant-plugin/common/conversation_complete';
|
||||
import { pick } from 'lodash';
|
||||
import type OpenAI from 'openai';
|
||||
import { Response } from 'supertest';
|
||||
import { createLlmProxy, LlmProxy, LlmResponseSimulator } from '../../common/create_llm_proxy';
|
||||
import { type AdHocInstruction } from '@kbn/observability-ai-assistant-plugin/common/types';
|
||||
import {
|
||||
createLlmProxy,
|
||||
isFunctionTitleRequest,
|
||||
LlmProxy,
|
||||
LlmResponseSimulator,
|
||||
} from '../../common/create_llm_proxy';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { createProxyActionConnector, deleteActionConnector } from '../../common/action_connectors';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const log = getService('log');
|
||||
|
||||
const PUBLIC_COMPLETE_API_URL = `/api/observability_ai_assistant/chat/complete`;
|
||||
const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantAPIClient');
|
||||
|
||||
const messages: Message[] = [
|
||||
{
|
||||
|
@ -46,8 +51,8 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
|
||||
interface RequestOptions {
|
||||
actions?: Array<Pick<FunctionDefinition, 'name' | 'description' | 'parameters'>>;
|
||||
instructions?: string[];
|
||||
format?: 'openai';
|
||||
instructions?: AdHocInstruction[];
|
||||
format?: 'openai' | 'default';
|
||||
}
|
||||
|
||||
type ConversationSimulatorCallback = (
|
||||
|
@ -55,7 +60,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
) => Promise<void>;
|
||||
|
||||
async function getResponseBody(
|
||||
{ actions, instructions, format }: RequestOptions,
|
||||
{ actions, instructions, format = 'default' }: RequestOptions,
|
||||
conversationSimulatorCallback: ConversationSimulatorCallback
|
||||
) {
|
||||
const titleInterceptor = proxy.intercept('title', (body) => isFunctionTitleRequest(body));
|
||||
|
@ -65,30 +70,18 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
(body) => !isFunctionTitleRequest(body)
|
||||
);
|
||||
|
||||
const responsePromise = new Promise<Response>((resolve, reject) => {
|
||||
supertest
|
||||
.post(PUBLIC_COMPLETE_API_URL)
|
||||
.query({
|
||||
format,
|
||||
})
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set('elastic-api-version', '2023-10-31')
|
||||
.send({
|
||||
const responsePromise = observabilityAIAssistantAPIClient.adminUser({
|
||||
endpoint: 'POST /api/observability_ai_assistant/chat/complete 2023-10-31',
|
||||
params: {
|
||||
query: { format },
|
||||
body: {
|
||||
messages,
|
||||
connectorId,
|
||||
persist: true,
|
||||
actions,
|
||||
instructions,
|
||||
})
|
||||
.end((err, response) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
if (response.status !== 200) {
|
||||
return reject(new Error(`${response.status}: ${JSON.stringify(response.body)}`));
|
||||
}
|
||||
return resolve(response);
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const [conversationSimulator, titleSimulator] = await Promise.race([
|
||||
|
@ -141,32 +134,11 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
|
||||
before(async () => {
|
||||
proxy = await createLlmProxy(log);
|
||||
|
||||
const response = await supertest
|
||||
.post('/api/actions/connector')
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'OpenAI Proxy',
|
||||
connector_type_id: '.gen-ai',
|
||||
config: {
|
||||
apiProvider: 'OpenAI',
|
||||
apiUrl: `http://localhost:${proxy.getPort()}`,
|
||||
},
|
||||
secrets: {
|
||||
apiKey: 'my-api-key',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
connectorId = response.body.id;
|
||||
connectorId = await createProxyActionConnector({ supertest, log, port: proxy.getPort() });
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await supertest
|
||||
.delete(`/api/actions/connector/${connectorId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.expect(204);
|
||||
|
||||
await deleteActionConnector({ supertest, connectorId, log });
|
||||
proxy.close();
|
||||
});
|
||||
|
||||
|
@ -225,12 +197,17 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
describe('after adding an instruction', async () => {
|
||||
let body: string;
|
||||
let body: OpenAI.Chat.ChatCompletionCreateParamsNonStreaming;
|
||||
|
||||
before(async () => {
|
||||
await getEvents(
|
||||
{
|
||||
instructions: ['This is a random instruction'],
|
||||
instructions: [
|
||||
{
|
||||
text: 'This is a random instruction',
|
||||
instruction_type: 'user_instruction',
|
||||
},
|
||||
],
|
||||
},
|
||||
async (conversationSimulator) => {
|
||||
body = conversationSimulator.body;
|
||||
|
@ -244,9 +221,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
it('includes the instruction in the system message', async () => {
|
||||
const request = JSON.parse(body) as OpenAI.ChatCompletionCreateParams;
|
||||
|
||||
expect(request.messages[0].content).to.contain('This is a random instruction');
|
||||
expect(body.messages[0].content).to.contain('This is a random instruction');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -317,8 +292,3 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isFunctionTitleRequest(body: string) {
|
||||
const parsedBody = JSON.parse(body) as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming;
|
||||
return parsedBody.tools?.find((fn) => fn.function.name === 'title_conversation') !== undefined;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
import { apm, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import expect from '@kbn/expect';
|
||||
import moment from 'moment';
|
||||
import OpenAI from 'openai';
|
||||
import {
|
||||
createLlmProxy,
|
||||
LlmProxy,
|
||||
|
@ -123,11 +122,9 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte
|
|||
it('should show the contextual insight component on the APM error details page', async () => {
|
||||
await navigateToError();
|
||||
|
||||
const interceptor = proxy.intercept(
|
||||
'conversation',
|
||||
(body) => !isFunctionTitleRequest(body),
|
||||
'This error is nothing to worry about. Have a nice day!'
|
||||
);
|
||||
const interceptor = proxy.interceptConversation({
|
||||
response: 'This error is nothing to worry about. Have a nice day!',
|
||||
});
|
||||
|
||||
await openContextualInsights();
|
||||
|
||||
|
@ -141,8 +138,3 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte
|
|||
});
|
||||
});
|
||||
}
|
||||
|
||||
function isFunctionTitleRequest(body: string) {
|
||||
const parsedBody = JSON.parse(body) as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming;
|
||||
return parsedBody.functions?.find((fn) => fn.name === 'title_conversation') !== undefined;
|
||||
}
|
||||
|
|
|
@ -9,9 +9,9 @@ import expect from '@kbn/expect';
|
|||
import { MessageRole } from '@kbn/observability-ai-assistant-plugin/common';
|
||||
import { ChatFeedback } from '@kbn/observability-ai-assistant-plugin/public/analytics/schemas/chat_feedback';
|
||||
import { pick } from 'lodash';
|
||||
import type OpenAI from 'openai';
|
||||
import {
|
||||
createLlmProxy,
|
||||
isFunctionTitleRequest,
|
||||
LlmProxy,
|
||||
} from '../../../observability_ai_assistant_api_integration/common/create_llm_proxy';
|
||||
import { interceptRequest } from '../../common/intercept_request';
|
||||
|
@ -227,20 +227,15 @@ export default function ApiTest({ getService, getPageObjects }: FtrProviderConte
|
|||
|
||||
describe('and sending over some text', () => {
|
||||
before(async () => {
|
||||
const titleInterceptor = proxy.intercept(
|
||||
'title',
|
||||
(body) =>
|
||||
(
|
||||
JSON.parse(body) as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming
|
||||
).tools?.find((fn) => fn.function.name === 'title_conversation') !== undefined
|
||||
const titleInterceptor = proxy.intercept('title', (body) =>
|
||||
isFunctionTitleRequest(body)
|
||||
);
|
||||
|
||||
const conversationInterceptor = proxy.intercept(
|
||||
'conversation',
|
||||
(body) =>
|
||||
(
|
||||
JSON.parse(body) as OpenAI.Chat.ChatCompletionCreateParamsNonStreaming
|
||||
).tools?.find((fn) => fn.function.name === 'title_conversation') === undefined
|
||||
body.tools?.find((fn) => fn.function.name === 'title_conversation') ===
|
||||
undefined
|
||||
);
|
||||
|
||||
await testSubjects.setValue(ui.pages.conversations.chatInput, 'hello');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue