Skip to content

Commit

Permalink
Introduce the InferenceChatModel for langchain (#206429)
Browse files Browse the repository at this point in the history
## Summary

Part of #206710

This PR introduces the `InferenceChatModel` class, which is a langchain
chatModel utilizing the inference APIs (`chatComplete`) under the hood.

Creating instances of `InferenceChatModel` can either be done by
manually importing the class from the new `@kbn/inference-langchain`
package, or by using the new `createChatModel` API exposes from the
inference plugin's start contract.

The main upside of using this chatModel is that the unification and
normalization layers are already being taken care of by the inference
plugin, making sure that the underlying models are being used with the
exact same capabilities. More details on the upsides and reasoning in
the associated issue.

### Usage

Usage is very straightforward

```ts
const chatModel = await inferenceStart.getChatModel({
  request,
  connectorId: myInferenceConnectorId,
  chatModelOptions: {
    temperature: 0.2,
  },
});

// just use it as another langchain chatModel, e.g.
const response = await chatModel.stream('What is Kibana?');
for await (const chunk of response) {
     // do something with the chunk
}
```

### Important

This PR is only adding the implementation, and not wiring it anywhere or
using it in any existing code. This is meant to be done in a later
stage. Merging that implementation first will allow to have distinct PRs
for the integration with search (playground) and security (assistant +
other workflows), with proper testing

---------

Co-authored-by: kibanamachine <[email protected]>
(cherry picked from commit 1c218f9)

# Conflicts:
#	.github/CODEOWNERS
  • Loading branch information
pgayvallet committed Feb 3, 2025
1 parent 94bd9eb commit 445353b
Show file tree
Hide file tree
Showing 57 changed files with 2,460 additions and 103 deletions.
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@
"@kbn/inference-common": "link:x-pack/platform/packages/shared/ai-infra/inference-common",
"@kbn/inference-endpoint-plugin": "link:x-pack/platform/plugins/shared/inference_endpoint",
"@kbn/inference-endpoint-ui-common": "link:x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common",
"@kbn/inference-langchain": "link:x-pack/platform/packages/shared/ai-infra/inference-langchain",
"@kbn/inference-plugin": "link:x-pack/platform/plugins/shared/inference",
"@kbn/inference_integration_flyout": "link:x-pack/platform/packages/private/ml/inference_integration_flyout",
"@kbn/infra-forge": "link:x-pack/platform/packages/private/kbn-infra-forge",
Expand Down Expand Up @@ -1304,7 +1305,8 @@
"yaml": "^2.5.1",
"yauzl": "^2.10.0",
"yazl": "^2.5.1",
"zod": "^3.22.3"
"zod": "^3.22.3",
"zod-to-json-schema": "^3.23.0"
},
"devDependencies": {
"@apidevtools/swagger-parser": "^10.1.1",
Expand Down Expand Up @@ -1877,8 +1879,7 @@
"xml-crypto": "^6.0.0",
"xmlbuilder": "13.0.2",
"yargs": "^15.4.1",
"yarn-deduplicate": "^6.0.2",
"zod-to-json-schema": "^3.23.0"
"yarn-deduplicate": "^6.0.2"
},
"packageManager": "[email protected]"
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
*/

import { z, isZod } from '@kbn/zod';
// eslint-disable-next-line import/no-extraneous-dependencies
import zodToJsonSchema from 'zod-to-json-schema';
import type { OpenAPIV3 } from 'openapi-types';

Expand Down
2 changes: 2 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -1080,6 +1080,8 @@
"@kbn/inference-endpoint-plugin/*": ["x-pack/platform/plugins/shared/inference_endpoint/*"],
"@kbn/inference-endpoint-ui-common": ["x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common"],
"@kbn/inference-endpoint-ui-common/*": ["x-pack/platform/packages/shared/kbn-inference-endpoint-ui-common/*"],
"@kbn/inference-langchain": ["x-pack/platform/packages/shared/ai-infra/inference-langchain"],
"@kbn/inference-langchain/*": ["x-pack/platform/packages/shared/ai-infra/inference-langchain/*"],
"@kbn/inference-plugin": ["x-pack/platform/plugins/shared/inference"],
"@kbn/inference-plugin/*": ["x-pack/platform/plugins/shared/inference/*"],
"@kbn/infra-forge": ["x-pack/platform/packages/private/kbn-infra-forge"],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,17 @@ export {
isInferenceRequestError,
isInferenceRequestAbortedError,
} from './src/errors';
export { generateFakeToolCallId } from './src/utils';
export { elasticModelDictionary } from './src/const';

