mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[onechat] Introduce plugin and tool registry (#220889)
## Summary Implements the onechat tool registry RFC. Fix https://github.com/elastic/search-team/issues/9938 Fix https://github.com/elastic/search-team/issues/10019 This PR introduces the following artifacts: **plugins:** - `onechat` **packages:** - `@kbn/onechat-common` - `@kbn/onechat-server` - `@kbn/onechat-browser` ## Tool APIs overview ### Registering a tool ```ts class MyPlugin { setup(core: CoreSetup, { onechat }: { onechat: OnechatPluginSetup }) { onechat.tools.register({ id: 'my_tool', name: 'My Tool', description: 'My very first tool', meta: { tags: ['foo', 'bar'], }, schema: z.object({ someNumber: z.number().describe('Some random number'), }), handler: ({ someNumber }, context) => { return 42 + someNumber; }, }); } } ``` ### Executing a tool Using the `execute` API: ```ts const { result } = await onechat.tools.execute({ toolId: 'my_tool', toolParams: { someNumber: 9000 }, request, }); ``` Using a tool descriptor: ```ts const tool = await onechat.tools.registry.get({ toolId: 'my_tool', request }); const { result } = await tool.execute({ toolParams: { someNumber: 9000 } }); ``` With error handling: ```ts import { isToolNotFoundError } from '@kbn/onechat-common'; try { const { result } = await onechat.tools.execute({ toolId: 'my_tool', toolParams: { someNumber: 9000 }, request, }); } catch (e) { if (isToolNotFoundError(e)) { throw new Error(`run ${e.meta.runId} failed because tool was not found`); } } ``` ### Listing tools ```ts const tools = await onechat.tools.registry.list({ request }); ``` *More details and example in the plugin's readme.* ### What is **not** included in this PR: - tool access control / authorization - we have a dedicated RFC - dynamic tool registration / permissions checks part/of depends on the authorization RFC - feature / capabilities - will come with browser-side and HTTP APIs - fully defining tool meta - hard to do now - filter parameters for the tool list API - depends on the meta being defined *Those will be follow-ups*. Everything else from the RFC should be there. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
6d5acfca0d
commit
f3b4975c8c
82 changed files with 4009 additions and 0 deletions
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
|
@ -854,6 +854,9 @@ x-pack/platform/packages/shared/ml/random_sampler_utils @elastic/ml-ui
|
|||
x-pack/platform/packages/shared/ml/response_stream @elastic/ml-ui
|
||||
x-pack/platform/packages/shared/ml/runtime_field_utils @elastic/ml-ui
|
||||
x-pack/platform/packages/shared/ml/trained_models_utils @elastic/ml-ui
|
||||
x-pack/platform/packages/shared/onechat/onechat-browser @elastic/workchat-eng
|
||||
x-pack/platform/packages/shared/onechat/onechat-common @elastic/workchat-eng
|
||||
x-pack/platform/packages/shared/onechat/onechat-server @elastic/workchat-eng
|
||||
x-pack/platform/packages/shared/security/api_key_management @elastic/kibana-security
|
||||
x-pack/platform/packages/shared/security/form_components @elastic/kibana-security
|
||||
x-pack/platform/packages/shared/security/plugin_types_common @elastic/kibana-security
|
||||
|
@ -932,6 +935,7 @@ x-pack/platform/plugins/shared/maps @elastic/kibana-presentation
|
|||
x-pack/platform/plugins/shared/ml @elastic/ml-ui
|
||||
x-pack/platform/plugins/shared/notifications @elastic/appex-sharedux
|
||||
x-pack/platform/plugins/shared/observability_ai_assistant @elastic/obs-ai-assistant
|
||||
x-pack/platform/plugins/shared/onechat @elastic/workchat-eng
|
||||
x-pack/platform/plugins/shared/osquery @elastic/security-defend-workflows
|
||||
x-pack/platform/plugins/shared/rule_registry @elastic/response-ops @elastic/obs-ux-management-team
|
||||
x-pack/platform/plugins/shared/saved_objects_tagging @elastic/appex-sharedux
|
||||
|
|
|
@ -180,6 +180,7 @@ mapped_pages:
|
|||
| [observabilityLogsExplorer](https://github.com/elastic/kibana/blob/main/x-pack/solutions/observability/plugins/observability_logs_explorer/README.md) | This plugin provides an app based on the LogsExplorer component from the logs_explorer plugin, but adds observability-specific affordances. |
|
||||
| [observabilityOnboarding](https://github.com/elastic/kibana/blob/main/x-pack/solutions/observability/plugins/observability_onboarding/README.md) | This plugin provides an onboarding framework for observability solutions: Logs and APM. |
|
||||
| [observabilityShared](https://github.com/elastic/kibana/blob/main/x-pack/solutions/observability/plugins/observability_shared/README.md) | A plugin that contains components and utilities shared by all Observability plugins. |
|
||||
| [onechat](https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/shared/onechat/README.md) | Home of the workchat framework. |
|
||||
| [osquery](https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/shared/osquery/README.md) | This plugin adds extended support to Security Solution Fleet Osquery integration |
|
||||
| [painlessLab](https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/private/painless_lab/README.md) | This plugin helps users learn how to use the Painless scripting language. |
|
||||
| [productDocBase](https://github.com/elastic/kibana/blob/main/x-pack/platform/plugins/shared/ai_infra/product_doc_base/README.md) | This plugin contains the product documentation base service. |
|
||||
|
|
|
@ -726,6 +726,10 @@
|
|||
"@kbn/observability-utils-common": "link:x-pack/solutions/observability/packages/utils-common",
|
||||
"@kbn/observability-utils-server": "link:x-pack/solutions/observability/packages/utils-server",
|
||||
"@kbn/oidc-provider-plugin": "link:x-pack/test/security_api_integration/plugins/oidc_provider",
|
||||
"@kbn/onechat-browser": "link:x-pack/platform/packages/shared/onechat/onechat-browser",
|
||||
"@kbn/onechat-common": "link:x-pack/platform/packages/shared/onechat/onechat-common",
|
||||
"@kbn/onechat-plugin": "link:x-pack/platform/plugins/shared/onechat",
|
||||
"@kbn/onechat-server": "link:x-pack/platform/packages/shared/onechat/onechat-server",
|
||||
"@kbn/open-telemetry-instrumented-plugin": "link:src/platform/test/common/plugins/otel_metrics",
|
||||
"@kbn/openapi-common": "link:src/platform/packages/shared/kbn-openapi-common",
|
||||
"@kbn/osquery-io-ts-types": "link:src/platform/packages/shared/kbn-osquery-io-ts-types",
|
||||
|
|
|
@ -113,6 +113,7 @@ pageLoadAssetSize:
|
|||
observabilityLogsExplorer: 46650
|
||||
observabilityOnboarding: 19573
|
||||
observabilityShared: 111036
|
||||
onechat: 25000
|
||||
osquery: 107090
|
||||
painlessLab: 179748
|
||||
presentationPanel: 11550
|
||||
|
|
|
@ -1380,6 +1380,14 @@
|
|||
"@kbn/observability-utils-server/*": ["x-pack/solutions/observability/packages/utils-server/*"],
|
||||
"@kbn/oidc-provider-plugin": ["x-pack/test/security_api_integration/plugins/oidc_provider"],
|
||||
"@kbn/oidc-provider-plugin/*": ["x-pack/test/security_api_integration/plugins/oidc_provider/*"],
|
||||
"@kbn/onechat-browser": ["x-pack/platform/packages/shared/onechat/onechat-browser"],
|
||||
"@kbn/onechat-browser/*": ["x-pack/platform/packages/shared/onechat/onechat-browser/*"],
|
||||
"@kbn/onechat-common": ["x-pack/platform/packages/shared/onechat/onechat-common"],
|
||||
"@kbn/onechat-common/*": ["x-pack/platform/packages/shared/onechat/onechat-common/*"],
|
||||
"@kbn/onechat-plugin": ["x-pack/platform/plugins/shared/onechat"],
|
||||
"@kbn/onechat-plugin/*": ["x-pack/platform/plugins/shared/onechat/*"],
|
||||
"@kbn/onechat-server": ["x-pack/platform/packages/shared/onechat/onechat-server"],
|
||||
"@kbn/onechat-server/*": ["x-pack/platform/packages/shared/onechat/onechat-server/*"],
|
||||
"@kbn/open-telemetry-instrumented-plugin": ["src/platform/test/common/plugins/otel_metrics"],
|
||||
"@kbn/open-telemetry-instrumented-plugin/*": ["src/platform/test/common/plugins/otel_metrics/*"],
|
||||
"@kbn/openapi-bundler": ["src/platform/packages/shared/kbn-openapi-bundler"],
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
# @kbn/onechat-browser
|
||||
|
||||
Browser-side types and utilities for the onechat framework.
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export function onechat() {
|
||||
return 'You know...';
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../../../..',
|
||||
roots: ['<rootDir>/x-pack/platform/packages/shared/onechat/onechat-browser'],
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/onechat-browser",
|
||||
"owner": "@elastic/workchat-eng",
|
||||
"group": "platform",
|
||||
"visibility": "shared"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "@kbn/onechat-browser",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0",
|
||||
"sideEffects": false
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node",
|
||||
"react"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": []
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
# @kbn/onechat-common
|
||||
|
||||
Common (server+browser) types and utilities for the onechat framework.
|
||||
|
||||
## Tool identifier utilities
|
||||
|
||||
The package exposes various helper function and typeguard to manipulate and
|
||||
convert tool identifiers
|
||||
|
||||
- `isSerializedToolIdentifier`
|
||||
- `isStructuredToolIdentifier`
|
||||
- `isPlainToolIdentifier`
|
||||
- `toStructuredToolIdentifier`
|
||||
- `toSerializedToolIdentifier`
|
||||
- `createBuiltinToolId`
|
||||
|
||||
## Error utilities
|
||||
|
||||
The package exposes type guards for all onechat error types
|
||||
|
||||
- `isOnechatError`
|
||||
- `isToolNotFoundError`
|
||||
- `isInternalError`
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type { OnechatEvent } from './src/events';
|
||||
export {
|
||||
type ToolDescriptor,
|
||||
type ToolDescriptorMeta,
|
||||
type PlainIdToolIdentifier,
|
||||
type SerializedToolIdentifier,
|
||||
type StructuredToolIdentifier,
|
||||
type ToolIdentifier,
|
||||
ToolSourceType,
|
||||
isSerializedToolIdentifier,
|
||||
isStructuredToolIdentifier,
|
||||
isPlainToolIdentifier,
|
||||
toStructuredToolIdentifier,
|
||||
toSerializedToolIdentifier,
|
||||
createBuiltinToolId,
|
||||
builtinSourceId,
|
||||
} from './src/tools';
|
||||
export {
|
||||
OnechatErrorCode,
|
||||
OnechatErrorUtils,
|
||||
isInternalError,
|
||||
isToolNotFoundError,
|
||||
isOnechatError,
|
||||
createInternalError,
|
||||
createToolNotFoundError,
|
||||
type OnechatError,
|
||||
type OnechatInternalError,
|
||||
type OnechatToolNotFoundError,
|
||||
} from './src/errors';
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test/jest_node',
|
||||
rootDir: '../../../../../..',
|
||||
roots: ['<rootDir>/x-pack/platform/packages/shared/onechat/onechat-common'],
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/onechat-common",
|
||||
"owner": "@elastic/workchat-eng",
|
||||
"group": "platform",
|
||||
"visibility": "shared"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "@kbn/onechat-common",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0",
|
||||
"sideEffects": false
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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 {
|
||||
isOnechatError,
|
||||
createInternalError,
|
||||
createToolNotFoundError,
|
||||
isInternalError,
|
||||
isToolNotFoundError,
|
||||
OnechatErrorCode,
|
||||
} from './errors';
|
||||
import { toSerializedToolIdentifier } from './tools';
|
||||
|
||||
describe('Onechat errors', () => {
|
||||
describe('isOnechatError', () => {
|
||||
it('should return true for a OnechatError instance', () => {
|
||||
const error = createInternalError('test error');
|
||||
expect(isOnechatError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for a regular Error', () => {
|
||||
const error = new Error('test error');
|
||||
expect(isOnechatError(error)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for non-error values', () => {
|
||||
expect(isOnechatError(null)).toBe(false);
|
||||
expect(isOnechatError(undefined)).toBe(false);
|
||||
expect(isOnechatError('string')).toBe(false);
|
||||
expect(isOnechatError({})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isInternalError', () => {
|
||||
it('should return true for an internal error', () => {
|
||||
const error = createInternalError('test error');
|
||||
expect(isInternalError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for a tool not found error', () => {
|
||||
const error = createToolNotFoundError({ toolId: toSerializedToolIdentifier('test-tool') });
|
||||
expect(isInternalError(error)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for a regular Error', () => {
|
||||
const error = new Error('test error');
|
||||
expect(isInternalError(error)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isToolNotFoundError', () => {
|
||||
it('should return true for a tool not found error', () => {
|
||||
const error = createToolNotFoundError({ toolId: toSerializedToolIdentifier('test-tool') });
|
||||
expect(isToolNotFoundError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for an internal error', () => {
|
||||
const error = createInternalError('test error');
|
||||
expect(isToolNotFoundError(error)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for a regular Error', () => {
|
||||
const error = new Error('test error');
|
||||
expect(isToolNotFoundError(error)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createInternalError', () => {
|
||||
it('should create an error with the correct code and message', () => {
|
||||
const error = createInternalError('test error');
|
||||
expect(error.code).toBe(OnechatErrorCode.internalError);
|
||||
expect(error.message).toBe('test error');
|
||||
});
|
||||
|
||||
it('should include optional metadata', () => {
|
||||
const meta = { foo: 'bar' };
|
||||
const error = createInternalError('test error', meta);
|
||||
expect(error.meta).toEqual(meta);
|
||||
});
|
||||
|
||||
it('should use empty object as default metadata', () => {
|
||||
const error = createInternalError('test error');
|
||||
expect(error.meta).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createToolNotFoundError', () => {
|
||||
it('should create an error with the correct code and default message', () => {
|
||||
const error = createToolNotFoundError({ toolId: toSerializedToolIdentifier('test-tool') });
|
||||
expect(error.code).toBe(OnechatErrorCode.toolNotFound);
|
||||
expect(error.message).toBe(`Tool ${toSerializedToolIdentifier('test-tool')} not found`);
|
||||
expect(error.meta).toEqual({
|
||||
toolId: toSerializedToolIdentifier('test-tool'),
|
||||
statusCode: 404,
|
||||
});
|
||||
});
|
||||
|
||||
it('should use custom message when provided', () => {
|
||||
const error = createToolNotFoundError({
|
||||
toolId: toSerializedToolIdentifier('test-tool'),
|
||||
customMessage: 'Custom error message',
|
||||
});
|
||||
expect(error.message).toBe('Custom error message');
|
||||
expect(error.meta).toEqual({
|
||||
toolId: toSerializedToolIdentifier('test-tool'),
|
||||
statusCode: 404,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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 { ServerSentEventError } from '@kbn/sse-utils';
|
||||
import type { SerializedToolIdentifier } from './tools';
|
||||
|
||||
/**
|
||||
* Code to identify onechat errors
|
||||
*/
|
||||
export enum OnechatErrorCode {
|
||||
internalError = 'internalError',
|
||||
toolNotFound = 'toolNotFound',
|
||||
}
|
||||
|
||||
const OnechatError = ServerSentEventError;
|
||||
|
||||
/**
|
||||
* Base error class used for all onechat errors.
|
||||
*/
|
||||
export type OnechatError<
|
||||
TCode extends OnechatErrorCode,
|
||||
TMeta extends Record<string, any> = Record<string, any>
|
||||
> = ServerSentEventError<TCode, TMeta>;
|
||||
|
||||
export const isOnechatError = (err: unknown): err is OnechatError<OnechatErrorCode> => {
|
||||
return err instanceof OnechatError;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents an internal error
|
||||
*/
|
||||
export type OnechatInternalError = OnechatError<OnechatErrorCode.internalError>;
|
||||
|
||||
/**
|
||||
* Checks if the given error is a {@link OnechatInternalError}
|
||||
*/
|
||||
export const isInternalError = (err: unknown): err is OnechatInternalError => {
|
||||
return isOnechatError(err) && err.code === OnechatErrorCode.internalError;
|
||||
};
|
||||
|
||||
export const createInternalError = (
|
||||
message: string,
|
||||
meta?: Record<string, any>
|
||||
): OnechatInternalError => {
|
||||
return new OnechatError(OnechatErrorCode.internalError, message, meta ?? {});
|
||||
};
|
||||
|
||||
/**
|
||||
* Error thrown when trying to retrieve or execute a tool not present or available in the current context.
|
||||
*/
|
||||
export type OnechatToolNotFoundError = OnechatError<OnechatErrorCode.toolNotFound>;
|
||||
|
||||
/**
|
||||
* Checks if the given error is a {@link OnechatInternalError}
|
||||
*/
|
||||
export const isToolNotFoundError = (err: unknown): err is OnechatInternalError => {
|
||||
return isOnechatError(err) && err.code === OnechatErrorCode.toolNotFound;
|
||||
};
|
||||
|
||||
export const createToolNotFoundError = ({
|
||||
toolId,
|
||||
customMessage,
|
||||
meta = {},
|
||||
}: {
|
||||
toolId: SerializedToolIdentifier;
|
||||
customMessage?: string;
|
||||
meta?: Record<string, any>;
|
||||
}): OnechatToolNotFoundError => {
|
||||
return new OnechatError(
|
||||
OnechatErrorCode.toolNotFound,
|
||||
customMessage ?? `Tool ${toolId} not found`,
|
||||
{ ...meta, toolId, statusCode: 404 }
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Global utility exposing all error utilities from a single export.
|
||||
*/
|
||||
export const OnechatErrorUtils = {
|
||||
isOnechatError,
|
||||
isInternalError,
|
||||
isToolNotFoundError,
|
||||
createInternalError,
|
||||
createToolNotFoundError,
|
||||
};
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Base type for all onechat events
|
||||
*/
|
||||
export interface OnechatEvent<
|
||||
TEventType extends string,
|
||||
TData extends Record<string, any>,
|
||||
TMeta extends Record<string, any>
|
||||
> {
|
||||
/**
|
||||
* Unique type identifier for the event.
|
||||
*/
|
||||
type: TEventType;
|
||||
/**
|
||||
* Data bound to this event.
|
||||
*/
|
||||
data: TData;
|
||||
/**
|
||||
* Metadata bound to this event.
|
||||
*/
|
||||
meta: TMeta;
|
||||
}
|
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
* 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 {
|
||||
isPlainToolIdentifier,
|
||||
isStructuredToolIdentifier,
|
||||
isSerializedToolIdentifier,
|
||||
createBuiltinToolId,
|
||||
toStructuredToolIdentifier,
|
||||
toSerializedToolIdentifier,
|
||||
ToolSourceType,
|
||||
StructuredToolIdentifier,
|
||||
} from './tools';
|
||||
|
||||
describe('Tool Identifier utilities', () => {
|
||||
describe('isPlainToolIdentifier', () => {
|
||||
it('should return true for a plain string identifier', () => {
|
||||
expect(isPlainToolIdentifier('my-tool')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for an unstructured identifier', () => {
|
||||
expect(isPlainToolIdentifier('my-tool||builtIn||none')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for a structured identifier', () => {
|
||||
expect(
|
||||
isPlainToolIdentifier({
|
||||
toolId: 'my-tool',
|
||||
sourceType: 'builtIn' as ToolSourceType,
|
||||
sourceId: 'none',
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isStructuredToolIdentifier', () => {
|
||||
it('should return true for a valid structured identifier', () => {
|
||||
const structuredId: StructuredToolIdentifier = {
|
||||
toolId: 'my-tool',
|
||||
sourceType: ToolSourceType.builtIn,
|
||||
sourceId: 'none',
|
||||
};
|
||||
expect(isStructuredToolIdentifier(structuredId)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for a plain string identifier', () => {
|
||||
expect(isStructuredToolIdentifier('my-tool')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for an unstructured identifier', () => {
|
||||
expect(isStructuredToolIdentifier('my-tool||builtIn||none')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isUnstructuredToolIdentifier', () => {
|
||||
it('should return true for a valid unstructured identifier', () => {
|
||||
expect(isSerializedToolIdentifier('my-tool||builtIn||none')).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for a plain string identifier', () => {
|
||||
expect(isSerializedToolIdentifier('my-tool')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for a structured identifier', () => {
|
||||
expect(
|
||||
isSerializedToolIdentifier({
|
||||
toolId: 'my-tool',
|
||||
sourceType: ToolSourceType.builtIn,
|
||||
sourceId: 'none',
|
||||
})
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('builtinToolId', () => {
|
||||
it('should create a structured identifier for a builtIn tool', () => {
|
||||
const result = createBuiltinToolId('my-tool');
|
||||
expect(result).toEqual({
|
||||
toolId: 'my-tool',
|
||||
sourceType: ToolSourceType.builtIn,
|
||||
sourceId: 'builtIn',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toSerializedToolIdentifier', () => {
|
||||
it('should return the same string for a serialized identifier', () => {
|
||||
const serializedId = 'my-tool||builtIn||none';
|
||||
expect(toSerializedToolIdentifier(serializedId)).toBe(serializedId);
|
||||
});
|
||||
|
||||
it('should convert a structured identifier to serialized format', () => {
|
||||
const structuredId = {
|
||||
toolId: 'my-tool',
|
||||
sourceType: ToolSourceType.builtIn,
|
||||
sourceId: 'none',
|
||||
};
|
||||
expect(toSerializedToolIdentifier(structuredId)).toBe('my-tool||builtIn||none');
|
||||
});
|
||||
|
||||
it('should convert a plain identifier to serialized format with unknown source', () => {
|
||||
expect(toSerializedToolIdentifier('my-tool')).toBe('my-tool||unknown||unknown');
|
||||
});
|
||||
|
||||
it('should throw an error for malformed identifiers', () => {
|
||||
expect(() => toSerializedToolIdentifier('invalid||format')).toThrow(
|
||||
'Malformed tool identifier'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toStructuredToolIdentifier', () => {
|
||||
it('should return the same object for a structured identifier', () => {
|
||||
const structuredId = {
|
||||
toolId: 'my-tool',
|
||||
sourceType: ToolSourceType.builtIn,
|
||||
sourceId: 'none',
|
||||
};
|
||||
expect(toStructuredToolIdentifier(structuredId)).toEqual(structuredId);
|
||||
});
|
||||
|
||||
it('should convert an unstructured identifier to structured', () => {
|
||||
const result = toStructuredToolIdentifier('my-tool||builtIn||none');
|
||||
expect(result).toEqual({
|
||||
toolId: 'my-tool',
|
||||
sourceType: ToolSourceType.builtIn,
|
||||
sourceId: 'none',
|
||||
});
|
||||
});
|
||||
|
||||
it('should convert a plain identifier to structured', () => {
|
||||
const result = toStructuredToolIdentifier('my-tool');
|
||||
expect(result).toEqual({
|
||||
toolId: 'my-tool',
|
||||
sourceType: ToolSourceType.unknown,
|
||||
sourceId: 'unknown',
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw an error for malformed identifiers', () => {
|
||||
expect(() => toStructuredToolIdentifier('invalid||format')).toThrow(
|
||||
'Malformed tool identifier'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
* 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 { createInternalError } from './errors';
|
||||
|
||||
/**
|
||||
* Represents a plain toolId, without source information attached to it.
|
||||
*/
|
||||
export type PlainIdToolIdentifier = string;
|
||||
|
||||
/**
|
||||
* Represents the source type for a tool.
|
||||
*/
|
||||
export enum ToolSourceType {
|
||||
/**
|
||||
* Source used for built-in tools
|
||||
*/
|
||||
builtIn = 'builtIn',
|
||||
/**
|
||||
* Unknown source - used when converting plain ids to structured or serialized format.
|
||||
*/
|
||||
unknown = 'unknown',
|
||||
}
|
||||
|
||||
/**
|
||||
* Structured representation of a tool identifier.
|
||||
*/
|
||||
export interface StructuredToolIdentifier {
|
||||
/** The unique ID of this tool, relative to the source **/
|
||||
toolId: string;
|
||||
/** The type of source the tool is being provided from, e.g. builtIn or MCP **/
|
||||
sourceType: ToolSourceType;
|
||||
/** Id of the source, e.g. for MCP server it will be the server/connector ID */
|
||||
sourceId: string;
|
||||
}
|
||||
|
||||
export const serializedPartsSeparator = '||';
|
||||
|
||||
/**
|
||||
* The singleton sourceId used for all builtIn tools.
|
||||
*/
|
||||
export const builtinSourceId = 'builtIn';
|
||||
/**
|
||||
* Unknown sourceId used from converting plain Ids to structured or serialized ids.
|
||||
*/
|
||||
export const unknownSourceId = 'unknown';
|
||||
|
||||
/**
|
||||
* Build a structured tool identifier for given builtin tool ID.
|
||||
*/
|
||||
export const createBuiltinToolId = (plainId: PlainIdToolIdentifier): StructuredToolIdentifier => {
|
||||
return {
|
||||
toolId: plainId,
|
||||
sourceType: ToolSourceType.builtIn,
|
||||
sourceId: builtinSourceId,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* String representation of {@link StructuredToolIdentifier}
|
||||
* Follow a `{toolId}||{sourceType}||{sourceId}` format.
|
||||
*/
|
||||
export type SerializedToolIdentifier = `${PlainIdToolIdentifier}||${ToolSourceType}||${string}`;
|
||||
|
||||
/**
|
||||
* Defines all possible shapes for a tool identifier.
|
||||
*/
|
||||
export type ToolIdentifier =
|
||||
| PlainIdToolIdentifier
|
||||
| StructuredToolIdentifier
|
||||
| SerializedToolIdentifier;
|
||||
|
||||
/**
|
||||
* Check if the given {@link ToolIdentifier} is a {@link SerializedToolIdentifier}
|
||||
*/
|
||||
export const isSerializedToolIdentifier = (
|
||||
identifier: ToolIdentifier
|
||||
): identifier is SerializedToolIdentifier => {
|
||||
return typeof identifier === 'string' && identifier.split(serializedPartsSeparator).length === 3;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the given {@link ToolIdentifier} is a {@link StructuredToolIdentifier}
|
||||
*/
|
||||
export const isStructuredToolIdentifier = (
|
||||
identifier: ToolIdentifier
|
||||
): identifier is StructuredToolIdentifier => {
|
||||
return typeof identifier === 'object' && 'toolId' in identifier && 'sourceType' in identifier;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the given {@link ToolIdentifier} is a {@link PlainIdToolIdentifier}
|
||||
*/
|
||||
export const isPlainToolIdentifier = (
|
||||
identifier: ToolIdentifier
|
||||
): identifier is PlainIdToolIdentifier => {
|
||||
return typeof identifier === 'string' && identifier.split(serializedPartsSeparator).length === 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert the given {@link ToolIdentifier} to a {@link SerializedToolIdentifier}
|
||||
*/
|
||||
export const toSerializedToolIdentifier = (
|
||||
identifier: ToolIdentifier
|
||||
): SerializedToolIdentifier => {
|
||||
if (isSerializedToolIdentifier(identifier)) {
|
||||
return identifier;
|
||||
}
|
||||
if (isStructuredToolIdentifier(identifier)) {
|
||||
return `${identifier.toolId}||${identifier.sourceType}||${identifier.sourceId}`;
|
||||
}
|
||||
if (isPlainToolIdentifier(identifier)) {
|
||||
return `${identifier}||${ToolSourceType.unknown}||${unknownSourceId}`;
|
||||
}
|
||||
|
||||
throw createInternalError(`Malformed tool identifier: "${identifier}"`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert the given {@link ToolIdentifier} to a {@link StructuredToolIdentifier}
|
||||
*/
|
||||
export const toStructuredToolIdentifier = (
|
||||
identifier: ToolIdentifier
|
||||
): StructuredToolIdentifier => {
|
||||
if (isStructuredToolIdentifier(identifier)) {
|
||||
return identifier;
|
||||
}
|
||||
if (isSerializedToolIdentifier(identifier)) {
|
||||
const [toolId, sourceType, sourceId] = identifier.split(serializedPartsSeparator);
|
||||
return {
|
||||
toolId,
|
||||
sourceType: sourceType as ToolSourceType,
|
||||
sourceId,
|
||||
};
|
||||
}
|
||||
if (isPlainToolIdentifier(identifier)) {
|
||||
return {
|
||||
toolId: identifier,
|
||||
sourceType: ToolSourceType.unknown,
|
||||
sourceId: unknownSourceId,
|
||||
};
|
||||
}
|
||||
|
||||
throw createInternalError(`Malformed tool identifier: "${identifier}"`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Serializable representation of a tool, without its handler or schema.
|
||||
*
|
||||
* Use as a common base for browser-side and server-side tool types.
|
||||
*/
|
||||
export interface ToolDescriptor {
|
||||
/**
|
||||
* A unique id for this tool.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Name of the tool, which will be exposed to the LLM.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The description for this tool, which will be exposed to the LLM.
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Meta associated with this tool
|
||||
*/
|
||||
meta: ToolDescriptorMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Metadata associated with a tool.
|
||||
*
|
||||
* Some of them are specified by the tool owner during registration,
|
||||
* others are automatically added by the framework.
|
||||
*/
|
||||
export interface ToolDescriptorMeta {
|
||||
/**
|
||||
* The type of the source this tool is provided by.
|
||||
*/
|
||||
sourceType: ToolSourceType;
|
||||
/**
|
||||
* The id of the source this tool is provided by.
|
||||
* E.g. for MCP source, this will be the ID of the MCP connector.
|
||||
*/
|
||||
sourceId: string;
|
||||
/**
|
||||
* Optional list of tags attached to this tool.
|
||||
* For built-in tools, this is specified during registration.
|
||||
*/
|
||||
tags: string[];
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"extends": "../../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/sse-utils",
|
||||
]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
# @kbn/onechat-server
|
||||
|
||||
Server-side types and utilities for the onechat framework.
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type {
|
||||
RegisteredTool,
|
||||
ToolHandlerFn,
|
||||
ToolHandlerContext,
|
||||
ToolProvider,
|
||||
ToolProviderHasOptions,
|
||||
ToolProviderGetOptions,
|
||||
ToolProviderListOptions,
|
||||
ExecutableTool,
|
||||
ExecutableToolHandlerParams,
|
||||
ExecutableToolHandlerFn,
|
||||
} from './src/tools';
|
||||
export type { ModelProvider, ScopedModel } from './src/model_provider';
|
||||
export type {
|
||||
ScopedRunner,
|
||||
ScopedRunToolFn,
|
||||
ScopedRunnerRunToolsParams,
|
||||
RunContext,
|
||||
RunContextStackEntry,
|
||||
RunToolParams,
|
||||
RunToolFn,
|
||||
Runner,
|
||||
RunToolReturn,
|
||||
} from './src/runner';
|
||||
export {
|
||||
OnechatRunEventType,
|
||||
isToolResponseEvent,
|
||||
isToolCallEvent,
|
||||
type OnechatRunEvent,
|
||||
type RunEventHandlerFn,
|
||||
type ToolResponseEventData,
|
||||
type RunEventEmitter,
|
||||
type RunEventEmitterFn,
|
||||
type InternalRunEvent,
|
||||
type OnechatRunEventMeta,
|
||||
type ToolCallEventData,
|
||||
type ToolResponseEvent,
|
||||
type ToolCallEvent,
|
||||
} from './src/events';
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test/jest_node',
|
||||
rootDir: '../../../../../..',
|
||||
roots: ['<rootDir>/x-pack/platform/packages/shared/onechat/onechat-server'],
|
||||
};
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"type": "shared-common",
|
||||
"id": "@kbn/onechat-server",
|
||||
"owner": "@elastic/workchat-eng",
|
||||
"group": "platform",
|
||||
"visibility": "shared"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"name": "@kbn/onechat-server",
|
||||
"private": true,
|
||||
"version": "1.0.0",
|
||||
"license": "Elastic License 2.0",
|
||||
"sideEffects": false
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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 { OnechatEvent } from '@kbn/onechat-common';
|
||||
import type { RunContextStackEntry } from './runner';
|
||||
|
||||
export enum OnechatRunEventType {
|
||||
toolCall = 'toolCall',
|
||||
toolResponse = 'toolResponse',
|
||||
}
|
||||
|
||||
/**
|
||||
* Base set of meta for all onechat run events.
|
||||
*/
|
||||
export interface OnechatRunEventMeta {
|
||||
/**
|
||||
* Current runId
|
||||
*/
|
||||
runId: string;
|
||||
/**
|
||||
* Execution stack
|
||||
*/
|
||||
stack: RunContextStackEntry[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Public-facing events, as received by the API consumer.
|
||||
*/
|
||||
export type OnechatRunEvent<
|
||||
TEventType extends string = string,
|
||||
TData extends Record<string, any> = Record<string, any>,
|
||||
TMeta extends OnechatRunEventMeta = OnechatRunEventMeta
|
||||
> = OnechatEvent<TEventType, TData, TMeta>;
|
||||
|
||||
/**
|
||||
* Internal-facing events, as emitted by tool or agent owners.
|
||||
*/
|
||||
export type InternalRunEvent<
|
||||
TEventType extends string = string,
|
||||
TData extends Record<string, any> = Record<string, any>,
|
||||
TMeta extends Record<string, any> = Record<string, any>
|
||||
> = Omit<OnechatEvent<TEventType, TData, TMeta>, 'meta'> & {
|
||||
meta?: TMeta;
|
||||
};
|
||||
/**
|
||||
* Event handler function to listen to run events during execution of tools, agents or other onechat primitives.
|
||||
*/
|
||||
export type RunEventHandlerFn = (event: OnechatRunEvent) => void;
|
||||
|
||||
/**
|
||||
* Event emitter function, exposed from tool or agent runnable context.
|
||||
*/
|
||||
export type RunEventEmitterFn = (event: InternalRunEvent) => void;
|
||||
|
||||
export interface RunEventEmitter {
|
||||
emit: RunEventEmitterFn;
|
||||
}
|
||||
|
||||
// toolCall
|
||||
|
||||
export type ToolCallEvent = OnechatRunEvent<OnechatRunEventType.toolCall, ToolCallEventData>;
|
||||
|
||||
export interface ToolCallEventData {
|
||||
toolId: string;
|
||||
toolParams: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export const isToolCallEvent = (event: OnechatEvent<any, any, any>): event is ToolCallEvent => {
|
||||
return event.type === OnechatRunEventType.toolCall;
|
||||
};
|
||||
|
||||
// toolResponse
|
||||
|
||||
export type ToolResponseEvent = OnechatRunEvent<
|
||||
OnechatRunEventType.toolResponse,
|
||||
ToolResponseEventData
|
||||
>;
|
||||
|
||||
export interface ToolResponseEventData {
|
||||
toolId: string;
|
||||
toolResult: unknown;
|
||||
}
|
||||
|
||||
export const isToolResponseEvent = (
|
||||
event: OnechatEvent<any, any, any>
|
||||
): event is ToolResponseEvent => {
|
||||
return event.type === OnechatRunEventType.toolResponse;
|
||||
};
|
|
@ -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 type { InferenceChatModel } from '@kbn/inference-langchain';
|
||||
import type { BoundInferenceClient } from '@kbn/inference-plugin/server';
|
||||
|
||||
/**
|
||||
* Represents a model that can be used within the onechat framework (e.g. tools).
|
||||
*
|
||||
* It exposes different interfaces to models.
|
||||
*/
|
||||
export interface ScopedModel {
|
||||
/**
|
||||
* langchain chat model.
|
||||
*/
|
||||
chatModel: InferenceChatModel;
|
||||
/**
|
||||
* Inference client.
|
||||
*/
|
||||
inferenceClient: BoundInferenceClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider, allowing to select various models depending on the needs.
|
||||
*/
|
||||
export interface ModelProvider {
|
||||
/**
|
||||
* Returns the default model to be used for LLM tasks.
|
||||
*
|
||||
* Will use Elasticsearch LLMs by default if present, otherwise will pick
|
||||
* the first GenAI compatible connector.
|
||||
*/
|
||||
getDefaultModel: () => Promise<ScopedModel>;
|
||||
/**
|
||||
* Returns a model using the given connectorId.
|
||||
*
|
||||
* Will throw if connector doesn't exist, user has no access, or connector
|
||||
* is not a GenAI connector.
|
||||
*/
|
||||
getModel: (options: { connectorId: string }) => Promise<ScopedModel>;
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* 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 { KibanaRequest } from '@kbn/core-http-server';
|
||||
import type { ToolIdentifier, SerializedToolIdentifier } from '@kbn/onechat-common';
|
||||
import type { RunEventHandlerFn } from './events';
|
||||
|
||||
/**
|
||||
* Return type for tool invocation APIs.
|
||||
*
|
||||
* Wrapping the plain result to allow extending the shape later without
|
||||
* introducing breaking changes.
|
||||
*/
|
||||
export interface RunToolReturn<TResult = unknown> {
|
||||
/**
|
||||
* The result value as returned by the tool.
|
||||
*/
|
||||
result: TResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a runner, which is the entry point to execute all onechat primitives,
|
||||
* such as tools or agents.
|
||||
*
|
||||
* This version is pre-scoped to a given request, meaning APIs don't need to be passed
|
||||
* down a request object.
|
||||
*/
|
||||
export interface ScopedRunner {
|
||||
/**
|
||||
* Execute a tool.
|
||||
*/
|
||||
runTool: ScopedRunToolFn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public onechat API to execute a tools.
|
||||
*/
|
||||
export type ScopedRunToolFn = <TParams = Record<string, unknown>, TResult = unknown>(
|
||||
params: ScopedRunnerRunToolsParams<TParams>
|
||||
) => Promise<RunToolReturn<TResult>>;
|
||||
|
||||
/**
|
||||
* Context bound to a run execution.
|
||||
* Contains metadata associated with the run's current state.
|
||||
* Will be attached to errors thrown during a run.
|
||||
*/
|
||||
export interface RunContext {
|
||||
/**
|
||||
* The run identifier, which can be used for tracing
|
||||
*/
|
||||
runId: string;
|
||||
/**
|
||||
* The current execution stack
|
||||
*/
|
||||
stack: RunContextStackEntry[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents an element in the run context's stack.
|
||||
* Used to follow nested / chained execution.
|
||||
*/
|
||||
export type RunContextStackEntry =
|
||||
/** tool invocation */
|
||||
| { type: 'tool'; toolId: SerializedToolIdentifier }
|
||||
/** agent invocation */
|
||||
| { type: 'agent'; agentId: string };
|
||||
|
||||
/**
|
||||
* Params for {@link RunToolFn}
|
||||
*/
|
||||
export interface RunToolParams<TParams = Record<string, unknown>> {
|
||||
/**
|
||||
* ID of the tool to call.
|
||||
*/
|
||||
toolId: ToolIdentifier;
|
||||
/**
|
||||
* Parameters to call the tool with.
|
||||
*/
|
||||
toolParams: TParams;
|
||||
/**
|
||||
* Optional event handler.
|
||||
*/
|
||||
onEvent?: RunEventHandlerFn;
|
||||
/**
|
||||
* The request that initiated that run.
|
||||
*/
|
||||
request: KibanaRequest;
|
||||
/**
|
||||
* Optional genAI connector id to use as default.
|
||||
* If unspecified, will use internal logic to use the default connector
|
||||
* (EIS if there, otherwise openAI, otherwise any GenAI)
|
||||
*/
|
||||
defaultConnectorId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Params for {@link ScopedRunner.runTool}
|
||||
*/
|
||||
export type ScopedRunnerRunToolsParams<TParams = Record<string, unknown>> = Omit<
|
||||
RunToolParams<TParams>,
|
||||
'request'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Public onechat API to execute a tools.
|
||||
*/
|
||||
export type RunToolFn = <TParams = Record<string, unknown>, TResult = unknown>(
|
||||
params: RunToolParams<TParams>
|
||||
) => Promise<RunToolReturn<TResult>>;
|
||||
|
||||
/**
|
||||
* Public onechat runner.
|
||||
*/
|
||||
export interface Runner {
|
||||
/**
|
||||
* Execute a tool.
|
||||
*/
|
||||
runTool: RunToolFn;
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
/*
|
||||
* 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 { z, ZodObject } from '@kbn/zod';
|
||||
import type { MaybePromise } from '@kbn/utility-types';
|
||||
import type { IScopedClusterClient } from '@kbn/core-elasticsearch-server';
|
||||
import type { KibanaRequest } from '@kbn/core-http-server';
|
||||
import type { ToolDescriptor, ToolDescriptorMeta, ToolIdentifier } from '@kbn/onechat-common';
|
||||
import type { ModelProvider } from './model_provider';
|
||||
import type { ScopedRunner, RunToolReturn, ScopedRunnerRunToolsParams } from './runner';
|
||||
import type { RunEventEmitter } from './events';
|
||||
|
||||
/**
|
||||
* Subset of {@link ToolDescriptorMeta} that can be defined during tool registration.
|
||||
*/
|
||||
export type RegisteredToolMeta = Partial<Omit<ToolDescriptorMeta, 'sourceType' | 'sourceId'>>;
|
||||
|
||||
/**
|
||||
* Onechat tool, as registered by built-in tool providers.
|
||||
*/
|
||||
export interface RegisteredTool<
|
||||
RunInput extends ZodObject<any> = ZodObject<any>,
|
||||
RunOutput = unknown
|
||||
> extends Omit<ToolDescriptor, 'meta'> {
|
||||
/**
|
||||
* Tool's input schema, defined as a zod schema.
|
||||
*/
|
||||
schema: RunInput;
|
||||
/**
|
||||
* Handler to call to execute the tool.
|
||||
*/
|
||||
handler: ToolHandlerFn<z.infer<RunInput>, RunOutput>;
|
||||
/**
|
||||
* Optional set of metadata for this tool.
|
||||
*/
|
||||
meta?: RegisteredToolMeta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Onechat tool, as exposed by the onechat tool registry.
|
||||
*/
|
||||
export interface ExecutableTool<
|
||||
RunInput extends ZodObject<any> = ZodObject<any>,
|
||||
RunOutput = unknown
|
||||
> extends ToolDescriptor {
|
||||
/**
|
||||
* Tool's input schema, defined as a zod schema.
|
||||
*/
|
||||
schema: RunInput;
|
||||
/**
|
||||
* Run handler that can be used to execute the tool.
|
||||
*/
|
||||
execute: ExecutableToolHandlerFn<z.infer<RunInput>, RunOutput>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Param type for {@link ExecutableToolHandlerFn}
|
||||
*/
|
||||
export type ExecutableToolHandlerParams<TParams = Record<string, unknown>> = Omit<
|
||||
ScopedRunnerRunToolsParams<TParams>,
|
||||
'toolId'
|
||||
>;
|
||||
|
||||
/**
|
||||
* Execution handler for {@link ExecutableTool}
|
||||
*/
|
||||
export type ExecutableToolHandlerFn<TParams = Record<string, unknown>, TResult = unknown> = (
|
||||
params: ExecutableToolHandlerParams<TParams>
|
||||
) => Promise<RunToolReturn<TResult>>;
|
||||
|
||||
/**
|
||||
* Tool handler function for {@link RegisteredTool} handlers.
|
||||
*/
|
||||
export type ToolHandlerFn<
|
||||
TParams extends Record<string, unknown> = Record<string, unknown>,
|
||||
RunOutput = unknown
|
||||
> = (args: TParams, context: ToolHandlerContext) => MaybePromise<RunOutput>;
|
||||
|
||||
/**
|
||||
* Scoped context which can be used during tool execution to access
|
||||
* a panel of built-in services, such as a pre-scoped elasticsearch client.
|
||||
*/
|
||||
export interface ToolHandlerContext {
|
||||
/**
|
||||
* The request that was provided when initiating that tool execution.
|
||||
* Can be used to create scoped services not directly exposed by this context.
|
||||
*/
|
||||
request: KibanaRequest;
|
||||
/**
|
||||
* A cluster client scoped to the current user.
|
||||
* Can be used to access ES on behalf of either the current user or the system user.
|
||||
*/
|
||||
esClient: IScopedClusterClient;
|
||||
/**
|
||||
* Inference model provider scoped to the current user.
|
||||
* Can be used to access the inference APIs or chatModel.
|
||||
*/
|
||||
modelProvider: ModelProvider;
|
||||
/**
|
||||
* Onechat runner scoped to the current execution.
|
||||
* Can be used to run other workchat primitive as part of the tool execution.
|
||||
*/
|
||||
runner: ScopedRunner;
|
||||
/**
|
||||
* Event emitter that can be used to emits custom events
|
||||
*/
|
||||
events: RunEventEmitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Common interface shared across all tool providers.
|
||||
*/
|
||||
export interface ToolProvider {
|
||||
/**
|
||||
* Check if a tool is available in the provider
|
||||
*/
|
||||
has(options: ToolProviderHasOptions): Promise<boolean>;
|
||||
/**
|
||||
* Retrieve a tool based on its identifier.
|
||||
* If not found, will throw a toolNotFound error.
|
||||
*/
|
||||
get(options: ToolProviderGetOptions): Promise<ExecutableTool>;
|
||||
/**
|
||||
* List all tools based on the provided filters
|
||||
*/
|
||||
list(options: ToolProviderListOptions): Promise<ExecutableTool[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for {@link ToolProvider.has}
|
||||
*/
|
||||
export interface ToolProviderHasOptions {
|
||||
toolId: ToolIdentifier;
|
||||
request: KibanaRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for {@link ToolProvider.get}
|
||||
*/
|
||||
export interface ToolProviderGetOptions {
|
||||
toolId: ToolIdentifier;
|
||||
request: KibanaRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for {@link ToolProvider.list}
|
||||
*/
|
||||
export interface ToolProviderListOptions {
|
||||
request: KibanaRequest;
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"extends": "../../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/inference-langchain",
|
||||
"@kbn/inference-plugin",
|
||||
"@kbn/zod",
|
||||
"@kbn/utility-types",
|
||||
"@kbn/core-elasticsearch-server",
|
||||
"@kbn/core-http-server",
|
||||
"@kbn/onechat-common",
|
||||
]
|
||||
}
|
221
x-pack/platform/plugins/shared/onechat/README.md
Normal file
221
x-pack/platform/plugins/shared/onechat/README.md
Normal file
|
@ -0,0 +1,221 @@
|
|||
# Onechat plugin
|
||||
|
||||
Home of the workchat framework.
|
||||
|
||||
Note: as many other platform features, onechat isolates its public types and static utils, exposed from packages,
|
||||
from its APIs, exposed from the plugin.
|
||||
|
||||
The onechat plugin has 3 main packages:
|
||||
|
||||
- `@kbn/onechat-common`: types and utilities which are shared between browser and server
|
||||
- `@kbn/onechat-server`: server-specific types and utilities
|
||||
- `@kbn/onechat-browser`: browser-specific types and utilities.
|
||||
|
||||
## Overview
|
||||
|
||||
The onechat plugin exposes APIs to interact with onechat primitives.
|
||||
|
||||
The main primitives are:
|
||||
|
||||
- tools
|
||||
|
||||
## Tools
|
||||
|
||||
A tool can be thought of as a LLM-friendly function, with the metadata required for the LLM to understand its purpose
|
||||
and how to call it attached to it.
|
||||
|
||||
Tool can come from multiple sources: built-in from Kibana, from MCP servers, and so on. At the moment,
|
||||
only built-in tools are implemented
|
||||
|
||||
### Registering a built-in tool
|
||||
|
||||
#### Basic example
|
||||
|
||||
```ts
|
||||
class MyPlugin {
|
||||
setup(core: CoreSetup, { onechat }: { onechat: OnechatPluginSetup }) {
|
||||
onechat.tools.register({
|
||||
id: 'my_tool',
|
||||
name: 'My Tool',
|
||||
description: 'My very first tool',
|
||||
meta: {
|
||||
tags: ['foo', 'bar'],
|
||||
},
|
||||
schema: z.object({
|
||||
someNumber: z.number().describe('Some random number'),
|
||||
}),
|
||||
handler: ({ someNumber }, context) => {
|
||||
return 42 + someNumber;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### using the handler context to use scoped services
|
||||
|
||||
```ts
|
||||
onechat.tools.register({
|
||||
id: 'my_es_tool',
|
||||
name: 'My Tool',
|
||||
description: 'Some example',
|
||||
schema: z.object({
|
||||
indexPattern: z.string().describe('Index pattern to filter on'),
|
||||
}),
|
||||
handler: async ({ indexPattern }, { modelProvider, esClient }) => {
|
||||
const indices = await esClient.asCurrentUser.cat.indices({ index: indexPattern });
|
||||
|
||||
const model = await modelProvider.getDefaultModel();
|
||||
const response = await model.inferenceClient.chatComplete(somethingWith(indices));
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### emitting events
|
||||
|
||||
```ts
|
||||
onechat.tools.register({
|
||||
id: 'my_es_tool',
|
||||
name: 'My Tool',
|
||||
description: 'Some example',
|
||||
schema: z.object({}),
|
||||
handler: async ({}, { events }) => {
|
||||
events.emit({
|
||||
type: 'my_custom_event',
|
||||
data: { stage: 'before' },
|
||||
});
|
||||
|
||||
const response = doSomething();
|
||||
|
||||
events.emit({
|
||||
type: 'my_custom_event',
|
||||
data: { stage: 'after' },
|
||||
});
|
||||
|
||||
return response;
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Executing a tool
|
||||
|
||||
Executing a tool can be done using the `execute` API of the onechat tool start service:
|
||||
|
||||
```ts
|
||||
const { result } = await onechat.tools.execute({
|
||||
toolId: 'my_tool',
|
||||
toolParams: { someNumber: 9000 },
|
||||
request,
|
||||
});
|
||||
```
|
||||
|
||||
It can also be done directly from a tool definition:
|
||||
|
||||
```ts
|
||||
const tool = await onechat.tools.registry.get({ toolId: 'my_tool', request });
|
||||
const { result } = await tool.execute({ toolParams: { someNumber: 9000 } });
|
||||
```
|
||||
|
||||
### Event handling
|
||||
|
||||
Tool execution emits `toolCall` and `toolResponse` events:
|
||||
|
||||
```ts
|
||||
import { isToolCallEvent, isToolResponseEvent } from '@kbn/onechat-server';
|
||||
|
||||
const { result } = await onechat.tools.execute({
|
||||
toolId: 'my_tool',
|
||||
toolParams: { someNumber: 9000 },
|
||||
request,
|
||||
onEvent: (event) => {
|
||||
if (isToolCallEvent(event)) {
|
||||
const {
|
||||
data: { toolId, toolParams },
|
||||
} = event;
|
||||
}
|
||||
if (isToolResponseEvent(event)) {
|
||||
const {
|
||||
data: { toolResult },
|
||||
} = event;
|
||||
}
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Tool identifiers
|
||||
|
||||
Because tools are coming from multiple sources, and because we need to be able to identify
|
||||
which source a given tool is coming from (e.g. for execution), we're using the concept of tool identifier
|
||||
to represent more than a plain id.
|
||||
|
||||
Tool identifier come into 3 shapes:
|
||||
|
||||
- `PlainIdToolIdentifier`: plain tool identifiers
|
||||
- `StructuredToolIdentifier`: structured (object version)
|
||||
- `SerializedToolIdentifier`: serialized string version
|
||||
|
||||
Using a `plain` id is always possible but discouraged, as in case of id conflict,
|
||||
the system will then just pick an arbitrary tool in any source available.
|
||||
|
||||
E.g. avoid doing:
|
||||
|
||||
```ts
|
||||
await onechat.tools.execute({
|
||||
toolId: 'my_tool',
|
||||
toolParams: { someNumber: 9000 },
|
||||
request,
|
||||
});
|
||||
```
|
||||
|
||||
And instead do:
|
||||
|
||||
```ts
|
||||
import { ToolSourceType, builtinSourceId } from '@kbn/onechat-common';
|
||||
|
||||
await onechat.tools.execute({
|
||||
toolId: {
|
||||
toolId: 'my_tool',
|
||||
sourceType: ToolSourceType.builtIn,
|
||||
sourceId: builtinSourceId,
|
||||
},
|
||||
toolParams: { someNumber: 9000 },
|
||||
request,
|
||||
});
|
||||
```
|
||||
|
||||
Or, with the corresponding utility:
|
||||
|
||||
```ts
|
||||
import { createBuiltinToolId } from '@kbn/onechat-common';
|
||||
|
||||
await onechat.tools.execute({
|
||||
toolId: createBuiltinToolId('my_tool'),
|
||||
toolParams: { someNumber: 9000 },
|
||||
request,
|
||||
});
|
||||
```
|
||||
|
||||
### Error handling
|
||||
|
||||
All onechat errors inherit from the `OnechatError` error type. Various error utilities
|
||||
are exposed from the `@kbn/onechat-common` package to identify and handle those errors.
|
||||
|
||||
Some simple example of handling a specific type of error:
|
||||
|
||||
```ts
|
||||
import { isToolNotFoundError } from '@kbn/onechat-common';
|
||||
|
||||
try {
|
||||
const { result } = await onechat.tools.execute({
|
||||
toolId: 'my_tool',
|
||||
toolParams: { someNumber: 9000 },
|
||||
request,
|
||||
});
|
||||
} catch (e) {
|
||||
if (isToolNotFoundError(e)) {
|
||||
throw new Error(`run ${e.meta.runId} failed because tool was not found`);
|
||||
}
|
||||
}
|
||||
```
|
10
x-pack/platform/plugins/shared/onechat/common/features.ts
Normal file
10
x-pack/platform/plugins/shared/onechat/common/features.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const ONECHAT_FRAMEWORK_FEATURE_ID = 'onechat_framework';
|
||||
export const ONECHAT_FRAMEWORK_FEATURE_NAME = 'onechat_framework';
|
||||
export const ONECHAT_FRAMEWORK_APP_ID = 'onechat_framework';
|
23
x-pack/platform/plugins/shared/onechat/jest.config.js
Normal file
23
x-pack/platform/plugins/shared/onechat/jest.config.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../../../../..',
|
||||
roots: [
|
||||
'<rootDir>/x-pack/platform/plugins/shared/onechat/public',
|
||||
'<rootDir>/x-pack/platform/plugins/shared/onechat/server',
|
||||
'<rootDir>/x-pack/platform/plugins/shared/onechat/common',
|
||||
],
|
||||
setupFiles: [],
|
||||
collectCoverage: true,
|
||||
collectCoverageFrom: [
|
||||
'<rootDir>/x-pack/platform/plugins/shared/onechat/{public,server,common}/**/*.{js,ts,tsx}',
|
||||
],
|
||||
|
||||
coverageReporters: ['html'],
|
||||
};
|
17
x-pack/platform/plugins/shared/onechat/kibana.jsonc
Normal file
17
x-pack/platform/plugins/shared/onechat/kibana.jsonc
Normal file
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"type": "plugin",
|
||||
"id": "@kbn/onechat-plugin",
|
||||
"owner": "@elastic/workchat-eng",
|
||||
"group": "platform",
|
||||
"visibility": "shared",
|
||||
"plugin": {
|
||||
"id": "onechat",
|
||||
"server": true,
|
||||
"browser": true,
|
||||
"configPath": ["xpack", "onechat"],
|
||||
"requiredPlugins": ["actions", "inference", "features"],
|
||||
"requiredBundles": [],
|
||||
"optionalPlugins": [],
|
||||
"extraPublicDirs": []
|
||||
}
|
||||
}
|
27
x-pack/platform/plugins/shared/onechat/public/index.ts
Normal file
27
x-pack/platform/plugins/shared/onechat/public/index.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 { PluginInitializer, PluginInitializerContext } from '@kbn/core/public';
|
||||
import type {
|
||||
OnechatPluginSetup,
|
||||
OnechatPluginStart,
|
||||
OnechatSetupDependencies,
|
||||
OnechatStartDependencies,
|
||||
ConfigSchema,
|
||||
} from './types';
|
||||
import { OnechatPlugin } from './plugin';
|
||||
|
||||
export type { OnechatPluginSetup, OnechatPluginStart };
|
||||
|
||||
export const plugin: PluginInitializer<
|
||||
OnechatPluginSetup,
|
||||
OnechatPluginStart,
|
||||
OnechatSetupDependencies,
|
||||
OnechatStartDependencies
|
||||
> = (pluginInitializerContext: PluginInitializerContext<ConfigSchema>) => {
|
||||
return new OnechatPlugin(pluginInitializerContext);
|
||||
};
|
21
x-pack/platform/plugins/shared/onechat/public/mocks.ts
Normal file
21
x-pack/platform/plugins/shared/onechat/public/mocks.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { OnechatPluginSetup, OnechatPluginStart } from './types';
|
||||
|
||||
const createSetupContractMock = (): jest.Mocked<OnechatPluginSetup> => {
|
||||
return {};
|
||||
};
|
||||
|
||||
const createStartContractMock = (): jest.Mocked<OnechatPluginStart> => {
|
||||
return {};
|
||||
};
|
||||
|
||||
export const onechatMocks = {
|
||||
createSetup: createSetupContractMock,
|
||||
createStart: createStartContractMock,
|
||||
};
|
42
x-pack/platform/plugins/shared/onechat/public/plugin.tsx
Normal file
42
x-pack/platform/plugins/shared/onechat/public/plugin.tsx
Normal file
|
@ -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 { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/public';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import type {
|
||||
ConfigSchema,
|
||||
OnechatPluginSetup,
|
||||
OnechatPluginStart,
|
||||
OnechatSetupDependencies,
|
||||
OnechatStartDependencies,
|
||||
} from './types';
|
||||
|
||||
export class OnechatPlugin
|
||||
implements
|
||||
Plugin<
|
||||
OnechatPluginSetup,
|
||||
OnechatPluginStart,
|
||||
OnechatSetupDependencies,
|
||||
OnechatStartDependencies
|
||||
>
|
||||
{
|
||||
logger: Logger;
|
||||
|
||||
constructor(context: PluginInitializerContext<ConfigSchema>) {
|
||||
this.logger = context.logger.get();
|
||||
}
|
||||
setup(
|
||||
coreSetup: CoreSetup<OnechatStartDependencies, OnechatPluginStart>,
|
||||
pluginsSetup: OnechatSetupDependencies
|
||||
): OnechatPluginSetup {
|
||||
return {};
|
||||
}
|
||||
|
||||
start(coreStart: CoreStart, pluginsStart: OnechatStartDependencies): OnechatPluginStart {
|
||||
return {};
|
||||
}
|
||||
}
|
18
x-pack/platform/plugins/shared/onechat/public/types.ts
Normal file
18
x-pack/platform/plugins/shared/onechat/public/types.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-empty-interface*/
|
||||
|
||||
export interface ConfigSchema {}
|
||||
|
||||
export interface OnechatSetupDependencies {}
|
||||
|
||||
export interface OnechatStartDependencies {}
|
||||
|
||||
export interface OnechatPluginSetup {}
|
||||
|
||||
export interface OnechatPluginStart {}
|
19
x-pack/platform/plugins/shared/onechat/server/config.ts
Normal file
19
x-pack/platform/plugins/shared/onechat/server/config.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 { PluginConfigDescriptor } from '@kbn/core/server';
|
||||
import { schema, type TypeOf } from '@kbn/config-schema';
|
||||
|
||||
export const configSchema = schema.object({
|
||||
enabled: schema.boolean({ defaultValue: true }),
|
||||
});
|
||||
|
||||
export type OnechatConfig = TypeOf<typeof configSchema>;
|
||||
|
||||
export const config: PluginConfigDescriptor<OnechatConfig> = {
|
||||
schema: configSchema,
|
||||
};
|
30
x-pack/platform/plugins/shared/onechat/server/index.ts
Normal file
30
x-pack/platform/plugins/shared/onechat/server/index.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { PluginInitializer, PluginInitializerContext } from '@kbn/core/server';
|
||||
import type { OnechatConfig } from './config';
|
||||
import type {
|
||||
OnechatPluginSetup,
|
||||
OnechatPluginStart,
|
||||
OnechatSetupDependencies,
|
||||
OnechatStartDependencies,
|
||||
} from './types';
|
||||
import { OnechatPlugin } from './plugin';
|
||||
|
||||
export type { OnechatPluginSetup, OnechatPluginStart, ToolsSetup, ToolsStart } from './types';
|
||||
export type { ScopedPublicToolRegistry, ScopedPublicToolRegistryFactoryFn } from './services/tools';
|
||||
|
||||
export const plugin: PluginInitializer<
|
||||
OnechatPluginSetup,
|
||||
OnechatPluginStart,
|
||||
OnechatSetupDependencies,
|
||||
OnechatStartDependencies
|
||||
> = async (pluginInitializerContext: PluginInitializerContext<OnechatConfig>) => {
|
||||
return new OnechatPlugin(pluginInitializerContext);
|
||||
};
|
||||
|
||||
export { config } from './config';
|
44
x-pack/platform/plugins/shared/onechat/server/mocks.ts
Normal file
44
x-pack/platform/plugins/shared/onechat/server/mocks.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { OnechatPluginSetup, OnechatPluginStart, ScopedToolsStart } from './types';
|
||||
import {
|
||||
createMockedExecutableTool,
|
||||
createToolProviderMock,
|
||||
createScopedPublicToolRegistryMock,
|
||||
} from './test_utils/tools';
|
||||
|
||||
const createSetupContractMock = (): jest.Mocked<OnechatPluginSetup> => {
|
||||
return {
|
||||
tools: {
|
||||
register: jest.fn(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const createScopedToolStartMock = (): jest.Mocked<ScopedToolsStart> => {
|
||||
return {
|
||||
execute: jest.fn(),
|
||||
registry: createScopedPublicToolRegistryMock(),
|
||||
};
|
||||
};
|
||||
|
||||
const createStartContractMock = (): jest.Mocked<OnechatPluginStart> => {
|
||||
return {
|
||||
tools: {
|
||||
execute: jest.fn(),
|
||||
registry: createToolProviderMock(),
|
||||
asScoped: jest.fn().mockImplementation(() => createScopedToolStartMock()),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const onechatMocks = {
|
||||
createSetup: createSetupContractMock,
|
||||
createStart: createStartContractMock,
|
||||
createTool: createMockedExecutableTool,
|
||||
};
|
98
x-pack/platform/plugins/shared/onechat/server/plugin.ts
Normal file
98
x-pack/platform/plugins/shared/onechat/server/plugin.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* 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 { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from '@kbn/core/server';
|
||||
import type { Logger } from '@kbn/logging';
|
||||
import type { OnechatConfig } from './config';
|
||||
import type {
|
||||
OnechatPluginSetup,
|
||||
OnechatPluginStart,
|
||||
OnechatSetupDependencies,
|
||||
OnechatStartDependencies,
|
||||
} from './types';
|
||||
import { registerRoutes } from './routes';
|
||||
import { ServiceManager } from './services';
|
||||
|
||||
export class OnechatPlugin
|
||||
implements
|
||||
Plugin<
|
||||
OnechatPluginSetup,
|
||||
OnechatPluginStart,
|
||||
OnechatSetupDependencies,
|
||||
OnechatStartDependencies
|
||||
>
|
||||
{
|
||||
private logger: Logger;
|
||||
// @ts-expect-error unused for now
|
||||
private config: OnechatConfig;
|
||||
private serviceManager = new ServiceManager();
|
||||
|
||||
constructor(context: PluginInitializerContext<OnechatConfig>) {
|
||||
this.logger = context.logger.get();
|
||||
this.config = context.config.get();
|
||||
}
|
||||
|
||||
setup(
|
||||
coreSetup: CoreSetup<OnechatStartDependencies, OnechatPluginStart>,
|
||||
pluginsSetup: OnechatSetupDependencies
|
||||
): OnechatPluginSetup {
|
||||
const serviceSetups = this.serviceManager.setupServices();
|
||||
|
||||
const router = coreSetup.http.createRouter();
|
||||
registerRoutes({
|
||||
router,
|
||||
coreSetup,
|
||||
logger: this.logger,
|
||||
getInternalServices: () => {
|
||||
const services = this.serviceManager.internalStart;
|
||||
if (!services) {
|
||||
throw new Error('getInternalServices called before service init');
|
||||
}
|
||||
return services;
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
tools: {
|
||||
register: serviceSetups.tools.register.bind(serviceSetups.tools),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
start(
|
||||
{ elasticsearch, security }: CoreStart,
|
||||
{ actions, inference }: OnechatStartDependencies
|
||||
): OnechatPluginStart {
|
||||
const startServices = this.serviceManager.startServices({
|
||||
logger: this.logger.get('services'),
|
||||
security,
|
||||
elasticsearch,
|
||||
actions,
|
||||
inference,
|
||||
});
|
||||
|
||||
const { tools, runnerFactory } = startServices;
|
||||
const runner = runnerFactory.getRunner();
|
||||
|
||||
return {
|
||||
tools: {
|
||||
registry: tools.registry.asPublicRegistry(),
|
||||
execute: runner.runTool.bind(runner),
|
||||
asScoped: ({ request }) => {
|
||||
return {
|
||||
registry: tools.registry.asScopedPublicRegistry({ request }),
|
||||
execute: (args) => {
|
||||
return runner.runTool({ ...args, request });
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
stop() {}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* 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 { RouteDependencies } from './types';
|
||||
import { registerToolsRoutes } from './tools';
|
||||
|
||||
export const registerRoutes = (dependencies: RouteDependencies) => {
|
||||
registerToolsRoutes(dependencies);
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { RouteDependencies } from './types';
|
||||
import { getHandlerWrapper } from './wrap_handler';
|
||||
import { toolToDescriptor } from '../services/tools/utils/tool_conversion';
|
||||
|
||||
export function registerToolsRoutes({ router, getInternalServices, logger }: RouteDependencies) {
|
||||
const wrapHandler = getHandlerWrapper({ logger });
|
||||
|
||||
router.get(
|
||||
{
|
||||
path: '/api/onechat/tools/list',
|
||||
security: {
|
||||
authz: {
|
||||
enabled: false,
|
||||
reason: 'Platform feature - RBAC in lower layers',
|
||||
},
|
||||
},
|
||||
validate: false,
|
||||
},
|
||||
wrapHandler(async (ctx, request, response) => {
|
||||
const { tools: toolService } = getInternalServices();
|
||||
const registry = toolService.registry.asScopedPublicRegistry({ request });
|
||||
const tools = await registry.list({});
|
||||
return response.ok({
|
||||
body: {
|
||||
tools: tools.map(toolToDescriptor),
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 { Logger } from '@kbn/logging';
|
||||
import type { CoreSetup, IRouter } from '@kbn/core/server';
|
||||
import type { OnechatPluginStart, OnechatStartDependencies } from '../types';
|
||||
import type { InternalStartServices } from '../services';
|
||||
|
||||
export interface RouteDependencies {
|
||||
router: IRouter;
|
||||
logger: Logger;
|
||||
coreSetup: CoreSetup<OnechatStartDependencies, OnechatPluginStart>;
|
||||
getInternalServices: () => InternalStartServices;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { Logger, RequestHandler } from '@kbn/core/server';
|
||||
import { isOnechatError } from '@kbn/onechat-common';
|
||||
|
||||
export const getHandlerWrapper =
|
||||
({ logger }: { logger: Logger }) =>
|
||||
<P, Q, B>(handler: RequestHandler<P, Q, B>): RequestHandler<P, Q, B> => {
|
||||
return (ctx, req, res) => {
|
||||
try {
|
||||
return handler(ctx, req, res);
|
||||
} catch (e) {
|
||||
if (isOnechatError(e)) {
|
||||
logger.error(e);
|
||||
return res.customError({
|
||||
body: { message: e.message },
|
||||
statusCode: e.meta?.statusCode ?? 500,
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 { Runner } from '@kbn/onechat-server';
|
||||
import type { InternalSetupServices, InternalStartServices, ServicesStartDeps } from './types';
|
||||
import { ToolsService } from './tools';
|
||||
import { RunnerFactoryImpl } from './runner';
|
||||
|
||||
interface ServiceInstances {
|
||||
tools: ToolsService;
|
||||
}
|
||||
|
||||
export class ServiceManager {
|
||||
private services?: ServiceInstances;
|
||||
public internalSetup?: InternalSetupServices;
|
||||
public internalStart?: InternalStartServices;
|
||||
|
||||
setupServices(): InternalSetupServices {
|
||||
this.services = {
|
||||
tools: new ToolsService(),
|
||||
};
|
||||
|
||||
this.internalSetup = {
|
||||
tools: this.services.tools.setup(),
|
||||
};
|
||||
|
||||
return this.internalSetup;
|
||||
}
|
||||
|
||||
startServices({
|
||||
logger,
|
||||
security,
|
||||
elasticsearch,
|
||||
actions,
|
||||
inference,
|
||||
}: ServicesStartDeps): InternalStartServices {
|
||||
if (!this.services) {
|
||||
throw new Error('#startServices called before #setupServices');
|
||||
}
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
let runner: Runner | undefined;
|
||||
|
||||
const tools = this.services.tools.start({
|
||||
getRunner: () => {
|
||||
if (!runner) {
|
||||
throw new Error('Trying to access runner before initialization');
|
||||
}
|
||||
return runner;
|
||||
},
|
||||
});
|
||||
const runnerFactory = new RunnerFactoryImpl({
|
||||
logger: logger.get('runnerFactory'),
|
||||
security,
|
||||
elasticsearch,
|
||||
actions,
|
||||
inference,
|
||||
toolsService: tools,
|
||||
});
|
||||
runner = runnerFactory.getRunner();
|
||||
|
||||
this.internalStart = {
|
||||
tools,
|
||||
runnerFactory,
|
||||
};
|
||||
|
||||
return this.internalStart;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type { InternalSetupServices, InternalStartServices } from './types';
|
||||
export { ServiceManager } from './create_services';
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type { RunnerFactory, RunnerFactoryDeps, CreateScopedRunnerExtraParams } from './types';
|
||||
export { RunnerFactoryImpl } from './runner_factory';
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 { KibanaRequest } from '@kbn/core-http-server';
|
||||
import type { ModelProvider, ScopedModel } from '@kbn/onechat-server';
|
||||
import type { InferenceServerStart } from '@kbn/inference-plugin/server';
|
||||
import type { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server';
|
||||
import { getConnectorList, getDefaultConnector } from './utils';
|
||||
|
||||
export interface CreateModelProviderOpts {
|
||||
inference: InferenceServerStart;
|
||||
actions: ActionsPluginStart;
|
||||
request: KibanaRequest;
|
||||
defaultConnectorId?: string;
|
||||
}
|
||||
|
||||
export type CreateModelProviderFactoryFn = (
|
||||
opts: Omit<CreateModelProviderOpts, 'request' | 'defaultConnectorId'>
|
||||
) => ModelProviderFactoryFn;
|
||||
|
||||
export type ModelProviderFactoryFn = (
|
||||
opts: Pick<CreateModelProviderOpts, 'request' | 'defaultConnectorId'>
|
||||
) => ModelProvider;
|
||||
|
||||
/**
|
||||
* Utility function to creates a {@link ModelProviderFactoryFn}
|
||||
*/
|
||||
export const createModelProviderFactory: CreateModelProviderFactoryFn = (factoryOpts) => (opts) => {
|
||||
return createModelProvider({
|
||||
...factoryOpts,
|
||||
...opts,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility function to creates a {@link ModelProvider}
|
||||
*/
|
||||
export const createModelProvider = ({
|
||||
inference,
|
||||
actions,
|
||||
request,
|
||||
defaultConnectorId,
|
||||
}: CreateModelProviderOpts): ModelProvider => {
|
||||
const getDefaultConnectorId = async () => {
|
||||
if (defaultConnectorId) {
|
||||
return defaultConnectorId;
|
||||
}
|
||||
const connectors = await getConnectorList({ actions, request });
|
||||
const defaultConnector = getDefaultConnector({ connectors });
|
||||
return defaultConnector.connectorId;
|
||||
};
|
||||
|
||||
const getModel = async (connectorId: string): Promise<ScopedModel> => {
|
||||
const chatModel = await inference.getChatModel({
|
||||
request,
|
||||
connectorId,
|
||||
chatModelOptions: {},
|
||||
});
|
||||
const inferenceClient = inference.getClient({ request, bindTo: { connectorId } });
|
||||
return {
|
||||
chatModel,
|
||||
inferenceClient,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
getDefaultModel: async () => getModel(await getDefaultConnectorId()),
|
||||
getModel: ({ connectorId }) => getModel(connectorId),
|
||||
};
|
||||
};
|
|
@ -0,0 +1,211 @@
|
|||
/*
|
||||
* 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 { toSerializedToolIdentifier } from '@kbn/onechat-common';
|
||||
import type {
|
||||
ScopedRunnerRunToolsParams,
|
||||
RunToolParams,
|
||||
OnechatRunEvent,
|
||||
} from '@kbn/onechat-server';
|
||||
import {
|
||||
createScopedRunnerDepsMock,
|
||||
createMockedTool,
|
||||
CreateScopedRunnerDepsMock,
|
||||
MockedTool,
|
||||
} from '../../test_utils';
|
||||
import { createScopedRunner, createRunner, runTool, RunnerManager } from './runner';
|
||||
|
||||
describe('Onechat runner', () => {
|
||||
let runnerDeps: CreateScopedRunnerDepsMock;
|
||||
let runnerManager: RunnerManager;
|
||||
|
||||
beforeEach(() => {
|
||||
runnerDeps = createScopedRunnerDepsMock();
|
||||
runnerManager = new RunnerManager(runnerDeps);
|
||||
});
|
||||
|
||||
describe('runTool', () => {
|
||||
let tool: MockedTool;
|
||||
|
||||
beforeEach(() => {
|
||||
const {
|
||||
toolsService: { registry },
|
||||
} = runnerDeps;
|
||||
|
||||
tool = createMockedTool({});
|
||||
registry.get.mockResolvedValue(tool);
|
||||
});
|
||||
|
||||
it('calls the tool registry with the expected parameters', async () => {
|
||||
const {
|
||||
toolsService: { registry },
|
||||
} = runnerDeps;
|
||||
|
||||
const params: ScopedRunnerRunToolsParams = {
|
||||
toolId: 'test-tool',
|
||||
toolParams: { foo: 'bar' },
|
||||
};
|
||||
|
||||
await runTool({
|
||||
toolExecutionParams: params,
|
||||
parentManager: runnerManager,
|
||||
});
|
||||
|
||||
expect(registry.get).toHaveBeenCalledTimes(1);
|
||||
expect(registry.get).toHaveBeenCalledWith({
|
||||
toolId: params.toolId,
|
||||
request: runnerDeps.request,
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the tool handler with the expected parameters', async () => {
|
||||
const params: ScopedRunnerRunToolsParams = {
|
||||
toolId: 'test-tool',
|
||||
toolParams: { foo: 'bar' },
|
||||
};
|
||||
|
||||
await runTool({
|
||||
toolExecutionParams: params,
|
||||
parentManager: runnerManager,
|
||||
});
|
||||
|
||||
expect(tool.handler).toHaveBeenCalledTimes(1);
|
||||
expect(tool.handler).toHaveBeenCalledWith(params.toolParams, expect.any(Object));
|
||||
});
|
||||
|
||||
it('returns the expected value', async () => {
|
||||
const params: ScopedRunnerRunToolsParams = {
|
||||
toolId: 'test-tool',
|
||||
toolParams: {},
|
||||
};
|
||||
|
||||
tool.handler.mockReturnValue({ test: true, over: 9000 });
|
||||
|
||||
const result = await runTool({
|
||||
toolExecutionParams: params,
|
||||
parentManager: runnerManager,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
result: { test: true, over: 9000 },
|
||||
});
|
||||
});
|
||||
|
||||
it('exposes a context with the expected shape to the tool handler', async () => {
|
||||
const params: ScopedRunnerRunToolsParams = {
|
||||
toolId: 'test-tool',
|
||||
toolParams: {},
|
||||
};
|
||||
|
||||
tool.handler.mockImplementation((toolParams, context) => {
|
||||
return 'foo';
|
||||
});
|
||||
|
||||
await runTool({
|
||||
toolExecutionParams: params,
|
||||
parentManager: runnerManager,
|
||||
});
|
||||
|
||||
expect(tool.handler).toHaveBeenCalledTimes(1);
|
||||
const context = tool.handler.mock.lastCall![1];
|
||||
|
||||
expect(context).toEqual(
|
||||
expect.objectContaining({
|
||||
request: runnerDeps.request,
|
||||
esClient: expect.anything(),
|
||||
modelProvider: expect.anything(),
|
||||
runner: expect.anything(),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('exposes an event emitter to the tool handler caller can attach to using the onEvent param', async () => {
|
||||
const emittedEvents: OnechatRunEvent[] = [];
|
||||
|
||||
const params: ScopedRunnerRunToolsParams = {
|
||||
toolId: 'test-tool',
|
||||
toolParams: {},
|
||||
onEvent: (event) => {
|
||||
emittedEvents.push(event);
|
||||
},
|
||||
};
|
||||
|
||||
tool.handler.mockImplementation((toolParams, { events }) => {
|
||||
events.emit({
|
||||
type: 'test-event',
|
||||
data: { foo: 'bar' },
|
||||
meta: { hello: 'dolly' },
|
||||
});
|
||||
return 42;
|
||||
});
|
||||
|
||||
await runTool({
|
||||
toolExecutionParams: params,
|
||||
parentManager: runnerManager,
|
||||
});
|
||||
|
||||
expect(emittedEvents).toHaveLength(1);
|
||||
expect(emittedEvents[0]).toEqual({
|
||||
type: 'test-event',
|
||||
data: {
|
||||
foo: 'bar',
|
||||
},
|
||||
meta: {
|
||||
hello: 'dolly',
|
||||
runId: expect.any(String),
|
||||
stack: [
|
||||
{
|
||||
type: 'tool',
|
||||
toolId: toSerializedToolIdentifier('test-tool'),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('can be invoked through a scoped runner', async () => {
|
||||
tool.handler.mockReturnValue({ someProp: 'someValue' });
|
||||
|
||||
const params: ScopedRunnerRunToolsParams = {
|
||||
toolId: 'test-tool',
|
||||
toolParams: { foo: 'bar' },
|
||||
};
|
||||
|
||||
const runner = createScopedRunner(runnerDeps);
|
||||
const response = await runner.runTool(params);
|
||||
|
||||
expect(tool.handler).toHaveBeenCalledTimes(1);
|
||||
expect(tool.handler).toHaveBeenCalledWith(params.toolParams, expect.any(Object));
|
||||
|
||||
expect(response).toEqual({
|
||||
result: { someProp: 'someValue' },
|
||||
});
|
||||
});
|
||||
|
||||
it('can be invoked through a runner', async () => {
|
||||
tool.handler.mockReturnValue({ someProp: 'someValue' });
|
||||
|
||||
const { request, ...otherRunnerDeps } = runnerDeps;
|
||||
|
||||
const params: RunToolParams = {
|
||||
toolId: 'test-tool',
|
||||
toolParams: { foo: 'bar' },
|
||||
request,
|
||||
};
|
||||
|
||||
const runner = createRunner(otherRunnerDeps);
|
||||
const response = await runner.runTool(params);
|
||||
|
||||
expect(tool.handler).toHaveBeenCalledTimes(1);
|
||||
expect(tool.handler).toHaveBeenCalledWith(params.toolParams, expect.any(Object));
|
||||
|
||||
expect(response).toEqual({
|
||||
result: { someProp: 'someValue' },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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 { Logger } from '@kbn/logging';
|
||||
import type { ElasticsearchServiceStart } from '@kbn/core-elasticsearch-server';
|
||||
import type { KibanaRequest } from '@kbn/core-http-server';
|
||||
import type { SecurityServiceStart } from '@kbn/core-security-server';
|
||||
import { isOnechatError, createInternalError } from '@kbn/onechat-common';
|
||||
import type {
|
||||
ToolHandlerContext,
|
||||
ScopedRunner,
|
||||
ScopedRunnerRunToolsParams,
|
||||
RunContext,
|
||||
Runner,
|
||||
RunToolReturn,
|
||||
} from '@kbn/onechat-server';
|
||||
import type { ToolsServiceStart } from '../tools';
|
||||
import { ModelProviderFactoryFn } from './model_provider';
|
||||
import { createEmptyRunContext, forkContextForToolRun } from './utils/run_context';
|
||||
import { createEventEmitter, createNoopEventEmitter } from './utils/events';
|
||||
|
||||
export interface CreateScopedRunnerDeps {
|
||||
// core services
|
||||
elasticsearch: ElasticsearchServiceStart;
|
||||
security: SecurityServiceStart;
|
||||
// internal service deps
|
||||
modelProviderFactory: ModelProviderFactoryFn;
|
||||
toolsService: ToolsServiceStart;
|
||||
// other deps
|
||||
logger: Logger;
|
||||
request: KibanaRequest;
|
||||
defaultConnectorId?: string;
|
||||
}
|
||||
|
||||
export type CreateRunnerDeps = Omit<CreateScopedRunnerDeps, 'request' | 'defaultConnectorId'>;
|
||||
|
||||
export class RunnerManager {
|
||||
public readonly deps: CreateScopedRunnerDeps;
|
||||
public readonly context: RunContext;
|
||||
|
||||
constructor(deps: CreateScopedRunnerDeps, context?: RunContext) {
|
||||
this.deps = deps;
|
||||
this.context = context ?? createEmptyRunContext();
|
||||
}
|
||||
|
||||
getRunner(): ScopedRunner {
|
||||
return {
|
||||
runTool: <TParams = Record<string, unknown>, TResult = unknown>(
|
||||
toolExecutionParams: ScopedRunnerRunToolsParams<TParams>
|
||||
): Promise<RunToolReturn<TResult>> => {
|
||||
try {
|
||||
return runTool<TParams, TResult>({ toolExecutionParams, parentManager: this });
|
||||
} catch (e) {
|
||||
if (isOnechatError(e)) {
|
||||
throw e;
|
||||
} else {
|
||||
throw createInternalError(e.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
createChild(childContext: RunContext): RunnerManager {
|
||||
return new RunnerManager(this.deps, childContext);
|
||||
}
|
||||
}
|
||||
|
||||
export const runTool = async <TParams = Record<string, unknown>, TResult = unknown>({
|
||||
toolExecutionParams,
|
||||
parentManager,
|
||||
}: {
|
||||
toolExecutionParams: ScopedRunnerRunToolsParams<TParams>;
|
||||
parentManager: RunnerManager;
|
||||
}): Promise<RunToolReturn<TResult>> => {
|
||||
const { toolId, toolParams } = toolExecutionParams;
|
||||
|
||||
const context = forkContextForToolRun({ parentContext: parentManager.context, toolId });
|
||||
const manager = parentManager.createChild(context);
|
||||
|
||||
const { toolsService, request } = manager.deps;
|
||||
|
||||
const tool = await toolsService.registry.get({ toolId, request });
|
||||
|
||||
const toolHandlerContext = createToolHandlerContext<TParams>({ toolExecutionParams, manager });
|
||||
|
||||
const toolResult = await tool.handler(toolParams as Record<string, any>, toolHandlerContext);
|
||||
|
||||
return {
|
||||
result: toolResult as TResult,
|
||||
};
|
||||
};
|
||||
|
||||
export const createToolHandlerContext = <TParams = Record<string, unknown>>({
|
||||
manager,
|
||||
toolExecutionParams,
|
||||
}: {
|
||||
toolExecutionParams: ScopedRunnerRunToolsParams<TParams>;
|
||||
manager: RunnerManager;
|
||||
}): ToolHandlerContext => {
|
||||
const { onEvent } = toolExecutionParams;
|
||||
const { request, defaultConnectorId, elasticsearch, modelProviderFactory } = manager.deps;
|
||||
return {
|
||||
request,
|
||||
esClient: elasticsearch.client.asScoped(request),
|
||||
modelProvider: modelProviderFactory({ request, defaultConnectorId }),
|
||||
runner: manager.getRunner(),
|
||||
events: onEvent
|
||||
? createEventEmitter({ eventHandler: onEvent, context: manager.context })
|
||||
: createNoopEventEmitter(),
|
||||
};
|
||||
};
|
||||
|
||||
export const createScopedRunner = (deps: CreateScopedRunnerDeps): ScopedRunner => {
|
||||
const manager = new RunnerManager(deps, createEmptyRunContext());
|
||||
return manager.getRunner();
|
||||
};
|
||||
|
||||
export const createRunner = (deps: CreateRunnerDeps): Runner => {
|
||||
return {
|
||||
runTool: (runToolsParams) => {
|
||||
const { request, defaultConnectorId, ...otherParams } = runToolsParams;
|
||||
const allDeps = { ...deps, request, defaultConnectorId };
|
||||
const runner = createScopedRunner(allDeps);
|
||||
return runner.runTool(otherParams);
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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 { RunnerFactoryDeps } from './types';
|
||||
import type { CreateScopedRunnerExtraParams, RunnerFactory } from './types';
|
||||
import { createModelProviderFactory } from './model_provider';
|
||||
import {
|
||||
createScopedRunner as createScopedRunnerInternal,
|
||||
createRunner,
|
||||
type CreateRunnerDeps,
|
||||
} from './runner';
|
||||
|
||||
export class RunnerFactoryImpl implements RunnerFactory {
|
||||
private readonly deps: RunnerFactoryDeps;
|
||||
|
||||
constructor(deps: RunnerFactoryDeps) {
|
||||
this.deps = deps;
|
||||
}
|
||||
|
||||
getRunner() {
|
||||
return createRunner(this.createRunnerDeps());
|
||||
}
|
||||
|
||||
createScopedRunner(scopedParams: CreateScopedRunnerExtraParams) {
|
||||
return createScopedRunnerInternal({
|
||||
...this.createRunnerDeps(),
|
||||
...scopedParams,
|
||||
});
|
||||
}
|
||||
|
||||
private createRunnerDeps(): CreateRunnerDeps {
|
||||
const { inference, actions, ...otherDeps } = this.deps;
|
||||
return {
|
||||
...otherDeps,
|
||||
modelProviderFactory: createModelProviderFactory({ inference, actions }),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { Logger } from '@kbn/logging';
|
||||
import type { ElasticsearchServiceStart } from '@kbn/core-elasticsearch-server';
|
||||
import type { SecurityServiceStart } from '@kbn/core-security-server';
|
||||
import type { InferenceServerStart } from '@kbn/inference-plugin/server';
|
||||
import type { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server';
|
||||
import type { ScopedRunner, Runner } from '@kbn/onechat-server';
|
||||
import type { ToolsServiceStart } from '../tools';
|
||||
import type { CreateScopedRunnerDeps } from './runner';
|
||||
|
||||
export interface RunnerFactoryDeps {
|
||||
// core services
|
||||
logger: Logger;
|
||||
elasticsearch: ElasticsearchServiceStart;
|
||||
security: SecurityServiceStart;
|
||||
// plugin deps
|
||||
inference: InferenceServerStart;
|
||||
actions: ActionsPluginStart;
|
||||
// internal service deps
|
||||
toolsService: ToolsServiceStart;
|
||||
}
|
||||
|
||||
export type CreateScopedRunnerExtraParams = Pick<
|
||||
CreateScopedRunnerDeps,
|
||||
'request' | 'defaultConnectorId'
|
||||
>;
|
||||
|
||||
export interface RunnerFactory {
|
||||
getRunner(): Runner;
|
||||
createScopedRunner(params: CreateScopedRunnerExtraParams): ScopedRunner;
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* 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 { createEventEmitter, createNoopEventEmitter, convertInternalEvent } from './events';
|
||||
import type { InternalRunEvent, RunContext } from '@kbn/onechat-server';
|
||||
|
||||
describe('Event utilities', () => {
|
||||
describe('createEventEmitter', () => {
|
||||
it('should emit events with context metadata', () => {
|
||||
const mockEventHandler = jest.fn();
|
||||
const context: RunContext = {
|
||||
runId: 'test-run-id',
|
||||
stack: [],
|
||||
};
|
||||
|
||||
const emitter = createEventEmitter({
|
||||
eventHandler: mockEventHandler,
|
||||
context,
|
||||
});
|
||||
|
||||
const testEvent: InternalRunEvent = {
|
||||
type: 'test-event',
|
||||
data: { foo: 'bar' },
|
||||
meta: { baz: 'qux' },
|
||||
};
|
||||
|
||||
emitter.emit(testEvent);
|
||||
|
||||
expect(mockEventHandler).toHaveBeenCalledWith({
|
||||
type: 'test-event',
|
||||
data: { foo: 'bar' },
|
||||
meta: {
|
||||
baz: 'qux',
|
||||
runId: 'test-run-id',
|
||||
stack: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle events without meta data', () => {
|
||||
const mockEventHandler = jest.fn();
|
||||
const context: RunContext = {
|
||||
runId: 'test-run-id',
|
||||
stack: [],
|
||||
};
|
||||
|
||||
const emitter = createEventEmitter({
|
||||
eventHandler: mockEventHandler,
|
||||
context,
|
||||
});
|
||||
|
||||
const testEvent: InternalRunEvent = {
|
||||
type: 'test-event',
|
||||
data: { foo: 'bar' },
|
||||
};
|
||||
|
||||
emitter.emit(testEvent);
|
||||
|
||||
expect(mockEventHandler).toHaveBeenCalledWith({
|
||||
type: 'test-event',
|
||||
data: { foo: 'bar' },
|
||||
meta: {
|
||||
runId: 'test-run-id',
|
||||
stack: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createNoopEventEmitter', () => {
|
||||
it('should create an emitter that does nothing', () => {
|
||||
const emitter = createNoopEventEmitter();
|
||||
// This should not throw
|
||||
emitter.emit({ type: 'test-event', data: {} });
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertInternalEvent', () => {
|
||||
it('should convert internal event to public event with context', () => {
|
||||
const context: RunContext = {
|
||||
runId: 'test-run-id',
|
||||
stack: [],
|
||||
};
|
||||
|
||||
const internalEvent: InternalRunEvent = {
|
||||
type: 'test-event',
|
||||
data: { foo: 'bar' },
|
||||
meta: { baz: 'qux' },
|
||||
};
|
||||
|
||||
const publicEvent = convertInternalEvent({
|
||||
event: internalEvent,
|
||||
context,
|
||||
});
|
||||
|
||||
expect(publicEvent).toEqual({
|
||||
type: 'test-event',
|
||||
data: { foo: 'bar' },
|
||||
meta: {
|
||||
baz: 'qux',
|
||||
runId: 'test-run-id',
|
||||
stack: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle events without meta data', () => {
|
||||
const context: RunContext = {
|
||||
runId: 'test-run-id',
|
||||
stack: [],
|
||||
};
|
||||
|
||||
const internalEvent: InternalRunEvent = {
|
||||
type: 'test-event',
|
||||
data: { foo: 'bar' },
|
||||
};
|
||||
|
||||
const publicEvent = convertInternalEvent({
|
||||
event: internalEvent,
|
||||
context,
|
||||
});
|
||||
|
||||
expect(publicEvent).toEqual({
|
||||
type: 'test-event',
|
||||
data: { foo: 'bar' },
|
||||
meta: {
|
||||
runId: 'test-run-id',
|
||||
stack: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* 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 {
|
||||
OnechatRunEvent,
|
||||
InternalRunEvent,
|
||||
RunContext,
|
||||
OnechatRunEventMeta,
|
||||
RunEventHandlerFn,
|
||||
RunEventEmitter,
|
||||
} from '@kbn/onechat-server';
|
||||
|
||||
/**
|
||||
* Creates a run event emitter sending events to the provided event handler.
|
||||
*/
|
||||
export const createEventEmitter = ({
|
||||
eventHandler,
|
||||
context,
|
||||
}: {
|
||||
eventHandler: RunEventHandlerFn;
|
||||
context: RunContext;
|
||||
}): RunEventEmitter => {
|
||||
return {
|
||||
emit: (internalEvent) => {
|
||||
const event = convertInternalEvent({ event: internalEvent, context });
|
||||
eventHandler(event);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a run event emitter sending events to the provided event handler.
|
||||
*/
|
||||
export const createNoopEventEmitter = (): RunEventEmitter => {
|
||||
return {
|
||||
emit: () => {},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert an internal onechat run event to its public-facing format.
|
||||
*/
|
||||
export const convertInternalEvent = <
|
||||
TEventType extends string = string,
|
||||
TData extends Record<string, any> = Record<string, any>,
|
||||
TMeta extends Record<string, any> = Record<string, any>
|
||||
>({
|
||||
event: { type, data, meta },
|
||||
context,
|
||||
}: {
|
||||
event: InternalRunEvent<TEventType, TData, TMeta>;
|
||||
context: RunContext;
|
||||
}): OnechatRunEvent<TEventType, TData, TMeta & OnechatRunEventMeta> => {
|
||||
return {
|
||||
type,
|
||||
data,
|
||||
meta: {
|
||||
...((meta ?? {}) as TMeta),
|
||||
runId: context.runId,
|
||||
stack: context.stack,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { KibanaRequest } from '@kbn/core/server';
|
||||
import type { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server';
|
||||
import {
|
||||
isSupportedConnector,
|
||||
connectorToInference,
|
||||
InferenceConnector,
|
||||
} from '@kbn/inference-common';
|
||||
|
||||
export const getConnectorList = async ({
|
||||
actions,
|
||||
request,
|
||||
}: {
|
||||
actions: ActionsPluginStart;
|
||||
request: KibanaRequest;
|
||||
}): Promise<InferenceConnector[]> => {
|
||||
const actionClient = await actions.getActionsClientWithRequest(request);
|
||||
|
||||
const allConnectors = await actionClient.getAll({
|
||||
includeSystemActions: false,
|
||||
});
|
||||
|
||||
return allConnectors
|
||||
.filter((connector) => isSupportedConnector(connector))
|
||||
.map(connectorToInference);
|
||||
};
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { InferenceConnector, InferenceConnectorType } from '@kbn/inference-common';
|
||||
|
||||
/**
|
||||
* Naive utility function to consistently return the "best" default connector for onechat features.
|
||||
*
|
||||
* For now, the logic is, by order of priority:
|
||||
* - any inference connector
|
||||
* - any openAI connector
|
||||
* - any other GenAI-compatible connector.
|
||||
*
|
||||
*/
|
||||
export const getDefaultConnector = ({ connectors }: { connectors: InferenceConnector[] }) => {
|
||||
const inferenceConnector = connectors.find(
|
||||
(connector) => connector.type === InferenceConnectorType.Inference
|
||||
);
|
||||
if (inferenceConnector) {
|
||||
return inferenceConnector;
|
||||
}
|
||||
|
||||
const openAIConnector = connectors.find(
|
||||
(connector) => connector.type === InferenceConnectorType.OpenAI
|
||||
);
|
||||
if (openAIConnector) {
|
||||
return openAIConnector;
|
||||
}
|
||||
|
||||
return connectors[0];
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { getConnectorList } from './get_connector_list';
|
||||
export { getDefaultConnector } from './get_default_connector';
|
||||
export { createEmptyRunContext, forkContextForToolRun } from './run_context';
|
||||
export { convertInternalEvent, createEventEmitter, createNoopEventEmitter } from './events';
|
|
@ -0,0 +1,85 @@
|
|||
/*
|
||||
* 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 { createBuiltinToolId, toSerializedToolIdentifier } from '@kbn/onechat-common';
|
||||
import type { RunContext } from '@kbn/onechat-server';
|
||||
import { createEmptyRunContext, forkContextForToolRun } from './run_context';
|
||||
|
||||
describe('RunContext utilities', () => {
|
||||
describe('creatEmptyRunContext', () => {
|
||||
it('should create an empty run context with a generated UUID', () => {
|
||||
const context = createEmptyRunContext();
|
||||
expect(context).toEqual({
|
||||
runId: expect.any(String),
|
||||
stack: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should create an empty run context with provided runId', () => {
|
||||
const runId = 'test-run-id';
|
||||
const context = createEmptyRunContext({ runId });
|
||||
expect(context).toEqual({
|
||||
runId,
|
||||
stack: [],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('forkContextForToolRun', () => {
|
||||
it('should fork context and add tool to stack', () => {
|
||||
const parentContext: RunContext = {
|
||||
runId: 'parent-run-id',
|
||||
stack: [],
|
||||
};
|
||||
|
||||
const toolId = createBuiltinToolId('test-tool');
|
||||
const expectedSerializedToolId = toSerializedToolIdentifier(toolId);
|
||||
const forkedContext = forkContextForToolRun({ toolId, parentContext });
|
||||
|
||||
expect(forkedContext).toEqual({
|
||||
runId: 'parent-run-id',
|
||||
stack: [
|
||||
{
|
||||
type: 'tool',
|
||||
toolId: expectedSerializedToolId,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should preserve existing stack entries when forking', () => {
|
||||
const existingToolId = createBuiltinToolId('existing-tool');
|
||||
const newToolId = createBuiltinToolId('new-tool');
|
||||
|
||||
const parentContext: RunContext = {
|
||||
runId: 'parent-run-id',
|
||||
stack: [
|
||||
{
|
||||
type: 'tool',
|
||||
toolId: toSerializedToolIdentifier(existingToolId),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const forkedContext = forkContextForToolRun({ toolId: newToolId, parentContext });
|
||||
|
||||
expect(forkedContext).toEqual({
|
||||
runId: 'parent-run-id',
|
||||
stack: [
|
||||
{
|
||||
type: 'tool',
|
||||
toolId: toSerializedToolIdentifier(existingToolId),
|
||||
},
|
||||
{
|
||||
type: 'tool',
|
||||
toolId: toSerializedToolIdentifier(newToolId),
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 { v4 as uuidv4 } from 'uuid';
|
||||
import { type ToolIdentifier, toSerializedToolIdentifier } from '@kbn/onechat-common';
|
||||
import type { RunContext } from '@kbn/onechat-server';
|
||||
|
||||
export const createEmptyRunContext = ({
|
||||
runId = uuidv4(),
|
||||
}: { runId?: string } = {}): RunContext => {
|
||||
return {
|
||||
runId,
|
||||
stack: [],
|
||||
};
|
||||
};
|
||||
|
||||
export const forkContextForToolRun = ({
|
||||
toolId,
|
||||
parentContext,
|
||||
}: {
|
||||
toolId: ToolIdentifier;
|
||||
parentContext: RunContext;
|
||||
}): RunContext => {
|
||||
return {
|
||||
...parentContext,
|
||||
stack: [...parentContext.stack, { type: 'tool', toolId: toSerializedToolIdentifier(toolId) }],
|
||||
};
|
||||
};
|
|
@ -0,0 +1,132 @@
|
|||
/*
|
||||
* 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 { z } from '@kbn/zod';
|
||||
import type { RegisteredTool } from '@kbn/onechat-server';
|
||||
import type { KibanaRequest } from '@kbn/core-http-server';
|
||||
import { httpServerMock } from '@kbn/core-http-server-mocks';
|
||||
import { BuiltinToolRegistry, createBuiltinToolRegistry } from './builtin_registry';
|
||||
import { addBuiltinSystemMeta } from './utils/tool_conversion';
|
||||
|
||||
describe('BuiltinToolRegistry', () => {
|
||||
let registry: BuiltinToolRegistry;
|
||||
let mockRequest: KibanaRequest;
|
||||
|
||||
beforeEach(() => {
|
||||
registry = createBuiltinToolRegistry();
|
||||
mockRequest = httpServerMock.createKibanaRequest();
|
||||
});
|
||||
|
||||
describe('register', () => {
|
||||
it('should register a tool', async () => {
|
||||
const mockTool: RegisteredTool = {
|
||||
id: 'test-tool',
|
||||
name: 'Test Tool',
|
||||
description: 'A test tool',
|
||||
schema: z.object({}),
|
||||
handler: async () => 'test',
|
||||
};
|
||||
|
||||
registry.register(mockTool);
|
||||
await expect(registry.list({ request: mockRequest })).resolves.toEqual([
|
||||
addBuiltinSystemMeta(mockTool),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('has', () => {
|
||||
it('should return true when tool exists', async () => {
|
||||
const mockTool: RegisteredTool = {
|
||||
id: 'test-tool',
|
||||
name: 'Test Tool',
|
||||
description: 'A test tool',
|
||||
schema: z.object({}),
|
||||
handler: async () => 'test',
|
||||
};
|
||||
|
||||
registry.register(mockTool);
|
||||
const exists = await registry.has({ toolId: 'test-tool', request: mockRequest });
|
||||
expect(exists).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when tool does not exist', async () => {
|
||||
const mockTool: RegisteredTool = {
|
||||
id: 'test-tool',
|
||||
name: 'Test Tool',
|
||||
description: 'A test tool',
|
||||
schema: z.object({}),
|
||||
handler: async () => 'test',
|
||||
};
|
||||
|
||||
registry.register(mockTool);
|
||||
const exists = await registry.has({ toolId: 'non-existent-tool', request: mockRequest });
|
||||
expect(exists).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should return the tool when it exists', async () => {
|
||||
const mockTool: RegisteredTool = {
|
||||
id: 'test-tool',
|
||||
name: 'Test Tool',
|
||||
description: 'A test tool',
|
||||
schema: z.object({}),
|
||||
handler: async () => 'test',
|
||||
};
|
||||
|
||||
registry.register(mockTool);
|
||||
const tool = await registry.get({ toolId: 'test-tool', request: mockRequest });
|
||||
expect(tool).toEqual(addBuiltinSystemMeta(mockTool));
|
||||
});
|
||||
|
||||
it('should throw an error when tool does not exist', async () => {
|
||||
const mockTool: RegisteredTool = {
|
||||
id: 'test-tool',
|
||||
name: 'Test Tool',
|
||||
description: 'A test tool',
|
||||
schema: z.object({}),
|
||||
handler: async () => 'test',
|
||||
};
|
||||
|
||||
registry.register(mockTool);
|
||||
await expect(
|
||||
registry.get({ toolId: 'non-existent-tool', request: mockRequest })
|
||||
).rejects.toThrow(/not found/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('list', () => {
|
||||
it('should return all registered tools', async () => {
|
||||
const mockTool1: RegisteredTool = {
|
||||
id: 'test-tool-1',
|
||||
name: 'Test Tool 1',
|
||||
description: 'A test tool',
|
||||
schema: z.object({}),
|
||||
handler: async () => 'test1',
|
||||
};
|
||||
|
||||
const mockTool2: RegisteredTool = {
|
||||
id: 'test-tool-2',
|
||||
name: 'Test Tool 2',
|
||||
description: 'Another test tool',
|
||||
schema: z.object({}),
|
||||
handler: async () => 'test2',
|
||||
};
|
||||
|
||||
registry.register(mockTool1);
|
||||
registry.register(mockTool2);
|
||||
|
||||
const tools = await registry.list({ request: mockRequest });
|
||||
expect(tools).toEqual([addBuiltinSystemMeta(mockTool1), addBuiltinSystemMeta(mockTool2)]);
|
||||
});
|
||||
|
||||
it('should return empty array when no tools are registered', async () => {
|
||||
const tools = await registry.list({ request: mockRequest });
|
||||
expect(tools).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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 { ZodObject } from '@kbn/zod';
|
||||
import type { MaybePromise } from '@kbn/utility-types';
|
||||
import type { KibanaRequest } from '@kbn/core-http-server';
|
||||
import {
|
||||
OnechatErrorUtils,
|
||||
ToolSourceType,
|
||||
type ToolIdentifier,
|
||||
toSerializedToolIdentifier,
|
||||
toStructuredToolIdentifier,
|
||||
} from '@kbn/onechat-common';
|
||||
import type { RegisteredTool } from '@kbn/onechat-server';
|
||||
import type { RegisteredToolWithMeta, InternalToolProvider } from './types';
|
||||
import { addBuiltinSystemMeta } from './utils/tool_conversion';
|
||||
|
||||
export type ToolRegistrationFn = (opts: {
|
||||
request: KibanaRequest;
|
||||
}) => MaybePromise<RegisteredTool[]>;
|
||||
export type ToolDirectRegistration = RegisteredTool;
|
||||
|
||||
export type ToolRegistration<
|
||||
RunInput extends ZodObject<any> = ZodObject<any>,
|
||||
RunOutput = unknown
|
||||
> = RegisteredTool<RunInput, RunOutput> | ToolRegistrationFn;
|
||||
|
||||
export const isToolRegistrationFn = (tool: ToolRegistration): tool is ToolRegistrationFn => {
|
||||
return typeof tool === 'function';
|
||||
};
|
||||
|
||||
export const wrapToolRegistration = (tool: ToolDirectRegistration): ToolRegistrationFn => {
|
||||
return () => {
|
||||
return [tool];
|
||||
};
|
||||
};
|
||||
|
||||
export interface BuiltinToolRegistry extends InternalToolProvider {
|
||||
register(tool: RegisteredTool): void;
|
||||
}
|
||||
|
||||
export const createBuiltinToolRegistry = (): BuiltinToolRegistry => {
|
||||
return new BuiltinToolRegistryImpl();
|
||||
};
|
||||
|
||||
const isValidSource = (source: ToolSourceType) => {
|
||||
return source === ToolSourceType.builtIn || source === ToolSourceType.unknown;
|
||||
};
|
||||
|
||||
export class BuiltinToolRegistryImpl implements BuiltinToolRegistry {
|
||||
private registrations: ToolRegistrationFn[] = [];
|
||||
|
||||
constructor() {}
|
||||
|
||||
register(registration: RegisteredTool) {
|
||||
this.registrations.push(
|
||||
isToolRegistrationFn(registration) ? registration : wrapToolRegistration(registration)
|
||||
);
|
||||
}
|
||||
|
||||
async has(options: { toolId: ToolIdentifier; request: KibanaRequest }): Promise<boolean> {
|
||||
const { toolId: structuredToolId, request } = options;
|
||||
const { toolId, sourceType } = toStructuredToolIdentifier(structuredToolId);
|
||||
|
||||
if (!isValidSource(sourceType)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const registration of this.registrations) {
|
||||
const tools = await this.eval(registration, { request });
|
||||
for (const tool of tools) {
|
||||
if (tool.id === toolId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
async get(options: {
|
||||
toolId: ToolIdentifier;
|
||||
request: KibanaRequest;
|
||||
}): Promise<RegisteredToolWithMeta> {
|
||||
const { toolId: structuredToolId, request } = options;
|
||||
const { toolId, sourceType } = toStructuredToolIdentifier(structuredToolId);
|
||||
|
||||
if (!isValidSource(sourceType)) {
|
||||
throw OnechatErrorUtils.createToolNotFoundError({
|
||||
toolId: toSerializedToolIdentifier(toolId),
|
||||
});
|
||||
}
|
||||
|
||||
for (const registration of this.registrations) {
|
||||
const tools = await this.eval(registration, { request });
|
||||
for (const tool of tools) {
|
||||
if (tool.id === toolId) {
|
||||
return addBuiltinSystemMeta(tool);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw OnechatErrorUtils.createToolNotFoundError({
|
||||
toolId: toSerializedToolIdentifier(toolId),
|
||||
});
|
||||
}
|
||||
|
||||
async list(options: { request: KibanaRequest }): Promise<RegisteredToolWithMeta[]> {
|
||||
const { request } = options;
|
||||
const matchingTools: RegisteredToolWithMeta[] = [];
|
||||
|
||||
for (const registration of this.registrations) {
|
||||
const tools = await this.eval(registration, { request });
|
||||
matchingTools.push(...tools.map((tool) => addBuiltinSystemMeta(tool)));
|
||||
}
|
||||
|
||||
return matchingTools;
|
||||
}
|
||||
|
||||
private async eval(
|
||||
registration: ToolRegistrationFn,
|
||||
{ request }: { request: KibanaRequest }
|
||||
): Promise<RegisteredTool[]> {
|
||||
return await registration({ request });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type {
|
||||
ToolsServiceSetup,
|
||||
ToolsServiceStart,
|
||||
ScopedPublicToolRegistryFactoryFn,
|
||||
ScopedPublicToolRegistry,
|
||||
} from './types';
|
||||
export { ToolsService } from './tools_service';
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { Runner, RegisteredTool } from '@kbn/onechat-server';
|
||||
import { createBuiltinToolRegistry, type BuiltinToolRegistry } from './builtin_registry';
|
||||
import type { ToolsServiceSetup, ToolsServiceStart } from './types';
|
||||
import { createInternalRegistry } from './utils';
|
||||
|
||||
export interface ToolsServiceStartDeps {
|
||||
getRunner: () => Runner;
|
||||
}
|
||||
|
||||
export class ToolsService {
|
||||
private builtinRegistry: BuiltinToolRegistry;
|
||||
|
||||
constructor() {
|
||||
this.builtinRegistry = createBuiltinToolRegistry();
|
||||
}
|
||||
|
||||
setup(): ToolsServiceSetup {
|
||||
return {
|
||||
register: (reg) => this.register(reg),
|
||||
};
|
||||
}
|
||||
|
||||
start({ getRunner }: ToolsServiceStartDeps): ToolsServiceStart {
|
||||
const registry = createInternalRegistry({ providers: [this.builtinRegistry], getRunner });
|
||||
|
||||
return {
|
||||
registry,
|
||||
};
|
||||
}
|
||||
|
||||
private register(toolRegistration: RegisteredTool<any, any>) {
|
||||
this.builtinRegistry.register(toolRegistration);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* 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 { ZodObject } from '@kbn/zod';
|
||||
import type { KibanaRequest } from '@kbn/core-http-server';
|
||||
import type { ToolDescriptorMeta, ToolIdentifier } from '@kbn/onechat-common';
|
||||
import type {
|
||||
ToolProvider,
|
||||
RegisteredTool,
|
||||
ToolProviderHasOptions,
|
||||
ToolProviderGetOptions,
|
||||
ToolProviderListOptions,
|
||||
ExecutableTool,
|
||||
} from '@kbn/onechat-server';
|
||||
|
||||
export interface ToolsServiceSetup {
|
||||
register<RunInput extends ZodObject<any>, RunOutput = unknown>(
|
||||
tool: RegisteredTool<RunInput, RunOutput>
|
||||
): void;
|
||||
}
|
||||
|
||||
export interface ToolsServiceStart {
|
||||
/**
|
||||
* Internal tool registry, exposing internal APIs to interact with tool providers.
|
||||
*/
|
||||
registry: InternalToolRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registered tool with full meta.
|
||||
*/
|
||||
export type RegisteredToolWithMeta<
|
||||
RunInput extends ZodObject<any> = ZodObject<any>,
|
||||
RunOutput = unknown
|
||||
> = Omit<RegisteredTool<RunInput, RunOutput>, 'meta'> & {
|
||||
meta: ToolDescriptorMeta;
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal tool provider interface
|
||||
*/
|
||||
export interface InternalToolProvider {
|
||||
has(options: ToolProviderHasOptions): Promise<boolean>;
|
||||
get(options: ToolProviderGetOptions): Promise<RegisteredToolWithMeta>;
|
||||
list(options: ToolProviderListOptions): Promise<RegisteredToolWithMeta[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal registry interface for the runner to interact with
|
||||
*/
|
||||
export interface InternalToolRegistry extends InternalToolProvider {
|
||||
asPublicRegistry: () => PublicToolRegistry;
|
||||
asScopedPublicRegistry: ScopedPublicToolRegistryFactoryFn;
|
||||
}
|
||||
|
||||
// type alias for now, we may extend later.
|
||||
export type PublicToolRegistry = ToolProvider;
|
||||
|
||||
/**
|
||||
* Public tool registry exposed from the plugin's contract,
|
||||
* and pre-bound to a given request.
|
||||
*/
|
||||
export interface ScopedPublicToolRegistry {
|
||||
has(toolId: ToolIdentifier): Promise<boolean>;
|
||||
get(toolId: ToolIdentifier): Promise<ExecutableTool>;
|
||||
list(options?: {}): Promise<ExecutableTool[]>;
|
||||
}
|
||||
|
||||
export type ScopedPublicToolRegistryFactoryFn = (opts: {
|
||||
request: KibanaRequest;
|
||||
}) => ScopedPublicToolRegistry;
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* 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 { z } from '@kbn/zod';
|
||||
import type { KibanaRequest } from '@kbn/core-http-server';
|
||||
import { httpServerMock } from '@kbn/core-http-server-mocks';
|
||||
import type { RegisteredTool } from '@kbn/onechat-server';
|
||||
import type { InternalToolProvider } from '../types';
|
||||
import { combineToolProviders } from './combine_tool_providers';
|
||||
|
||||
describe('combineToolProviders', () => {
|
||||
let mockRequest: KibanaRequest;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequest = httpServerMock.createKibanaRequest();
|
||||
});
|
||||
|
||||
const createMockTool = (id: string): RegisteredTool => ({
|
||||
id,
|
||||
name: `Tool ${id}`,
|
||||
description: `Description for tool ${id}`,
|
||||
schema: z.object({}),
|
||||
handler: jest.fn(),
|
||||
});
|
||||
|
||||
const createMockProvider = (tools: RegisteredTool[]): InternalToolProvider => ({
|
||||
has: jest.fn().mockImplementation(async ({ toolId }) => {
|
||||
return tools.some((t) => t.id === toolId);
|
||||
}),
|
||||
get: jest.fn().mockImplementation(async ({ toolId }) => {
|
||||
const tool = tools.find((t) => t.id === toolId);
|
||||
if (!tool) {
|
||||
throw new Error(`Tool ${toolId} not found`);
|
||||
}
|
||||
return tool;
|
||||
}),
|
||||
list: jest.fn().mockResolvedValue(tools),
|
||||
});
|
||||
|
||||
it('should return a tool from the first provider that has it', async () => {
|
||||
const tool1 = createMockTool('tool1');
|
||||
const tool2 = createMockTool('tool2');
|
||||
const provider1 = createMockProvider([tool1]);
|
||||
const provider2 = createMockProvider([tool2]);
|
||||
|
||||
const combined = combineToolProviders(provider1, provider2);
|
||||
const result = await combined.get({ toolId: 'tool1', request: mockRequest });
|
||||
|
||||
expect(result).toBe(tool1);
|
||||
expect(provider1.get).toHaveBeenCalledWith({ toolId: 'tool1', request: mockRequest });
|
||||
expect(provider2.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should throw an error if no provider has the requested tool', async () => {
|
||||
const provider1 = createMockProvider([]);
|
||||
const provider2 = createMockProvider([]);
|
||||
|
||||
const combined = combineToolProviders(provider1, provider2);
|
||||
|
||||
await expect(combined.get({ toolId: 'nonexistent', request: mockRequest })).rejects.toThrow(
|
||||
'Tool with id nonexistent not found'
|
||||
);
|
||||
});
|
||||
|
||||
it('should combine tools from all providers without duplicates', async () => {
|
||||
const tool1 = createMockTool('tool1');
|
||||
const tool2 = createMockTool('tool2');
|
||||
const tool3 = createMockTool('tool3');
|
||||
const provider1 = createMockProvider([tool1, tool2]);
|
||||
const provider2 = createMockProvider([tool2, tool3]); // tool2 is duplicated
|
||||
|
||||
const combined = combineToolProviders(provider1, provider2);
|
||||
const result = await combined.list({ request: mockRequest });
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result).toContainEqual(tool1);
|
||||
expect(result).toContainEqual(tool2);
|
||||
expect(result).toContainEqual(tool3);
|
||||
// Verify tool2 only appears once
|
||||
expect(result.filter((t) => t.id === 'tool2')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should handle empty providers', async () => {
|
||||
const combined = combineToolProviders();
|
||||
const result = await combined.list({ request: mockRequest });
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle providers with no tools', async () => {
|
||||
const provider1 = createMockProvider([]);
|
||||
const provider2 = createMockProvider([]);
|
||||
const combined = combineToolProviders(provider1, provider2);
|
||||
const result = await combined.list({ request: mockRequest });
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should preserve tool order from providers', async () => {
|
||||
const tool1 = createMockTool('tool1');
|
||||
const tool2 = createMockTool('tool2');
|
||||
const tool3 = createMockTool('tool3');
|
||||
const provider1 = createMockProvider([tool1, tool2]);
|
||||
const provider2 = createMockProvider([tool3]);
|
||||
|
||||
const combined = combineToolProviders(provider1, provider2);
|
||||
const result = await combined.list({ request: mockRequest });
|
||||
|
||||
expect(result).toEqual([tool1, tool2, tool3]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* 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 {
|
||||
ToolProviderHasOptions,
|
||||
ToolProviderGetOptions,
|
||||
ToolProviderListOptions,
|
||||
} from '@kbn/onechat-server';
|
||||
import type { InternalToolProvider, RegisteredToolWithMeta } from '../types';
|
||||
|
||||
/**
|
||||
* Creates a tool provider that combines multiple tool providers.
|
||||
*
|
||||
* Note: order matters - providers will be checked in the order they are in the list (in case of ID conflict)
|
||||
*/
|
||||
export const combineToolProviders = (
|
||||
...providers: InternalToolProvider[]
|
||||
): InternalToolProvider => {
|
||||
return {
|
||||
has: async (options: ToolProviderHasOptions) => {
|
||||
for (const provider of providers) {
|
||||
const providerHasTool = await provider.has(options);
|
||||
if (providerHasTool) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
get: async (options: ToolProviderGetOptions) => {
|
||||
for (const provider of providers) {
|
||||
if (await provider.has(options)) {
|
||||
return provider.get(options);
|
||||
}
|
||||
}
|
||||
throw new Error(`Tool with id ${options.toolId} not found`);
|
||||
},
|
||||
list: async (options: ToolProviderListOptions) => {
|
||||
const tools: RegisteredToolWithMeta[] = [];
|
||||
const toolIds = new Set<string>();
|
||||
|
||||
for (const provider of providers) {
|
||||
const providerTools = await provider.list(options);
|
||||
for (const tool of providerTools) {
|
||||
if (!toolIds.has(tool.id)) {
|
||||
tools.push(tool);
|
||||
toolIds.add(tool.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tools;
|
||||
},
|
||||
};
|
||||
};
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 {
|
||||
ExecutableTool,
|
||||
Runner,
|
||||
ToolProviderGetOptions,
|
||||
ToolProviderHasOptions,
|
||||
ToolProviderListOptions,
|
||||
} from '@kbn/onechat-server';
|
||||
import {
|
||||
InternalToolRegistry,
|
||||
InternalToolProvider,
|
||||
ScopedPublicToolRegistryFactoryFn,
|
||||
PublicToolRegistry,
|
||||
} from '../types';
|
||||
import { combineToolProviders } from './combine_tool_providers';
|
||||
import { toExecutableTool } from './tool_conversion';
|
||||
|
||||
export const createInternalRegistry = ({
|
||||
providers,
|
||||
getRunner,
|
||||
}: {
|
||||
providers: InternalToolProvider[];
|
||||
getRunner: () => Runner;
|
||||
}): InternalToolRegistry => {
|
||||
const mainProvider = combineToolProviders(...providers);
|
||||
const publicRegistry = internalProviderToPublic({ provider: mainProvider, getRunner });
|
||||
|
||||
return Object.assign(mainProvider, {
|
||||
asPublicRegistry: () => publicRegistry,
|
||||
asScopedPublicRegistry: createPublicFactory(publicRegistry),
|
||||
});
|
||||
};
|
||||
|
||||
export const internalProviderToPublic = ({
|
||||
provider,
|
||||
getRunner,
|
||||
}: {
|
||||
provider: InternalToolProvider;
|
||||
getRunner: () => Runner;
|
||||
}): PublicToolRegistry => {
|
||||
return {
|
||||
has(options: ToolProviderHasOptions): Promise<boolean> {
|
||||
return provider.has(options);
|
||||
},
|
||||
async get(options: ToolProviderGetOptions): Promise<ExecutableTool> {
|
||||
const tool = await provider.get(options);
|
||||
return toExecutableTool({ tool, runner: getRunner(), request: options.request });
|
||||
},
|
||||
async list(options: ToolProviderListOptions): Promise<ExecutableTool[]> {
|
||||
const tools = await provider.list(options);
|
||||
return tools.map((tool) =>
|
||||
toExecutableTool({ tool, runner: getRunner(), request: options.request })
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const createPublicFactory = (
|
||||
provider: PublicToolRegistry
|
||||
): ScopedPublicToolRegistryFactoryFn => {
|
||||
return ({ request }) => {
|
||||
return {
|
||||
has: (toolId) => {
|
||||
return provider.has({ toolId, request });
|
||||
},
|
||||
get: async (toolId) => {
|
||||
return provider.get({ toolId, request });
|
||||
},
|
||||
list: async (options) => {
|
||||
return provider.list({ request, ...options });
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { combineToolProviders } from './combine_tool_providers';
|
||||
export { toolToDescriptor, toExecutableTool, addBuiltinSystemMeta } from './tool_conversion';
|
||||
export { createInternalRegistry } from './create_internal_registry';
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* 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 { ZodObject } from '@kbn/zod';
|
||||
import type { KibanaRequest } from '@kbn/core-http-server';
|
||||
import { builtinSourceId, ToolSourceType, ToolDescriptor } from '@kbn/onechat-common';
|
||||
import type { Runner, RegisteredTool, ExecutableTool } from '@kbn/onechat-server';
|
||||
import { RegisteredToolWithMeta } from '../types';
|
||||
|
||||
export const addBuiltinSystemMeta = <
|
||||
RunInput extends ZodObject<any> = ZodObject<any>,
|
||||
RunOutput = unknown
|
||||
>(
|
||||
tool: RegisteredTool<RunInput, RunOutput>
|
||||
): RegisteredToolWithMeta<RunInput, RunOutput> => {
|
||||
return {
|
||||
...tool,
|
||||
meta: {
|
||||
tags: tool.meta?.tags ?? [],
|
||||
sourceType: ToolSourceType.builtIn,
|
||||
sourceId: builtinSourceId,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const toExecutableTool = <RunInput extends ZodObject<any>, RunOutput>({
|
||||
tool,
|
||||
runner,
|
||||
request,
|
||||
}: {
|
||||
tool: RegisteredTool<RunInput, RunOutput>;
|
||||
runner: Runner;
|
||||
request: KibanaRequest;
|
||||
}): ExecutableTool<RunInput, RunOutput> => {
|
||||
const { handler, ...toolParts } = addBuiltinSystemMeta(tool);
|
||||
|
||||
return {
|
||||
...toolParts,
|
||||
execute: (params) => {
|
||||
return runner.runTool({ ...params, toolId: tool.id, request });
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove all additional properties from a tool descriptor.
|
||||
*
|
||||
* Can be used to convert/clean tool registration for public-facing APIs.
|
||||
*/
|
||||
export const toolToDescriptor = <T extends ToolDescriptor>(tool: T): ToolDescriptor => {
|
||||
const { id, name, description, meta } = tool;
|
||||
return { id, name, description, meta };
|
||||
};
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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 { Logger } from '@kbn/logging';
|
||||
import type { ElasticsearchServiceStart } from '@kbn/core-elasticsearch-server';
|
||||
import type { SecurityServiceStart } from '@kbn/core-security-server';
|
||||
import type { InferenceServerStart } from '@kbn/inference-plugin/server';
|
||||
import type { PluginStartContract as ActionsPluginStart } from '@kbn/actions-plugin/server';
|
||||
import type { ToolsServiceSetup, ToolsServiceStart } from './tools';
|
||||
import type { RunnerFactory } from './runner';
|
||||
|
||||
export interface InternalSetupServices {
|
||||
tools: ToolsServiceSetup;
|
||||
}
|
||||
|
||||
export interface InternalStartServices {
|
||||
tools: ToolsServiceStart;
|
||||
runnerFactory: RunnerFactory;
|
||||
}
|
||||
|
||||
export interface ServicesStartDeps {
|
||||
// core services
|
||||
logger: Logger;
|
||||
elasticsearch: ElasticsearchServiceStart;
|
||||
security: SecurityServiceStart;
|
||||
// plugin deps
|
||||
inference: InferenceServerStart;
|
||||
actions: ActionsPluginStart;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type ChangeReturnType<T, NewReturn> = T extends (...args: infer A) => any
|
||||
? (...args: A) => NewReturn
|
||||
: never;
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export {
|
||||
createModelProviderMock,
|
||||
createModelProviderFactoryMock,
|
||||
type ModelProviderMock,
|
||||
type ModelProviderFactoryMock,
|
||||
} from './model_provider';
|
||||
export { createScopedRunnerDepsMock, type CreateScopedRunnerDepsMock } from './runner';
|
||||
export {
|
||||
createToolsServiceStartMock,
|
||||
createToolProviderMock,
|
||||
createScopedPublicToolRegistryMock,
|
||||
createMockedTool,
|
||||
createMockedExecutableTool,
|
||||
type ScopedPublicToolRegistryFactoryFnMock,
|
||||
type ScopedPublicToolRegistryMock,
|
||||
type ToolProviderMock,
|
||||
type ToolsServiceStartMock,
|
||||
type MockedTool,
|
||||
type MockedExecutableTool,
|
||||
} from './tools';
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 { ModelProvider } from '@kbn/onechat-server';
|
||||
import type { ModelProviderFactoryFn } from '../services/runner/model_provider';
|
||||
import { ChangeReturnType } from './common';
|
||||
|
||||
export type ModelProviderMock = jest.Mocked<ModelProvider>;
|
||||
export type ModelProviderFactoryMock = jest.MockedFn<
|
||||
ChangeReturnType<ModelProviderFactoryFn, ModelProviderMock>
|
||||
>;
|
||||
|
||||
export const createModelProviderMock = (): ModelProviderMock => {
|
||||
return {
|
||||
getDefaultModel: jest.fn(),
|
||||
getModel: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
export const createModelProviderFactoryMock = (): ModelProviderFactoryMock => {
|
||||
return jest.fn().mockImplementation(() => createModelProviderMock());
|
||||
};
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 { loggerMock, MockedLogger } from '@kbn/logging-mocks';
|
||||
import {
|
||||
elasticsearchServiceMock,
|
||||
httpServerMock,
|
||||
securityServiceMock,
|
||||
} from '@kbn/core/server/mocks';
|
||||
import type { KibanaRequest } from '@kbn/core-http-server';
|
||||
import type { CreateScopedRunnerDeps } from '../services/runner/runner';
|
||||
import { ModelProviderFactoryMock, createModelProviderFactoryMock } from './model_provider';
|
||||
import { ToolsServiceStartMock, createToolsServiceStartMock } from './tools';
|
||||
|
||||
export interface CreateScopedRunnerDepsMock extends CreateScopedRunnerDeps {
|
||||
elasticsearch: ReturnType<typeof elasticsearchServiceMock.createStart>;
|
||||
security: ReturnType<typeof securityServiceMock.createStart>;
|
||||
modelProviderFactory: ModelProviderFactoryMock;
|
||||
toolsService: ToolsServiceStartMock;
|
||||
logger: MockedLogger;
|
||||
request: KibanaRequest;
|
||||
}
|
||||
|
||||
export const createScopedRunnerDepsMock = (): CreateScopedRunnerDepsMock => {
|
||||
return {
|
||||
elasticsearch: elasticsearchServiceMock.createStart(),
|
||||
security: securityServiceMock.createStart(),
|
||||
modelProviderFactory: createModelProviderFactoryMock(),
|
||||
toolsService: createToolsServiceStartMock(),
|
||||
logger: loggerMock.create(),
|
||||
request: httpServerMock.createKibanaRequest(),
|
||||
};
|
||||
};
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* 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 { z } from '@kbn/zod';
|
||||
import { ToolSourceType } from '@kbn/onechat-common';
|
||||
import type {
|
||||
ToolProvider,
|
||||
ToolHandlerFn,
|
||||
ExecutableTool,
|
||||
ExecutableToolHandlerFn,
|
||||
} from '@kbn/onechat-server';
|
||||
import type {
|
||||
ToolsServiceStart,
|
||||
InternalToolRegistry,
|
||||
ScopedPublicToolRegistry,
|
||||
ScopedPublicToolRegistryFactoryFn,
|
||||
RegisteredToolWithMeta,
|
||||
} from '../services/tools/types';
|
||||
import { ChangeReturnType } from './common';
|
||||
|
||||
export type ToolProviderMock = jest.Mocked<ToolProvider>;
|
||||
export type ScopedPublicToolRegistryMock = jest.Mocked<ScopedPublicToolRegistry>;
|
||||
export type ScopedPublicToolRegistryFactoryFnMock = jest.MockedFn<
|
||||
ChangeReturnType<ScopedPublicToolRegistryFactoryFn, ScopedPublicToolRegistryMock>
|
||||
>;
|
||||
export type InternalToolRegistryMock = jest.Mocked<InternalToolRegistry>;
|
||||
|
||||
export type ToolsServiceStartMock = ToolsServiceStart & {
|
||||
registry: InternalToolRegistryMock;
|
||||
};
|
||||
|
||||
export const createToolProviderMock = (): ToolProviderMock => {
|
||||
return {
|
||||
has: jest.fn(),
|
||||
get: jest.fn(),
|
||||
list: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
export const createInternalToolRegistryMock = (): InternalToolRegistryMock => {
|
||||
return {
|
||||
has: jest.fn(),
|
||||
get: jest.fn(),
|
||||
list: jest.fn(),
|
||||
asPublicRegistry: jest.fn().mockImplementation(() => createToolProviderMock()),
|
||||
asScopedPublicRegistry: jest
|
||||
.fn()
|
||||
.mockImplementation(() => createScopedPublicToolRegistryMock()),
|
||||
};
|
||||
};
|
||||
|
||||
export const createScopedPublicToolRegistryMock = (): ScopedPublicToolRegistryMock => {
|
||||
return {
|
||||
has: jest.fn(),
|
||||
get: jest.fn(),
|
||||
list: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
export const createToolsServiceStartMock = (): ToolsServiceStartMock => {
|
||||
return {
|
||||
registry: createInternalToolRegistryMock(),
|
||||
};
|
||||
};
|
||||
|
||||
export type MockedTool = Omit<RegisteredToolWithMeta, 'handler'> & {
|
||||
handler: jest.MockedFunction<ToolHandlerFn>;
|
||||
};
|
||||
|
||||
export type MockedExecutableTool = Omit<ExecutableTool, 'execute'> & {
|
||||
execute: jest.MockedFunction<ExecutableToolHandlerFn>;
|
||||
};
|
||||
|
||||
export const createMockedTool = (parts: Partial<RegisteredToolWithMeta> = {}): MockedTool => {
|
||||
return {
|
||||
id: 'test-tool',
|
||||
name: 'my test tool',
|
||||
description: 'test description',
|
||||
schema: z.object({}),
|
||||
meta: {
|
||||
sourceType: ToolSourceType.builtIn,
|
||||
sourceId: 'foo',
|
||||
tags: ['tag-1', 'tag-2'],
|
||||
},
|
||||
...parts,
|
||||
handler: jest.fn(parts.handler),
|
||||
};
|
||||
};
|
||||
|
||||
export const createMockedExecutableTool = (
|
||||
parts: Partial<ExecutableTool> = {}
|
||||
): MockedExecutableTool => {
|
||||
return {
|
||||
id: 'test-tool',
|
||||
name: 'my test tool',
|
||||
description: 'test description',
|
||||
schema: z.object({}),
|
||||
meta: {
|
||||
sourceType: ToolSourceType.builtIn,
|
||||
sourceId: 'foo',
|
||||
tags: ['tag-1', 'tag-2'],
|
||||
},
|
||||
...parts,
|
||||
execute: jest.fn(parts.execute),
|
||||
};
|
||||
};
|
83
x-pack/platform/plugins/shared/onechat/server/types.ts
Normal file
83
x-pack/platform/plugins/shared/onechat/server/types.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 { KibanaRequest } from '@kbn/core-http-server';
|
||||
import type { RunToolFn, ScopedRunToolFn, ToolProvider } from '@kbn/onechat-server';
|
||||
import type {
|
||||
PluginStartContract as ActionsPluginStart,
|
||||
PluginSetupContract as ActionsPluginSetup,
|
||||
} from '@kbn/actions-plugin/server';
|
||||
import type { InferenceServerSetup, InferenceServerStart } from '@kbn/inference-plugin/server';
|
||||
import type { ToolsServiceSetup, ScopedPublicToolRegistry } from './services/tools';
|
||||
|
||||
export interface OnechatSetupDependencies {
|
||||
actions: ActionsPluginSetup;
|
||||
inference: InferenceServerSetup;
|
||||
}
|
||||
|
||||
export interface OnechatStartDependencies {
|
||||
actions: ActionsPluginStart;
|
||||
inference: InferenceServerStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Onechat tool service's setup contract
|
||||
*/
|
||||
export interface ToolsSetup {
|
||||
/**
|
||||
* Register a built-in tool to be available in onechat.
|
||||
*
|
||||
* Refer to {@link ToolRegistration}
|
||||
*/
|
||||
register: ToolsServiceSetup['register'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Onechat tool service's start contract
|
||||
*/
|
||||
export interface ToolsStart {
|
||||
/**
|
||||
* Access the tool registry's APIs.
|
||||
*/
|
||||
registry: ToolProvider;
|
||||
/**
|
||||
* Execute a tool.
|
||||
*/
|
||||
execute: RunToolFn;
|
||||
/**
|
||||
* Return a version of the tool APIs scoped to the provided request.
|
||||
*/
|
||||
asScoped: (opts: { request: KibanaRequest }) => ScopedToolsStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scoped tools APIs.
|
||||
*/
|
||||
export interface ScopedToolsStart {
|
||||
/**
|
||||
* scoped tools registry
|
||||
*/
|
||||
registry: ScopedPublicToolRegistry;
|
||||
/**
|
||||
* Scoped tool runner
|
||||
*/
|
||||
execute: ScopedRunToolFn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup contract of the onechat plugin.
|
||||
*/
|
||||
export interface OnechatPluginSetup {
|
||||
tools: ToolsSetup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start contract of the onechat plugin.
|
||||
*/
|
||||
export interface OnechatPluginStart {
|
||||
tools: ToolsStart;
|
||||
}
|
33
x-pack/platform/plugins/shared/onechat/tsconfig.json
Normal file
33
x-pack/platform/plugins/shared/onechat/tsconfig.json
Normal file
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "target/types"
|
||||
},
|
||||
"include": [
|
||||
"../../../../../typings/**/*",
|
||||
"common/**/*",
|
||||
"public/**/*",
|
||||
"server/**/*",
|
||||
],
|
||||
"exclude": [
|
||||
"target/**/*",
|
||||
".storybook/**/*.js"
|
||||
],
|
||||
"kbn_references": [
|
||||
"@kbn/core",
|
||||
"@kbn/logging",
|
||||
"@kbn/actions-plugin",
|
||||
"@kbn/config-schema",
|
||||
"@kbn/inference-plugin",
|
||||
"@kbn/utility-types",
|
||||
"@kbn/onechat-server",
|
||||
"@kbn/core-http-server",
|
||||
"@kbn/core-http-server-mocks",
|
||||
"@kbn/onechat-common",
|
||||
"@kbn/core-elasticsearch-server",
|
||||
"@kbn/core-security-server",
|
||||
"@kbn/inference-common",
|
||||
"@kbn/logging-mocks",
|
||||
"@kbn/zod"
|
||||
]
|
||||
}
|
16
yarn.lock
16
yarn.lock
|
@ -6456,6 +6456,22 @@
|
|||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/onechat-browser@link:x-pack/platform/packages/shared/onechat/onechat-browser":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/onechat-common@link:x-pack/platform/packages/shared/onechat/onechat-common":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/onechat-plugin@link:x-pack/platform/plugins/shared/onechat":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/onechat-server@link:x-pack/platform/packages/shared/onechat/onechat-server":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
||||
"@kbn/open-telemetry-instrumented-plugin@link:src/platform/test/common/plugins/otel_metrics":
|
||||
version "0.0.0"
|
||||
uid ""
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue