From 48e0018eb13423023c0b950b11720822e7817ad7 Mon Sep 17 00:00:00 2001 From: GermanVor Date: Fri, 20 Dec 2024 16:16:51 +0100 Subject: [PATCH 1/5] feat: foundation-model sdk --- .../sdk/embeddingSdk.ts | 30 ++++ .../sdk/imageGenerationSdk.ts | 38 +++++ clients/ai-foundation_models-v1/sdk/index.ts | 4 + .../sdk/textClassificationSdk.ts | 63 ++++++++ .../sdk/textGenerationSdk.ts | 84 +++++++++++ clients/ai-foundation_models-v1/sdk/types.ts | 138 ++++++++++++++++++ package.json | 2 +- 7 files changed, 358 insertions(+), 1 deletion(-) create mode 100644 clients/ai-foundation_models-v1/sdk/embeddingSdk.ts create mode 100644 clients/ai-foundation_models-v1/sdk/imageGenerationSdk.ts create mode 100644 clients/ai-foundation_models-v1/sdk/index.ts create mode 100644 clients/ai-foundation_models-v1/sdk/textClassificationSdk.ts create mode 100644 clients/ai-foundation_models-v1/sdk/textGenerationSdk.ts create mode 100644 clients/ai-foundation_models-v1/sdk/types.ts diff --git a/clients/ai-foundation_models-v1/sdk/embeddingSdk.ts b/clients/ai-foundation_models-v1/sdk/embeddingSdk.ts new file mode 100644 index 00000000..964b0d94 --- /dev/null +++ b/clients/ai-foundation_models-v1/sdk/embeddingSdk.ts @@ -0,0 +1,30 @@ +import { Client } from 'nice-grpc'; +import { embeddingService } from '..'; +import { + EmbeddingsServiceService, + TextEmbeddingRequest, +} from '../generated/yandex/cloud/ai/foundation_models/v1/embedding/embedding_service'; +import { ClientCallArgs, SessionArg, TypeFromProtoc } from './types'; + +export type TextEmbeddingProps = Omit, 'modelUri'> & { + modelId: string; + folderId: string; +}; + +export class EmbeddingSdk { + private embeddingClient: Client; + + constructor(session: SessionArg) { + this.embeddingClient = session.client(embeddingService.EmbeddingsServiceClient); + } + + textEmbedding(params: TextEmbeddingProps, args?: ClientCallArgs) { + const { modelId, folderId, ...restParams } = params; + const modelUri = `gpt://${folderId}/${modelId}`; + + return this.embeddingClient.textEmbedding( + embeddingService.TextEmbeddingRequest.fromPartial({ ...restParams, modelUri }), + args, + ); + } +} diff --git a/clients/ai-foundation_models-v1/sdk/imageGenerationSdk.ts b/clients/ai-foundation_models-v1/sdk/imageGenerationSdk.ts new file mode 100644 index 00000000..3fa66660 --- /dev/null +++ b/clients/ai-foundation_models-v1/sdk/imageGenerationSdk.ts @@ -0,0 +1,38 @@ +import { Client } from 'nice-grpc'; +import { imageGenerationService } from '..'; +import { + ImageGenerationAsyncServiceService, + ImageGenerationRequest, +} from '../generated/yandex/cloud/ai/foundation_models/v1/image_generation/image_generation_service'; +import { ClientCallArgs, SessionArg, TypeFromProtoc } from './types'; + +export type GenerateImageProps = Omit< + TypeFromProtoc, + 'modelUri' +> & { + modelId: string; + folderId: string; +}; + +export class ImageGenerationSdk { + private imageGenerationClient: Client< + typeof ImageGenerationAsyncServiceService, + ClientCallArgs + >; + + constructor(session: SessionArg) { + this.imageGenerationClient = session.client( + imageGenerationService.ImageGenerationAsyncServiceClient, + ); + } + + generateImage(params: GenerateImageProps, args?: ClientCallArgs) { + const { modelId, folderId, ...restParams } = params; + const modelUri = `gpt://${folderId}/${modelId}`; + + return this.imageGenerationClient.generate( + imageGenerationService.ImageGenerationRequest.fromPartial({ ...restParams, modelUri }), + args, + ); + } +} diff --git a/clients/ai-foundation_models-v1/sdk/index.ts b/clients/ai-foundation_models-v1/sdk/index.ts new file mode 100644 index 00000000..9f504b0d --- /dev/null +++ b/clients/ai-foundation_models-v1/sdk/index.ts @@ -0,0 +1,4 @@ +export * from './embeddingSdk'; +export * from './imageGenerationSdk'; +export * from './textClassificationSdk'; +export * from './textGenerationSdk'; diff --git a/clients/ai-foundation_models-v1/sdk/textClassificationSdk.ts b/clients/ai-foundation_models-v1/sdk/textClassificationSdk.ts new file mode 100644 index 00000000..214ceaa3 --- /dev/null +++ b/clients/ai-foundation_models-v1/sdk/textClassificationSdk.ts @@ -0,0 +1,63 @@ +import { Client } from 'nice-grpc'; +import { ClientCallArgs, SessionArg, TypeFromProtoc } from './types'; +import { + FewShotTextClassificationRequest, + TextClassificationRequest, + TextClassificationServiceService, +} from '../generated/yandex/cloud/ai/foundation_models/v1/text_classification/text_classification_service'; +import { textClassificationService } from '..'; + +export type TextClassificationProps = Omit< + TypeFromProtoc, + 'modelUri' +> & { + modelId: string; + folderId: string; +}; + +export type FewShotTextClassificationProps = Omit< + TypeFromProtoc, + 'modelUri' +> & { + modelId: string; + folderId: string; +}; + +export class TextClassificationSdk { + private textClassificationClient: Client< + typeof TextClassificationServiceService, + ClientCallArgs + >; + + constructor(session: SessionArg) { + this.textClassificationClient = session.client( + textClassificationService.TextClassificationServiceClient, + ); + } + + classifyText(params: TextClassificationProps, args?: ClientCallArgs) { + const { modelId, folderId, ...restParams } = params; + const modelUri = `gpt://${folderId}/${modelId}`; + + return this.textClassificationClient.classify( + textClassificationService.TextClassificationRequest.fromPartial({ + ...restParams, + modelUri, + }), + args, + ); + } + + classifyTextFewShort(params: FewShotTextClassificationProps, args?: ClientCallArgs) { + const { modelId, folderId, ...restParams } = params; + const modelUri = `gpt://${folderId}/${modelId}`; + + return this.textClassificationClient.fewShotClassify( + textClassificationService.FewShotTextClassificationRequest.fromPartial({ + ...restParams, + modelUri, + }), + args, + ); + } +} diff --git a/clients/ai-foundation_models-v1/sdk/textGenerationSdk.ts b/clients/ai-foundation_models-v1/sdk/textGenerationSdk.ts new file mode 100644 index 00000000..c2c82241 --- /dev/null +++ b/clients/ai-foundation_models-v1/sdk/textGenerationSdk.ts @@ -0,0 +1,84 @@ +import { Client } from 'nice-grpc'; +import { textGenerationService } from '..'; + +import { ClientCallArgs, SessionArg, TypeFromProtoc } from './types'; +import { + CompletionRequest, + TextGenerationAsyncServiceService, + TextGenerationServiceService, + TokenizeRequest, + TokenizerServiceService, +} from '../generated/yandex/cloud/ai/foundation_models/v1/text_generation/text_generation_service'; + +export type CompletionProps = Omit, 'modelUri'> & { + modelId: string; + folderId: string; +}; + +export type TokenizeProps = Omit, 'modelUri'> & { + modelId: string; + folderId: string; +}; + +export class TextGenerationSdk { + private textGenerationClient: Client; + private tokenizerClient: Client; + private textGenerationAsyncClient: Client< + typeof TextGenerationAsyncServiceService, + ClientCallArgs + >; + + constructor(session: SessionArg) { + this.textGenerationClient = session.client( + textGenerationService.TextGenerationServiceClient, + ); + + this.tokenizerClient = session.client(textGenerationService.TokenizerServiceClient); + + this.textGenerationAsyncClient = session.client( + textGenerationService.TextGenerationAsyncServiceClient, + ); + } + + tokenize(params: TokenizeProps, args?: ClientCallArgs) { + const { modelId, folderId, ...restParams } = params; + const modelUri = `gpt://${folderId}/${modelId}`; + + return this.tokenizerClient.tokenize( + textGenerationService.TokenizeRequest.fromPartial({ ...restParams, modelUri }), + args, + ); + } + + tokenizeCompletion(params: CompletionProps, args?: ClientCallArgs) { + const { modelId, folderId, ...restParams } = params; + const modelUri = `gpt://${folderId}/${modelId}`; + + return this.tokenizerClient.tokenizeCompletion( + textGenerationService.CompletionRequest.fromPartial({ ...restParams, modelUri }), + args, + ); + } + + completion(params: CompletionProps, args?: ClientCallArgs) { + const { modelId, folderId, ...restParams } = params; + const modelUri = `gpt://${folderId}/${modelId}`; + + return this.textGenerationClient.completion( + textGenerationService.CompletionRequest.fromPartial({ ...restParams, modelUri }), + args, + ); + } + + completionAsOperation(params: CompletionProps, args?: ClientCallArgs) { + const { modelId, folderId, ...restParams } = params; + const modelUri = `gpt://${folderId}/${modelId}`; + + const operationP = this.textGenerationAsyncClient.completion( + textGenerationService.CompletionRequest.fromPartial({ ...restParams, modelUri }), + args, + ); + + return operationP; + } +} diff --git a/clients/ai-foundation_models-v1/sdk/types.ts b/clients/ai-foundation_models-v1/sdk/types.ts new file mode 100644 index 00000000..38e1bb12 --- /dev/null +++ b/clients/ai-foundation_models-v1/sdk/types.ts @@ -0,0 +1,138 @@ +import { ChannelCredentials, ChannelOptions, Client, ServiceDefinition } from '@grpc/grpc-js'; +import { ClientError, RawClient, Status } from 'nice-grpc'; +import { DeadlineOptions } from 'nice-grpc-client-middleware-deadline'; +import { NormalizedServiceDefinition } from 'nice-grpc/lib/service-definitions'; + +import { DeepPartial } from '../generated/typeRegistry'; + +type RetryOptions = { + /** + * Boolean indicating whether retries are enabled. + * + * If the method is marked as idempotent in Protobuf, i.e. has + * + * option idempotency_level = IDEMPOTENT; + * + * then the default is `true`. Otherwise, the default is `false`. + * + * Method options currently work only when compiling with `ts-proto`. + */ + retry?: boolean; + /** + * Base delay between retry attempts in milliseconds. + * + * Defaults to 1000. + * + * Example: if `retryBaseDelayMs` is 100, then retries will be attempted in + * 100ms, 200ms, 400ms etc. (not counting jitter). + */ + retryBaseDelayMs?: number; + /** + * Maximum delay between attempts in milliseconds. + * + * Defaults to 15 seconds. + * + * Example: if `retryBaseDelayMs` is 1000 and `retryMaxDelayMs` is 3000, then + * retries will be attempted in 1000ms, 2000ms, 3000ms, 3000ms etc (not + * counting jitter). + */ + retryMaxDelayMs?: number; + /** + * Maximum for the total number of attempts. `Infinity` is supported. + * + * Defaults to 1, i.e. a single retry will be attempted. + */ + retryMaxAttempts?: number; + /** + * Array of retryable status codes. + * + * Default is `[UNKNOWN, RESOURCE_EXHAUSTED, INTERNAL, UNAVAILABLE]`. + */ + retryableStatuses?: Status[]; + /** + * Called after receiving error with retryable status code before setting + * backoff delay timer. + * + * If the error code is not retryable, or the maximum attempts exceeded, this + * function will not be called and the error will be thrown from the client + * method. + */ + onRetryableError?(error: ClientError, attempt: number, delayMs: number): void; +}; + +export interface TokenService { + getToken: () => Promise; +} + +export interface GeneratedServiceClientCtor { + service: T; + + new ( + address: string, + credentials: ChannelCredentials, + options?: Partial, + ): Client; +} + +export interface IIAmCredentials { + serviceAccountId: string; + accessKeyId: string; + privateKey: Buffer | string; +} + +export interface ISslCredentials { + rootCertificates?: Buffer; + clientPrivateKey?: Buffer; + clientCertChain?: Buffer; +} + +export interface ChannelSslOptions { + rootCerts?: Buffer; + privateKey?: Buffer; + certChain?: Buffer; +} + +export interface GenericCredentialsConfig { + pollInterval?: number; + ssl?: ChannelSslOptions; + headers?: Record; +} + +export interface OAuthCredentialsConfig extends GenericCredentialsConfig { + oauthToken: string; +} + +export interface IamTokenCredentialsConfig extends GenericCredentialsConfig { + iamToken: string; +} + +export interface ServiceAccountCredentialsConfig extends GenericCredentialsConfig { + serviceAccountJson: IIAmCredentials; +} + +export type SessionConfig = + | OAuthCredentialsConfig + | IamTokenCredentialsConfig + | ServiceAccountCredentialsConfig + | GenericCredentialsConfig; + +export type ClientCallArgs = DeadlineOptions & RetryOptions; + +export type WrappedServiceClientType = RawClient< + NormalizedServiceDefinition, + ClientCallArgs +>; + +export type ClientType = ( + clientClass: GeneratedServiceClientCtor, + customEndpoint?: string, +) => WrappedServiceClientType; + +export type SessionArg = { client: ClientType }; + +export type TypeFromProtoc< + T extends { $type: string }, + NotPartialKey extends keyof Omit = never, +> = { + [Key in NotPartialKey]: T[Key]; +} & DeepPartial; diff --git a/package.json b/package.json index 4997b817..05c11fee 100644 --- a/package.json +++ b/package.json @@ -158,7 +158,7 @@ }, "scripts": { "test": "cross-env NODE_OPTIONS=\"--max-old-space-size=4096\" jest -c config/jest.ts --passWithNoTests '.*\\.test\\.ts$'", - "lint": "eslint src config", + "lint": "eslint src config clients", "generate-code": "ts-node scripts/generate-code.ts", "check-endpoints": "ts-node scripts/check-endpoints.ts", "prettier:fix:clients": "prettier clients/ --write", From 3d6c993dbcf58aa6e6f168c416ccc843b011c719 Mon Sep 17 00:00:00 2001 From: GermanVor Date: Tue, 24 Dec 2024 00:12:00 +0100 Subject: [PATCH 2/5] chore: qwerty --- .../sdk/imageGenerationSdk.ts | 5 +- clients/operation/sdk/index.ts | 1 + clients/operation/sdk/operationSdk.ts | 111 ++++++++++++++ clients/operation/sdk/types.ts | 139 ++++++++++++++++++ examples/generate-image.ts | 63 ++++++++ 5 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 clients/operation/sdk/index.ts create mode 100644 clients/operation/sdk/operationSdk.ts create mode 100644 clients/operation/sdk/types.ts create mode 100644 examples/generate-image.ts diff --git a/clients/ai-foundation_models-v1/sdk/imageGenerationSdk.ts b/clients/ai-foundation_models-v1/sdk/imageGenerationSdk.ts index 3fa66660..261fc5eb 100644 --- a/clients/ai-foundation_models-v1/sdk/imageGenerationSdk.ts +++ b/clients/ai-foundation_models-v1/sdk/imageGenerationSdk.ts @@ -20,15 +20,16 @@ export class ImageGenerationSdk { ClientCallArgs >; - constructor(session: SessionArg) { + constructor(session: SessionArg, endpoint = 'llm.api.cloud.yandex.net:443') { this.imageGenerationClient = session.client( imageGenerationService.ImageGenerationAsyncServiceClient, + endpoint, ); } generateImage(params: GenerateImageProps, args?: ClientCallArgs) { const { modelId, folderId, ...restParams } = params; - const modelUri = `gpt://${folderId}/${modelId}`; + const modelUri = `art://${folderId}/${modelId}`; return this.imageGenerationClient.generate( imageGenerationService.ImageGenerationRequest.fromPartial({ ...restParams, modelUri }), diff --git a/clients/operation/sdk/index.ts b/clients/operation/sdk/index.ts new file mode 100644 index 00000000..66c8968a --- /dev/null +++ b/clients/operation/sdk/index.ts @@ -0,0 +1 @@ +export * from './operationSdk'; diff --git a/clients/operation/sdk/operationSdk.ts b/clients/operation/sdk/operationSdk.ts new file mode 100644 index 00000000..1b6822df --- /dev/null +++ b/clients/operation/sdk/operationSdk.ts @@ -0,0 +1,111 @@ +import { isObject, noop } from 'lodash'; +import { operationService } from '..'; +import { + CancelOperationRequest, + GetOperationRequest, + OperationServiceService, +} from '../generated/yandex/cloud/operation/operation_service'; + +import { ClientCallArgs, SessionArg, TypeFromProtoc } from './types'; +import { Client } from 'nice-grpc'; +import { Operation } from '../generated/yandex/cloud/operation/operation'; + +export type GetOperationProps = TypeFromProtoc; + +export type CancelOperationProps = TypeFromProtoc; + +type PollArgs = { + operationCallback?: (operation: Operation) => void; +}; + +interface CancellablePromise extends Promise { + cancelPolling?: () => void; +} + +export class PollOperationWasCanceled { + operation?: Operation; + constructor(operation?: Operation) { + this.operation = operation; + } +} + +export class OperationSdk { + private operationClient: Client; + + constructor(session: SessionArg) { + this.operationClient = session.client(operationService.OperationServiceClient); + } + + static PollOperationWasCanceled = PollOperationWasCanceled; + + public pollOperation( + operation: Operation | string, + intervalMs: number, + args?: PollArgs, + ): CancellablePromise { + let outputReject: (reason?: unknown) => void = noop; + let timeoutId: NodeJS.Timeout | undefined; + + const operationStatusHandler = (operation: Operation) => { + if (operation.done || operation.error) { + if (timeoutId !== undefined) clearTimeout(timeoutId); + return true; + } + + return false; + }; + + if (isObject(operation) && operationStatusHandler(operation)) { + return Promise.resolve(operation); + } + + const p = new Promise((resolver, reject) => { + outputReject = reject; + const operationId = isObject(operation) ? operation.id : operation; + + const f = () => { + this.get({ operationId }).then((operation) => { + args?.operationCallback?.(operation); + + if (operationStatusHandler(operation)) { + timeoutId = undefined; + resolver(operation); + return; + } + + timeoutId = setTimeout(() => f(), intervalMs); + }); + }; + + f(); + }); + + (p as CancellablePromise).cancelPolling = () => { + outputReject?.( + new PollOperationWasCanceled(isObject(operation) ? operation : undefined), + ); + + if (timeoutId !== undefined) clearTimeout(timeoutId); + }; + + return p as CancellablePromise; + } + + public get(params: GetOperationProps, args?: ClientCallArgs) { + return this.operationClient.get( + operationService.GetOperationRequest.fromPartial(params), + args, + ); + } + + public cancel(params: CancelOperationProps, args?: ClientCallArgs) { + return this.operationClient.cancel( + operationService.CancelOperationRequest.fromPartial(params), + args, + ); + } +} + +export const initOperationSdk = (session: SessionArg) => { + return new OperationSdk(session); +}; diff --git a/clients/operation/sdk/types.ts b/clients/operation/sdk/types.ts new file mode 100644 index 00000000..b887f49b --- /dev/null +++ b/clients/operation/sdk/types.ts @@ -0,0 +1,139 @@ +import { ChannelCredentials, ChannelOptions, Client, ServiceDefinition } from '@grpc/grpc-js'; +import { ClientError, RawClient, Status } from 'nice-grpc'; +import { DeadlineOptions } from 'nice-grpc-client-middleware-deadline'; +import { NormalizedServiceDefinition } from 'nice-grpc/lib/service-definitions'; +import { Operation as YandexCloudOperation } from '../generated/yandex/cloud/operation/operation'; + +import { DeepPartial } from '../generated/typeRegistry'; + +type RetryOptions = { + /** + * Boolean indicating whether retries are enabled. + * + * If the method is marked as idempotent in Protobuf, i.e. has + * + * option idempotency_level = IDEMPOTENT; + * + * then the default is `true`. Otherwise, the default is `false`. + * + * Method options currently work only when compiling with `ts-proto`. + */ + retry?: boolean; + /** + * Base delay between retry attempts in milliseconds. + * + * Defaults to 1000. + * + * Example: if `retryBaseDelayMs` is 100, then retries will be attempted in + * 100ms, 200ms, 400ms etc. (not counting jitter). + */ + retryBaseDelayMs?: number; + /** + * Maximum delay between attempts in milliseconds. + * + * Defaults to 15 seconds. + * + * Example: if `retryBaseDelayMs` is 1000 and `retryMaxDelayMs` is 3000, then + * retries will be attempted in 1000ms, 2000ms, 3000ms, 3000ms etc (not + * counting jitter). + */ + retryMaxDelayMs?: number; + /** + * Maximum for the total number of attempts. `Infinity` is supported. + * + * Defaults to 1, i.e. a single retry will be attempted. + */ + retryMaxAttempts?: number; + /** + * Array of retryable status codes. + * + * Default is `[UNKNOWN, RESOURCE_EXHAUSTED, INTERNAL, UNAVAILABLE]`. + */ + retryableStatuses?: Status[]; + /** + * Called after receiving error with retryable status code before setting + * backoff delay timer. + * + * If the error code is not retryable, or the maximum attempts exceeded, this + * function will not be called and the error will be thrown from the client + * method. + */ + onRetryableError?(error: ClientError, attempt: number, delayMs: number): void; +}; + +export interface TokenService { + getToken: () => Promise; +} + +export interface GeneratedServiceClientCtor { + service: T; + + new ( + address: string, + credentials: ChannelCredentials, + options?: Partial, + ): Client; +} + +export interface IIAmCredentials { + serviceAccountId: string; + accessKeyId: string; + privateKey: Buffer | string; +} + +export interface ISslCredentials { + rootCertificates?: Buffer; + clientPrivateKey?: Buffer; + clientCertChain?: Buffer; +} + +export interface ChannelSslOptions { + rootCerts?: Buffer; + privateKey?: Buffer; + certChain?: Buffer; +} + +export interface GenericCredentialsConfig { + pollInterval?: number; + ssl?: ChannelSslOptions; + headers?: Record; +} + +export interface OAuthCredentialsConfig extends GenericCredentialsConfig { + oauthToken: string; +} + +export interface IamTokenCredentialsConfig extends GenericCredentialsConfig { + iamToken: string; +} + +export interface ServiceAccountCredentialsConfig extends GenericCredentialsConfig { + serviceAccountJson: IIAmCredentials; +} + +export type SessionConfig = + | OAuthCredentialsConfig + | IamTokenCredentialsConfig + | ServiceAccountCredentialsConfig + | GenericCredentialsConfig; + +export type ClientCallArgs = DeadlineOptions & RetryOptions; + +export type WrappedServiceClientType = RawClient< + NormalizedServiceDefinition, + ClientCallArgs +>; + +export type ClientType = ( + clientClass: GeneratedServiceClientCtor, + customEndpoint?: string, +) => WrappedServiceClientType; + +export type SessionArg = { client: ClientType }; + +export type TypeFromProtoc< + T extends { $type: string }, + NotPartialKey extends keyof Omit = never, +> = { + [Key in NotPartialKey]: T[Key]; +} & DeepPartial; diff --git a/examples/generate-image.ts b/examples/generate-image.ts new file mode 100644 index 00000000..52a0e029 --- /dev/null +++ b/examples/generate-image.ts @@ -0,0 +1,63 @@ +import path from 'path'; +import dotenv from 'dotenv'; + +import { Session } from '@yandex-cloud/nodejs-sdk/dist/session'; + +import { initOperationSdk } from '@yandex-cloud/nodejs-sdk/operation/sdk'; + +import { ImageGenerationSdk } from '@yandex-cloud/nodejs-sdk/ai-foundation_models-v1/sdk'; +import { imageGeneration } from '@yandex-cloud/nodejs-sdk/ai-foundation_models-v1'; +import { ImageGenerationResponse } from '@yandex-cloud/nodejs-sdk/ai-foundation_models-v1/generated/yandex/cloud/ai/foundation_models/v1/image_generation/image_generation_service'; +import { writeFile } from 'fs'; + +dotenv.config({ path: path.resolve(__dirname, '.env') }); + +const getEnv = (envName: string, defaultValue?: string): string => { + const envValue = process.env[envName] || defaultValue; + + if (!envValue) { + throw new Error(`Env variable ${envName} is not defined`); + } + + return envValue; +}; + +const iamToken = getEnv('YC_IAM_TOKEN'); +const folderId = getEnv('YC_FOLDER_ID'); + +(async function () { + const session = new Session({ iamToken }); + + const operationSdk = initOperationSdk(session); + + const imageGenerationSdk = new ImageGenerationSdk(session); + + const generateImageOperation = await imageGenerationSdk.generateImage({ + folderId, + modelId: 'yandex-art/free-tier', + generationOptions: { + mimeType: 'image/jpeg', + seed: 1165508436334210, + }, + messages: [imageGeneration.Message.fromPartial({ text: 'Кот', weight: 1 })], + }); + + const generateImageFinalOperation = await operationSdk.pollOperation( + generateImageOperation, + 1_000, + { operationCallback: () => console.log('In Process') }, + ); + + if (generateImageFinalOperation.response) { + console.log(ImageGenerationResponse.decode(generateImageFinalOperation.response.value)); + + writeFile( + './image.png', + ImageGenerationResponse.decode(generateImageFinalOperation.response.value).image, + { encoding: 'base64' }, + function (err) { + console.log('File created'); + }, + ); + } +})(); From c33e717da5b756e14e2f3ccc1242ad2e96e1a78f Mon Sep 17 00:00:00 2001 From: GermanVor Date: Fri, 27 Dec 2024 20:23:40 +0100 Subject: [PATCH 3/5] chore: decoder Handler --- clients/operation/sdk/operationSdk.ts | 34 ++++++++++++++++++++++----- examples/chat-with-assistant.ts | 2 +- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/clients/operation/sdk/operationSdk.ts b/clients/operation/sdk/operationSdk.ts index 1b6822df..0f3e40ec 100644 --- a/clients/operation/sdk/operationSdk.ts +++ b/clients/operation/sdk/operationSdk.ts @@ -9,13 +9,15 @@ import { import { ClientCallArgs, SessionArg, TypeFromProtoc } from './types'; import { Client } from 'nice-grpc'; import { Operation } from '../generated/yandex/cloud/operation/operation'; +import { Reader } from 'protobufjs'; export type GetOperationProps = TypeFromProtoc; export type CancelOperationProps = TypeFromProtoc; -type PollArgs = { +type PollArgs = { operationCallback?: (operation: Operation) => void; + decoder?: (input: Reader | Uint8Array, length?: number) => DecoderT; }; interface CancellablePromise extends Promise { @@ -29,6 +31,13 @@ export class PollOperationWasCanceled { } } +export class PollOperationEmptyResponseForDecoder { + operation?: Operation; + constructor(operation?: Operation) { + this.operation = operation; + } +} + export class OperationSdk { private operationClient: Client; @@ -37,12 +46,13 @@ export class OperationSdk { } static PollOperationWasCanceled = PollOperationWasCanceled; + static PollOperationEmptyResponseForDecoder = PollOperationEmptyResponseForDecoder; - public pollOperation( + public pollOperation( operation: Operation | string, intervalMs: number, - args?: PollArgs, - ): CancellablePromise { + args?: PollArgs, + ): CancellablePromise { let outputReject: (reason?: unknown) => void = noop; let timeoutId: NodeJS.Timeout | undefined; @@ -55,8 +65,20 @@ export class OperationSdk { return false; }; + const operationDecoderHandler = (operation: Operation): DecoderT => { + if (args?.decoder) { + if (operation.response === undefined) { + throw new PollOperationEmptyResponseForDecoder(operation); + } + + return args.decoder(operation.response.value); + } + + return operation as DecoderT; + }; + if (isObject(operation) && operationStatusHandler(operation)) { - return Promise.resolve(operation); + return Promise.resolve(operation).then(operationDecoderHandler); } const p = new Promise((resolver, reject) => { @@ -88,7 +110,7 @@ export class OperationSdk { if (timeoutId !== undefined) clearTimeout(timeoutId); }; - return p as CancellablePromise; + return p.then(operationDecoderHandler); } public get(params: GetOperationProps, args?: ClientCallArgs) { diff --git a/examples/chat-with-assistant.ts b/examples/chat-with-assistant.ts index 24981b4d..71c30bad 100644 --- a/examples/chat-with-assistant.ts +++ b/examples/chat-with-assistant.ts @@ -26,7 +26,7 @@ const iamToken = getEnv('YC_IAM_TOKEN'); const folderId = getEnv('YC_FOLDER_ID'); (async function () { - const session = new Session({ }); + const session = new Session({ iamToken }); const assistantClient = session.client(assistantService.AssistantServiceClient); const messageClient = session.client(messageService.MessageServiceClient); From aa465ac6b75404a6a0e1e4e19b8044555a74b086 Mon Sep 17 00:00:00 2001 From: GermanVor Date: Fri, 27 Dec 2024 21:41:07 +0100 Subject: [PATCH 4/5] chore: add example for search index --- examples/assistant-with-search-index.ts | 126 ++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 examples/assistant-with-search-index.ts diff --git a/examples/assistant-with-search-index.ts b/examples/assistant-with-search-index.ts new file mode 100644 index 00000000..f4f46923 --- /dev/null +++ b/examples/assistant-with-search-index.ts @@ -0,0 +1,126 @@ +import path from 'path'; +import dotenv from 'dotenv'; + +import { Session } from '@yandex-cloud/nodejs-sdk/dist/session'; + +import { initFileSdk } from '@yandex-cloud/nodejs-sdk/ai-files-v1/sdk'; +import { ExpirationConfig_ExpirationPolicy } from '@yandex-cloud/nodejs-sdk/ai-files-v1/generated/yandex/cloud/ai/common/common'; + +import { + initAssistantSdk, + initSearchIndexSdk, + initThreadSdk, + MessageSdk, +} from '@yandex-cloud/nodejs-sdk/ai-assistants-v1/sdk'; +import { SearchIndex } from '@yandex-cloud/nodejs-sdk/ai-assistants-v1/generated/yandex/cloud/ai/assistants/v1/searchindex/search_index'; + +import { initOperationSdk } from '@yandex-cloud/nodejs-sdk/operation/sdk'; + +import { readFile } from 'fs/promises'; + +dotenv.config({ path: path.resolve(__dirname, '.env') }); + +const getEnv = (envName: string, defaultValue?: string): string => { + const envValue = process.env[envName] || defaultValue; + + if (!envValue) { + throw new Error(`Env variable ${envName} is not defined`); + } + + return envValue; +}; + +const iamToken = getEnv('YC_IAM_TOKEN'); +const folderId = getEnv('YC_FOLDER_ID'); + +const expirationConfig = { + ttlDays: 2, + expirationPolicy: ExpirationConfig_ExpirationPolicy.STATIC, +}; + +const session = new Session({ iamToken }); + +const createFile = async () => { + const fileSdk = initFileSdk(session); + + const fileToSaveContent = await readFile('./SomeFileToSave.pdf', { + encoding: 'binary', + }); + + const content = Buffer.from(fileToSaveContent, 'binary'); + + const file = await fileSdk.create({ + folderId, + expirationConfig, + mimeType: 'application/pdf', + content, + }); + + return file; +}; + +const createSearchIndex = async (fileId: string) => { + const searchIndexSdk = initSearchIndexSdk(session); + + const createSearchIndexOperation = await searchIndexSdk.create({ + folderId, + fileIds: [fileId], + expirationConfig, + }); + + const operationSdk = initOperationSdk(session); + + const searchIndex = await operationSdk.pollOperation(createSearchIndexOperation, 500, { + decoder: SearchIndex.decode, + }); + + return searchIndex; +}; + +const createAssistantWithSearchIndex = async (searchIndexId: string) => { + const assistantSdk = initAssistantSdk(session); + const assistant = await assistantSdk.create({ + folderId, + modelId: 'yandexgpt/latest', + tools: [{ searchIndex: { searchIndexIds: [searchIndexId] } }], + }); + + return assistant; +}; + +(async function () { + const file = await createFile(); + console.log(file); + + const searchIndex = await createSearchIndex(file.id); + + const assistant = await createAssistantWithSearchIndex(searchIndex.id); + console.log(assistant); + + const threadSdk = initThreadSdk(session); + const thread = await threadSdk.create({ folderId, name: '' }).withSdk(); + + console.log(thread); + + const asyncIterableStreamEvent = await thread + .sendMessage({ + content: MessageSdk.getMessageContent('О чем был текст файла ?'), + }) + .getAssistantResponse(assistant); + + for await (const streamEvent of asyncIterableStreamEvent) { + console.log('\n---------------------\n'); + + if (streamEvent.partialMessage) { + console.log('Partial message:\n'); + console.log(MessageSdk.messageContentToString(streamEvent.partialMessage)); + continue; + } + + if (streamEvent.completedMessage) { + console.log('Completed message:\n'); + console.log(MessageSdk.messageContentToString(streamEvent.completedMessage.content)); + console.log('\n'); + } + } +})(); From 4d937e761e0227132d08fa8535ebb636b31f6366 Mon Sep 17 00:00:00 2001 From: GermanVor Date: Mon, 30 Dec 2024 00:18:33 +0100 Subject: [PATCH 5/5] chore: exports --- clients/ai-assistants-v1/sdk/index.ts | 1 + clients/ai-files-v1/sdk/index.ts | 1 + .../sdk/embeddingSdk.ts | 10 ++++- .../sdk/imageGenerationSdk.ts | 11 ++++- clients/ai-foundation_models-v1/sdk/index.ts | 1 + .../sdk/textClassificationSdk.ts | 12 +++++- .../sdk/textGenerationSdk.ts | 18 ++++++++- clients/operation/sdk/index.ts | 1 + examples/generate-image.ts | 40 +++++++++---------- 9 files changed, 69 insertions(+), 26 deletions(-) diff --git a/clients/ai-assistants-v1/sdk/index.ts b/clients/ai-assistants-v1/sdk/index.ts index fd48c062..bcd20641 100644 --- a/clients/ai-assistants-v1/sdk/index.ts +++ b/clients/ai-assistants-v1/sdk/index.ts @@ -5,3 +5,4 @@ export * from './threadSdk'; export * from './searchIndexFileSdk'; export * from './searchIndexSdk'; export * from './userSdk'; +export * from '..'; diff --git a/clients/ai-files-v1/sdk/index.ts b/clients/ai-files-v1/sdk/index.ts index 807a43b9..7dd4daca 100644 --- a/clients/ai-files-v1/sdk/index.ts +++ b/clients/ai-files-v1/sdk/index.ts @@ -1 +1,2 @@ export * from './fileSdk'; +export * from '..'; diff --git a/clients/ai-foundation_models-v1/sdk/embeddingSdk.ts b/clients/ai-foundation_models-v1/sdk/embeddingSdk.ts index 964b0d94..0970be56 100644 --- a/clients/ai-foundation_models-v1/sdk/embeddingSdk.ts +++ b/clients/ai-foundation_models-v1/sdk/embeddingSdk.ts @@ -14,8 +14,10 @@ export type TextEmbeddingProps = Omit; - constructor(session: SessionArg) { - this.embeddingClient = session.client(embeddingService.EmbeddingsServiceClient); + static ENDPOINT = 'llm.api.cloud.yandex.net:443'; + + constructor(session: SessionArg, endpoint = EmbeddingSdk.ENDPOINT) { + this.embeddingClient = session.client(embeddingService.EmbeddingsServiceClient, endpoint); } textEmbedding(params: TextEmbeddingProps, args?: ClientCallArgs) { @@ -28,3 +30,7 @@ export class EmbeddingSdk { ); } } + +export const initEmbeddingSdk = (session: SessionArg, endpoint = EmbeddingSdk.ENDPOINT) => { + return new EmbeddingSdk(session, endpoint); +}; diff --git a/clients/ai-foundation_models-v1/sdk/imageGenerationSdk.ts b/clients/ai-foundation_models-v1/sdk/imageGenerationSdk.ts index 261fc5eb..cf917795 100644 --- a/clients/ai-foundation_models-v1/sdk/imageGenerationSdk.ts +++ b/clients/ai-foundation_models-v1/sdk/imageGenerationSdk.ts @@ -20,7 +20,9 @@ export class ImageGenerationSdk { ClientCallArgs >; - constructor(session: SessionArg, endpoint = 'llm.api.cloud.yandex.net:443') { + static ENDPOINT = 'llm.api.cloud.yandex.net:443'; + + constructor(session: SessionArg, endpoint = ImageGenerationSdk.ENDPOINT) { this.imageGenerationClient = session.client( imageGenerationService.ImageGenerationAsyncServiceClient, endpoint, @@ -37,3 +39,10 @@ export class ImageGenerationSdk { ); } } + +export const initImageGenerationSdk = ( + session: SessionArg, + endpoint = ImageGenerationSdk.ENDPOINT, +) => { + return new ImageGenerationSdk(session, endpoint); +}; diff --git a/clients/ai-foundation_models-v1/sdk/index.ts b/clients/ai-foundation_models-v1/sdk/index.ts index 9f504b0d..16c5d90d 100644 --- a/clients/ai-foundation_models-v1/sdk/index.ts +++ b/clients/ai-foundation_models-v1/sdk/index.ts @@ -2,3 +2,4 @@ export * from './embeddingSdk'; export * from './imageGenerationSdk'; export * from './textClassificationSdk'; export * from './textGenerationSdk'; +export * from '..'; diff --git a/clients/ai-foundation_models-v1/sdk/textClassificationSdk.ts b/clients/ai-foundation_models-v1/sdk/textClassificationSdk.ts index 214ceaa3..94712d85 100644 --- a/clients/ai-foundation_models-v1/sdk/textClassificationSdk.ts +++ b/clients/ai-foundation_models-v1/sdk/textClassificationSdk.ts @@ -29,9 +29,12 @@ export class TextClassificationSdk { ClientCallArgs >; - constructor(session: SessionArg) { + static ENDPOINT = 'llm.api.cloud.yandex.net:443'; + + constructor(session: SessionArg, endpoint = TextClassificationSdk.ENDPOINT) { this.textClassificationClient = session.client( textClassificationService.TextClassificationServiceClient, + endpoint, ); } @@ -61,3 +64,10 @@ export class TextClassificationSdk { ); } } + +export const initTextClassificationSdk = ( + session: SessionArg, + endpoint = TextClassificationSdk.ENDPOINT, +) => { + return new TextClassificationSdk(session, endpoint); +}; diff --git a/clients/ai-foundation_models-v1/sdk/textGenerationSdk.ts b/clients/ai-foundation_models-v1/sdk/textGenerationSdk.ts index c2c82241..8066b9df 100644 --- a/clients/ai-foundation_models-v1/sdk/textGenerationSdk.ts +++ b/clients/ai-foundation_models-v1/sdk/textGenerationSdk.ts @@ -28,15 +28,22 @@ export class TextGenerationSdk { ClientCallArgs >; - constructor(session: SessionArg) { + static ENDPOINT = 'llm.api.cloud.yandex.net:443'; + + constructor(session: SessionArg, endpoint = TextGenerationSdk.ENDPOINT) { this.textGenerationClient = session.client( textGenerationService.TextGenerationServiceClient, + endpoint, ); - this.tokenizerClient = session.client(textGenerationService.TokenizerServiceClient); + this.tokenizerClient = session.client( + textGenerationService.TokenizerServiceClient, + endpoint, + ); this.textGenerationAsyncClient = session.client( textGenerationService.TextGenerationAsyncServiceClient, + endpoint, ); } @@ -82,3 +89,10 @@ export class TextGenerationSdk { return operationP; } } + +export const initTextGenerationSdk = ( + session: SessionArg, + endpoint = TextGenerationSdk.ENDPOINT, +) => { + return new TextGenerationSdk(session, endpoint); +}; diff --git a/clients/operation/sdk/index.ts b/clients/operation/sdk/index.ts index 66c8968a..6e5c0f0c 100644 --- a/clients/operation/sdk/index.ts +++ b/clients/operation/sdk/index.ts @@ -1 +1,2 @@ export * from './operationSdk'; +export * from '..'; diff --git a/examples/generate-image.ts b/examples/generate-image.ts index 52a0e029..8333d51d 100644 --- a/examples/generate-image.ts +++ b/examples/generate-image.ts @@ -5,8 +5,10 @@ import { Session } from '@yandex-cloud/nodejs-sdk/dist/session'; import { initOperationSdk } from '@yandex-cloud/nodejs-sdk/operation/sdk'; -import { ImageGenerationSdk } from '@yandex-cloud/nodejs-sdk/ai-foundation_models-v1/sdk'; -import { imageGeneration } from '@yandex-cloud/nodejs-sdk/ai-foundation_models-v1'; +import { + initImageGenerationSdk, + imageGeneration, +} from '@yandex-cloud/nodejs-sdk/ai-foundation_models-v1/sdk'; import { ImageGenerationResponse } from '@yandex-cloud/nodejs-sdk/ai-foundation_models-v1/generated/yandex/cloud/ai/foundation_models/v1/image_generation/image_generation_service'; import { writeFile } from 'fs'; @@ -30,34 +32,32 @@ const folderId = getEnv('YC_FOLDER_ID'); const operationSdk = initOperationSdk(session); - const imageGenerationSdk = new ImageGenerationSdk(session); + const imageGenerationSdk = initImageGenerationSdk(session); const generateImageOperation = await imageGenerationSdk.generateImage({ folderId, - modelId: 'yandex-art/free-tier', + modelId: 'yandex-art', generationOptions: { mimeType: 'image/jpeg', - seed: 1165508436334210, }, - messages: [imageGeneration.Message.fromPartial({ text: 'Кот', weight: 1 })], + messages: [ + imageGeneration.Message.fromPartial({ + text: 'Three cats', + weight: 1, + }), + ], }); - const generateImageFinalOperation = await operationSdk.pollOperation( + const imageGenerationResponse = await operationSdk.pollOperation( generateImageOperation, 1_000, - { operationCallback: () => console.log('In Process') }, + { + operationCallback: console.log, + decoder: ImageGenerationResponse.decode, + }, ); - if (generateImageFinalOperation.response) { - console.log(ImageGenerationResponse.decode(generateImageFinalOperation.response.value)); - - writeFile( - './image.png', - ImageGenerationResponse.decode(generateImageFinalOperation.response.value).image, - { encoding: 'base64' }, - function (err) { - console.log('File created'); - }, - ); - } + writeFile('./image.png', imageGenerationResponse.image, { encoding: 'base64' }, function (err) { + console.log('File created'); + }); })();