export { truncateList } from './src/truncate_list';
export {
InferenceConnectorType,
isSupportedConnectorType,
isSupportedConnector,
getConnectorDefaultModel,
getConnectorProvider,
connectorToInference,
type InferenceConnector,
} from './src/connectors';
export {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export type ToolMessage<
TToolResponse extends Record<string, any> | unknown = Record<string, any> | unknown,
TToolData extends Record<string, any> | undefined = Record<string, any> | undefined
> = MessageBase<MessageRole.Tool> & {
/*
/**
* The name of the tool called. Used for refining the type of the response.
*/
name: TName;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 { type InferenceConnector, InferenceConnectorType } from './connectors';

/**
* Returns the default model as defined in the connector's config, if available.
*
* Note: preconfigured connectors only expose their config if their `exposeConfig` flag
* is set to true.
*/
export const getConnectorDefaultModel = (connector: InferenceConnector): string | undefined => {
switch (connector.type) {
case InferenceConnectorType.OpenAI:
case InferenceConnectorType.Gemini:
case InferenceConnectorType.Bedrock:
return connector.config?.defaultModel ?? undefined;
case InferenceConnectorType.Inference:
return connector.config?.providerConfig?.model_id ?? undefined;
}
};

/**
* Returns the provider used for the given connector
*
* Inferred from the type for "legacy" connectors,
* and from the provider config field for inference connectors.
*/
export const getConnectorProvider = (connector: InferenceConnector): string => {
switch (connector.type) {
case InferenceConnectorType.OpenAI:
return 'openai';
case InferenceConnectorType.Gemini:
return 'gemini';
case InferenceConnectorType.Bedrock:
return 'bedrock';
case InferenceConnectorType.Inference:
return connector.config?.provider ?? 'unknown';
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* 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 { createInferenceRequestError } from '../errors';
import type { InferenceConnector, RawConnector } from './connectors';
import { isSupportedConnector } from './is_supported_connector';

/**
* Converts an action connector to the internal inference connector format.
*
* The function will throw if the provided connector is not compatible
*/
export const connectorToInference = (connector: RawConnector): InferenceConnector => {
if (!isSupportedConnector(connector)) {
throw createInferenceRequestError(
`Connector '${connector.id}' of type '${connector.actionTypeId}' not recognized as a supported connector`,
400
);
}

return {
connectorId: connector.id,
name: connector.name,
type: connector.actionTypeId,
config: connector.config ?? {},
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* 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.
*/

/**
* The list of connector types that can be used with the inference APIs
*/
export enum InferenceConnectorType {
OpenAI = '.gen-ai',
Bedrock = '.bedrock',
Gemini = '.gemini',
Inference = '.inference',
}

export const allSupportedConnectorTypes = Object.values(InferenceConnectorType);

/**
* Represents a stack connector that can be used for inference.
*/
export interface InferenceConnector {
/** the type of the connector, see {@link InferenceConnectorType} */
type: InferenceConnectorType;
/** the name of the connector */
name: string;
/** the id of the connector */
connectorId: string;
/**
* configuration (without secrets) of the connector.
* the list of properties depends on the connector type (and subtype for inference)
*/
config: Record<string, any>;
}

/**
* Connector types are living in the actions plugin and we can't afford
* having dependencies from this package to some mid-level plugin,
* so we're just using our own connector mixin type.
*/
export interface RawConnector {
id: string;
actionTypeId: string;
name: string;
config?: Record<string, any>;
}

export interface RawInferenceConnector {
id: string;
actionTypeId: InferenceConnectorType;
name: string;
config?: Record<string, any>;
}
Original file line number Diff line number Diff line change
@@ -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 { isSupportedConnectorType, isSupportedConnector } from './is_supported_connector';
export { connectorToInference } from './connector_to_inference';
export { getConnectorDefaultModel, getConnectorProvider } from './connector_config';
export { InferenceConnectorType, type InferenceConnector } from './connectors';
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@
* 2.0.
*/

import { InferenceConnectorType, RawConnector } from './connectors';
import {
InferenceConnectorType,
isSupportedConnectorType,
isSupportedConnector,
RawConnector,
COMPLETION_TASK_TYPE,
} from './connectors';
} from './is_supported_connector';

const createRawConnector = (parts: Partial<RawConnector>): RawConnector => {
return {
Expand All @@ -36,8 +35,6 @@ describe('isSupportedConnectorType', () => {
});

describe('isSupportedConnector', () => {
// TODO

it('returns true for OpenAI connectors', () => {
expect(
isSupportedConnector(createRawConnector({ actionTypeId: InferenceConnectorType.OpenAI }))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,37 +5,15 @@
* 2.0.
*/

/**
* The list of connector types that can be used with the inference APIs
*/
export enum InferenceConnectorType {
OpenAI = '.gen-ai',
Bedrock = '.bedrock',
Gemini = '.gemini',
Inference = '.inference',
}
import {
InferenceConnectorType,
RawInferenceConnector,
RawConnector,
allSupportedConnectorTypes,
} from './connectors';

export const COMPLETION_TASK_TYPE = 'chat_completion';

const allSupportedConnectorTypes = Object.values(InferenceConnectorType);

/**
* Represents a stack connector that can be used for inference.
*/
export interface InferenceConnector {
/** the type of the connector, see {@link InferenceConnectorType} */
type: InferenceConnectorType;
/** the name of the connector */
name: string;
/** the id of the connector */
connectorId: string;
/**
* configuration (without secrets) of the connector.
* the list of properties depends on the connector type (and subtype for inference)
*/
config: Record<string, any>;
}

/**
* Checks if a given connector type is compatible for inference.
*
Expand Down Expand Up @@ -67,22 +45,3 @@ export function isSupportedConnector(connector: RawConnector): connector is RawI
}
return true;
}

/**
* Connector types are living in the actions plugin and we can't afford
* having dependencies from this package to some mid-level plugin,
* so we're just using our own connector mixin type.
*/
export interface RawConnector {
id: string;
actionTypeId: string;
name: string;
config?: Record<string, any>;
}

interface RawInferenceConnector {
id: string;
actionTypeId: InferenceConnectorType;
name: string;
config?: Record<string, any>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ export class InferenceTaskError<
super(message);
}

public get status() {
if (typeof this.meta === 'object' && this.meta.status) {
return this.meta.status as number;
}
return undefined;
}

toJSON(): InferenceTaskErrorEvent {
return {
type: InferenceTaskEventType.error,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* 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 { generateFakeToolCallId } from './tool_calls';
Original file line number Diff line number Diff line change
@@ -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.
*/

import { v4 } from 'uuid';

export function generateFakeToolCallId() {
return v4().substr(0, 6);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# @kbn/inference-langchain

This package exposes utilities to use the inference APIs and plugin with langchain

## InferenceChatModel

The inference chat model is a langchain model leveraging the inference APIs under the hood.

The main upside is that the unification and normalization layers are then fully handled
by the inference plugin. The developer / consumer doesn't even need to know which provider
is being used under the hood.

The easiest way to create an `InferenceChatModel` is by using the inference APIs:

```ts
const chatModel = await inferenceStart.getChatModel({
request,
connectorId: myInferenceConnectorId,
chatModelOptions: {
temperature: 0.2,
},
});

// just use it as another langchain chatModel
```

But the chatModel can also be instantiated directly if needed:

```ts
import { connectorToInference } from '@kbn/inference-common';

const chatModel = new InferenceChatModel({
chatComplete: inference.chatComplete,
connector: connectorToInference(someInferenceConnector),
logger: myPluginLogger,
});

// just use it as another langchain chatModel
```
Original file line number Diff line number Diff line change
@@ -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.
*/

export {
InferenceChatModel,
type InferenceChatModelParams,
type InferenceChatModelCallOptions,
} from './src/chat_model';
Loading

0 comments on commit 445353b

Please sign in to comment.