diff --git a/common/api-review/vertexai.api.md b/common/api-review/vertexai.api.md index ff33d56f269..0a925cf580c 100644 --- a/common/api-review/vertexai.api.md +++ b/common/api-review/vertexai.api.md @@ -9,6 +9,15 @@ import { FirebaseApp } from '@firebase/app'; import { FirebaseAuthTokenData } from '@firebase/auth-interop-types'; import { FirebaseError } from '@firebase/util'; +// @public +export class ArraySchema extends Schema { + constructor(schemaParams: SchemaParams, items: TypedSchema); + // (undocumented) + items: TypedSchema; + // @internal (undocumented) + toJSON(): SchemaRequest; +} + // @public export interface BaseParams { // (undocumented) @@ -27,6 +36,11 @@ export enum BlockReason { SAFETY = "SAFETY" } +// @public +export class BooleanSchema extends Schema { + constructor(schemaParams?: SchemaParams); +} + // @public export class ChatSession { // Warning: (ae-forgotten-export) The symbol "ApiSettings" needs to be exported by the entry point index.d.ts @@ -203,42 +217,7 @@ export interface FunctionCallPart { export interface FunctionDeclaration { description?: string; name: string; - parameters?: FunctionDeclarationSchema; -} - -// @public -export interface FunctionDeclarationSchema { - description?: string; - properties: { - [k: string]: FunctionDeclarationSchemaProperty; - }; - required?: string[]; - type: FunctionDeclarationSchemaType; -} - -// @public -export interface FunctionDeclarationSchemaProperty { - description?: string; - enum?: string[]; - example?: unknown; - format?: string; - items?: FunctionDeclarationSchema; - nullable?: boolean; - properties?: { - [k: string]: FunctionDeclarationSchema; - }; - required?: string[]; - type?: FunctionDeclarationSchemaType; -} - -// @public -export enum FunctionDeclarationSchemaType { - ARRAY = "ARRAY", - BOOLEAN = "BOOLEAN", - INTEGER = "INTEGER", - NUMBER = "NUMBER", - OBJECT = "OBJECT", - STRING = "STRING" + parameters?: ObjectSchemaInterface; } // @public @@ -331,6 +310,7 @@ export interface GenerationConfig { // (undocumented) presencePenalty?: number; responseMimeType?: string; + responseSchema?: TypedSchema | SchemaRequest; // (undocumented) stopSequences?: string[]; // (undocumented) @@ -478,6 +458,11 @@ export interface InlineDataPart { videoMetadata?: VideoMetadata; } +// @public +export class IntegerSchema extends Schema { + constructor(schemaParams?: SchemaParams); +} + // @public export interface ModelParams extends BaseParams { // (undocumented) @@ -490,6 +475,34 @@ export interface ModelParams extends BaseParams { tools?: Tool[]; } +// @public +export class NumberSchema extends Schema { + constructor(schemaParams?: SchemaParams); +} + +// @public +export class ObjectSchema extends Schema { + constructor(schemaParams: SchemaParams, properties: { + [k: string]: TypedSchema; + }, optionalProperties?: string[]); + // (undocumented) + optionalProperties: string[]; + // (undocumented) + properties: { + [k: string]: TypedSchema; + }; + // @internal (undocumented) + toJSON(): SchemaRequest; +} + +// @public +export interface ObjectSchemaInterface extends SchemaInterface { + // (undocumented) + optionalProperties?: string[]; + // (undocumented) + type: SchemaType.OBJECT; +} + // @public export type Part = TextPart | InlineDataPart | FunctionCallPart | FunctionResponsePart | FileDataPart; @@ -549,6 +562,82 @@ export interface SafetySetting { threshold: HarmBlockThreshold; } +// @public +export abstract class Schema implements SchemaInterface { + constructor(schemaParams: SchemaInterface); + [key: string]: unknown; + // (undocumented) + static array(arrayParams: SchemaParams & { + items: Schema; + }): ArraySchema; + // (undocumented) + static boolean(booleanParams?: SchemaParams): BooleanSchema; + description?: string; + // (undocumented) + static enumString(stringParams: SchemaParams & { + enum: string[]; + }): StringSchema; + example?: unknown; + format?: string; + // (undocumented) + static integer(integerParams?: SchemaParams): IntegerSchema; + nullable: boolean; + // (undocumented) + static number(numberParams?: SchemaParams): NumberSchema; + // (undocumented) + static object(objectParams: SchemaParams & { + properties: { + [k: string]: Schema; + }; + optionalProperties?: string[]; + }): ObjectSchema; + // (undocumented) + static string(stringParams?: SchemaParams): StringSchema; + // @internal + toJSON(): SchemaRequest; + type: SchemaType; +} + +// @public +export interface SchemaInterface extends SchemaShared { + type: SchemaType; +} + +// @public +export interface SchemaParams extends SchemaShared { +} + +// @public +export interface SchemaRequest extends SchemaShared { + required?: string[]; + type: SchemaType; +} + +// @public +export interface SchemaShared { + // (undocumented) + [key: string]: unknown; + description?: string; + enum?: string[]; + example?: unknown; + format?: string; + items?: T; + nullable?: boolean; + properties?: { + [k: string]: T; + }; +} + +// @public +export enum SchemaType { + ARRAY = "array", + BOOLEAN = "boolean", + INTEGER = "integer", + NUMBER = "number", + OBJECT = "object", + STRING = "string" +} + // @public (undocumented) export interface Segment { // (undocumented) @@ -571,6 +660,15 @@ export interface StartChatParams extends BaseParams { tools?: Tool[]; } +// @public +export class StringSchema extends Schema { + constructor(schemaParams?: SchemaParams, enumValues?: string[]); + // (undocumented) + enum?: string[]; + // @internal (undocumented) + toJSON(): SchemaRequest; +} + // @public export interface TextPart { // (undocumented) @@ -592,6 +690,9 @@ export interface ToolConfig { functionCallingConfig: FunctionCallingConfig; } +// @public +export type TypedSchema = IntegerSchema | NumberSchema | StringSchema | BooleanSchema | ObjectSchema | ArraySchema; + // @public export interface UsageMetadata { // (undocumented) @@ -616,8 +717,6 @@ export class VertexAIError extends FirebaseError { readonly code: VertexAIErrorCode; // (undocumented) readonly customErrorData?: CustomErrorData | undefined; - // (undocumented) - readonly message: string; } // @public @@ -625,6 +724,7 @@ export const enum VertexAIErrorCode { ERROR = "error", FETCH_ERROR = "fetch-error", INVALID_CONTENT = "invalid-content", + INVALID_SCHEMA = "invalid-schema", NO_API_KEY = "no-api-key", NO_MODEL = "no-model", NO_PROJECT_ID = "no-project-id", diff --git a/packages/vertexai/src/api.test.ts b/packages/vertexai/src/api.test.ts index 794c5045012..b6c96923856 100644 --- a/packages/vertexai/src/api.test.ts +++ b/packages/vertexai/src/api.test.ts @@ -39,8 +39,9 @@ describe('Top level API', () => { getGenerativeModel(fakeVertexAI, {} as ModelParams); } catch (e) { expect((e as VertexAIError).code).includes(VertexAIErrorCode.NO_MODEL); - expect((e as VertexAIError).message).equals( - `Must provide a model name. Example: getGenerativeModel({ model: 'my-model-name' })` + expect((e as VertexAIError).message).includes( + `VertexAI: Must provide a model name. Example: ` + + `getGenerativeModel({ model: 'my-model-name' }) (vertexAI/${VertexAIErrorCode.NO_MODEL})` ); } }); @@ -54,7 +55,9 @@ describe('Top level API', () => { } catch (e) { expect((e as VertexAIError).code).includes(VertexAIErrorCode.NO_API_KEY); expect((e as VertexAIError).message).equals( - `The "apiKey" field is empty in the local Firebase config. Firebase VertexAI requires this field to contain a valid API key.` + `VertexAI: The "apiKey" field is empty in the local ` + + `Firebase config. Firebase VertexAI requires this field to` + + ` contain a valid API key. (vertexAI/${VertexAIErrorCode.NO_API_KEY})` ); } }); @@ -70,7 +73,9 @@ describe('Top level API', () => { VertexAIErrorCode.NO_PROJECT_ID ); expect((e as VertexAIError).message).equals( - `The "projectId" field is empty in the local Firebase config. Firebase VertexAI requires this field to contain a valid project ID.` + `VertexAI: The "projectId" field is empty in the local` + + ` Firebase config. Firebase VertexAI requires this field ` + + `to contain a valid project ID. (vertexAI/${VertexAIErrorCode.NO_PROJECT_ID})` ); } }); diff --git a/packages/vertexai/src/api.ts b/packages/vertexai/src/api.ts index 92d5aac7144..23c502895d8 100644 --- a/packages/vertexai/src/api.ts +++ b/packages/vertexai/src/api.ts @@ -26,6 +26,7 @@ import { VertexAIError } from './errors'; import { GenerativeModel } from './models/generative-model'; export { ChatSession } from './methods/chat-session'; +export * from './requests/schema-builder'; export { GenerativeModel }; diff --git a/packages/vertexai/src/errors.ts b/packages/vertexai/src/errors.ts index 46db2fd271f..b0b5d9f25ec 100644 --- a/packages/vertexai/src/errors.ts +++ b/packages/vertexai/src/errors.ts @@ -34,15 +34,15 @@ export class VertexAIError extends FirebaseError { */ constructor( readonly code: VertexAIErrorCode, - readonly message: string, + message: string, readonly customErrorData?: CustomErrorData ) { // Match error format used by FirebaseError from ErrorFactory const service = VERTEX_TYPE; const serviceName = 'VertexAI'; const fullCode = `${service}/${code}`; - const fullMessage = `${serviceName}: ${message} (${fullCode}).`; - super(fullCode, fullMessage); + const fullMessage = `${serviceName}: ${message} (${fullCode})`; + super(code, fullMessage); // FirebaseError initializes a stack trace, but it assumes the error is created from the error // factory. Since we break this assumption, we set the stack trace to be originating from this diff --git a/packages/vertexai/src/requests/schema-builder.test.ts b/packages/vertexai/src/requests/schema-builder.test.ts new file mode 100644 index 00000000000..b95acaae9f1 --- /dev/null +++ b/packages/vertexai/src/requests/schema-builder.test.ts @@ -0,0 +1,393 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, use } from 'chai'; +import sinonChai from 'sinon-chai'; +import { Schema } from './schema-builder'; +import { VertexAIErrorCode } from '../types'; + +use(sinonChai); + +describe('Schema builder', () => { + it('builds integer schema', () => { + const schema = Schema.integer(); + expect(schema.toJSON()).to.eql({ + type: 'integer', + nullable: false + }); + }); + it('builds integer schema with options and overrides', () => { + const schema = Schema.integer({ nullable: true, format: 'int32' }); + expect(schema.toJSON()).to.eql({ + type: 'integer', + format: 'int32', + nullable: true + }); + }); + it('builds number schema', () => { + const schema = Schema.number(); + expect(schema.toJSON()).to.eql({ + type: 'number', + nullable: false + }); + }); + it('builds number schema with options and unknown options', () => { + const schema = Schema.number({ format: 'float', futureOption: 'test' }); + expect(schema.toJSON()).to.eql({ + type: 'number', + format: 'float', + futureOption: 'test', + nullable: false + }); + }); + it('builds boolean schema', () => { + const schema = Schema.boolean(); + expect(schema.toJSON()).to.eql({ + type: 'boolean', + nullable: false + }); + }); + it('builds string schema', () => { + const schema = Schema.string({ description: 'hey' }); + expect(schema.toJSON()).to.eql({ + type: 'string', + description: 'hey', + nullable: false + }); + }); + it('builds enumString schema', () => { + const schema = Schema.enumString({ + example: 'east', + enum: ['east', 'west'] + }); + expect(schema.toJSON()).to.eql({ + type: 'string', + example: 'east', + enum: ['east', 'west'], + nullable: false + }); + }); + it('builds an object schema', () => { + const schema = Schema.object({ + properties: { + 'someInput': Schema.string() + } + }); + expect(schema.toJSON()).to.eql({ + type: 'object', + nullable: false, + properties: { + 'someInput': { + type: 'string', + nullable: false + } + }, + required: ['someInput'] + }); + }); + it('builds an object schema with optional properties', () => { + const schema = Schema.object({ + properties: { + 'someInput': Schema.string(), + 'someBool': Schema.boolean() + }, + optionalProperties: ['someBool'] + }); + expect(schema.toJSON()).to.eql({ + type: 'object', + nullable: false, + properties: { + 'someInput': { + type: 'string', + nullable: false + }, + 'someBool': { + type: 'boolean', + nullable: false + } + }, + required: ['someInput'] + }); + }); + it('builds layered schema - partially filled out', () => { + const schema = Schema.array({ + items: Schema.object({ + properties: { + country: Schema.string({ + description: 'A country name' + }), + population: Schema.integer(), + coordinates: Schema.object({ + properties: { + latitude: Schema.number({ format: 'float' }), + longitude: Schema.number({ format: 'double' }) + } + }), + hemisphere: Schema.object({ + properties: { + latitudinal: Schema.enumString({ enum: ['N', 'S'] }), + longitudinal: Schema.enumString({ enum: ['E', 'W'] }) + } + }), + isCapital: Schema.boolean() + } + }) + }); + expect(schema.toJSON()).to.eql(layeredSchemaOutputPartial); + }); + it('builds layered schema - fully filled out', () => { + const schema = Schema.array({ + items: Schema.object({ + description: 'A country profile', + nullable: false, + properties: { + country: Schema.string({ + nullable: false, + description: 'Country name', + format: undefined + }), + population: Schema.integer({ + nullable: false, + description: 'Number of people in country', + format: 'int64' + }), + coordinates: Schema.object({ + nullable: false, + description: 'Latitude and longitude', + properties: { + latitude: Schema.number({ + nullable: false, + description: 'Latitude of capital', + format: 'float' + }), + longitude: Schema.number({ + nullable: false, + description: 'Longitude of capital', + format: 'double' + }) + } + }), + hemisphere: Schema.object({ + nullable: false, + description: 'Hemisphere(s) country is in', + properties: { + latitudinal: Schema.enumString({ enum: ['N', 'S'] }), + longitudinal: Schema.enumString({ enum: ['E', 'W'] }) + } + }), + isCapital: Schema.boolean({ + nullable: false, + description: "This doesn't make a lot of sense but it's a demo" + }), + elevation: Schema.integer({ + nullable: false, + description: 'Average elevation', + format: 'float' + }) + }, + optionalProperties: [] + }) + }); + + expect(schema.toJSON()).to.eql(layeredSchemaOutput); + }); + it('can override "nullable" and set optional properties', () => { + const schema = Schema.object({ + properties: { + country: Schema.string(), + elevation: Schema.number(), + population: Schema.integer({ nullable: true }) + }, + optionalProperties: ['elevation'] + }); + expect(schema.toJSON()).to.eql({ + 'type': 'object', + 'nullable': false, + 'properties': { + 'country': { + 'type': 'string', + 'nullable': false + }, + 'elevation': { + 'type': 'number', + 'nullable': false + }, + 'population': { + 'type': 'integer', + 'nullable': true + } + }, + 'required': ['country', 'population'] + }); + }); + it('throws if an optionalProperties item does not exist', () => { + const schema = Schema.object({ + properties: { + country: Schema.string(), + elevation: Schema.number(), + population: Schema.integer({ nullable: true }) + }, + optionalProperties: ['cat'] + }); + expect(() => schema.toJSON()).to.throw(VertexAIErrorCode.INVALID_SCHEMA); + }); +}); + +const layeredSchemaOutputPartial = { + 'type': 'array', + 'nullable': false, + 'items': { + 'type': 'object', + 'nullable': false, + 'properties': { + 'country': { + 'type': 'string', + 'description': 'A country name', + 'nullable': false + }, + 'population': { + 'type': 'integer', + 'nullable': false + }, + 'coordinates': { + 'type': 'object', + 'nullable': false, + 'properties': { + 'latitude': { + 'type': 'number', + 'format': 'float', + 'nullable': false + }, + 'longitude': { + 'type': 'number', + 'format': 'double', + 'nullable': false + } + }, + 'required': ['latitude', 'longitude'] + }, + 'hemisphere': { + 'type': 'object', + 'nullable': false, + 'properties': { + 'latitudinal': { + 'type': 'string', + 'nullable': false, + 'enum': ['N', 'S'] + }, + 'longitudinal': { + 'type': 'string', + 'nullable': false, + 'enum': ['E', 'W'] + } + }, + 'required': ['latitudinal', 'longitudinal'] + }, + 'isCapital': { + 'type': 'boolean', + 'nullable': false + } + }, + 'required': [ + 'country', + 'population', + 'coordinates', + 'hemisphere', + 'isCapital' + ] + } +}; + +const layeredSchemaOutput = { + 'type': 'array', + 'nullable': false, + 'items': { + 'type': 'object', + 'description': 'A country profile', + 'nullable': false, + 'required': [ + 'country', + 'population', + 'coordinates', + 'hemisphere', + 'isCapital', + 'elevation' + ], + 'properties': { + 'country': { + 'type': 'string', + 'description': 'Country name', + 'nullable': false + }, + 'population': { + 'type': 'integer', + 'format': 'int64', + 'description': 'Number of people in country', + 'nullable': false + }, + 'coordinates': { + 'type': 'object', + 'description': 'Latitude and longitude', + 'nullable': false, + 'required': ['latitude', 'longitude'], + 'properties': { + 'latitude': { + 'type': 'number', + 'format': 'float', + 'description': 'Latitude of capital', + 'nullable': false + }, + 'longitude': { + 'type': 'number', + 'format': 'double', + 'description': 'Longitude of capital', + 'nullable': false + } + } + }, + 'hemisphere': { + 'type': 'object', + 'description': 'Hemisphere(s) country is in', + 'nullable': false, + 'required': ['latitudinal', 'longitudinal'], + 'properties': { + 'latitudinal': { + 'type': 'string', + 'nullable': false, + 'enum': ['N', 'S'] + }, + 'longitudinal': { + 'type': 'string', + 'nullable': false, + 'enum': ['E', 'W'] + } + } + }, + 'isCapital': { + 'type': 'boolean', + 'description': "This doesn't make a lot of sense but it's a demo", + 'nullable': false + }, + 'elevation': { + 'type': 'integer', + 'format': 'float', + 'description': 'Average elevation', + 'nullable': false + } + } + } +}; diff --git a/packages/vertexai/src/requests/schema-builder.ts b/packages/vertexai/src/requests/schema-builder.ts new file mode 100644 index 00000000000..8a36aaa8997 --- /dev/null +++ b/packages/vertexai/src/requests/schema-builder.ts @@ -0,0 +1,292 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { VertexAIError } from '../errors'; +import { VertexAIErrorCode } from '../types'; +import { + SchemaInterface, + SchemaType, + SchemaParams, + SchemaRequest, + ObjectSchemaInterface +} from '../types/schema'; + +/** + * Parent class encompassing all Schema types, with static methods that + * allow building specific Schema types. This class can be converted with + * `JSON.stringify()` into a JSON string accepted by Vertex AI REST endpoints. + * (This string conversion is automatically done when calling SDK methods.) + * @public + */ +export abstract class Schema implements SchemaInterface { + /** + * Optional. The type of the property. {@link + * SchemaType}. + */ + type: SchemaType; + /** Optional. The format of the property. + * Supported formats:
+ *
    + *
  • for NUMBER type: "float", "double"
  • + *
  • for INTEGER type: "int32", "int64"
  • + *
  • for STRING type: "email", "byte", etc
  • + *
