diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index b45d35a72e6d4..32ae21955c375 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -109,7 +109,7 @@ "xpack.observabilityLogsOverview": [ "platform/packages/shared/logs-overview/src/components" ], - "xpack.onechat": ["platform/plugins/shared/onechat"], + "xpack.onechat": ["platform/plugins/shared/onechat", "platform/packages/shared/onechat"], "xpack.osquery": [ "platform/plugins/shared/osquery" ], diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/chat/conversation.ts b/x-pack/platform/packages/shared/onechat/onechat-common/chat/conversation.ts index 9bfdad791e109..c4e8def6b3f52 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/chat/conversation.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/chat/conversation.ts @@ -39,10 +39,20 @@ export type ConversationRoundStepMixin { +export interface ToolCallWithResult { /** * Id of the tool call, as returned by the LLM */ @@ -55,15 +65,19 @@ export interface ToolCallWithResult { * Arguments the tool was called with. */ params: Record; + /** + * List of progress message which were send during that tool call + */ + progression?: ToolCallProgress[]; /** * Result of the tool */ - results: T; + results: ToolResult[]; } -export type ToolCallStep = ConversationRoundStepMixin< +export type ToolCallStep = ConversationRoundStepMixin< ConversationRoundStepType.toolCall, - ToolCallWithResult + ToolCallWithResult >; export const createToolCallStep = (toolCallWithResult: ToolCallWithResult): ToolCallStep => { @@ -96,17 +110,17 @@ export const isReasoningStep = (step: ConversationRoundStep): step is ReasoningS /** * Defines all possible types for round steps. */ -export type ConversationRoundStep = ToolCallStep | ReasoningStep; +export type ConversationRoundStep = ToolCallStep | ReasoningStep; /** * Represents a round in a conversation, containing all the information * related to this particular round. */ -export interface ConversationRound { +export interface ConversationRound { /** The user input that initiated the round */ input: RoundInput; /** List of intermediate steps before the end result, such as tool calls */ - steps: Array>; + steps: ConversationRoundStep[]; /** The final response from the assistant */ response: AssistantResponse; /** when tracing is enabled, contains the traceId associated with this round */ diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/chat/events.ts b/x-pack/platform/packages/shared/onechat/onechat-common/chat/events.ts index 6a9d50f9bd48e..adbfc0901e60c 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/chat/events.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/chat/events.ts @@ -11,6 +11,7 @@ import type { ConversationRound } from './conversation'; export enum ChatEventType { toolCall = 'tool_call', + toolProgress = 'tool_progress', toolResult = 'tool_result', reasoning = 'reasoning', messageChunk = 'message_chunk', @@ -39,6 +40,21 @@ export const isToolCallEvent = (event: OnechatEvent): event is Tool return event.type === ChatEventType.toolCall; }; +// Tool progress + +export interface ToolProgressEventData { + tool_call_id: string; + message: string; +} + +export type ToolProgressEvent = ChatEventBase; + +export const isToolProgressEvent = ( + event: OnechatEvent +): event is ToolProgressEvent => { + return event.type === ChatEventType.toolProgress; +}; + // Tool result export interface ToolResultEventData { @@ -158,6 +174,7 @@ export const isConversationUpdatedEvent = ( */ export type ChatAgentEvent = | ToolCallEvent + | ToolProgressEvent | ToolResultEvent | ReasoningEvent | MessageChunkEvent diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/chat/index.ts b/x-pack/platform/packages/shared/onechat/onechat-common/chat/index.ts index 973b9424e4e3a..1cbcd3024b41e 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/chat/index.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/chat/index.ts @@ -17,6 +17,7 @@ export { type ConversationRoundStep, type ReasoningStepData, type ReasoningStep, + type ToolCallProgress, ConversationRoundStepType, isToolCallStep, isReasoningStep, @@ -32,6 +33,8 @@ export { type ChatAgentEvent, type ToolResultEvent, type ToolResultEventData, + type ToolProgressEvent, + type ToolProgressEventData, type ToolCallEvent, type ToolCallEventData, type ReasoningEvent, @@ -44,6 +47,7 @@ export { type RoundCompleteEvent, isToolCallEvent, isToolResultEvent, + isToolProgressEvent, isReasoningEvent, isMessageChunkEvent, isMessageCompleteEvent, diff --git a/x-pack/platform/packages/shared/onechat/onechat-common/index.ts b/x-pack/platform/packages/shared/onechat/onechat-common/index.ts index b1ac5d4f385bc..75db8d35e1c95 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-common/index.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-common/index.ts @@ -98,6 +98,8 @@ export { type ConversationUpdatedEvent, type ConversationUpdatedEventData, type ChatAgentEvent, + type ToolProgressEvent, + type ToolProgressEventData, type ToolResultEvent, type ToolResultEventData, type ToolCallEvent, @@ -110,6 +112,7 @@ export { type MessageCompleteEvent, type RoundCompleteEventData, type RoundCompleteEvent, + type ToolCallProgress, isToolCallEvent, isToolResultEvent, isReasoningEvent, @@ -118,4 +121,5 @@ export { isRoundCompleteEvent, isConversationCreatedEvent, isConversationUpdatedEvent, + isToolProgressEvent, } from './chat'; diff --git a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/langchain/messages.test.ts b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/langchain/messages.test.ts index ee96d4da199f5..3702395a183c1 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/langchain/messages.test.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/langchain/messages.test.ts @@ -46,7 +46,6 @@ describe('extractToolReturn', () => { }, }, ], - runId: 'unknown', }); }); diff --git a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/langchain/messages.ts b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/langchain/messages.ts index 951f1cf7f0da9..ad6f1a3fa73fb 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/langchain/messages.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/langchain/messages.ts @@ -77,7 +77,6 @@ export const extractToolReturn = (message: ToolMessage): RunToolReturn => { if (content.startsWith('Error:')) { return { results: [{ type: ToolResultType.error, data: { message: content } }], - runId: 'unknown', }; } else { throw new Error(`No artifact attached to tool message: ${JSON.stringify(message)}`); diff --git a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/langchain/tools.ts b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/langchain/tools.ts index 4299ae38625d6..777f33fe5f075 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/langchain/tools.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/langchain/tools.ts @@ -9,7 +9,16 @@ import type { StructuredTool } from '@langchain/core/tools'; import { tool as toTool } from '@langchain/core/tools'; import type { Logger } from '@kbn/logging'; import type { KibanaRequest } from '@kbn/core-http-server'; -import type { ToolProvider, ExecutableTool, RunToolReturn } from '@kbn/onechat-server'; +import type { ChatAgentEvent } from '@kbn/onechat-common'; +import { ChatEventType } from '@kbn/onechat-common'; +import type { + AgentEventEmitterFn, + ExecutableTool, + OnechatToolEvent, + RunToolReturn, + ToolProvider, + ToolEventHandlerFn, +} from '@kbn/onechat-server'; import { ToolResultType } from '@kbn/onechat-common/tools/tool_result'; import type { ToolCall } from './messages'; @@ -30,10 +39,12 @@ export const toolsToLangchain = async ({ request, tools, logger, + sendEvent, }: { request: KibanaRequest; tools: ToolProvider | ExecutableTool[]; logger: Logger; + sendEvent?: AgentEventEmitterFn; }): Promise => { const allTools = Array.isArray(tools) ? tools : await tools.list({ request }); const onechatToLangchainIdMap = createToolIdMappings(allTools); @@ -41,7 +52,7 @@ export const toolsToLangchain = async ({ const convertedTools = await Promise.all( allTools.map((tool) => { const toolId = onechatToLangchainIdMap.get(tool.id); - return toolToLangchain({ tool, logger, toolId }); + return toolToLangchain({ tool, logger, toolId, sendEvent }); }) ); @@ -83,17 +94,28 @@ export const toolToLangchain = ({ tool, toolId, logger, + sendEvent, }: { tool: ExecutableTool; toolId?: string; logger: Logger; + sendEvent?: AgentEventEmitterFn; }): StructuredTool => { return toTool( - async (input): Promise<[string, RunToolReturn]> => { + async (input, config): Promise<[string, RunToolReturn]> => { + let onEvent: ToolEventHandlerFn | undefined; + if (sendEvent) { + const toolCallId = config.configurable?.tool_call_id ?? config.toolCall?.id ?? 'unknown'; + const convertEvent = getToolEventConverter({ toolCallId }); + onEvent = (event) => { + sendEvent(convertEvent(event)); + }; + } + try { logger.debug(`Calling tool ${tool.id} with params: ${JSON.stringify(input, null, 2)}`); - const toolReturn = await tool.execute({ toolParams: input }); - const content = JSON.stringify({ results: toolReturn.results }); // wrap in a results object to conform to bedrock format + const toolReturn = await tool.execute({ toolParams: input, onEvent }); + const content = JSON.stringify({ results: toolReturn.results }); logger.debug(`Tool ${tool.id} returned reply of length ${content.length}`); return [content, toolReturn]; } catch (e) { @@ -101,7 +123,6 @@ export const toolToLangchain = ({ logger.debug(e.stack); const errorToolReturn: RunToolReturn = { - runId: tool.id, results: [ { type: ToolResultType.error, @@ -141,3 +162,18 @@ function reverseMap(map: Map): Map { } return reversed; } + +const getToolEventConverter = ({ toolCallId }: { toolCallId: string }) => { + return (toolEvent: OnechatToolEvent): ChatAgentEvent => { + if (toolEvent.type === ChatEventType.toolProgress) { + return { + type: ChatEventType.toolProgress, + data: { + ...toolEvent.data, + tool_call_id: toolCallId, + }, + }; + } + throw new Error(`Invalid tool call type ${toolEvent.type}`); + }; +}; diff --git a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/graph.ts b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/graph.ts index 81eef66f28db0..24609a485d7a2 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/graph.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/graph.ts @@ -10,7 +10,7 @@ import type { BaseMessage } from '@langchain/core/messages'; import { isToolMessage } from '@langchain/core/messages'; import { messagesStateReducer } from '@langchain/langgraph'; import { ToolNode } from '@langchain/langgraph/prebuilt'; -import type { ScopedModel } from '@kbn/onechat-server'; +import type { ScopedModel, ToolEventEmitter } from '@kbn/onechat-server'; import type { ElasticsearchClient, Logger } from '@kbn/core/server'; import type { ToolResult } from '@kbn/onechat-common/tools'; import { ToolResultType } from '@kbn/onechat-common/tools'; @@ -19,6 +19,7 @@ import { indexExplorer } from '../index_explorer'; import { createNaturalLanguageSearchTool, createRelevanceSearchTool } from './inner_tools'; import { getSearchPrompt } from './prompts'; import type { SearchTarget } from './types'; +import { progressMessages } from './i18n'; const StateAnnotation = Annotation.Root({ // inputs @@ -45,19 +46,23 @@ export const createSearchToolGraph = ({ model, esClient, logger, + events, }: { model: ScopedModel; esClient: ElasticsearchClient; logger: Logger; + events?: ToolEventEmitter; }) => { const tools = [ - createRelevanceSearchTool({ model, esClient }), - createNaturalLanguageSearchTool({ model, esClient }), + createRelevanceSearchTool({ model, esClient, events }), + createNaturalLanguageSearchTool({ model, esClient, events }), ]; const toolNode = new ToolNode(tools); const selectAndValidateIndex = async (state: StateType) => { + events?.reportProgress(progressMessages.selectingTarget()); + const explorerRes = await indexExplorer({ nlQuery: state.nlQuery, indexPattern: state.targetPattern ?? '*', @@ -69,6 +74,8 @@ export const createSearchToolGraph = ({ if (explorerRes.resources.length > 0) { const selectedResource = explorerRes.resources[0]; + events?.reportProgress(progressMessages.selectedTarget(selectedResource.name)); + return { indexIsValid: true, searchTarget: { type: selectedResource.type, name: selectedResource.name }, @@ -90,6 +97,7 @@ export const createSearchToolGraph = ({ }); const callSearchAgent = async (state: StateType) => { + events?.reportProgress(progressMessages.resolvingSearchStrategy()); const response = await searchModel.invoke( getSearchPrompt({ nlQuery: state.nlQuery, searchTarget: state.searchTarget }) ); diff --git a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/i18n.ts b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/i18n.ts new file mode 100644 index 0000000000000..1fbd8e3f22c8b --- /dev/null +++ b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/i18n.ts @@ -0,0 +1,45 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const progressMessages = { + selectingTarget: () => { + return i18n.translate('xpack.onechat.tools.search.progress.selectingTarget', { + defaultMessage: 'Selecting the best target for this query', + }); + }, + selectedTarget: (target: string) => { + return i18n.translate('xpack.onechat.tools.search.progress.selectedTarget', { + defaultMessage: 'Selected "{target}" as the next search target', + values: { + target, + }, + }); + }, + resolvingSearchStrategy: () => { + return i18n.translate('xpack.onechat.tools.search.progress.searchStrategy', { + defaultMessage: 'Thinking about the search strategy to use', + }); + }, + performingRelevanceSearch: ({ term }: { term: string }) => { + return i18n.translate('xpack.onechat.tools.search.progress.performingRelevanceSearch', { + defaultMessage: 'Searching documents for "{term}"', + values: { + term, + }, + }); + }, + performingNlSearch: ({ query }: { query: string }) => { + return i18n.translate('xpack.onechat.tools.search.progress.performingTextSearch', { + defaultMessage: 'Generating an ES|QL for "{query}"', + values: { + query, + }, + }); + }, +}; diff --git a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/inner_tools.ts b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/inner_tools.ts index 19db0c6b8179f..1c7dbe743d7c3 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/inner_tools.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/inner_tools.ts @@ -8,7 +8,7 @@ import { z } from '@kbn/zod'; import { withExecuteToolSpan } from '@kbn/inference-tracing'; import { tool as toTool } from '@langchain/core/tools'; -import type { ScopedModel } from '@kbn/onechat-server'; +import type { ScopedModel, ToolEventEmitter } from '@kbn/onechat-server'; import type { ResourceResult, ToolResult } from '@kbn/onechat-common/tools'; import { ToolResultType } from '@kbn/onechat-common/tools'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; @@ -16,6 +16,7 @@ import { getToolResultId } from '@kbn/onechat-server/src/tools'; import { relevanceSearch } from '../relevance_search'; import { naturalLanguageSearch } from '../nl_search'; import type { MatchResult } from '../steps/perform_match_search'; +import { progressMessages } from './i18n'; const convertMatchResult = (result: MatchResult): ResourceResult => { return { @@ -38,9 +39,11 @@ export const relevanceSearchToolName = 'relevance_search'; export const createRelevanceSearchTool = ({ model, esClient, + events, }: { model: ScopedModel; esClient: ElasticsearchClient; + events?: ToolEventEmitter; }) => { return toTool( async ({ term, index, size }) => { @@ -48,6 +51,7 @@ export const createRelevanceSearchTool = ({ relevanceSearchToolName, { tool: { input: { term, index, size } } }, async () => { + events?.reportProgress(progressMessages.performingRelevanceSearch({ term })); const { results: rawResults } = await relevanceSearch({ target: index, term, @@ -87,9 +91,11 @@ export const naturalLanguageSearchToolName = 'natural_language_search'; export const createNaturalLanguageSearchTool = ({ model, esClient, + events, }: { model: ScopedModel; esClient: ElasticsearchClient; + events?: ToolEventEmitter; }) => { return toTool( async ({ query, index }) => { @@ -97,6 +103,7 @@ export const createNaturalLanguageSearchTool = ({ naturalLanguageSearchToolName, { tool: { input: { query, index } } }, async () => { + events?.reportProgress(progressMessages.performingNlSearch({ query })); const response = await naturalLanguageSearch({ nlQuery: query, target: index, diff --git a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/run_search_tool.ts b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/run_search_tool.ts index f3b27fcee1a28..cd3536904f9f9 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/run_search_tool.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tools/search/run_search_tool.ts @@ -9,6 +9,7 @@ import { withActiveInferenceSpan, ElasticGenAIAttributes } from '@kbn/inference- import type { ScopedModel } from '@kbn/onechat-server'; import type { Logger } from '@kbn/logging'; import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import type { ToolEventEmitter } from '@kbn/onechat-server'; import type { ToolResult } from '@kbn/onechat-common/tools'; import { ToolResultType } from '@kbn/onechat-common/tools'; import { createSearchToolGraph } from './graph'; @@ -19,14 +20,16 @@ export const runSearchTool = async ({ model, esClient, logger, + events, }: { nlQuery: string; index?: string; model: ScopedModel; esClient: ElasticsearchClient; logger: Logger; + events: ToolEventEmitter; }): Promise => { - const toolGraph = createSearchToolGraph({ model, esClient, logger }); + const toolGraph = createSearchToolGraph({ model, esClient, logger, events }); return withActiveInferenceSpan( 'SearchToolGraph', diff --git a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tsconfig.json b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tsconfig.json index 01192a7af5717..cadca411f1576 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tsconfig.json +++ b/x-pack/platform/packages/shared/onechat/onechat-genai-utils/tsconfig.json @@ -26,5 +26,6 @@ "@kbn/core", "@kbn/inference-tracing", "@kbn/es-errors", + "@kbn/i18n", ] } diff --git a/x-pack/platform/packages/shared/onechat/onechat-server/agents/index.ts b/x-pack/platform/packages/shared/onechat/onechat-server/agents/index.ts index 957d06e0607af..7d9b3e382b50f 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-server/agents/index.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-server/agents/index.ts @@ -11,6 +11,7 @@ export type { AgentHandlerReturn, AgentHandlerFn, AgentEventEmitter, + AgentEventEmitterFn, } from './provider'; export type { RunAgentFn, diff --git a/x-pack/platform/packages/shared/onechat/onechat-server/agents/runner.ts b/x-pack/platform/packages/shared/onechat/onechat-server/agents/runner.ts index fdb10a20d9481..28a99030f70d5 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-server/agents/runner.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-server/agents/runner.ts @@ -12,8 +12,6 @@ import type { AgentParams, AgentResponse } from './provider'; export interface RunAgentReturn { /** return from the agent */ result: AgentResponse; - /** ID of this run */ - runId: string; } /** diff --git a/x-pack/platform/packages/shared/onechat/onechat-server/index.ts b/x-pack/platform/packages/shared/onechat/onechat-server/index.ts index cb3809d3d95ae..eeb309ab387cc 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-server/index.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-server/index.ts @@ -34,8 +34,7 @@ export { type OnechatToolEvent, type ToolEventHandlerFn, type ToolEventEmitter, - type ToolEventEmitterFn, - type InternalToolEvent, + type ToolProgressEmitterFn, } from './src/events'; export type { AgentHandlerParams, @@ -48,6 +47,7 @@ export type { ScopedRunAgentFn, ScopedRunnerRunAgentParams, AgentEventEmitter, + AgentEventEmitterFn, RunAgentOnEventFn, } from './agents'; export { chatSystemIndex, chatSystemIndexPrefix } from './src/indices'; diff --git a/x-pack/platform/packages/shared/onechat/onechat-server/src/events.ts b/x-pack/platform/packages/shared/onechat/onechat-server/src/events.ts index 206f0f67c7ff5..e4e17177ad6cf 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-server/src/events.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-server/src/events.ts @@ -5,33 +5,33 @@ * 2.0. */ -import type { OnechatEvent } from '@kbn/onechat-common'; +import type { ChatEventType, ToolProgressEventData, ChatEventBase } from '@kbn/onechat-common'; -/** - * Public-facing events, as received by the API consumer. - */ -export type OnechatToolEvent< - TEventType extends string = string, - TData extends Record = Record -> = OnechatEvent; +export type InternalToolProgressEventData = Omit; + +export type InternalToolProgressEvent = ChatEventBase< + ChatEventType.toolProgress, + InternalToolProgressEventData +>; + +export type OnechatToolEvent = InternalToolProgressEvent; -/** - * Internal-facing events, as emitted by tool or agent owners. - */ -export type InternalToolEvent< - TEventType extends string = string, - TData extends Record = Record -> = OnechatToolEvent; /** * Event handler function to listen to run events during execution of tools, agents or other onechat primitives. */ export type ToolEventHandlerFn = (event: OnechatToolEvent) => void; /** - * Event emitter function, exposed from tool or agent runnable context. + * Progress event reporter, sending a tool progress event based on the provided progress info */ -export type ToolEventEmitterFn = (event: InternalToolEvent) => void; +export type ToolProgressEmitterFn = (progressMessage: string) => void; +/** + * Tool event emitter, exposed to tool handlers + */ export interface ToolEventEmitter { - emit: ToolEventEmitterFn; + /** + * Emit a tool progress event based on the provided progress text. + */ + reportProgress: ToolProgressEmitterFn; } diff --git a/x-pack/platform/packages/shared/onechat/onechat-server/src/runner.ts b/x-pack/platform/packages/shared/onechat/onechat-server/src/runner.ts index fcc1e542d1213..5881eecd9883e 100644 --- a/x-pack/platform/packages/shared/onechat/onechat-server/src/runner.ts +++ b/x-pack/platform/packages/shared/onechat/onechat-server/src/runner.ts @@ -21,10 +21,6 @@ export interface RunToolReturn { * The result value as returned by the tool. */ results: ToolResult[]; - /** - * ID of this run - */ - runId: string; } /** diff --git a/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/default/run_chat_agent.ts b/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/default/run_chat_agent.ts index 9c35f04187f02..c20a722c55e0c 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/default/run_chat_agent.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/default/run_chat_agent.ts @@ -6,10 +6,11 @@ */ import { v4 as uuidv4 } from 'uuid'; -import { from, filter, shareReplay } from 'rxjs'; +import { from, filter, shareReplay, merge, Subject, finalize } from 'rxjs'; import { isStreamEvent, toolsToLangchain } from '@kbn/onechat-genai-utils/langchain'; +import type { ChatAgentEvent } from '@kbn/onechat-common'; import { allToolsSelection } from '@kbn/onechat-common'; -import type { AgentHandlerContext } from '@kbn/onechat-server'; +import type { AgentHandlerContext, AgentEventEmitterFn } from '@kbn/onechat-server'; import { addRoundCompleteEvent, extractRound, @@ -53,10 +54,16 @@ export const runDefaultAgentMode: RunChatAgentFn = async ( request, }); + const manualEvents$ = new Subject(); + const eventEmitter: AgentEventEmitterFn = (event) => { + manualEvents$.next(event); + }; + const { tools: langchainTools, idMappings: toolIdMapping } = await toolsToLangchain({ tools: selectedTools, logger, request, + sendEvent: eventEmitter, }); const initialMessages = conversationToLangchainMessages({ @@ -89,13 +96,17 @@ export const runDefaultAgentMode: RunChatAgentFn = async ( } ); - const events$ = from(eventStream).pipe( + const graphEvents$ = from(eventStream).pipe( filter(isStreamEvent), convertGraphEvents({ graphName: chatAgentGraphName, toolIdMapping, logger, }), + finalize(() => manualEvents$.complete()) + ); + + const events$ = merge(graphEvents$, manualEvents$).pipe( addRoundCompleteEvent({ userInput: nextInput }), shareReplay() ); diff --git a/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/utils/add_round_complete_event.ts b/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/utils/add_round_complete_event.ts index 52e63299405cf..453e1cd23ee7c 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/utils/add_round_complete_event.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/agents/modes/utils/add_round_complete_event.ts @@ -22,11 +22,12 @@ import { isMessageCompleteEvent, isToolCallEvent, isToolResultEvent, + isToolProgressEvent, isReasoningEvent, } from '@kbn/onechat-common'; import { getCurrentTraceId } from '../../../../tracing'; -type SourceEvents = Exclude; +type SourceEvents = ChatAgentEvent; type StepEvents = ReasoningEvent | ToolCallEvent; @@ -70,20 +71,29 @@ const createRoundFromEvents = ({ input: RoundInput; }): ConversationRound => { const toolResults = events.filter(isToolResultEvent).map((event) => event.data); + const toolProgressions = events.filter(isToolProgressEvent).map((event) => event.data); const messages = events.filter(isMessageCompleteEvent).map((event) => event.data); const stepEvents = events.filter(isStepEvent); const eventToStep = (event: StepEvents): ConversationRoundStep => { if (isToolCallEvent(event)) { const toolCall = event.data; + const toolResult = toolResults.find( (result) => result.tool_call_id === toolCall.tool_call_id ); + const toolProgress = toolProgressions + .filter((progressEvent) => progressEvent.tool_call_id === toolCall.tool_call_id) + .map((progress) => ({ + message: progress.message, + })); + return { type: ConversationRoundStepType.toolCall, tool_call_id: toolCall.tool_call_id, tool_id: toolCall.tool_id, + progression: toolProgress, params: toolCall.params, results: toolResult?.results ?? [], }; diff --git a/x-pack/platform/plugins/shared/onechat/server/services/conversation/converters.test.ts b/x-pack/platform/plugins/shared/onechat/server/services/conversation/converters.test.ts new file mode 100644 index 0000000000000..f08b003535313 --- /dev/null +++ b/x-pack/platform/plugins/shared/onechat/server/services/conversation/converters.test.ts @@ -0,0 +1,199 @@ +/* + * 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 type { Conversation } from '@kbn/onechat-common'; +import { ConversationRoundStepType, ToolResultType } from '@kbn/onechat-common'; +import { fromEs, toEs, type Document as ConversationDocument } from './converters'; +import { expect } from '@kbn/scout'; + +describe('conversation model converters', () => { + const creationDate = '2024-09-04T06:44:17.944Z'; + const updateDate = '2025-08-04T06:44:19.123Z'; + + describe('fromEs', () => { + const documentBase = (): ConversationDocument => { + return { + _id: 'conv_id', + _source: { + agent_id: 'agent_id', + title: 'conv_title', + user_id: 'user_id', + user_name: 'user_name', + rounds: [ + { + input: { + message: 'some message', + }, + response: { + message: 'some response', + }, + steps: [], + }, + ], + created_at: creationDate, + updated_at: updateDate, + }, + }; + }; + + it('deserialize the conversation', () => { + const serialized = documentBase(); + + const deserialized = fromEs(serialized); + + expect(deserialized).toEqual({ + id: 'conv_id', + title: 'conv_title', + agent_id: 'agent_id', + user: { + id: 'user_id', + username: 'user_name', + }, + created_at: '2024-09-04T06:44:17.944Z', + updated_at: '2025-08-04T06:44:19.123Z', + + rounds: [ + { + input: { + message: 'some message', + }, + response: { + message: 'some response', + }, + steps: [], + }, + ], + }); + }); + + it('deserializes the steps', () => { + const serialized = documentBase(); + serialized._source!.rounds[0].steps = [ + { + type: ConversationRoundStepType.toolCall, + tool_call_id: 'tool_call_id', + tool_id: 'tool_id', + params: { + param1: 'value1', + }, + results: '[{"type":"other","data":{"someData":"someValue"}}]', + }, + { + type: ConversationRoundStepType.reasoning, + reasoning: 'reasoning', + }, + ]; + + const deserialized = fromEs(serialized); + + expect(deserialized.rounds[0].steps).toEqual([ + { + type: ConversationRoundStepType.toolCall, + tool_call_id: 'tool_call_id', + tool_id: 'tool_id', + params: { + param1: 'value1', + }, + progression: [], + results: [ + { + type: ToolResultType.other, + data: { someData: 'someValue' }, + }, + ], + }, + { + type: ConversationRoundStepType.reasoning, + reasoning: 'reasoning', + }, + ]); + }); + }); + + describe('toEs', () => { + const conversationBase = (): Conversation => { + return { + id: 'conv_id', + agent_id: 'agent_id', + user: { id: 'user_id', username: 'user_name' }, + title: 'conv_title', + created_at: creationDate, + updated_at: updateDate, + rounds: [ + { + input: { + message: 'some message', + }, + steps: [], + response: { + message: 'some response', + }, + }, + ], + }; + }; + + it('serializes the conversation', () => { + const conversation = conversationBase(); + const serialized = toEs(conversation); + + expect(serialized).toEqual({ + agent_id: 'agent_id', + title: 'conv_title', + user_id: 'user_id', + user_name: 'user_name', + rounds: [ + { + input: { + message: 'some message', + }, + response: { + message: 'some response', + }, + steps: [], + }, + ], + created_at: creationDate, + updated_at: updateDate, + }); + }); + + it('serializes the steps', () => { + const conversation = conversationBase(); + conversation.rounds[0].steps = [ + { + type: ConversationRoundStepType.toolCall, + tool_call_id: 'tool_call_id', + tool_id: 'tool_id', + params: { param1: 'value1' }, + results: [{ type: ToolResultType.other, data: { someData: 'someValue' } }], + }, + { + type: ConversationRoundStepType.reasoning, + reasoning: 'reasoning', + }, + ]; + const serialized = toEs(conversation); + + expect(serialized.rounds[0].steps).toEqual([ + { + type: ConversationRoundStepType.toolCall, + tool_call_id: 'tool_call_id', + tool_id: 'tool_id', + params: { + param1: 'value1', + }, + results: '[{"type":"other","data":{"someData":"someValue"}}]', + }, + { + type: ConversationRoundStepType.reasoning, + reasoning: 'reasoning', + }, + ]); + }); + }); +}); diff --git a/x-pack/platform/plugins/shared/onechat/server/services/conversation/converters.ts b/x-pack/platform/plugins/shared/onechat/server/services/conversation/converters.ts index eb2614c827e49..a3d49a55faf66 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/conversation/converters.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/conversation/converters.ts @@ -6,20 +6,24 @@ */ import type { GetResponse } from '@elastic/elasticsearch/lib/api/types'; -import type { ConversationWithoutRounds, ConversationRound } from '@kbn/onechat-common'; -import { type UserIdAndName, type Conversation } from '@kbn/onechat-common'; -import type { ToolResult } from '@kbn/onechat-common/tools/tool_result'; +import type { + Conversation, + ConversationRound, + ConversationRoundStep, + ConversationWithoutRounds, + UserIdAndName, +} from '@kbn/onechat-common'; +import { ConversationRoundStepType } from '@kbn/onechat-common'; import type { ConversationCreateRequest, ConversationUpdateRequest, } from '../../../common/conversations'; import type { ConversationProperties } from './storage'; +import type { PersistentConversationRound, PersistentConversationRoundStep } from './types'; export type Document = Pick, '_source' | '_id'>; -const convertBaseFromEs = ( - document: Pick, '_source' | '_id'> -) => { +const convertBaseFromEs = (document: Document) => { if (!document._source) { throw new Error('No source found on get conversation response'); } @@ -37,33 +41,40 @@ const convertBaseFromEs = ( }; }; -function serializeStepResults( - rounds: Array> -): Array> { - return rounds.map((round) => ({ +function serializeStepResults(rounds: ConversationRound[]): PersistentConversationRound[] { + return rounds.map((round) => ({ ...round, - steps: round.steps.map((step) => ({ - ...step, - results: 'results' in step ? JSON.stringify(step.results) : '[]', - })), + steps: round.steps.map((step) => { + if (step.type === ConversationRoundStepType.toolCall) { + return { + ...step, + results: JSON.stringify(step.results), + }; + } else { + return step; + } + }), })); } -function deserializeStepResults( - rounds: Array> -): Array> { - return rounds.map((round) => ({ +function deserializeStepResults(rounds: PersistentConversationRound[]): ConversationRound[] { + return rounds.map((round) => ({ ...round, - steps: round.steps.map((step) => ({ - ...step, - results: 'results' in step ? JSON.parse(step.results) : [], - })), + steps: round.steps.map((step) => { + if (step.type === ConversationRoundStepType.toolCall) { + return { + ...step, + results: JSON.parse(step.results), + progression: step.progression ?? [], + }; + } else { + return step; + } + }), })); } -export const fromEs = ( - document: Pick, '_source' | '_id'> -): Conversation => { +export const fromEs = (document: Document): Conversation => { const base = convertBaseFromEs(document); return { ...base, @@ -71,9 +82,7 @@ export const fromEs = ( }; }; -export const fromEsWithoutRounds = ( - document: Pick, '_source' | '_id'> -): ConversationWithoutRounds => { +export const fromEsWithoutRounds = (document: Document): ConversationWithoutRounds => { return convertBaseFromEs(document); }; diff --git a/x-pack/platform/plugins/shared/onechat/server/services/conversation/storage.ts b/x-pack/platform/plugins/shared/onechat/server/services/conversation/storage.ts index 3ba3b784fa844..68fdd4b07bf1d 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/conversation/storage.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/conversation/storage.ts @@ -8,8 +8,8 @@ import type { Logger, ElasticsearchClient } from '@kbn/core/server'; import type { IndexStorageSettings } from '@kbn/storage-adapter'; import { StorageIndexAdapter, types } from '@kbn/storage-adapter'; -import type { ConversationRound } from '@kbn/onechat-common'; import { chatSystemIndex } from '@kbn/onechat-server'; +import type { PersistentConversationRound } from './types'; export const conversationIndexName = chatSystemIndex('conversations'); @@ -35,7 +35,7 @@ export interface ConversationProperties { title: string; created_at: string; updated_at: string; - rounds: Array>; + rounds: PersistentConversationRound[]; } export type ConversationStorageSettings = typeof storageSettings; diff --git a/x-pack/platform/plugins/shared/onechat/server/services/conversation/types.ts b/x-pack/platform/plugins/shared/onechat/server/services/conversation/types.ts new file mode 100644 index 0000000000000..57d9dad73d042 --- /dev/null +++ b/x-pack/platform/plugins/shared/onechat/server/services/conversation/types.ts @@ -0,0 +1,42 @@ +/* + * 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 type { + ConversationRound, + ToolCallWithResult, + ConversationRoundStepMixin, + ReasoningStep, + ConversationRoundStepType, +} from '@kbn/onechat-common/chat/conversation'; + +/** + * A version of ToolCallWithResult where 'results' is a serialized string. + */ +export type PersistentToolCallWithResult = Omit & { + results: string; +}; + +/** + * A version of ToolCallStep suitable for persistence. + */ +export type PersistentToolCallStep = ConversationRoundStepMixin< + ConversationRoundStepType.toolCall, + PersistentToolCallWithResult +>; + +/** + * A union of all possible persistent step types. + */ +export type PersistentConversationRoundStep = PersistentToolCallStep | ReasoningStep; + +/** + * Represents a conversation round suitable for persistence, with tool + * call results serialized to a string. + */ +export type PersistentConversationRound = Omit & { + steps: PersistentConversationRoundStep[]; +}; diff --git a/x-pack/platform/plugins/shared/onechat/server/services/runner/run_agent.ts b/x-pack/platform/plugins/shared/onechat/server/services/runner/run_agent.ts index 6d02d2f544edd..1eec82d149d33 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/runner/run_agent.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/runner/run_agent.ts @@ -71,7 +71,6 @@ export const runAgent = async ({ }); return { - runId: manager.context.runId, result: agentResult.result, }; }; diff --git a/x-pack/platform/plugins/shared/onechat/server/services/runner/run_tool.test.ts b/x-pack/platform/plugins/shared/onechat/server/services/runner/run_tool.test.ts index 96e078c3bc0e9..b2149bfb17699 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/runner/run_tool.test.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/runner/run_tool.test.ts @@ -125,7 +125,6 @@ describe('runTool', () => { }); expect(results).toEqual({ - runId: expect.any(String), results: [{ type: ToolResultType.other, data: { test: true, over: 9000 } }], }); }); @@ -174,10 +173,7 @@ describe('runTool', () => { }; tool.handler.mockImplementation((toolParams, { events }) => { - events.emit({ - type: 'test-event', - data: { foo: 'bar' }, - }); + events.reportProgress('some progress'); return { results: [{ type: ToolResultType.other, data: { foo: 'bar' } }] }; }); @@ -188,9 +184,9 @@ describe('runTool', () => { expect(emittedEvents).toHaveLength(1); expect(emittedEvents[0]).toEqual({ - type: 'test-event', + type: 'tool_progress', data: { - foo: 'bar', + message: 'some progress', }, }); }); diff --git a/x-pack/platform/plugins/shared/onechat/server/services/runner/run_tool.ts b/x-pack/platform/plugins/shared/onechat/server/services/runner/run_tool.ts index f42d9d188300a..4467760af3413 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/runner/run_tool.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/runner/run_tool.ts @@ -63,7 +63,6 @@ export const runTool = async >({ ); return { - runId: manager.context.runId, ...toolReturn, }; }; diff --git a/x-pack/platform/plugins/shared/onechat/server/services/runner/runner.test.ts b/x-pack/platform/plugins/shared/onechat/server/services/runner/runner.test.ts index e4f6b02c2cf4f..e68327b3a38f6 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/runner/runner.test.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/runner/runner.test.ts @@ -77,7 +77,6 @@ describe('Onechat runner', () => { expect(tool.handler).toHaveBeenCalledWith(params.toolParams, expect.any(Object)); expect(response).toEqual({ - runId: expect.any(String), results: [{ type: ToolResultType.other, data: { someProp: 'someValue' } }], }); }); @@ -102,7 +101,6 @@ describe('Onechat runner', () => { expect(tool.handler).toHaveBeenCalledWith(params.toolParams, expect.any(Object)); expect(response).toEqual({ - runId: expect.any(String), results: [{ type: ToolResultType.other, data: { someProp: 'someValue' } }], }); }); @@ -156,7 +154,6 @@ describe('Onechat runner', () => { ); expect(response).toEqual({ - runId: expect.any(String), result: 'someResult', }); }); @@ -185,7 +182,6 @@ describe('Onechat runner', () => { ); expect(response).toEqual({ - runId: expect.any(String), result: 'someResult', }); }); diff --git a/x-pack/platform/plugins/shared/onechat/server/services/runner/utils/events.test.ts b/x-pack/platform/plugins/shared/onechat/server/services/runner/utils/events.test.ts index 98342fcd3cf0e..0f0e6380bea80 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/runner/utils/events.test.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/runner/utils/events.test.ts @@ -6,7 +6,7 @@ */ import { createToolEventEmitter, createAgentEventEmitter } from './events'; -import type { InternalToolEvent, RunContext } from '@kbn/onechat-server'; +import type { RunContext } from '@kbn/onechat-server'; import { ChatEventType, type MessageChunkEvent } from '@kbn/onechat-common'; describe('Event utilities', () => { @@ -23,16 +23,11 @@ describe('Event utilities', () => { context, }); - const testEvent: InternalToolEvent = { - type: 'test-event', - data: { foo: 'bar' }, - }; - - emitter.emit(testEvent); + emitter.reportProgress('progress'); expect(mockEventHandler).toHaveBeenCalledWith({ - type: 'test-event', - data: { foo: 'bar' }, + type: ChatEventType.toolProgress, + data: { message: 'progress' }, }); }); }); diff --git a/x-pack/platform/plugins/shared/onechat/server/services/runner/utils/events.ts b/x-pack/platform/plugins/shared/onechat/server/services/runner/utils/events.ts index d77f0f80ed41f..f470dadb00c14 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/runner/utils/events.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/runner/utils/events.ts @@ -6,12 +6,14 @@ */ import type { - RunContext, - ToolEventHandlerFn, - ToolEventEmitter, AgentEventEmitter, RunAgentOnEventFn, + RunContext, + ToolEventEmitter, + ToolEventHandlerFn, } from '@kbn/onechat-server'; +import type { InternalToolProgressEvent } from '@kbn/onechat-server/src/events'; +import { ChatEventType } from '@kbn/onechat-common'; /** * Creates a run event emitter sending events to the provided event handler. @@ -45,16 +47,28 @@ export const createToolEventEmitter = ({ context: RunContext; }): ToolEventEmitter => { if (eventHandler === undefined) { - return createNoopEventEmitter(); + return createNoopToolEventEmitter(); } return { - emit: (event) => { + reportProgress: (progressMessage) => { + const event: InternalToolProgressEvent = { + type: ChatEventType.toolProgress, + data: { + message: progressMessage, + }, + }; eventHandler(event); }, }; }; +const createNoopToolEventEmitter = () => { + return { + reportProgress: () => {}, + }; +}; + const createNoopEventEmitter = () => { return { emit: () => {}, diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/search.ts b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/search.ts index 7337f8f13cbd0..b284c46ee6657 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/search.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/tools/builtin/definitions/search.ts @@ -43,13 +43,17 @@ Note: know about the index and fields you want to search on, e.g. if the user explicitly specified it. `, schema: searchSchema, - handler: async ({ query: nlQuery, index = '*' }, { esClient, modelProvider, logger }) => { + handler: async ( + { query: nlQuery, index = '*' }, + { esClient, modelProvider, logger, events } + ) => { logger.debug(`search tool called with query: ${nlQuery}, index: ${index}`); const results = await runSearchTool({ nlQuery, index, esClient: esClient.asCurrentUser, model: await modelProvider.getDefaultModel(), + events, logger, }); return { results }; diff --git a/x-pack/platform/plugins/shared/onechat/server/services/tools/persisted/tool_types/index_search/to_tool_definition.ts b/x-pack/platform/plugins/shared/onechat/server/services/tools/persisted/tool_types/index_search/to_tool_definition.ts index ae6b4180d5ec7..52fb825502cd8 100644 --- a/x-pack/platform/plugins/shared/onechat/server/services/tools/persisted/tool_types/index_search/to_tool_definition.ts +++ b/x-pack/platform/plugins/shared/onechat/server/services/tools/persisted/tool_types/index_search/to_tool_definition.ts @@ -29,13 +29,14 @@ export function toToolDefinition( tags, configuration, schema: searchSchema, - handler: async ({ nlQuery }, { esClient, modelProvider, logger }) => { + handler: async ({ nlQuery }, { esClient, modelProvider, logger, events }) => { const { pattern } = configuration; const results = await runSearchTool({ nlQuery, index: pattern, esClient: esClient.asCurrentUser, model: await modelProvider.getDefaultModel(), + events, logger, }); return { results }; diff --git a/x-pack/platform/plugins/shared/onechat/tsconfig.json b/x-pack/platform/plugins/shared/onechat/tsconfig.json index 2553865925d0a..83ab690e99f6f 100644 --- a/x-pack/platform/plugins/shared/onechat/tsconfig.json +++ b/x-pack/platform/plugins/shared/onechat/tsconfig.json @@ -63,5 +63,6 @@ "@kbn/data-views-plugin", "@kbn/expressions-plugin", "@kbn/lens-plugin", + "@kbn/scout", ] }