+ */ + format?: string; + /** Optional. The description of the property. */ + description?: string; + /** Optional. Whether the property is nullable. Defaults to false. */ + nullable: boolean; + /** Optional. The example of the property. */ + example?: unknown; + /** + * Allows user to add other schema properties that have not yet + * been officially added to the SDK. + */ + [key: string]: unknown; + + constructor(schemaParams: SchemaInterface) { + // eslint-disable-next-line guard-for-in + for (const paramKey in schemaParams) { + this[paramKey] = schemaParams[paramKey]; + } + // Ensure these are explicitly set to avoid TS errors. + this.type = schemaParams.type; + this.nullable = schemaParams.hasOwnProperty('nullable') + ? !!schemaParams.nullable + : false; + } + + /** + * Defines how this Schema should be serialized as JSON. + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#tojson_behavior + * @internal + */ + toJSON(): SchemaRequest { + const obj: { type: SchemaType; [key: string]: unknown } = { + type: this.type + }; + for (const prop in this) { + if (this.hasOwnProperty(prop) && this[prop] !== undefined) { + if (prop !== 'required' || this.type === SchemaType.OBJECT) { + obj[prop] = this[prop]; + } + } + } + return obj as SchemaRequest; + } + + static array(arrayParams: SchemaParams & { items: Schema }): ArraySchema { + return new ArraySchema(arrayParams, arrayParams.items); + } + + static object( + objectParams: SchemaParams & { + properties: { + [k: string]: Schema; + }; + optionalProperties?: string[]; + } + ): ObjectSchema { + return new ObjectSchema( + objectParams, + objectParams.properties, + objectParams.optionalProperties + ); + } + + // eslint-disable-next-line id-blacklist + static string(stringParams?: SchemaParams): StringSchema { + return new StringSchema(stringParams); + } + + static enumString( + stringParams: SchemaParams & { enum: string[] } + ): StringSchema { + return new StringSchema(stringParams, stringParams.enum); + } + + static integer(integerParams?: SchemaParams): IntegerSchema { + return new IntegerSchema(integerParams); + } + + // eslint-disable-next-line id-blacklist + static number(numberParams?: SchemaParams): NumberSchema { + return new NumberSchema(numberParams); + } + + // eslint-disable-next-line id-blacklist + static boolean(booleanParams?: SchemaParams): BooleanSchema { + return new BooleanSchema(booleanParams); + } +} + +/** + * A type that includes all specific Schema types. + * @public + */ +export type TypedSchema = + | IntegerSchema + | NumberSchema + | StringSchema + | BooleanSchema + | ObjectSchema + | ArraySchema; + +/** + * Schema class for "integer" types. + * @public + */ +export class IntegerSchema extends Schema { + constructor(schemaParams?: SchemaParams) { + super({ + type: SchemaType.INTEGER, + ...schemaParams + }); + } +} + +/** + * Schema class for "number" types. + * @public + */ +export class NumberSchema extends Schema { + constructor(schemaParams?: SchemaParams) { + super({ + type: SchemaType.NUMBER, + ...schemaParams + }); + } +} + +/** + * Schema class for "boolean" types. + * @public + */ +export class BooleanSchema extends Schema { + constructor(schemaParams?: SchemaParams) { + super({ + type: SchemaType.BOOLEAN, + ...schemaParams + }); + } +} + +/** + * Schema class for "string" types. Can be used with or without + * enum values. + * @public + */ +export class StringSchema extends Schema { + enum?: string[]; + constructor(schemaParams?: SchemaParams, enumValues?: string[]) { + super({ + type: SchemaType.STRING, + ...schemaParams + }); + this.enum = enumValues; + } + + /** + * @internal + */ + toJSON(): SchemaRequest { + const obj = super.toJSON(); + if (this.enum) { + obj['enum'] = this.enum; + } + return obj as SchemaRequest; + } +} + +/** + * Schema class for "array" types. + * The `items` param should refer to the type of item that can be a member + * of the array. + * @public + */ +export class ArraySchema extends Schema { + constructor(schemaParams: SchemaParams, public items: TypedSchema) { + super({ + type: SchemaType.ARRAY, + ...schemaParams + }); + } + + /** + * @internal + */ + toJSON(): SchemaRequest { + const obj = super.toJSON(); + obj.items = this.items.toJSON(); + return obj; + } +} + +/** + * Schema class for "object" types. + * The `properties` param must be a map of Schema. + * @public + */ +export class ObjectSchema extends Schema { + constructor( + schemaParams: SchemaParams, + public properties: { + [k: string]: TypedSchema; + }, + public optionalProperties: string[] = [] + ) { + super({ + type: SchemaType.OBJECT, + ...schemaParams + }); + } + + /** + * @internal + */ + toJSON(): SchemaRequest { + const obj = super.toJSON(); + obj.properties = { ...this.properties }; + const required = []; + if (this.optionalProperties) { + for (const propertyKey of this.optionalProperties) { + if (!this.properties.hasOwnProperty(propertyKey)) { + throw new VertexAIError( + VertexAIErrorCode.INVALID_SCHEMA, + `Property "${propertyKey}" specified in "optionalProperties" does not exist.` + ); + } + } + } + for (const propertyKey in this.properties) { + if (this.properties.hasOwnProperty(propertyKey)) { + obj.properties[propertyKey] = this.properties[ + propertyKey + ].toJSON() as SchemaRequest; + if (!this.optionalProperties.includes(propertyKey)) { + required.push(propertyKey); + } + } + } + if (required.length > 0) { + obj.required = required; + } + delete (obj as ObjectSchemaInterface).optionalProperties; + return obj as SchemaRequest; + } +} diff --git a/packages/vertexai/src/types/error.ts b/packages/vertexai/src/types/error.ts index 5ba594013c2..d624e64fa07 100644 --- a/packages/vertexai/src/types/error.ts +++ b/packages/vertexai/src/types/error.ts @@ -78,6 +78,9 @@ export const enum VertexAIErrorCode { /** An error associated with a Content object. */ INVALID_CONTENT = 'invalid-content', + /** An error due to invalid Schema input. */ + INVALID_SCHEMA = 'invalid-schema', + /** An error occurred due to a missing Firebase API key. */ NO_API_KEY = 'no-api-key', diff --git a/packages/vertexai/src/types/index.ts b/packages/vertexai/src/types/index.ts index 45365c39037..85133aa07c5 100644 --- a/packages/vertexai/src/types/index.ts +++ b/packages/vertexai/src/types/index.ts @@ -20,3 +20,4 @@ export * from './enums'; export * from './requests'; export * from './responses'; export * from './error'; +export * from './schema'; diff --git a/packages/vertexai/src/types/requests.ts b/packages/vertexai/src/types/requests.ts index 1e976b386b6..335e7e7dc72 100644 --- a/packages/vertexai/src/types/requests.ts +++ b/packages/vertexai/src/types/requests.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { TypedSchema } from '../requests/schema-builder'; import { Content, Part } from './content'; import { FunctionCallingMode, @@ -22,6 +23,7 @@ import { HarmBlockThreshold, HarmCategory } from './enums'; +import { ObjectSchemaInterface, SchemaRequest } from './schema'; /** * Base parameters for a number of methods. @@ -78,14 +80,20 @@ export interface GenerationConfig { presencePenalty?: number; frequencyPenalty?: number; /** - * Output response mimetype of the generated candidate text. - * Supported mimetypes are `text/plain` (default, text output) and `application/json` + * Output response MIME type of the generated candidate text. + * Supported MIME types are `text/plain` (default, text output) and `application/json` * (JSON response in the candidates). - * The model needs to be prompted to output the appropriate response type, - * otherwise the behavior is undefined. - * This is a preview feature. */ responseMimeType?: string; + /** + * Output response schema of the generated candidate text. This + * value can be a class generated with a {@link Schema} static method + * like `Schema.string()` or `Schema.object()` or it can be a plain + * JS object matching the {@link SchemaRequest} interface. + *
Note: This only applies when the specified `responseMIMEType` supports a schema; currently + * this is limited to `application/json`. + */ + responseSchema?: TypedSchema | SchemaRequest; } /** @@ -154,7 +162,7 @@ export declare interface FunctionDeclaration { * format. Reflects the Open API 3.03 Parameter Object. Parameter names are * case-sensitive. For a function with no parameters, this can be left unset. */ - parameters?: FunctionDeclarationSchema; + parameters?: ObjectSchemaInterface; } /** @@ -177,71 +185,6 @@ export declare interface FunctionDeclarationsTool { functionDeclarations?: FunctionDeclaration[]; } -/** - * Contains the list of OpenAPI data types - * as defined by https://swagger.io/docs/specification/data-models/data-types/ - * @public - */ -export enum FunctionDeclarationSchemaType { - /** String type. */ - STRING = 'STRING', - /** Number type. */ - NUMBER = 'NUMBER', - /** Integer type. */ - INTEGER = 'INTEGER', - /** Boolean type. */ - BOOLEAN = 'BOOLEAN', - /** Array type. */ - ARRAY = 'ARRAY', - /** Object type. */ - OBJECT = 'OBJECT' -} - -/** - * Schema for parameters passed to {@link FunctionDeclaration.parameters}. - * @public - */ -export interface FunctionDeclarationSchema { - /** The type of the parameter. */ - type: FunctionDeclarationSchemaType; - /** The format of the parameter. */ - properties: { [k: string]: FunctionDeclarationSchemaProperty }; - /** Optional. Description of the parameter. */ - description?: string; - /** Optional. Array of required parameters. */ - required?: string[]; -} - -/** - * Schema is used to define the format of input/output data. - * Represents a select subset of an OpenAPI 3.0 schema object. - * More fields may be added in the future as needed. - * @public - */ -export interface FunctionDeclarationSchemaProperty { - /** - * Optional. The type of the property. {@link - * FunctionDeclarationSchemaType}. - */ - type?: FunctionDeclarationSchemaType; - /** Optional. The format of the property. */ - format?: string; - /** Optional. The description of the property. */ - description?: string; - /** Optional. Whether the property is nullable. */ - nullable?: boolean; - /** Optional. The items of the property. {@link FunctionDeclarationSchema} */ - items?: FunctionDeclarationSchema; - /** Optional. The enum of the property. */ - enum?: string[]; - /** Optional. Map of {@link FunctionDeclarationSchema}. */ - properties?: { [k: string]: FunctionDeclarationSchema }; - /** Optional. Array of required property. */ - required?: string[]; - /** Optional. The example of the property. */ - example?: unknown; -} - /** * Tool config. This config is shared for all tools provided in the request. * @public diff --git a/packages/vertexai/src/types/schema.ts b/packages/vertexai/src/types/schema.ts new file mode 100644 index 00000000000..7b8f34d89d6 --- /dev/null +++ b/packages/vertexai/src/types/schema.ts @@ -0,0 +1,104 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Contains the list of OpenAPI data types + * as defined by the + * {@link https://swagger.io/docs/specification/data-models/data-types/ | OpenAPI specification} + * @public + */ +export enum SchemaType { + /** String type. */ + STRING = 'string', + /** Number type. */ + NUMBER = 'number', + /** Integer type. */ + INTEGER = 'integer', + /** Boolean type. */ + BOOLEAN = 'boolean', + /** Array type. */ + ARRAY = 'array', + /** Object type. */ + OBJECT = 'object' +} + +/** + * Basic {@link Schema} properties shared across several Schema-related + * types. + * @public + */ +export interface SchemaShared { + /** Optional. The format of the property. */ + format?: string; + /** Optional. The description of the property. */ + description?: string; + /** Optional. The items of the property. */ + items?: T; + /** Optional. Map of Schemas. */ + properties?: { + [k: string]: T; + }; + /** Optional. The enum of the property. */ + enum?: string[]; + /** Optional. The example of the property. */ + example?: unknown; + /** Optional. Whether the property is nullable. */ + nullable?: boolean; + [key: string]: unknown; +} + +/** + * Params passed to {@link Schema} static methods to create specific + * {@link Schema} classes. + * @public + */ +export interface SchemaParams extends SchemaShared {} + +/** + * Final format for {@link Schema} params passed to backend requests. + * @public + */ +export interface SchemaRequest extends SchemaShared { + /** + * The type of the property. {@link + * SchemaType}. + */ + type: SchemaType; + /** Optional. Array of required property. */ + required?: string[]; +} + +/** + * Interface for {@link Schema} class. + * @public + */ +export interface SchemaInterface extends SchemaShared { + /** + * The type of the property. {@link + * SchemaType}. + */ + type: SchemaType; +} + +/** + * Interface for {@link ObjectSchema} class. + * @public + */ +export interface ObjectSchemaInterface extends SchemaInterface { + type: SchemaType.OBJECT; + optionalProperties?: string[]; +}