From be507f98e63f4f86f7d1fa1d045ad0f3bcbc0f4c Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 7 Jan 2025 10:30:04 +0000 Subject: [PATCH 001/115] init --- packages/vertexai/README.md | 54 ++++++++++++++++++++++++++++++++++ packages/vertexai/package.json | 32 ++++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 packages/vertexai/README.md create mode 100644 packages/vertexai/package.json diff --git a/packages/vertexai/README.md b/packages/vertexai/README.md new file mode 100644 index 0000000000..49783ab637 --- /dev/null +++ b/packages/vertexai/README.md @@ -0,0 +1,54 @@ +

+ +
+
+

React Native Firebase - Cloud Storage

+

+ +

+ Coverage + NPM downloads + NPM version + License + Maintained with Lerna +

+ +

+ Chat on Discord + Follow on Twitter + Follow on Facebook +

+ +--- + +Cloud Storage for Firebase is a powerful, simple, and cost-effective object storage service built for Google scale. The Firebase SDKs for Cloud Storage add Google security to file uploads and downloads for your Firebase apps, regardless of network quality. You can use our SDKs to store images, audio, video, or other user-generated content. On the server, you can use [Google Cloud Storage](https://cloud.google.com/storage), to access the same files. + +[> Learn More](https://firebase.google.com/products/storage/) + +## Installation + +Requires `@react-native-firebase/app` to be installed. + +```bash +yarn add @react-native-firebase/storage +``` + +## Documentation + +- [Quick Start](https://rnfirebase.io/storage/usage) +- [Reference](https://rnfirebase.io/reference/storage) + +## License + +- See [LICENSE](/LICENSE) + +--- + +

+ +

+ Built and maintained with 💛 by Invertase. +

+

+ +--- diff --git a/packages/vertexai/package.json b/packages/vertexai/package.json new file mode 100644 index 0000000000..20e216010d --- /dev/null +++ b/packages/vertexai/package.json @@ -0,0 +1,32 @@ +{ + "name": "@react-native-firebase/vertexai", + "version": "0.0.1", + "author": "Invertase (http://invertase.io)", + "description": "React Native Firebase - Vertex AI is a fully-managed, unified AI development platform for building and using generative AI", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "genversion --semi lib/version.js", + "build:clean": "rimraf android/build && rimraf ios/build", + "prepare": "yarn run build" + }, + "repository": { + "type": "git", + "url": "https://github.com/invertase/react-native-firebase/tree/main/packages/vertexai" + }, + "license": "Apache-2.0", + "keywords": [ + "react", + "react-native", + "firebase", + "vertexai", + "gemini", + "generative-ai" + ], + "peerDependencies": { + "@react-native-firebase/app": "21.6.2" + }, + "publishConfig": { + "access": "public" + } +} From bbef576837b1083d403ae03bb464c14dd36f19ed Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 7 Jan 2025 10:37:41 +0000 Subject: [PATCH 002/115] update README --- packages/vertexai/README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/vertexai/README.md b/packages/vertexai/README.md index 49783ab637..3ba225ba15 100644 --- a/packages/vertexai/README.md +++ b/packages/vertexai/README.md @@ -2,13 +2,13 @@
-

React Native Firebase - Cloud Storage

+

React Native Firebase - Vertex AI

- Coverage - NPM downloads - NPM version + Coverage + NPM downloads + NPM version License Maintained with Lerna

@@ -21,22 +21,22 @@ --- -Cloud Storage for Firebase is a powerful, simple, and cost-effective object storage service built for Google scale. The Firebase SDKs for Cloud Storage add Google security to file uploads and downloads for your Firebase apps, regardless of network quality. You can use our SDKs to store images, audio, video, or other user-generated content. On the server, you can use [Google Cloud Storage](https://cloud.google.com/storage), to access the same files. +Vertex AI is a fully-managed, unified AI development platform for building and using generative AI. Access and utilize Vertex AI Studio, Agent Builder, and 150+ foundation models including Gemini 1.5 Pro and Gemini 1.5 Flash. -[> Learn More](https://firebase.google.com/products/storage/) +[> Learn More](https://firebase.google.com/docs/vertex-ai/) ## Installation Requires `@react-native-firebase/app` to be installed. ```bash -yarn add @react-native-firebase/storage +yarn add @react-native-firebase/vertexai ``` ## Documentation -- [Quick Start](https://rnfirebase.io/storage/usage) -- [Reference](https://rnfirebase.io/reference/storage) +- [Quick Start](https://rnfirebase.io/vertexai/usage) +- [Reference](https://rnfirebase.io/reference/vertexai) ## License From 3c9eef308afbee68eba641a83d4b2c2c1bc98fc8 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 7 Jan 2025 10:38:33 +0000 Subject: [PATCH 003/115] .npmignore --- packages/vertexai/.npmignore | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 packages/vertexai/.npmignore diff --git a/packages/vertexai/.npmignore b/packages/vertexai/.npmignore new file mode 100644 index 0000000000..e1c829bc0b --- /dev/null +++ b/packages/vertexai/.npmignore @@ -0,0 +1,26 @@ + +# OS-specific files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.dbandroid/gradle +android/gradlew +android/build +android/gradlew.bat +android/gradle/ + +.idea +coverage +yarn.lock +e2e/ +.github +.vscode +.nyc_output +android/.settings +*.coverage.json +.circleci +.eslintignore +type-test.ts From 304c1946386c457ea32f2da83eacdf3c439ef41f Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 7 Jan 2025 10:39:06 +0000 Subject: [PATCH 004/115] license --- packages/vertexai/LICENSE | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 packages/vertexai/LICENSE diff --git a/packages/vertexai/LICENSE b/packages/vertexai/LICENSE new file mode 100644 index 0000000000..ef3ed44f06 --- /dev/null +++ b/packages/vertexai/LICENSE @@ -0,0 +1,32 @@ +Apache-2.0 License +------------------ + +Copyright (c) 2016-present Invertase Limited & Contributors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this library except in compliance with the License. + +You may obtain a copy of the Apache-2.0 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. + + +Creative Commons Attribution 3.0 License +---------------------------------------- + +Copyright (c) 2016-present Invertase Limited & Contributors + +Documentation and other instructional materials provided for this project +(including on a separate documentation repository or it's documentation website) are +licensed under the Creative Commons Attribution 3.0 License. Code samples/blocks +contained therein are licensed under the Apache License, Version 2.0 (the "License"), as above. + +You may obtain a copy of the Creative Commons Attribution 3.0 License at + + https://creativecommons.org/licenses/by/3.0/ From d713b4e87007ce1de4ae4afddfc8033ec4844f5b Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 8 Jan 2025 14:30:44 +0000 Subject: [PATCH 005/115] initial port over of vertexAI from firebase-js-sdk --- packages/vertexai/lib/constants.ts | 32 ++ packages/vertexai/lib/errors.ts | 66 ++++ packages/vertexai/lib/index.ts | 73 +++++ packages/vertexai/lib/logger.ts | 247 +++++++++++++++ .../lib/methods/chat-session-helpers.ts | 116 +++++++ packages/vertexai/lib/methods/chat-session.ts | 182 +++++++++++ packages/vertexai/lib/methods/count-tokens.ts | 37 +++ .../vertexai/lib/methods/generate-content.ts | 66 ++++ .../vertexai/lib/models/generative-model.ts | 179 +++++++++++ packages/vertexai/lib/public-types.ts | 40 +++ .../vertexai/lib/requests/request-helpers.ts | 115 +++++++ packages/vertexai/lib/requests/request.ts | 230 ++++++++++++++ .../vertexai/lib/requests/response-helpers.ts | 198 ++++++++++++ .../vertexai/lib/requests/schema-builder.ts | 292 ++++++++++++++++++ .../vertexai/lib/requests/stream-reader.ts | 205 ++++++++++++ packages/vertexai/lib/service.ts | 50 +++ packages/vertexai/lib/types/content.ts | 162 ++++++++++ packages/vertexai/lib/types/enums.ts | 139 +++++++++ packages/vertexai/lib/types/error.ts | 98 ++++++ packages/vertexai/lib/types/index.ts | 23 ++ packages/vertexai/lib/types/internal.ts | 41 +++ packages/vertexai/lib/types/requests.ts | 198 ++++++++++++ packages/vertexai/lib/types/responses.ts | 209 +++++++++++++ packages/vertexai/lib/types/schema.ts | 103 ++++++ 24 files changed, 3101 insertions(+) create mode 100644 packages/vertexai/lib/constants.ts create mode 100644 packages/vertexai/lib/errors.ts create mode 100644 packages/vertexai/lib/index.ts create mode 100644 packages/vertexai/lib/logger.ts create mode 100644 packages/vertexai/lib/methods/chat-session-helpers.ts create mode 100644 packages/vertexai/lib/methods/chat-session.ts create mode 100644 packages/vertexai/lib/methods/count-tokens.ts create mode 100644 packages/vertexai/lib/methods/generate-content.ts create mode 100644 packages/vertexai/lib/models/generative-model.ts create mode 100644 packages/vertexai/lib/public-types.ts create mode 100644 packages/vertexai/lib/requests/request-helpers.ts create mode 100644 packages/vertexai/lib/requests/request.ts create mode 100644 packages/vertexai/lib/requests/response-helpers.ts create mode 100644 packages/vertexai/lib/requests/schema-builder.ts create mode 100644 packages/vertexai/lib/requests/stream-reader.ts create mode 100644 packages/vertexai/lib/service.ts create mode 100644 packages/vertexai/lib/types/content.ts create mode 100644 packages/vertexai/lib/types/enums.ts create mode 100644 packages/vertexai/lib/types/error.ts create mode 100644 packages/vertexai/lib/types/index.ts create mode 100644 packages/vertexai/lib/types/internal.ts create mode 100644 packages/vertexai/lib/types/requests.ts create mode 100644 packages/vertexai/lib/types/responses.ts create mode 100644 packages/vertexai/lib/types/schema.ts diff --git a/packages/vertexai/lib/constants.ts b/packages/vertexai/lib/constants.ts new file mode 100644 index 0000000000..357e6c4e77 --- /dev/null +++ b/packages/vertexai/lib/constants.ts @@ -0,0 +1,32 @@ +/** + * @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 { version } from '../package.json'; + +export const VERTEX_TYPE = 'vertexAI'; + +export const DEFAULT_LOCATION = 'us-central1'; + +export const DEFAULT_BASE_URL = 'https://firebasevertexai.googleapis.com'; + +export const DEFAULT_API_VERSION = 'v1beta'; + +export const PACKAGE_VERSION = version; + +export const LANGUAGE_TAG = 'gl-js'; + +export const DEFAULT_FETCH_TIMEOUT_MS = 180 * 1000; diff --git a/packages/vertexai/lib/errors.ts b/packages/vertexai/lib/errors.ts new file mode 100644 index 0000000000..0603b0350d --- /dev/null +++ b/packages/vertexai/lib/errors.ts @@ -0,0 +1,66 @@ +/** + * @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 { FirebaseError } from '@firebase/util'; +import { VertexAIErrorCode, CustomErrorData } from './types'; +import { VERTEX_TYPE } from './constants'; + +/** + * Error class for the Vertex AI in Firebase SDK. + * + * @public + */ +export class VertexAIError extends FirebaseError { + /** + * Constructs a new instance of the `VertexAIError` class. + * + * @param code - The error code from {@link VertexAIErrorCode}. + * @param message - A human-readable message describing the error. + * @param customErrorData - Optional error data. + */ + constructor( + readonly code: VertexAIErrorCode, + 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(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 + // constructor. + // This is only supported in V8. + if (Error.captureStackTrace) { + // Allows us to initialize the stack trace without including the constructor itself at the + // top level of the stack trace. + Error.captureStackTrace(this, VertexAIError); + } + + // Allows instanceof VertexAIError in ES5/ES6 + // https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work + // TODO(dlarocque): Replace this with `new.target`: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget + // which we can now use since we no longer target ES5. + Object.setPrototypeOf(this, VertexAIError.prototype); + + // Since Error is an interface, we don't inherit toString and so we define it ourselves. + this.toString = () => fullMessage; + } +} diff --git a/packages/vertexai/lib/index.ts b/packages/vertexai/lib/index.ts new file mode 100644 index 0000000000..20ac5384a1 --- /dev/null +++ b/packages/vertexai/lib/index.ts @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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 { getApp, FirebaseApp } from '@firebase/app'; +import { ModelParams, RequestOptions, VertexAIErrorCode } from './types'; +import { DEFAULT_LOCATION } from './constants'; +import { VertexAI, VertexAIOptions } from './public-types'; +// import { ModelParams, RequestOptions, VertexAIErrorCode } from './types'; +import { VertexAIError } from './errors'; +import { GenerativeModel } from './models/generative-model'; + +export { ChatSession } from './methods/chat-session'; +export * from './requests/schema-builder'; + +export { GenerativeModel }; + +export { VertexAIError }; + +/** + * Returns a {@link VertexAI} instance for the given app. + * + * @public + * + * @param app - The {@link @firebase/app#FirebaseApp} to use. + */ +export function getVertexAI(app: FirebaseApp = getApp(), options?: VertexAIOptions): VertexAI { + // app = getModularInstance(app); + // Dependencies + // const vertexProvider: Provider<'vertexAI'> = _getProvider(app, VERTEX_TYPE); + + // TODO - app used to get location and later the projectId + // return vertexProvider.getImmediate({ + // identifier: options?.location || DEFAULT_LOCATION, + // }); + return { + app, + location: options?.location || DEFAULT_LOCATION, + }; +} + +/** + * Returns a {@link GenerativeModel} class with methods for inference + * and other functionality. + * + * @public + */ +export function getGenerativeModel( + vertexAI: VertexAI, + modelParams: ModelParams, + requestOptions?: RequestOptions, +): GenerativeModel { + if (!modelParams.model) { + throw new VertexAIError( + VertexAIErrorCode.NO_MODEL, + `Must provide a model name. Example: getGenerativeModel({ model: 'my-model-name' })`, + ); + } + return new GenerativeModel(vertexAI, modelParams, requestOptions); +} diff --git a/packages/vertexai/lib/logger.ts b/packages/vertexai/lib/logger.ts new file mode 100644 index 0000000000..681e9f8ac0 --- /dev/null +++ b/packages/vertexai/lib/logger.ts @@ -0,0 +1,247 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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. + * + */ + +export type LogLevelString = 'debug' | 'verbose' | 'info' | 'warn' | 'error' | 'silent'; + +export interface LogOptions { + level: LogLevelString; +} + +export type LogCallback = (callbackParams: LogCallbackParams) => void; + +export interface LogCallbackParams { + level: LogLevelString; + message: string; + args: unknown[]; + type: string; +} + +/** + * A container for all of the Logger instances + */ +export const instances: Logger[] = []; + +/** + * The JS SDK supports 5 log levels and also allows a user the ability to + * silence the logs altogether. + * + * The order is a follows: + * DEBUG < VERBOSE < INFO < WARN < ERROR + * + * All of the log types above the current log level will be captured (i.e. if + * you set the log level to `INFO`, errors will still be logged, but `DEBUG` and + * `VERBOSE` logs will not) + */ +export enum LogLevel { + DEBUG, + VERBOSE, + INFO, + WARN, + ERROR, + SILENT, +} + +const levelStringToEnum: { [key in LogLevelString]: LogLevel } = { + debug: LogLevel.DEBUG, + verbose: LogLevel.VERBOSE, + info: LogLevel.INFO, + warn: LogLevel.WARN, + error: LogLevel.ERROR, + silent: LogLevel.SILENT, +}; + +/** + * The default log level + */ +const defaultLogLevel: LogLevel = LogLevel.INFO; + +/** + * We allow users the ability to pass their own log handler. We will pass the + * type of log, the current log level, and any other arguments passed (i.e. the + * messages that the user wants to log) to this function. + */ +export type LogHandler = (loggerInstance: Logger, logType: LogLevel, ...args: unknown[]) => void; + +/** + * By default, `console.debug` is not displayed in the developer console (in + * chrome). To avoid forcing users to have to opt-in to these logs twice + * (i.e. once for firebase, and once in the console), we are sending `DEBUG` + * logs to the `console.log` function. + */ +const ConsoleMethod = { + [LogLevel.DEBUG]: 'log', + [LogLevel.VERBOSE]: 'log', + [LogLevel.INFO]: 'info', + [LogLevel.WARN]: 'warn', + [LogLevel.ERROR]: 'error', +}; + +/** + * The default log handler will forward DEBUG, VERBOSE, INFO, WARN, and ERROR + * messages on to their corresponding console counterparts (if the log method + * is supported by the current log level) + */ +const defaultLogHandler: LogHandler = (instance, logType, ...args): void => { + if (logType < instance.logLevel) { + return; + } + const now = new Date().toISOString(); + const method = ConsoleMethod[logType as keyof typeof ConsoleMethod]; + if (method) { + console[method as 'log' | 'info' | 'warn' | 'error'](`[${now}] ${instance.name}:`, ...args); + } else { + throw new Error(`Attempted to log a message with an invalid logType (value: ${logType})`); + } +}; + +export class Logger { + /** + * Gives you an instance of a Logger to capture messages according to + * Firebase's logging scheme. + * + * @param name The name that the logs will be associated with + */ + constructor(public name: string) { + /** + * Capture the current instance for later use + */ + instances.push(this); + } + + /** + * The log level of the given Logger instance. + */ + private _logLevel = defaultLogLevel; + + get logLevel(): LogLevel { + return this._logLevel; + } + + set logLevel(val: LogLevel) { + if (!(val in LogLevel)) { + throw new TypeError(`Invalid value "${val}" assigned to \`logLevel\``); + } + this._logLevel = val; + } + + // Workaround for setter/getter having to be the same type. + setLogLevel(val: LogLevel | LogLevelString): void { + this._logLevel = typeof val === 'string' ? levelStringToEnum[val] : val; + } + + /** + * The main (internal) log handler for the Logger instance. + * Can be set to a new function in internal package code but not by user. + */ + private _logHandler: LogHandler = defaultLogHandler; + get logHandler(): LogHandler { + return this._logHandler; + } + set logHandler(val: LogHandler) { + if (typeof val !== 'function') { + throw new TypeError('Value assigned to `logHandler` must be a function'); + } + this._logHandler = val; + } + + /** + * The optional, additional, user-defined log handler for the Logger instance. + */ + private _userLogHandler: LogHandler | null = null; + get userLogHandler(): LogHandler | null { + return this._userLogHandler; + } + set userLogHandler(val: LogHandler | null) { + this._userLogHandler = val; + } + + /** + * The functions below are all based on the `console` interface + */ + + debug(...args: unknown[]): void { + this._userLogHandler && this._userLogHandler(this, LogLevel.DEBUG, ...args); + this._logHandler(this, LogLevel.DEBUG, ...args); + } + log(...args: unknown[]): void { + this._userLogHandler && this._userLogHandler(this, LogLevel.VERBOSE, ...args); + this._logHandler(this, LogLevel.VERBOSE, ...args); + } + info(...args: unknown[]): void { + this._userLogHandler && this._userLogHandler(this, LogLevel.INFO, ...args); + this._logHandler(this, LogLevel.INFO, ...args); + } + warn(...args: unknown[]): void { + this._userLogHandler && this._userLogHandler(this, LogLevel.WARN, ...args); + this._logHandler(this, LogLevel.WARN, ...args); + } + error(...args: unknown[]): void { + this._userLogHandler && this._userLogHandler(this, LogLevel.ERROR, ...args); + this._logHandler(this, LogLevel.ERROR, ...args); + } +} + +export function setLogLevel(level: LogLevelString | LogLevel): void { + instances.forEach(inst => { + inst.setLogLevel(level); + }); +} + +export function setUserLogHandler(logCallback: LogCallback | null, options?: LogOptions): void { + for (const instance of instances) { + let customLogLevel: LogLevel | null = null; + if (options && options.level) { + customLogLevel = levelStringToEnum[options.level]; + } + if (logCallback === null) { + instance.userLogHandler = null; + } else { + instance.userLogHandler = (instance: Logger, level: LogLevel, ...args: unknown[]) => { + const message = args + .map(arg => { + if (arg == null) { + return null; + } else if (typeof arg === 'string') { + return arg; + } else if (typeof arg === 'number' || typeof arg === 'boolean') { + return arg.toString(); + } else if (arg instanceof Error) { + return arg.message; + } else { + try { + return JSON.stringify(arg); + } catch (ignored) { + return null; + } + } + }) + .filter(arg => arg) + .join(' '); + if (level >= (customLogLevel ?? instance.logLevel)) { + logCallback({ + level: LogLevel[level].toLowerCase() as LogLevelString, + message, + args, + type: instance.name, + }); + } + }; + } + } +} + +export const logger = new Logger('@firebase/vertexai'); diff --git a/packages/vertexai/lib/methods/chat-session-helpers.ts b/packages/vertexai/lib/methods/chat-session-helpers.ts new file mode 100644 index 0000000000..4b9bb56db0 --- /dev/null +++ b/packages/vertexai/lib/methods/chat-session-helpers.ts @@ -0,0 +1,116 @@ +/** + * @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 { Content, POSSIBLE_ROLES, Part, Role, VertexAIErrorCode } from '../types'; +import { VertexAIError } from '../errors'; + +// https://ai.google.dev/api/rest/v1beta/Content#part + +const VALID_PART_FIELDS: Array = [ + 'text', + 'inlineData', + 'functionCall', + 'functionResponse', +]; + +const VALID_PARTS_PER_ROLE: { [key in Role]: Array } = { + user: ['text', 'inlineData'], + function: ['functionResponse'], + model: ['text', 'functionCall'], + // System instructions shouldn't be in history anyway. + system: ['text'], +}; + +const VALID_PREVIOUS_CONTENT_ROLES: { [key in Role]: Role[] } = { + user: ['model'], + function: ['model'], + model: ['user', 'function'], + // System instructions shouldn't be in history. + system: [], +}; + +export function validateChatHistory(history: Content[]): void { + let prevContent: Content | null = null; + for (const currContent of history) { + const { role, parts } = currContent; + if (!prevContent && role !== 'user') { + throw new VertexAIError( + VertexAIErrorCode.INVALID_CONTENT, + `First Content should be with role 'user', got ${role}`, + ); + } + if (!POSSIBLE_ROLES.includes(role)) { + throw new VertexAIError( + VertexAIErrorCode.INVALID_CONTENT, + `Each item should include role field. Got ${role} but valid roles are: ${JSON.stringify( + POSSIBLE_ROLES, + )}`, + ); + } + + if (!Array.isArray(parts)) { + throw new VertexAIError( + VertexAIErrorCode.INVALID_CONTENT, + `Content should have 'parts' but property with an array of Parts`, + ); + } + + if (parts.length === 0) { + throw new VertexAIError( + VertexAIErrorCode.INVALID_CONTENT, + `Each Content should have at least one part`, + ); + } + + const countFields: Record = { + text: 0, + inlineData: 0, + functionCall: 0, + functionResponse: 0, + }; + + for (const part of parts) { + for (const key of VALID_PART_FIELDS) { + if (key in part) { + countFields[key] += 1; + } + } + } + const validParts = VALID_PARTS_PER_ROLE[role]; + for (const key of VALID_PART_FIELDS) { + if (!validParts.includes(key) && countFields[key] > 0) { + throw new VertexAIError( + VertexAIErrorCode.INVALID_CONTENT, + `Content with role '${role}' can't contain '${key}' part`, + ); + } + } + + if (prevContent) { + const validPreviousContentRoles = VALID_PREVIOUS_CONTENT_ROLES[role]; + if (!validPreviousContentRoles.includes(prevContent.role)) { + throw new VertexAIError( + VertexAIErrorCode.INVALID_CONTENT, + `Content with role '${role} can't follow '${ + prevContent.role + }'. Valid previous roles: ${JSON.stringify(VALID_PREVIOUS_CONTENT_ROLES)}`, + ); + } + } + prevContent = currContent; + } +} diff --git a/packages/vertexai/lib/methods/chat-session.ts b/packages/vertexai/lib/methods/chat-session.ts new file mode 100644 index 0000000000..d22393d5b7 --- /dev/null +++ b/packages/vertexai/lib/methods/chat-session.ts @@ -0,0 +1,182 @@ +/** + * @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 { + Content, + GenerateContentRequest, + GenerateContentResult, + GenerateContentStreamResult, + Part, + RequestOptions, + StartChatParams, +} from '../types'; +import { formatNewContent } from '../requests/request-helpers'; +import { formatBlockErrorMessage } from '../requests/response-helpers'; +import { validateChatHistory } from './chat-session-helpers'; +import { generateContent, generateContentStream } from './generate-content'; +import { ApiSettings } from '../types/internal'; +import { logger } from '../logger'; + +/** + * Do not log a message for this error. + */ +const SILENT_ERROR = 'SILENT_ERROR'; + +/** + * ChatSession class that enables sending chat messages and stores + * history of sent and received messages so far. + * + * @public + */ +export class ChatSession { + private _apiSettings: ApiSettings; + private _history: Content[] = []; + private _sendPromise: Promise = Promise.resolve(); + + constructor( + apiSettings: ApiSettings, + public model: string, + public params?: StartChatParams, + public requestOptions?: RequestOptions, + ) { + this._apiSettings = apiSettings; + if (params?.history) { + validateChatHistory(params.history); + this._history = params.history; + } + } + + /** + * Gets the chat history so far. Blocked prompts are not added to history. + * Neither blocked candidates nor the prompts that generated them are added + * to history. + */ + async getHistory(): Promise { + await this._sendPromise; + return this._history; + } + + /** + * Sends a chat message and receives a non-streaming + * {@link GenerateContentResult} + */ + async sendMessage(request: string | Array): Promise { + await this._sendPromise; + const newContent = formatNewContent(request); + const generateContentRequest: GenerateContentRequest = { + safetySettings: this.params?.safetySettings, + generationConfig: this.params?.generationConfig, + tools: this.params?.tools, + toolConfig: this.params?.toolConfig, + systemInstruction: this.params?.systemInstruction, + contents: [...this._history, newContent], + }; + let finalResult = {} as GenerateContentResult; + // Add onto the chain. + this._sendPromise = this._sendPromise + .then(() => + generateContent(this._apiSettings, this.model, generateContentRequest, this.requestOptions), + ) + .then(result => { + if (result.response.candidates && result.response.candidates.length > 0) { + this._history.push(newContent); + const responseContent: Content = { + parts: result.response.candidates?.[0].content.parts || [], + // Response seems to come back without a role set. + role: result.response.candidates?.[0].content.role || 'model', + }; + this._history.push(responseContent); + } else { + const blockErrorMessage = formatBlockErrorMessage(result.response); + if (blockErrorMessage) { + logger.warn( + `sendMessage() was unsuccessful. ${blockErrorMessage}. Inspect response object for details.`, + ); + } + } + finalResult = result; + }); + await this._sendPromise; + return finalResult; + } + + /** + * Sends a chat message and receives the response as a + * {@link GenerateContentStreamResult} containing an iterable stream + * and a response promise. + */ + async sendMessageStream( + request: string | Array, + ): Promise { + await this._sendPromise; + const newContent = formatNewContent(request); + const generateContentRequest: GenerateContentRequest = { + safetySettings: this.params?.safetySettings, + generationConfig: this.params?.generationConfig, + tools: this.params?.tools, + toolConfig: this.params?.toolConfig, + systemInstruction: this.params?.systemInstruction, + contents: [...this._history, newContent], + }; + const streamPromise = generateContentStream( + this._apiSettings, + this.model, + generateContentRequest, + this.requestOptions, + ); + + // Add onto the chain. + this._sendPromise = this._sendPromise + .then(() => streamPromise) + // This must be handled to avoid unhandled rejection, but jump + // to the final catch block with a label to not log this error. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .catch(_ignored => { + throw new Error(SILENT_ERROR); + }) + .then(streamResult => streamResult.response) + .then(response => { + if (response.candidates && response.candidates.length > 0) { + this._history.push(newContent); + const responseContent = { ...response.candidates[0].content }; + // Response seems to come back without a role set. + if (!responseContent.role) { + responseContent.role = 'model'; + } + this._history.push(responseContent); + } else { + const blockErrorMessage = formatBlockErrorMessage(response); + if (blockErrorMessage) { + logger.warn( + `sendMessageStream() was unsuccessful. ${blockErrorMessage}. Inspect response object for details.`, + ); + } + } + }) + .catch(e => { + // Errors in streamPromise are already catchable by the user as + // streamPromise is returned. + // Avoid duplicating the error message in logs. + if (e.message !== SILENT_ERROR) { + // Users do not have access to _sendPromise to catch errors + // downstream from streamPromise, so they should not throw. + logger.error(e); + } + }); + return streamPromise; + } +} diff --git a/packages/vertexai/lib/methods/count-tokens.ts b/packages/vertexai/lib/methods/count-tokens.ts new file mode 100644 index 0000000000..10d41cffa8 --- /dev/null +++ b/packages/vertexai/lib/methods/count-tokens.ts @@ -0,0 +1,37 @@ +/** + * @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 { CountTokensRequest, CountTokensResponse, RequestOptions } from '../types'; +import { Task, makeRequest } from '../requests/request'; +import { ApiSettings } from '../types/internal'; + +export async function countTokens( + apiSettings: ApiSettings, + model: string, + params: CountTokensRequest, + requestOptions?: RequestOptions, +): Promise { + const response = await makeRequest( + model, + Task.COUNT_TOKENS, + apiSettings, + false, + JSON.stringify(params), + requestOptions, + ); + return response.json(); +} diff --git a/packages/vertexai/lib/methods/generate-content.ts b/packages/vertexai/lib/methods/generate-content.ts new file mode 100644 index 0000000000..6d1a6ecb27 --- /dev/null +++ b/packages/vertexai/lib/methods/generate-content.ts @@ -0,0 +1,66 @@ +/** + * @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 { + GenerateContentRequest, + GenerateContentResponse, + GenerateContentResult, + GenerateContentStreamResult, + RequestOptions, +} from '../types'; +import { Task, makeRequest } from '../requests/request'; +import { createEnhancedContentResponse } from '../requests/response-helpers'; +import { processStream } from '../requests/stream-reader'; +import { ApiSettings } from '../types/internal'; + +export async function generateContentStream( + apiSettings: ApiSettings, + model: string, + params: GenerateContentRequest, + requestOptions?: RequestOptions, +): Promise { + const response = await makeRequest( + model, + Task.STREAM_GENERATE_CONTENT, + apiSettings, + /* stream */ true, + JSON.stringify(params), + requestOptions, + ); + return processStream(response); +} + +export async function generateContent( + apiSettings: ApiSettings, + model: string, + params: GenerateContentRequest, + requestOptions?: RequestOptions, +): Promise { + const response = await makeRequest( + model, + Task.GENERATE_CONTENT, + apiSettings, + /* stream */ false, + JSON.stringify(params), + requestOptions, + ); + const responseJson: GenerateContentResponse = await response.json(); + const enhancedResponse = createEnhancedContentResponse(responseJson); + return { + response: enhancedResponse, + }; +} diff --git a/packages/vertexai/lib/models/generative-model.ts b/packages/vertexai/lib/models/generative-model.ts new file mode 100644 index 0000000000..9df5d1c4ed --- /dev/null +++ b/packages/vertexai/lib/models/generative-model.ts @@ -0,0 +1,179 @@ +/** + * @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 { generateContent, generateContentStream } from '../methods/generate-content'; +import { + Content, + CountTokensRequest, + CountTokensResponse, + GenerateContentRequest, + GenerateContentResult, + GenerateContentStreamResult, + GenerationConfig, + ModelParams, + Part, + RequestOptions, + SafetySetting, + StartChatParams, + Tool, + ToolConfig, + VertexAIErrorCode, +} from '../types'; +import { VertexAIError } from '../errors'; +import { ChatSession } from '../methods/chat-session'; +import { countTokens } from '../methods/count-tokens'; +import { formatGenerateContentInput, formatSystemInstruction } from '../requests/request-helpers'; +import { VertexAI } from '../public-types'; +import { ApiSettings } from '../types/internal'; +import { VertexAIService } from '../service'; + +/** + * Class for generative model APIs. + * @public + */ +export class GenerativeModel { + private _apiSettings: ApiSettings; + model: string; + generationConfig: GenerationConfig; + safetySettings: SafetySetting[]; + requestOptions?: RequestOptions; + tools?: Tool[]; + toolConfig?: ToolConfig; + systemInstruction?: Content; + + constructor(vertexAI: VertexAI, modelParams: ModelParams, requestOptions?: RequestOptions) { + if (!vertexAI.app?.options?.apiKey) { + throw new VertexAIError( + VertexAIErrorCode.NO_API_KEY, + `The "apiKey" field is empty in the local Firebase config. Firebase VertexAI requires this field to contain a valid API key.`, + ); + } else if (!vertexAI.app?.options?.projectId) { + throw new VertexAIError( + VertexAIErrorCode.NO_PROJECT_ID, + `The "projectId" field is empty in the local Firebase config. Firebase VertexAI requires this field to contain a valid project ID.`, + ); + } else { + this._apiSettings = { + apiKey: vertexAI.app.options.apiKey, + project: vertexAI.app.options.projectId, + location: vertexAI.location, + }; + if ((vertexAI as VertexAIService).appCheck) { + this._apiSettings.getAppCheckToken = () => + (vertexAI as VertexAIService).appCheck!.getToken(); + } + + if ((vertexAI as VertexAIService).auth) { + this._apiSettings.getAuthToken = () => (vertexAI as VertexAIService).auth!.getToken(); + } + } + if (modelParams.model.includes('/')) { + if (modelParams.model.startsWith('models/')) { + // Add "publishers/google" if the user is only passing in 'models/model-name'. + this.model = `publishers/google/${modelParams.model}`; + } else { + // Any other custom format (e.g. tuned models) must be passed in correctly. + this.model = modelParams.model; + } + } else { + // If path is not included, assume it's a non-tuned model. + this.model = `publishers/google/models/${modelParams.model}`; + } + this.generationConfig = modelParams.generationConfig || {}; + this.safetySettings = modelParams.safetySettings || []; + this.tools = modelParams.tools; + this.toolConfig = modelParams.toolConfig; + this.systemInstruction = formatSystemInstruction(modelParams.systemInstruction); + this.requestOptions = requestOptions || {}; + } + + /** + * Makes a single non-streaming call to the model + * and returns an object containing a single {@link GenerateContentResponse}. + */ + async generateContent( + request: GenerateContentRequest | string | Array, + ): Promise { + const formattedParams = formatGenerateContentInput(request); + return generateContent( + this._apiSettings, + this.model, + { + generationConfig: this.generationConfig, + safetySettings: this.safetySettings, + tools: this.tools, + toolConfig: this.toolConfig, + systemInstruction: this.systemInstruction, + ...formattedParams, + }, + this.requestOptions, + ); + } + + /** + * Makes a single streaming call to the model + * and returns an object containing an iterable stream that iterates + * over all chunks in the streaming response as well as + * a promise that returns the final aggregated response. + */ + async generateContentStream( + request: GenerateContentRequest | string | Array, + ): Promise { + const formattedParams = formatGenerateContentInput(request); + return generateContentStream( + this._apiSettings, + this.model, + { + generationConfig: this.generationConfig, + safetySettings: this.safetySettings, + tools: this.tools, + toolConfig: this.toolConfig, + systemInstruction: this.systemInstruction, + ...formattedParams, + }, + this.requestOptions, + ); + } + + /** + * Gets a new {@link ChatSession} instance which can be used for + * multi-turn chats. + */ + startChat(startChatParams?: StartChatParams): ChatSession { + return new ChatSession( + this._apiSettings, + this.model, + { + tools: this.tools, + toolConfig: this.toolConfig, + systemInstruction: this.systemInstruction, + ...startChatParams, + }, + this.requestOptions, + ); + } + + /** + * Counts the tokens in the provided request. + */ + async countTokens( + request: CountTokensRequest | string | Array, + ): Promise { + const formattedParams = formatGenerateContentInput(request); + return countTokens(this._apiSettings, this.model, formattedParams); + } +} diff --git a/packages/vertexai/lib/public-types.ts b/packages/vertexai/lib/public-types.ts new file mode 100644 index 0000000000..280fee9d1c --- /dev/null +++ b/packages/vertexai/lib/public-types.ts @@ -0,0 +1,40 @@ +/** + * @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 { FirebaseApp } from '@firebase/app'; + +export * from './types'; + +/** + * An instance of the Vertex AI in Firebase SDK. + * @public + */ +export interface VertexAI { + /** + * The {@link @firebase/app#FirebaseApp} this {@link VertexAI} instance is associated with. + */ + app: FirebaseApp; + location: string; +} + +/** + * Options when initializing the Vertex AI in Firebase SDK. + * @public + */ +export interface VertexAIOptions { + location?: string; +} diff --git a/packages/vertexai/lib/requests/request-helpers.ts b/packages/vertexai/lib/requests/request-helpers.ts new file mode 100644 index 0000000000..44405cb6f4 --- /dev/null +++ b/packages/vertexai/lib/requests/request-helpers.ts @@ -0,0 +1,115 @@ +/** + * @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 { Content, GenerateContentRequest, Part, VertexAIErrorCode } from '../types'; +import { VertexAIError } from '../errors'; + +export function formatSystemInstruction(input?: string | Part | Content): Content | undefined { + // null or undefined + if (input == null) { + return undefined; + } else if (typeof input === 'string') { + return { role: 'system', parts: [{ text: input }] } as Content; + } else if ((input as Part).text) { + return { role: 'system', parts: [input as Part] }; + } else if ((input as Content).parts) { + if (!(input as Content).role) { + return { role: 'system', parts: (input as Content).parts }; + } else { + return input as Content; + } + } +} + +export function formatNewContent(request: string | Array): Content { + let newParts: Part[] = []; + if (typeof request === 'string') { + newParts = [{ text: request }]; + } else { + for (const partOrString of request) { + if (typeof partOrString === 'string') { + newParts.push({ text: partOrString }); + } else { + newParts.push(partOrString); + } + } + } + return assignRoleToPartsAndValidateSendMessageRequest(newParts); +} + +/** + * When multiple Part types (i.e. FunctionResponsePart and TextPart) are + * passed in a single Part array, we may need to assign different roles to each + * part. Currently only FunctionResponsePart requires a role other than 'user'. + * @private + * @param parts Array of parts to pass to the model + * @returns Array of content items + */ +function assignRoleToPartsAndValidateSendMessageRequest(parts: Part[]): Content { + const userContent: Content = { role: 'user', parts: [] }; + const functionContent: Content = { role: 'function', parts: [] }; + let hasUserContent = false; + let hasFunctionContent = false; + for (const part of parts) { + if ('functionResponse' in part) { + functionContent.parts.push(part); + hasFunctionContent = true; + } else { + userContent.parts.push(part); + hasUserContent = true; + } + } + + if (hasUserContent && hasFunctionContent) { + throw new VertexAIError( + VertexAIErrorCode.INVALID_CONTENT, + 'Within a single message, FunctionResponse cannot be mixed with other type of Part in the request for sending chat message.', + ); + } + + if (!hasUserContent && !hasFunctionContent) { + throw new VertexAIError( + VertexAIErrorCode.INVALID_CONTENT, + 'No Content is provided for sending chat message.', + ); + } + + if (hasUserContent) { + return userContent; + } + + return functionContent; +} + +export function formatGenerateContentInput( + params: GenerateContentRequest | string | Array, +): GenerateContentRequest { + let formattedRequest: GenerateContentRequest; + if ((params as GenerateContentRequest).contents) { + formattedRequest = params as GenerateContentRequest; + } else { + // Array or string + const content = formatNewContent(params as string | Array); + formattedRequest = { contents: [content] }; + } + if ((params as GenerateContentRequest).systemInstruction) { + formattedRequest.systemInstruction = formatSystemInstruction( + (params as GenerateContentRequest).systemInstruction, + ); + } + return formattedRequest; +} diff --git a/packages/vertexai/lib/requests/request.ts b/packages/vertexai/lib/requests/request.ts new file mode 100644 index 0000000000..f81b40635e --- /dev/null +++ b/packages/vertexai/lib/requests/request.ts @@ -0,0 +1,230 @@ +/** + * @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 { ErrorDetails, RequestOptions, VertexAIErrorCode } from '../types'; +import { VertexAIError } from '../errors'; +import { ApiSettings } from '../types/internal'; +import { + DEFAULT_API_VERSION, + DEFAULT_BASE_URL, + DEFAULT_FETCH_TIMEOUT_MS, + LANGUAGE_TAG, + PACKAGE_VERSION +} from '../constants'; +import { logger } from '../logger'; + +export enum Task { + GENERATE_CONTENT = 'generateContent', + STREAM_GENERATE_CONTENT = 'streamGenerateContent', + COUNT_TOKENS = 'countTokens' +} + +export class RequestUrl { + constructor( + public model: string, + public task: Task, + public apiSettings: ApiSettings, + public stream: boolean, + public requestOptions?: RequestOptions + ) {} + toString(): string { + // TODO: allow user-set option if that feature becomes available + const apiVersion = DEFAULT_API_VERSION; + const baseUrl = this.requestOptions?.baseUrl || DEFAULT_BASE_URL; + let url = `${baseUrl}/${apiVersion}`; + url += `/projects/${this.apiSettings.project}`; + url += `/locations/${this.apiSettings.location}`; + url += `/${this.model}`; + url += `:${this.task}`; + if (this.stream) { + url += '?alt=sse'; + } + return url; + } + + /** + * If the model needs to be passed to the backend, it needs to + * include project and location path. + */ + get fullModelString(): string { + let modelString = `projects/${this.apiSettings.project}`; + modelString += `/locations/${this.apiSettings.location}`; + modelString += `/${this.model}`; + return modelString; + } +} + +/** + * Log language and "fire/version" to x-goog-api-client + */ +function getClientHeaders(): string { + const loggingTags = []; + loggingTags.push(`${LANGUAGE_TAG}/${PACKAGE_VERSION}`); + loggingTags.push(`fire/${PACKAGE_VERSION}`); + return loggingTags.join(' '); +} + +export async function getHeaders(url: RequestUrl): Promise { + const headers = new Headers(); + headers.append('Content-Type', 'application/json'); + headers.append('x-goog-api-client', getClientHeaders()); + headers.append('x-goog-api-key', url.apiSettings.apiKey); + if (url.apiSettings.getAppCheckToken) { + const appCheckToken = await url.apiSettings.getAppCheckToken(); + if (appCheckToken) { + headers.append('X-Firebase-AppCheck', appCheckToken.token); + if (appCheckToken.error) { + logger.warn( + `Unable to obtain a valid App Check token: ${appCheckToken.error.message}` + ); + } + } + } + + if (url.apiSettings.getAuthToken) { + const authToken = await url.apiSettings.getAuthToken(); + if (authToken) { + headers.append('Authorization', `Firebase ${authToken.accessToken}`); + } + } + + return headers; +} + +export async function constructRequest( + model: string, + task: Task, + apiSettings: ApiSettings, + stream: boolean, + body: string, + requestOptions?: RequestOptions +): Promise<{ url: string; fetchOptions: RequestInit }> { + const url = new RequestUrl(model, task, apiSettings, stream, requestOptions); + return { + url: url.toString(), + fetchOptions: { + method: 'POST', + headers: await getHeaders(url), + body + } + }; +} + +export async function makeRequest( + model: string, + task: Task, + apiSettings: ApiSettings, + stream: boolean, + body: string, + requestOptions?: RequestOptions +): Promise { + const url = new RequestUrl(model, task, apiSettings, stream, requestOptions); + let response; + let fetchTimeoutId: string | number | NodeJS.Timeout | undefined; + try { + const request = await constructRequest( + model, + task, + apiSettings, + stream, + body, + requestOptions + ); + // Timeout is 180s by default + const timeoutMillis = + requestOptions?.timeout != null && requestOptions.timeout >= 0 + ? requestOptions.timeout + : DEFAULT_FETCH_TIMEOUT_MS; + const abortController = new AbortController(); + fetchTimeoutId = setTimeout(() => abortController.abort(), timeoutMillis); + request.fetchOptions.signal = abortController.signal; + + response = await fetch(request.url, request.fetchOptions); + if (!response.ok) { + let message = ''; + let errorDetails; + try { + const json = await response.json(); + message = json.error.message; + if (json.error.details) { + message += ` ${JSON.stringify(json.error.details)}`; + errorDetails = json.error.details; + } + } catch (e) { + // ignored + } + if ( + response.status === 403 && + errorDetails.some( + (detail: ErrorDetails) => detail.reason === 'SERVICE_DISABLED' + ) && + errorDetails.some((detail: ErrorDetails) => + ( + detail.links as Array> + )?.[0]?.description.includes( + 'Google developers console API activation' + ) + ) + ) { + throw new VertexAIError( + VertexAIErrorCode.API_NOT_ENABLED, + `The Vertex AI in Firebase SDK requires the Vertex AI in Firebase ` + + `API ('firebasevertexai.googleapis.com') to be enabled in your ` + + `Firebase project. Enable this API by visiting the Firebase Console ` + + `at https://console.firebase.google.com/project/${url.apiSettings.project}/genai/ ` + + `and clicking "Get started". If you enabled this API recently, ` + + `wait a few minutes for the action to propagate to our systems and ` + + `then retry.`, + { + status: response.status, + statusText: response.statusText, + errorDetails + } + ); + } + throw new VertexAIError( + VertexAIErrorCode.FETCH_ERROR, + `Error fetching from ${url}: [${response.status} ${response.statusText}] ${message}`, + { + status: response.status, + statusText: response.statusText, + errorDetails + } + ); + } + } catch (e) { + let err = e as Error; + if ( + (e as VertexAIError).code !== VertexAIErrorCode.FETCH_ERROR && + (e as VertexAIError).code !== VertexAIErrorCode.API_NOT_ENABLED && + e instanceof Error + ) { + err = new VertexAIError( + VertexAIErrorCode.ERROR, + `Error fetching from ${url.toString()}: ${e.message}` + ); + err.stack = e.stack; + } + + throw err; + } finally { + if (fetchTimeoutId) { + clearTimeout(fetchTimeoutId); + } + } + return response; +} diff --git a/packages/vertexai/lib/requests/response-helpers.ts b/packages/vertexai/lib/requests/response-helpers.ts new file mode 100644 index 0000000000..27347d10f0 --- /dev/null +++ b/packages/vertexai/lib/requests/response-helpers.ts @@ -0,0 +1,198 @@ +/** + * @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 { + EnhancedGenerateContentResponse, + FinishReason, + FunctionCall, + GenerateContentCandidate, + GenerateContentResponse, + VertexAIErrorCode +} from '../types'; +import { VertexAIError } from '../errors'; +import { logger } from '../logger'; + +/** + * Creates an EnhancedGenerateContentResponse object that has helper functions and + * other modifications that improve usability. + */ +export function createEnhancedContentResponse( + response: GenerateContentResponse +): EnhancedGenerateContentResponse { + /** + * The Vertex AI backend omits default values. + * This causes the `index` property to be omitted from the first candidate in the + * response, since it has index 0, and 0 is a default value. + * See: https://github.com/firebase/firebase-js-sdk/issues/8566 + */ + if (response.candidates && !response.candidates[0].hasOwnProperty('index')) { + response.candidates[0].index = 0; + } + + const responseWithHelpers = addHelpers(response); + return responseWithHelpers; +} + +/** + * Adds convenience helper methods to a response object, including stream + * chunks (as long as each chunk is a complete GenerateContentResponse JSON). + */ +export function addHelpers( + response: GenerateContentResponse +): EnhancedGenerateContentResponse { + (response as EnhancedGenerateContentResponse).text = () => { + if (response.candidates && response.candidates.length > 0) { + if (response.candidates.length > 1) { + logger.warn( + `This response had ${response.candidates.length} ` + + `candidates. Returning text from the first candidate only. ` + + `Access response.candidates directly to use the other candidates.` + ); + } + if (hadBadFinishReason(response.candidates[0])) { + throw new VertexAIError( + VertexAIErrorCode.RESPONSE_ERROR, + `Response error: ${formatBlockErrorMessage( + response + )}. Response body stored in error.response`, + { + response + } + ); + } + return getText(response); + } else if (response.promptFeedback) { + throw new VertexAIError( + VertexAIErrorCode.RESPONSE_ERROR, + `Text not available. ${formatBlockErrorMessage(response)}`, + { + response + } + ); + } + return ''; + }; + (response as EnhancedGenerateContentResponse).functionCalls = () => { + if (response.candidates && response.candidates.length > 0) { + if (response.candidates.length > 1) { + logger.warn( + `This response had ${response.candidates.length} ` + + `candidates. Returning function calls from the first candidate only. ` + + `Access response.candidates directly to use the other candidates.` + ); + } + if (hadBadFinishReason(response.candidates[0])) { + throw new VertexAIError( + VertexAIErrorCode.RESPONSE_ERROR, + `Response error: ${formatBlockErrorMessage( + response + )}. Response body stored in error.response`, + { + response + } + ); + } + return getFunctionCalls(response); + } else if (response.promptFeedback) { + throw new VertexAIError( + VertexAIErrorCode.RESPONSE_ERROR, + `Function call not available. ${formatBlockErrorMessage(response)}`, + { + response + } + ); + } + return undefined; + }; + return response as EnhancedGenerateContentResponse; +} + +/** + * Returns all text found in all parts of first candidate. + */ +export function getText(response: GenerateContentResponse): string { + const textStrings = []; + if (response.candidates?.[0].content?.parts) { + for (const part of response.candidates?.[0].content?.parts) { + if (part.text) { + textStrings.push(part.text); + } + } + } + if (textStrings.length > 0) { + return textStrings.join(''); + } else { + return ''; + } +} + +/** + * Returns {@link FunctionCall}s associated with first candidate. + */ +export function getFunctionCalls( + response: GenerateContentResponse +): FunctionCall[] | undefined { + const functionCalls: FunctionCall[] = []; + if (response.candidates?.[0].content?.parts) { + for (const part of response.candidates?.[0].content?.parts) { + if (part.functionCall) { + functionCalls.push(part.functionCall); + } + } + } + if (functionCalls.length > 0) { + return functionCalls; + } else { + return undefined; + } +} + +const badFinishReasons = [FinishReason.RECITATION, FinishReason.SAFETY]; + +function hadBadFinishReason(candidate: GenerateContentCandidate): boolean { + return ( + !!candidate.finishReason && + badFinishReasons.includes(candidate.finishReason) + ); +} + +export function formatBlockErrorMessage( + response: GenerateContentResponse +): string { + let message = ''; + if ( + (!response.candidates || response.candidates.length === 0) && + response.promptFeedback + ) { + message += 'Response was blocked'; + if (response.promptFeedback?.blockReason) { + message += ` due to ${response.promptFeedback.blockReason}`; + } + if (response.promptFeedback?.blockReasonMessage) { + message += `: ${response.promptFeedback.blockReasonMessage}`; + } + } else if (response.candidates?.[0]) { + const firstCandidate = response.candidates[0]; + if (hadBadFinishReason(firstCandidate)) { + message += `Candidate was blocked due to ${firstCandidate.finishReason}`; + if (firstCandidate.finishMessage) { + message += `: ${firstCandidate.finishMessage}`; + } + } + } + return message; +} diff --git a/packages/vertexai/lib/requests/schema-builder.ts b/packages/vertexai/lib/requests/schema-builder.ts new file mode 100644 index 0000000000..3d219d58b1 --- /dev/null +++ b/packages/vertexai/lib/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` objects. + * @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/lib/requests/stream-reader.ts b/packages/vertexai/lib/requests/stream-reader.ts new file mode 100644 index 0000000000..8162407d90 --- /dev/null +++ b/packages/vertexai/lib/requests/stream-reader.ts @@ -0,0 +1,205 @@ +/** + * @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 { + EnhancedGenerateContentResponse, + GenerateContentCandidate, + GenerateContentResponse, + GenerateContentStreamResult, + Part, + VertexAIErrorCode +} from '../types'; +import { VertexAIError } from '../errors'; +import { createEnhancedContentResponse } from './response-helpers'; + +const responseLineRE = /^data\: (.*)(?:\n\n|\r\r|\r\n\r\n)/; + +/** + * Process a response.body stream from the backend and return an + * iterator that provides one complete GenerateContentResponse at a time + * and a promise that resolves with a single aggregated + * GenerateContentResponse. + * + * @param response - Response from a fetch call + */ +export function processStream(response: Response): GenerateContentStreamResult { + const inputStream = response.body!.pipeThrough( + new TextDecoderStream('utf8', { fatal: true }) + ); + const responseStream = + getResponseStream(inputStream); + const [stream1, stream2] = responseStream.tee(); + return { + stream: generateResponseSequence(stream1), + response: getResponsePromise(stream2) + }; +} + +async function getResponsePromise( + stream: ReadableStream +): Promise { + const allResponses: GenerateContentResponse[] = []; + const reader = stream.getReader(); + while (true) { + const { done, value } = await reader.read(); + if (done) { + const enhancedResponse = createEnhancedContentResponse( + aggregateResponses(allResponses) + ); + return enhancedResponse; + } + allResponses.push(value); + } +} + +async function* generateResponseSequence( + stream: ReadableStream +): AsyncGenerator { + const reader = stream.getReader(); + while (true) { + const { value, done } = await reader.read(); + if (done) { + break; + } + + const enhancedResponse = createEnhancedContentResponse(value); + yield enhancedResponse; + } +} + +/** + * Reads a raw stream from the fetch response and join incomplete + * chunks, returning a new stream that provides a single complete + * GenerateContentResponse in each iteration. + */ +export function getResponseStream( + inputStream: ReadableStream +): ReadableStream { + const reader = inputStream.getReader(); + const stream = new ReadableStream({ + start(controller) { + let currentText = ''; + return pump(); + function pump(): Promise<(() => Promise) | undefined> { + return reader.read().then(({ value, done }) => { + if (done) { + if (currentText.trim()) { + controller.error( + new VertexAIError( + VertexAIErrorCode.PARSE_FAILED, + 'Failed to parse stream' + ) + ); + return; + } + controller.close(); + return; + } + + currentText += value; + let match = currentText.match(responseLineRE); + let parsedResponse: T; + while (match) { + try { + parsedResponse = JSON.parse(match[1]); + } catch (e) { + controller.error( + new VertexAIError( + VertexAIErrorCode.PARSE_FAILED, + `Error parsing JSON response: "${match[1]}` + ) + ); + return; + } + controller.enqueue(parsedResponse); + currentText = currentText.substring(match[0].length); + match = currentText.match(responseLineRE); + } + return pump(); + }); + } + } + }); + return stream; +} + +/** + * Aggregates an array of `GenerateContentResponse`s into a single + * GenerateContentResponse. + */ +export function aggregateResponses( + responses: GenerateContentResponse[] +): GenerateContentResponse { + const lastResponse = responses[responses.length - 1]; + const aggregatedResponse: GenerateContentResponse = { + promptFeedback: lastResponse?.promptFeedback + }; + for (const response of responses) { + if (response.candidates) { + for (const candidate of response.candidates) { + // Index will be undefined if it's the first index (0), so we should use 0 if it's undefined. + // See: https://github.com/firebase/firebase-js-sdk/issues/8566 + const i = candidate.index || 0; + if (!aggregatedResponse.candidates) { + aggregatedResponse.candidates = []; + } + if (!aggregatedResponse.candidates[i]) { + aggregatedResponse.candidates[i] = { + index: candidate.index + } as GenerateContentCandidate; + } + // Keep overwriting, the last one will be final + aggregatedResponse.candidates[i].citationMetadata = + candidate.citationMetadata; + aggregatedResponse.candidates[i].finishReason = candidate.finishReason; + aggregatedResponse.candidates[i].finishMessage = + candidate.finishMessage; + aggregatedResponse.candidates[i].safetyRatings = + candidate.safetyRatings; + + /** + * Candidates should always have content and parts, but this handles + * possible malformed responses. + */ + if (candidate.content && candidate.content.parts) { + if (!aggregatedResponse.candidates[i].content) { + aggregatedResponse.candidates[i].content = { + role: candidate.content.role || 'user', + parts: [] + }; + } + const newPart: Partial = {}; + for (const part of candidate.content.parts) { + if (part.text) { + newPart.text = part.text; + } + if (part.functionCall) { + newPart.functionCall = part.functionCall; + } + if (Object.keys(newPart).length === 0) { + newPart.text = ''; + } + aggregatedResponse.candidates[i].content.parts.push( + newPart as Part + ); + } + } + } + } + } + return aggregatedResponse; +} diff --git a/packages/vertexai/lib/service.ts b/packages/vertexai/lib/service.ts new file mode 100644 index 0000000000..1c1573a926 --- /dev/null +++ b/packages/vertexai/lib/service.ts @@ -0,0 +1,50 @@ +/** + * @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 { FirebaseApp } from '@firebase/app'; +import { VertexAI, VertexAIOptions } from './public-types'; +import { + AppCheckInternalComponentName, + FirebaseAppCheckInternal, +} from '@firebase/app-check-interop-types'; +import { Provider } from '@firebase/component'; +import { FirebaseAuthInternal, FirebaseAuthInternalName } from '@firebase/auth-interop-types'; +import { DEFAULT_LOCATION } from './constants'; +import { _FirebaseService } from './types/internal'; + +export class VertexAIService implements VertexAI, _FirebaseService { + auth: FirebaseAuthInternal | null; + appCheck: FirebaseAppCheckInternal | null; + location: string; + + constructor( + public app: FirebaseApp, + authProvider?: Provider, + appCheckProvider?: Provider, + public options?: VertexAIOptions, + ) { + const appCheck = appCheckProvider?.getImmediate({ optional: true }); + const auth = authProvider?.getImmediate({ optional: true }); + this.auth = auth || null; + this.appCheck = appCheck || null; + this.location = this.options?.location || DEFAULT_LOCATION; + } + + _delete(): Promise { + return Promise.resolve(); + } +} diff --git a/packages/vertexai/lib/types/content.ts b/packages/vertexai/lib/types/content.ts new file mode 100644 index 0000000000..316769856a --- /dev/null +++ b/packages/vertexai/lib/types/content.ts @@ -0,0 +1,162 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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 { Role } from './enums'; + +/** + * Content type for both prompts and response candidates. + * @public + */ +export interface Content { + role: Role; + parts: Part[]; +} + +/** + * Content part - includes text, image/video, or function call/response + * part types. + * @public + */ +export type Part = + | TextPart + | InlineDataPart + | FunctionCallPart + | FunctionResponsePart + | FileDataPart; + +/** + * Content part interface if the part represents a text string. + * @public + */ +export interface TextPart { + text: string; + inlineData?: never; + functionCall?: never; + functionResponse?: never; +} + +/** + * Content part interface if the part represents an image. + * @public + */ +export interface InlineDataPart { + text?: never; + inlineData: GenerativeContentBlob; + functionCall?: never; + functionResponse?: never; + /** + * Applicable if `inlineData` is a video. + */ + videoMetadata?: VideoMetadata; +} + +/** + * Describes the input video content. + * @public + */ +export interface VideoMetadata { + /** + * The start offset of the video in + * protobuf {@link https://cloud.google.com/ruby/docs/reference/google-cloud-workflows-v1/latest/Google-Protobuf-Duration#json-mapping | Duration} format. + */ + startOffset: string; + /** + * The end offset of the video in + * protobuf {@link https://cloud.google.com/ruby/docs/reference/google-cloud-workflows-v1/latest/Google-Protobuf-Duration#json-mapping | Duration} format. + */ + endOffset: string; +} + +/** + * Content part interface if the part represents a {@link FunctionCall}. + * @public + */ +export interface FunctionCallPart { + text?: never; + inlineData?: never; + functionCall: FunctionCall; + functionResponse?: never; +} + +/** + * Content part interface if the part represents {@link FunctionResponse}. + * @public + */ +export interface FunctionResponsePart { + text?: never; + inlineData?: never; + functionCall?: never; + functionResponse: FunctionResponse; +} + +/** + * Content part interface if the part represents {@link FileData} + * @public + */ +export interface FileDataPart { + text?: never; + inlineData?: never; + functionCall?: never; + functionResponse?: never; + fileData: FileData; +} + +/** + * A predicted {@link FunctionCall} returned from the model + * that contains a string representing the {@link FunctionDeclaration.name} + * and a structured JSON object containing the parameters and their values. + * @public + */ +export interface FunctionCall { + name: string; + args: object; +} + +/** + * The result output from a {@link FunctionCall} that contains a string + * representing the {@link FunctionDeclaration.name} + * and a structured JSON object containing any output + * from the function is used as context to the model. + * This should contain the result of a {@link FunctionCall} + * made based on model prediction. + * @public + */ +export interface FunctionResponse { + name: string; + response: object; +} + +/** + * Interface for sending an image. + * @public + */ +export interface GenerativeContentBlob { + mimeType: string; + /** + * Image as a base64 string. + */ + data: string; +} + +/** + * Data pointing to a file uploaded on Google Cloud Storage. + * @public + */ +export interface FileData { + mimeType: string; + fileUri: string; +} diff --git a/packages/vertexai/lib/types/enums.ts b/packages/vertexai/lib/types/enums.ts new file mode 100644 index 0000000000..de2a7109ae --- /dev/null +++ b/packages/vertexai/lib/types/enums.ts @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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. + * + */ + +/** + * Role is the producer of the content. + * @public + */ +export type Role = (typeof POSSIBLE_ROLES)[number]; + +/** + * Possible roles. + * @public + */ +export const POSSIBLE_ROLES = ['user', 'model', 'function', 'system'] as const; + +/** + * Harm categories that would cause prompts or candidates to be blocked. + * @public + */ +export enum HarmCategory { + HARM_CATEGORY_HATE_SPEECH = 'HARM_CATEGORY_HATE_SPEECH', + HARM_CATEGORY_SEXUALLY_EXPLICIT = 'HARM_CATEGORY_SEXUALLY_EXPLICIT', + HARM_CATEGORY_HARASSMENT = 'HARM_CATEGORY_HARASSMENT', + HARM_CATEGORY_DANGEROUS_CONTENT = 'HARM_CATEGORY_DANGEROUS_CONTENT', +} + +/** + * Threshold above which a prompt or candidate will be blocked. + * @public + */ +export enum HarmBlockThreshold { + // Content with NEGLIGIBLE will be allowed. + BLOCK_LOW_AND_ABOVE = 'BLOCK_LOW_AND_ABOVE', + // Content with NEGLIGIBLE and LOW will be allowed. + BLOCK_MEDIUM_AND_ABOVE = 'BLOCK_MEDIUM_AND_ABOVE', + // Content with NEGLIGIBLE, LOW, and MEDIUM will be allowed. + BLOCK_ONLY_HIGH = 'BLOCK_ONLY_HIGH', + // All content will be allowed. + BLOCK_NONE = 'BLOCK_NONE', +} + +/** + * @public + */ +export enum HarmBlockMethod { + // The harm block method uses both probability and severity scores. + SEVERITY = 'SEVERITY', + // The harm block method uses the probability score. + PROBABILITY = 'PROBABILITY', +} + +/** + * Probability that a prompt or candidate matches a harm category. + * @public + */ +export enum HarmProbability { + // Content has a negligible chance of being unsafe. + NEGLIGIBLE = 'NEGLIGIBLE', + // Content has a low chance of being unsafe. + LOW = 'LOW', + // Content has a medium chance of being unsafe. + MEDIUM = 'MEDIUM', + // Content has a high chance of being unsafe. + HIGH = 'HIGH', +} + +/** + * Harm severity levels. + * @public + */ +export enum HarmSeverity { + // Negligible level of harm severity. + HARM_SEVERITY_NEGLIGIBLE = 'HARM_SEVERITY_NEGLIGIBLE', + // Low level of harm severity. + HARM_SEVERITY_LOW = 'HARM_SEVERITY_LOW', + // Medium level of harm severity. + HARM_SEVERITY_MEDIUM = 'HARM_SEVERITY_MEDIUM', + // High level of harm severity. + HARM_SEVERITY_HIGH = 'HARM_SEVERITY_HIGH', +} + +/** + * Reason that a prompt was blocked. + * @public + */ +export enum BlockReason { + // Content was blocked by safety settings. + SAFETY = 'SAFETY', + // Content was blocked, but the reason is uncategorized. + OTHER = 'OTHER', +} + +/** + * Reason that a candidate finished. + * @public + */ +export enum FinishReason { + // Natural stop point of the model or provided stop sequence. + STOP = 'STOP', + // The maximum number of tokens as specified in the request was reached. + MAX_TOKENS = 'MAX_TOKENS', + // The candidate content was flagged for safety reasons. + SAFETY = 'SAFETY', + // The candidate content was flagged for recitation reasons. + RECITATION = 'RECITATION', + // Unknown reason. + OTHER = 'OTHER', +} + +/** + * @public + */ +export enum FunctionCallingMode { + // Default model behavior, model decides to predict either a function call + // or a natural language response. + AUTO = 'AUTO', + // Model is constrained to always predicting a function call only. + // If "allowed_function_names" is set, the predicted function call will be + // limited to any one of "allowed_function_names", else the predicted + // function call will be any one of the provided "function_declarations". + ANY = 'ANY', + // Model will not predict any function call. Model behavior is same as when + // not passing any function declarations. + NONE = 'NONE', +} diff --git a/packages/vertexai/lib/types/error.ts b/packages/vertexai/lib/types/error.ts new file mode 100644 index 0000000000..33c268204f --- /dev/null +++ b/packages/vertexai/lib/types/error.ts @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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 { GenerateContentResponse } from './responses'; + +/** + * Details object that may be included in an error response. + * + * @public + */ +export interface ErrorDetails { + '@type'?: string; + + /** The reason for the error. */ + reason?: string; + + /** The domain where the error occurred. */ + domain?: string; + + /** Additional metadata about the error. */ + metadata?: Record; + + /** Any other relevant information about the error. */ + [key: string]: unknown; +} + +/** + * Details object that contains data originating from a bad HTTP response. + * + * @public + */ +export interface CustomErrorData { + /** HTTP status code of the error response. */ + status?: number; + + /** HTTP status text of the error response. */ + statusText?: string; + + /** Response from a {@link GenerateContentRequest} */ + response?: GenerateContentResponse; + + /** Optional additional details about the error. */ + errorDetails?: ErrorDetails[]; +} + +/** + * Standardized error codes that {@link VertexAIError} can have. + * + * @public + */ +export const enum VertexAIErrorCode { + /** A generic error occurred. */ + ERROR = 'error', + + /** An error occurred in a request. */ + REQUEST_ERROR = 'request-error', + + /** An error occurred in a response. */ + RESPONSE_ERROR = 'response-error', + + /** An error occurred while performing a fetch. */ + FETCH_ERROR = 'fetch-error', + + /** An error associated with a Content object. */ + INVALID_CONTENT = 'invalid-content', + + /** An error due to the Firebase API not being enabled in the Console. */ + API_NOT_ENABLED = 'api-not-enabled', + + /** 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', + + /** An error occurred due to a model name not being specified during initialization. */ + NO_MODEL = 'no-model', + + /** An error occurred due to a missing project ID. */ + NO_PROJECT_ID = 'no-project-id', + + /** An error occurred while parsing. */ + PARSE_FAILED = 'parse-failed', +} diff --git a/packages/vertexai/lib/types/index.ts b/packages/vertexai/lib/types/index.ts new file mode 100644 index 0000000000..5a2b42a426 --- /dev/null +++ b/packages/vertexai/lib/types/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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. + * + */ + +export * from './content'; +export * from './enums'; +export * from './requests'; +export * from './responses'; +export * from './error'; +export * from './schema'; diff --git a/packages/vertexai/lib/types/internal.ts b/packages/vertexai/lib/types/internal.ts new file mode 100644 index 0000000000..1aa36f783f --- /dev/null +++ b/packages/vertexai/lib/types/internal.ts @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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 { AppCheckTokenResult } from '@firebase/app-check-interop-types'; +import { FirebaseAuthTokenData } from '@firebase/auth-interop-types'; +import { FirebaseApp } from '@firebase/app'; + +export interface ApiSettings { + apiKey: string; + project: string; + location: string; + getAuthToken?: () => Promise; + getAppCheckToken?: () => Promise; +} + +/** + * @internal + */ +// eslint-disable-next-line @typescript-eslint/naming-convention +export interface _FirebaseService { + app: FirebaseApp; + /** + * Delete the service and free it's resources - called from + * {@link @firebase/app#deleteApp | deleteApp()} + */ + _delete(): Promise; +} diff --git a/packages/vertexai/lib/types/requests.ts b/packages/vertexai/lib/types/requests.ts new file mode 100644 index 0000000000..8b36f9c37a --- /dev/null +++ b/packages/vertexai/lib/types/requests.ts @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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 { TypedSchema } from '../requests/schema-builder'; +import { Content, Part } from './content'; +import { FunctionCallingMode, HarmBlockMethod, HarmBlockThreshold, HarmCategory } from './enums'; +import { ObjectSchemaInterface, SchemaRequest } from './schema'; + +/** + * Base parameters for a number of methods. + * @public + */ +export interface BaseParams { + safetySettings?: SafetySetting[]; + generationConfig?: GenerationConfig; +} + +/** + * Params passed to {@link getGenerativeModel}. + * @public + */ +export interface ModelParams extends BaseParams { + model: string; + tools?: Tool[]; + toolConfig?: ToolConfig; + systemInstruction?: string | Part | Content; +} + +/** + * Request sent through {@link GenerativeModel.generateContent} + * @public + */ +export interface GenerateContentRequest extends BaseParams { + contents: Content[]; + tools?: Tool[]; + toolConfig?: ToolConfig; + systemInstruction?: string | Part | Content; +} + +/** + * Safety setting that can be sent as part of request parameters. + * @public + */ +export interface SafetySetting { + category: HarmCategory; + threshold: HarmBlockThreshold; + method?: HarmBlockMethod; +} + +/** + * Config options for content-related requests + * @public + */ +export interface GenerationConfig { + candidateCount?: number; + stopSequences?: string[]; + maxOutputTokens?: number; + temperature?: number; + topP?: number; + topK?: number; + presencePenalty?: number; + frequencyPenalty?: number; + /** + * Output response MIME type of the generated candidate text. + * Supported MIME types are `text/plain` (default, text output), + * `application/json` (JSON response in the candidates), and + * `text/x.enum`. + */ + 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` and `text/x.enum`. + */ + responseSchema?: TypedSchema | SchemaRequest; +} + +/** + * Params for {@link GenerativeModel.startChat}. + * @public + */ +export interface StartChatParams extends BaseParams { + history?: Content[]; + tools?: Tool[]; + toolConfig?: ToolConfig; + systemInstruction?: string | Part | Content; +} + +/** + * Params for calling {@link GenerativeModel.countTokens} + * @public + */ +export interface CountTokensRequest { + contents: Content[]; +} + +/** + * Params passed to {@link getGenerativeModel}. + * @public + */ +export interface RequestOptions { + /** + * Request timeout in milliseconds. Defaults to 180 seconds (180000ms). + */ + timeout?: number; + /** + * Base url for endpoint. Defaults to https://firebasevertexai.googleapis.com + */ + baseUrl?: string; +} + +/** + * Defines a tool that model can call to access external knowledge. + * @public + */ +export declare type Tool = FunctionDeclarationsTool; + +/** + * Structured representation of a function declaration as defined by the + * {@link https://spec.openapis.org/oas/v3.0.3 | OpenAPI 3.0 specification}. + * Included + * in this declaration are the function name and parameters. This + * `FunctionDeclaration` is a representation of a block of code that can be used + * as a Tool by the model and executed by the client. + * @public + */ +export declare interface FunctionDeclaration { + /** + * The name of the function to call. Must start with a letter or an + * underscore. Must be a-z, A-Z, 0-9, or contain underscores and dashes, with + * a max length of 64. + */ + name: string; + /** + * Description and purpose of the function. Model uses it to decide + * how and whether to call the function. + */ + description: string; + /** + * Optional. Describes the parameters to this function in JSON Schema Object + * 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?: ObjectSchemaInterface; +} + +/** + * A `FunctionDeclarationsTool` is a piece of code that enables the system to + * interact with external systems to perform an action, or set of actions, + * outside of knowledge and scope of the model. + * @public + */ +export declare interface FunctionDeclarationsTool { + /** + * Optional. One or more function declarations + * to be passed to the model along with the current user query. Model may + * decide to call a subset of these functions by populating + * {@link FunctionCall} in the response. User should + * provide a {@link FunctionResponse} for each + * function call in the next turn. Based on the function responses, the model will + * generate the final response back to the user. Maximum 64 function + * declarations can be provided. + */ + functionDeclarations?: FunctionDeclaration[]; +} + +/** + * Tool config. This config is shared for all tools provided in the request. + * @public + */ +export interface ToolConfig { + functionCallingConfig?: FunctionCallingConfig; +} + +/** + * @public + */ +export interface FunctionCallingConfig { + mode?: FunctionCallingMode; + allowedFunctionNames?: string[]; +} diff --git a/packages/vertexai/lib/types/responses.ts b/packages/vertexai/lib/types/responses.ts new file mode 100644 index 0000000000..bcb645677b --- /dev/null +++ b/packages/vertexai/lib/types/responses.ts @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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 { Content, FunctionCall } from './content'; +import { BlockReason, FinishReason, HarmCategory, HarmProbability, HarmSeverity } from './enums'; + +/** + * Result object returned from {@link GenerativeModel.generateContent} call. + * + * @public + */ +export interface GenerateContentResult { + response: EnhancedGenerateContentResponse; +} + +/** + * Result object returned from {@link GenerativeModel.generateContentStream} call. + * Iterate over `stream` to get chunks as they come in and/or + * use the `response` promise to get the aggregated response when + * the stream is done. + * + * @public + */ +export interface GenerateContentStreamResult { + stream: AsyncGenerator; + response: Promise; +} + +/** + * Response object wrapped with helper methods. + * + * @public + */ +export interface EnhancedGenerateContentResponse extends GenerateContentResponse { + /** + * Returns the text string from the response, if available. + * Throws if the prompt or candidate was blocked. + */ + text: () => string; + functionCalls: () => FunctionCall[] | undefined; +} + +/** + * Individual response from {@link GenerativeModel.generateContent} and + * {@link GenerativeModel.generateContentStream}. + * `generateContentStream()` will return one in each chunk until + * the stream is done. + * @public + */ +export interface GenerateContentResponse { + candidates?: GenerateContentCandidate[]; + promptFeedback?: PromptFeedback; + usageMetadata?: UsageMetadata; +} + +/** + * Usage metadata about a {@link GenerateContentResponse}. + * + * @public + */ +export interface UsageMetadata { + promptTokenCount: number; + candidatesTokenCount: number; + totalTokenCount: number; +} + +/** + * If the prompt was blocked, this will be populated with `blockReason` and + * the relevant `safetyRatings`. + * @public + */ +export interface PromptFeedback { + blockReason?: BlockReason; + safetyRatings: SafetyRating[]; + blockReasonMessage?: string; +} + +/** + * A candidate returned as part of a {@link GenerateContentResponse}. + * @public + */ +export interface GenerateContentCandidate { + index: number; + content: Content; + finishReason?: FinishReason; + finishMessage?: string; + safetyRatings?: SafetyRating[]; + citationMetadata?: CitationMetadata; + groundingMetadata?: GroundingMetadata; +} + +/** + * Citation metadata that may be found on a {@link GenerateContentCandidate}. + * @public + */ +export interface CitationMetadata { + citations: Citation[]; +} + +/** + * A single citation. + * @public + */ +export interface Citation { + startIndex?: number; + endIndex?: number; + uri?: string; + license?: string; + title?: string; + publicationDate?: Date; +} + +/** + * Metadata returned to client when grounding is enabled. + * @public + */ +export interface GroundingMetadata { + webSearchQueries?: string[]; + retrievalQueries?: string[]; + groundingAttributions: GroundingAttribution[]; +} + +/** + * @public + */ +export interface GroundingAttribution { + segment: Segment; + confidenceScore?: number; + web?: WebAttribution; + retrievedContext?: RetrievedContextAttribution; +} + +/** + * @public + */ +export interface Segment { + partIndex: number; + startIndex: number; + endIndex: number; +} + +/** + * @public + */ +export interface WebAttribution { + uri: string; + title: string; +} + +/** + * @public + */ +export interface RetrievedContextAttribution { + uri: string; + title: string; +} + +/** + * Protobuf google.type.Date + * @public + */ +export interface Date { + year: number; + month: number; + day: number; +} + +/** + * A safety rating associated with a {@link GenerateContentCandidate} + * @public + */ +export interface SafetyRating { + category: HarmCategory; + probability: HarmProbability; + severity: HarmSeverity; + probabilityScore: number; + severityScore: number; + blocked: boolean; +} + +/** + * Response from calling {@link GenerativeModel.countTokens}. + * @public + */ +export interface CountTokensResponse { + /** + * The total number of tokens counted across all instances from the request. + */ + totalTokens: number; + /** + * The total number of billable characters counted across all instances + * from the request. + */ + totalBillableCharacters?: number; +} diff --git a/packages/vertexai/lib/types/schema.ts b/packages/vertexai/lib/types/schema.ts new file mode 100644 index 0000000000..f4ca82fdfc --- /dev/null +++ b/packages/vertexai/lib/types/schema.ts @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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 `Schema` objects. */ + 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[]; +} From 0b2cd5564b42991efe3cd6a3065d03fc1573d288 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 8 Jan 2025 15:26:34 +0000 Subject: [PATCH 006/115] react-native-builder-bob --- packages/vertexai/package.json | 60 +- packages/vertexai/tsconfig.json | 27 + yarn.lock | 1748 ++++++++++++++++++++++++++++++- 3 files changed, 1819 insertions(+), 16 deletions(-) create mode 100644 packages/vertexai/tsconfig.json diff --git a/packages/vertexai/package.json b/packages/vertexai/package.json index 20e216010d..5ef0749728 100644 --- a/packages/vertexai/package.json +++ b/packages/vertexai/package.json @@ -3,12 +3,12 @@ "version": "0.0.1", "author": "Invertase (http://invertase.io)", "description": "React Native Firebase - Vertex AI is a fully-managed, unified AI development platform for building and using generative AI", - "main": "lib/index.js", - "types": "lib/index.d.ts", + "main": "./dist/commonjs/index.js", + "types": "./dist/typescript/commonjs/lib/index.d.ts", "scripts": { "build": "genversion --semi lib/version.js", "build:clean": "rimraf android/build && rimraf ios/build", - "prepare": "yarn run build" + "prepare": "yarn run build && bob build" }, "repository": { "type": "git", @@ -28,5 +28,57 @@ }, "publishConfig": { "access": "public" - } + }, + "devDependencies": { + "react-native-builder-bob": "^0.35.2" + }, + "source": "./lib/index.ts", + "module": "./dist/module/index.js", + "exports": { + ".": { + "import": { + "types": "./dist/typescript/module/lib/index.d.ts", + "default": "./dist/module/index.js" + }, + "require": { + "types": "./dist/typescript/commonjs/lib/index.d.ts", + "default": "./dist/commonjs/index.js" + } + } + }, + "files": [ + "lib", + "dist", + "!**/__tests__", + "!**/__fixtures__", + "!**/__mocks__" + ], + "react-native-builder-bob": { + "source": "lib", + "output": "dist", + "targets": [ + [ + "commonjs", + { + "esm": true + } + ], + [ + "module", + { + "esm": true + } + ], + [ + "typescript", + { + "esm": true + } + ] + ] + }, + "eslintIgnore": [ + "node_modules/", + "dist/" + ] } diff --git a/packages/vertexai/tsconfig.json b/packages/vertexai/tsconfig.json new file mode 100644 index 0000000000..356b26154c --- /dev/null +++ b/packages/vertexai/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "rootDir": ".", + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "jsx": "react-jsx", + "lib": [ + "ESNext" + ], + "module": "ESNext", + "moduleResolution": "Bundler", + "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noImplicitUseStrict": false, + "noStrictGenericChecks": false, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ESNext", + "verbatimModuleSyntax": true + } +} diff --git a/yarn.lock b/yarn.lock index 4afe1d5349..675c0562bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -75,6 +75,17 @@ __metadata: languageName: node linkType: hard +"@babel/code-frame@npm:^7.25.9, @babel/code-frame@npm:^7.26.2": + version: 7.26.2 + resolution: "@babel/code-frame@npm:7.26.2" + dependencies: + "@babel/helper-validator-identifier": "npm:^7.25.9" + js-tokens: "npm:^4.0.0" + picocolors: "npm:^1.0.0" + checksum: 10/db2c2122af79d31ca916755331bb4bac96feb2b334cdaca5097a6b467fdd41963b89b14b6836a14f083de7ff887fc78fa1b3c10b14e743d33e12dbfe5ee3d223 + languageName: node + linkType: hard + "@babel/compat-data@npm:^7.20.5, @babel/compat-data@npm:^7.22.6, @babel/compat-data@npm:^7.23.3, @babel/compat-data@npm:^7.23.5": version: 7.23.5 resolution: "@babel/compat-data@npm:7.23.5" @@ -96,6 +107,13 @@ __metadata: languageName: node linkType: hard +"@babel/compat-data@npm:^7.25.9, @babel/compat-data@npm:^7.26.0": + version: 7.26.3 + resolution: "@babel/compat-data@npm:7.26.3" + checksum: 10/0bf4e491680722aa0eac26f770f2fae059f92e2ac083900b241c90a2c10f0fc80e448b1feccc2b332687fab4c3e33e9f83dee9ef56badca1fb9f3f71266d9ebf + languageName: node + linkType: hard + "@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.20.0, @babel/core@npm:^7.23.9": version: 7.23.9 resolution: "@babel/core@npm:7.23.9" @@ -213,6 +231,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.26.3": + version: 7.26.3 + resolution: "@babel/generator@npm:7.26.3" + dependencies: + "@babel/parser": "npm:^7.26.3" + "@babel/types": "npm:^7.26.3" + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.25" + jsesc: "npm:^3.0.2" + checksum: 10/c1d8710cc1c52af9d8d67f7d8ea775578aa500887b327d2a81e27494764a6ef99e438dd7e14cf7cd3153656492ee27a8362980dc438087c0ca39d4e75532c638 + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.18.6, @babel/helper-annotate-as-pure@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-annotate-as-pure@npm:7.22.5" @@ -231,6 +262,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-annotate-as-pure@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-annotate-as-pure@npm:7.25.9" + dependencies: + "@babel/types": "npm:^7.25.9" + checksum: 10/41edda10df1ae106a9b4fe617bf7c6df77db992992afd46192534f5cff29f9e49a303231733782dd65c5f9409714a529f215325569f14282046e9d3b7a1ffb6c + languageName: node + linkType: hard + "@babel/helper-builder-binary-assignment-operator-visitor@npm:^7.22.15": version: 7.22.15 resolution: "@babel/helper-builder-binary-assignment-operator-visitor@npm:7.22.15" @@ -289,6 +329,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-compilation-targets@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-compilation-targets@npm:7.25.9" + dependencies: + "@babel/compat-data": "npm:^7.25.9" + "@babel/helper-validator-option": "npm:^7.25.9" + browserslist: "npm:^4.24.0" + lru-cache: "npm:^5.1.1" + semver: "npm:^6.3.1" + checksum: 10/8053fbfc21e8297ab55c8e7f9f119e4809fa7e505268691e1bedc2cf5e7a5a7de8c60ad13da2515378621b7601c42e101d2d679904da395fa3806a1edef6b92e + languageName: node + linkType: hard + "@babel/helper-create-class-features-plugin@npm:^7.18.6, @babel/helper-create-class-features-plugin@npm:^7.21.0, @babel/helper-create-class-features-plugin@npm:^7.22.15, @babel/helper-create-class-features-plugin@npm:^7.23.6, @babel/helper-create-class-features-plugin@npm:^7.23.9": version: 7.23.10 resolution: "@babel/helper-create-class-features-plugin@npm:7.23.10" @@ -344,6 +397,23 @@ __metadata: languageName: node linkType: hard +"@babel/helper-create-class-features-plugin@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-create-class-features-plugin@npm:7.25.9" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.25.9" + "@babel/helper-member-expression-to-functions": "npm:^7.25.9" + "@babel/helper-optimise-call-expression": "npm:^7.25.9" + "@babel/helper-replace-supers": "npm:^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.9" + "@babel/traverse": "npm:^7.25.9" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/d1d47a7b5fd317c6cb1446b0e4f4892c19ddaa69ea0229f04ba8bea5f273fc8168441e7114ad36ff919f2d310f97310cec51adc79002e22039a7e1640ccaf248 + languageName: node + linkType: hard + "@babel/helper-create-regexp-features-plugin@npm:^7.18.6, @babel/helper-create-regexp-features-plugin@npm:^7.22.15, @babel/helper-create-regexp-features-plugin@npm:^7.22.5": version: 7.22.15 resolution: "@babel/helper-create-regexp-features-plugin@npm:7.22.15" @@ -383,6 +453,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-create-regexp-features-plugin@npm:^7.25.9": + version: 7.26.3 + resolution: "@babel/helper-create-regexp-features-plugin@npm:7.26.3" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.25.9" + regexpu-core: "npm:^6.2.0" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/4c44122ea11c4253ee78a9c083b7fbce96c725e2cb43cc864f0e8ea2749f7b6658617239c6278df9f132d09a7545c8fe0336ed2895ad7c80c71507828a7bc8ba + languageName: node + linkType: hard + "@babel/helper-define-polyfill-provider@npm:^0.5.0": version: 0.5.0 resolution: "@babel/helper-define-polyfill-provider@npm:0.5.0" @@ -511,6 +594,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-member-expression-to-functions@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-member-expression-to-functions@npm:7.25.9" + dependencies: + "@babel/traverse": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + checksum: 10/ef8cc1c1e600b012b312315f843226545a1a89f25d2f474ce2503fd939ca3f8585180f291a3a13efc56cf13eddc1d41a3a040eae9a521838fd59a6d04cc82490 + languageName: node + linkType: hard + "@babel/helper-module-imports@npm:^7.22.15": version: 7.22.15 resolution: "@babel/helper-module-imports@npm:7.22.15" @@ -530,6 +623,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-imports@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-module-imports@npm:7.25.9" + dependencies: + "@babel/traverse": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + checksum: 10/e090be5dee94dda6cd769972231b21ddfae988acd76b703a480ac0c96f3334557d70a965bf41245d6ee43891e7571a8b400ccf2b2be5803351375d0f4e5bcf08 + languageName: node + linkType: hard + "@babel/helper-module-transforms@npm:^7.23.3": version: 7.23.3 resolution: "@babel/helper-module-transforms@npm:7.23.3" @@ -574,6 +677,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-module-transforms@npm:^7.25.9, @babel/helper-module-transforms@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/helper-module-transforms@npm:7.26.0" + dependencies: + "@babel/helper-module-imports": "npm:^7.25.9" + "@babel/helper-validator-identifier": "npm:^7.25.9" + "@babel/traverse": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/9841d2a62f61ad52b66a72d08264f23052d533afc4ce07aec2a6202adac0bfe43014c312f94feacb3291f4c5aafe681955610041ece2c276271adce3f570f2f5 + languageName: node + linkType: hard + "@babel/helper-optimise-call-expression@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-optimise-call-expression@npm:7.22.5" @@ -592,6 +708,15 @@ __metadata: languageName: node linkType: hard +"@babel/helper-optimise-call-expression@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-optimise-call-expression@npm:7.25.9" + dependencies: + "@babel/types": "npm:^7.25.9" + checksum: 10/f09d0ad60c0715b9a60c31841b3246b47d67650c512ce85bbe24a3124f1a4d66377df793af393273bc6e1015b0a9c799626c48e53747581c1582b99167cc65dc + languageName: node + linkType: hard + "@babel/helper-plugin-utils@npm:^7.0.0, @babel/helper-plugin-utils@npm:^7.10.4, @babel/helper-plugin-utils@npm:^7.12.13, @babel/helper-plugin-utils@npm:^7.14.5, @babel/helper-plugin-utils@npm:^7.18.6, @babel/helper-plugin-utils@npm:^7.20.2, @babel/helper-plugin-utils@npm:^7.22.5, @babel/helper-plugin-utils@npm:^7.8.0, @babel/helper-plugin-utils@npm:^7.8.3": version: 7.22.5 resolution: "@babel/helper-plugin-utils@npm:7.22.5" @@ -620,6 +745,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-plugin-utils@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-plugin-utils@npm:7.25.9" + checksum: 10/e347d87728b1ab10b6976d46403941c8f9008c045ea6d99997a7ffca7b852dc34b6171380f7b17edf94410e0857ff26f3a53d8618f11d73744db86e8ca9b8c64 + languageName: node + linkType: hard + "@babel/helper-remap-async-to-generator@npm:^7.18.9, @babel/helper-remap-async-to-generator@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-remap-async-to-generator@npm:7.22.20" @@ -659,6 +791,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-remap-async-to-generator@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-remap-async-to-generator@npm:7.25.9" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.25.9" + "@babel/helper-wrap-function": "npm:^7.25.9" + "@babel/traverse": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/ea37ad9f8f7bcc27c109963b8ebb9d22bac7a5db2a51de199cb560e251d5593fe721e46aab2ca7d3e7a24b0aa4aff0eaf9c7307af9c2fd3a1d84268579073052 + languageName: node + linkType: hard + "@babel/helper-replace-supers@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-replace-supers@npm:7.22.20" @@ -711,6 +856,19 @@ __metadata: languageName: node linkType: hard +"@babel/helper-replace-supers@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-replace-supers@npm:7.25.9" + dependencies: + "@babel/helper-member-expression-to-functions": "npm:^7.25.9" + "@babel/helper-optimise-call-expression": "npm:^7.25.9" + "@babel/traverse": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/8ebf787016953e4479b99007bac735c9c860822fafc51bc3db67bc53814539888797238c81fa8b948b6da897eb7b1c1d4f04df11e501a7f0596b356be02de2ab + languageName: node + linkType: hard + "@babel/helper-simple-access@npm:^7.22.5": version: 7.22.5 resolution: "@babel/helper-simple-access@npm:7.22.5" @@ -749,6 +907,16 @@ __metadata: languageName: node linkType: hard +"@babel/helper-skip-transparent-expression-wrappers@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-skip-transparent-expression-wrappers@npm:7.25.9" + dependencies: + "@babel/traverse": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + checksum: 10/fdbb5248932198bc26daa6abf0d2ac42cab9c2dbb75b7e9f40d425c8f28f09620b886d40e7f9e4e08ffc7aaa2cefe6fc2c44be7c20e81f7526634702fb615bdc + languageName: node + linkType: hard + "@babel/helper-split-export-declaration@npm:^7.22.6": version: 7.22.6 resolution: "@babel/helper-split-export-declaration@npm:7.22.6" @@ -788,6 +956,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-string-parser@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-string-parser@npm:7.25.9" + checksum: 10/c28656c52bd48e8c1d9f3e8e68ecafd09d949c57755b0d353739eb4eae7ba4f7e67e92e4036f1cd43378cc1397a2c943ed7bcaf5949b04ab48607def0258b775 + languageName: node + linkType: hard + "@babel/helper-validator-identifier@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-validator-identifier@npm:7.22.20" @@ -802,6 +977,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-identifier@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-validator-identifier@npm:7.25.9" + checksum: 10/3f9b649be0c2fd457fa1957b694b4e69532a668866b8a0d81eabfa34ba16dbf3107b39e0e7144c55c3c652bf773ec816af8df4a61273a2bb4eb3145ca9cf478e + languageName: node + linkType: hard + "@babel/helper-validator-option@npm:^7.22.15, @babel/helper-validator-option@npm:^7.23.5": version: 7.23.5 resolution: "@babel/helper-validator-option@npm:7.23.5" @@ -823,6 +1005,13 @@ __metadata: languageName: node linkType: hard +"@babel/helper-validator-option@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-validator-option@npm:7.25.9" + checksum: 10/9491b2755948ebbdd68f87da907283698e663b5af2d2b1b02a2765761974b1120d5d8d49e9175b167f16f72748ffceec8c9cf62acfbee73f4904507b246e2b3d + languageName: node + linkType: hard + "@babel/helper-wrap-function@npm:^7.22.20": version: 7.22.20 resolution: "@babel/helper-wrap-function@npm:7.22.20" @@ -857,6 +1046,17 @@ __metadata: languageName: node linkType: hard +"@babel/helper-wrap-function@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/helper-wrap-function@npm:7.25.9" + dependencies: + "@babel/template": "npm:^7.25.9" + "@babel/traverse": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + checksum: 10/988dcf49159f1c920d6b9486762a93767a6e84b5e593a6342bc235f3e47cc1cb0c048d8fca531a48143e6b7fce1ff12ddbf735cf5f62cb2f07192cf7c27b89cf + languageName: node + linkType: hard + "@babel/helpers@npm:^7.23.9": version: 7.23.9 resolution: "@babel/helpers@npm:7.23.9" @@ -940,6 +1140,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.25.9, @babel/parser@npm:^7.26.3": + version: 7.26.3 + resolution: "@babel/parser@npm:7.26.3" + dependencies: + "@babel/types": "npm:^7.26.3" + bin: + parser: ./bin/babel-parser.js + checksum: 10/e7e3814b2dc9ee3ed605d38223471fa7d3a84cbe9474d2b5fa7ac57dc1ddf75577b1fd3a93bf7db8f41f28869bda795cddd80223f980be23623b6434bf4c88a8 + languageName: node + linkType: hard + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.25.3": version: 7.25.3 resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.25.3" @@ -952,6 +1163,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/traverse": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/3c23ef34e3fd7da3578428cb488180ab6b7b96c9c141438374b6d87fa814d87de099f28098e5fc64726c19193a1da397e4d2351d40b459bcd2489993557e2c74 + languageName: node + linkType: hard + "@babel/plugin-bugfix-safari-class-field-initializer-scope@npm:^7.25.0": version: 7.25.0 resolution: "@babel/plugin-bugfix-safari-class-field-initializer-scope@npm:7.25.0" @@ -963,6 +1186,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-bugfix-safari-class-field-initializer-scope@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-bugfix-safari-class-field-initializer-scope@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/d3e14ab1cb9cb50246d20cab9539f2fbd1e7ef1ded73980c8ad7c0561b4d5e0b144d362225f0976d47898e04cbd40f2000e208b0913bd788346cf7791b96af91 + languageName: node + linkType: hard + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.23.3" @@ -985,6 +1219,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/a9d1ee3fd100d3eb6799a2f2bbd785296f356c531d75c9369f71541811fa324270258a374db103ce159156d006da2f33370330558d0133e6f7584152c34997ca + languageName: node + linkType: hard + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.23.3" @@ -1011,6 +1256,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.9" + "@babel/plugin-transform-optional-chaining": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.13.0 + checksum: 10/5b298b28e156f64de51cdb03a2c5b80c7f978815ef1026f3ae8b9fc48d28bf0a83817d8fbecb61ef8fb94a7201f62cca5103cc6e7b9e8f28e38f766d7905b378 + languageName: node + linkType: hard + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.23.7": version: 7.23.7 resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.23.7" @@ -1035,6 +1293,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/traverse": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/cb893e5deb9312a0120a399835b6614a016c036714de7123c8edabccc56a09c4455016e083c5c4dd485248546d4e5e55fc0e9132b3c3a9bd16abf534138fe3f2 + languageName: node + linkType: hard + "@babel/plugin-proposal-async-generator-functions@npm:^7.0.0": version: 7.20.7 resolution: "@babel/plugin-proposal-async-generator-functions@npm:7.20.7" @@ -1307,6 +1577,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-flow@npm:^7.25.9": + version: 7.26.0 + resolution: "@babel/plugin-syntax-flow@npm:7.26.0" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/fdc0d0a7b512e00d933e12cf93c785ea4645a193f4b539230b7601cfaa8c704410199318ce9ea14e5fca7d13e9027822f7d81a7871d3e854df26b6af04cc3c6c + languageName: node + linkType: hard + "@babel/plugin-syntax-import-assertions@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-syntax-import-assertions@npm:7.23.3" @@ -1329,6 +1610,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-import-assertions@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/plugin-syntax-import-assertions@npm:7.26.0" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/b58f2306df4a690ca90b763d832ec05202c50af787158ff8b50cdf3354359710bce2e1eb2b5135fcabf284756ac8eadf09ca74764aa7e76d12a5cac5f6b21e67 + languageName: node + linkType: hard + "@babel/plugin-syntax-import-attributes@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-syntax-import-attributes@npm:7.23.3" @@ -1351,6 +1643,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-import-attributes@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/plugin-syntax-import-attributes@npm:7.26.0" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/c122aa577166c80ee67f75aebebeef4150a132c4d3109d25d7fc058bf802946f883e330f20b78c1d3e3a5ada631c8780c263d2d01b5dbaecc69efefeedd42916 + languageName: node + linkType: hard + "@babel/plugin-syntax-import-meta@npm:^7.10.4, @babel/plugin-syntax-import-meta@npm:^7.8.3": version: 7.10.4 resolution: "@babel/plugin-syntax-import-meta@npm:7.10.4" @@ -1395,6 +1698,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-jsx@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-syntax-jsx@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/bb609d1ffb50b58f0c1bac8810d0e46a4f6c922aa171c458f3a19d66ee545d36e782d3bffbbc1fed0dc65a558bdce1caf5279316583c0fff5a2c1658982a8563 + languageName: node + linkType: hard + "@babel/plugin-syntax-logical-assignment-operators@npm:^7.10.4, @babel/plugin-syntax-logical-assignment-operators@npm:^7.8.3": version: 7.10.4 resolution: "@babel/plugin-syntax-logical-assignment-operators@npm:7.10.4" @@ -1494,6 +1808,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-syntax-typescript@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-syntax-typescript@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/0e9821e8ba7d660c36c919654e4144a70546942ae184e85b8102f2322451eae102cbfadbcadd52ce077a2b44b400ee52394c616feab7b5b9f791b910e933fd33 + languageName: node + linkType: hard + "@babel/plugin-syntax-unicode-sets-regex@npm:^7.18.6": version: 7.18.6 resolution: "@babel/plugin-syntax-unicode-sets-regex@npm:7.18.6" @@ -1528,6 +1853,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-arrow-functions@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-arrow-functions@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/c29f081224859483accf55fb4d091db2aac0dcd0d7954bac5ca889030cc498d3f771aa20eb2e9cd8310084ec394d85fa084b97faf09298b6bc9541182b3eb5bb + languageName: node + linkType: hard + "@babel/plugin-transform-async-generator-functions@npm:^7.23.9": version: 7.23.9 resolution: "@babel/plugin-transform-async-generator-functions@npm:7.23.9" @@ -1556,6 +1892,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-async-generator-functions@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-async-generator-functions@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-remap-async-to-generator": "npm:^7.25.9" + "@babel/traverse": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/99306c44a4a791abd51a56d89fa61c4cfe805a58e070c7fb1cbf950886778a6c8c4f25a92d231f91da1746d14a338436073fd83038e607f03a2a98ac5340406b + languageName: node + linkType: hard + "@babel/plugin-transform-async-to-generator@npm:^7.20.0, @babel/plugin-transform-async-to-generator@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-async-to-generator@npm:7.23.3" @@ -1582,6 +1931,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-async-to-generator@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-async-to-generator@npm:7.25.9" + dependencies: + "@babel/helper-module-imports": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-remap-async-to-generator": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/b3ad50fb93c171644d501864620ed23952a46648c4df10dc9c62cc9ad08031b66bd272cfdd708faeee07c23b6251b16f29ce0350473e4c79f0c32178d38ce3a6 + languageName: node + linkType: hard + "@babel/plugin-transform-block-scoped-functions@npm:^7.0.0": version: 7.24.1 resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.24.1" @@ -1615,6 +1977,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-block-scoped-functions@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-block-scoped-functions@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/bf31896556b33a80f017af3d445ceb532ec0f5ca9d69bc211a963ac92514d172d5c24c5ac319f384d9dfa7f1a4d8dc23032c2fe3e74f98a59467ecd86f7033ae + languageName: node + linkType: hard + "@babel/plugin-transform-block-scoping@npm:^7.0.0, @babel/plugin-transform-block-scoping@npm:^7.23.4": version: 7.23.4 resolution: "@babel/plugin-transform-block-scoping@npm:7.23.4" @@ -1637,6 +2010,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-block-scoping@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-block-scoping@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/89dcdd7edb1e0c2f44e3c568a8ad8202e2574a8a8308248550a9391540bc3f5c9fbd8352c60ae90769d46f58d3ab36f2c3a0fbc1c3620813d92ff6fccdfa79c8 + languageName: node + linkType: hard + "@babel/plugin-transform-class-properties@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-class-properties@npm:7.23.3" @@ -1661,6 +2045,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-class-properties@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-class-properties@npm:7.25.9" + dependencies: + "@babel/helper-create-class-features-plugin": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/a8d69e2c285486b63f49193cbcf7a15e1d3a5f632c1c07d7a97f65306df7f554b30270b7378dde143f8b557d1f8f6336c643377943dec8ec405e4cd11e90b9ea + languageName: node + linkType: hard + "@babel/plugin-transform-class-static-block@npm:^7.23.4": version: 7.23.4 resolution: "@babel/plugin-transform-class-static-block@npm:7.23.4" @@ -1687,6 +2083,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-class-static-block@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/plugin-transform-class-static-block@npm:7.26.0" + dependencies: + "@babel/helper-create-class-features-plugin": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.12.0 + checksum: 10/60cba3f125a7bc4f90706af0a011697c7ffd2eddfba336ed6f84c5f358c44c3161af18b0202475241a96dee7964d96dd3a342f46dbf85b75b38bb789326e1766 + languageName: node + linkType: hard + "@babel/plugin-transform-classes@npm:^7.0.0, @babel/plugin-transform-classes@npm:^7.23.8": version: 7.23.8 resolution: "@babel/plugin-transform-classes@npm:7.23.8" @@ -1721,6 +2129,22 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-classes@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-classes@npm:7.25.9" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.25.9" + "@babel/helper-compilation-targets": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-replace-supers": "npm:^7.25.9" + "@babel/traverse": "npm:^7.25.9" + globals: "npm:^11.1.0" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/1914ebe152f35c667fba7bf17ce0d9d0f33df2fb4491990ce9bb1f9ec5ae8cbd11d95b0dc371f7a4cc5e7ce4cf89467c3e34857302911fc6bfb6494a77f7b37e + languageName: node + linkType: hard + "@babel/plugin-transform-computed-properties@npm:^7.0.0, @babel/plugin-transform-computed-properties@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-computed-properties@npm:7.23.3" @@ -1745,6 +2169,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-computed-properties@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-computed-properties@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/template": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/aa1a9064d6a9d3b569b8cae6972437315a38a8f6553ee618406da5122500a06c2f20b9fa93aeed04dd895923bf6f529c09fc79d4be987ec41785ceb7d2203122 + languageName: node + linkType: hard + "@babel/plugin-transform-destructuring@npm:^7.0.0": version: 7.24.1 resolution: "@babel/plugin-transform-destructuring@npm:7.24.1" @@ -1778,6 +2214,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-destructuring@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-destructuring@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/51b24fbead910ad0547463b2d214dd08076b22a66234b9f878b8bac117603dd23e05090ff86e9ffc373214de23d3e5bf1b095fe54cce2ca16b010264d90cf4f5 + languageName: node + linkType: hard + "@babel/plugin-transform-dotall-regex@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-dotall-regex@npm:7.23.3" @@ -1802,6 +2249,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-dotall-regex@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-dotall-regex@npm:7.25.9" + dependencies: + "@babel/helper-create-regexp-features-plugin": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/8bdf1bb9e6e3a2cc8154ae88a3872faa6dc346d6901994505fb43ac85f858728781f1219f40b67f7bb0687c507450236cb7838ac68d457e65637f98500aa161b + languageName: node + linkType: hard + "@babel/plugin-transform-duplicate-keys@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-duplicate-keys@npm:7.23.3" @@ -1824,6 +2283,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-duplicate-keys@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-duplicate-keys@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/10dbb87bc09582416f9f97ca6c40563655abf33e3fd0fee25eeaeff28e946a06651192112a2bc2b18c314a638fa15c55b8365a677ef67aa490848cefdc57e1d8 + languageName: node + linkType: hard + "@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:^7.25.0": version: 7.25.0 resolution: "@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:7.25.0" @@ -1836,6 +2306,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-duplicate-named-capturing-groups-regex@npm:7.25.9" + dependencies: + "@babel/helper-create-regexp-features-plugin": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/f7233cf596be8c6843d31951afaf2464a62a610cb89c72c818c044765827fab78403ab8a7d3a6386f838c8df574668e2a48f6c206b1d7da965aff9c6886cb8e6 + languageName: node + linkType: hard + "@babel/plugin-transform-dynamic-import@npm:^7.23.4": version: 7.23.4 resolution: "@babel/plugin-transform-dynamic-import@npm:7.23.4" @@ -1860,6 +2342,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-dynamic-import@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-dynamic-import@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/aaca1ccda819be9b2b85af47ba08ddd2210ff2dbea222f26e4cd33f97ab020884bf81a66197e50872721e9daf36ceb5659502c82199884ea74d5d75ecda5c58b + languageName: node + linkType: hard + "@babel/plugin-transform-exponentiation-operator@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.23.3" @@ -1884,6 +2377,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-exponentiation-operator@npm:^7.25.9": + version: 7.26.3 + resolution: "@babel/plugin-transform-exponentiation-operator@npm:7.26.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/0d8da2e552a50a775fe8e6e3c32621d20d3c5d1af7ab40ca2f5c7603de057b57b1b5850f74040e4ecbe36c09ac86d92173ad1e223a2a3b3df3cc359ca4349738 + languageName: node + linkType: hard + "@babel/plugin-transform-export-namespace-from@npm:^7.22.11": version: 7.24.1 resolution: "@babel/plugin-transform-export-namespace-from@npm:7.24.1" @@ -1920,6 +2424,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-export-namespace-from@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-export-namespace-from@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/4dfe8df86c5b1d085d591290874bb2d78a9063090d71567ed657a418010ad333c3f48af2c974b865f53bbb718987a065f89828d43279a7751db1a56c9229078d + languageName: node + linkType: hard + "@babel/plugin-transform-flow-strip-types@npm:^7.0.0": version: 7.24.1 resolution: "@babel/plugin-transform-flow-strip-types@npm:7.24.1" @@ -1944,6 +2459,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-flow-strip-types@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-flow-strip-types@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/plugin-syntax-flow": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/a3ffc76bbc922720debe973bccb501ccbda0d6d32d80c9efd599ab1b683fd72cae3198975d8609b37070fc32f921a9eb7d2db17b7b719395468773be41011822 + languageName: node + linkType: hard + "@babel/plugin-transform-for-of@npm:^7.0.0": version: 7.24.1 resolution: "@babel/plugin-transform-for-of@npm:7.24.1" @@ -1980,6 +2507,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-for-of@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-for-of@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/63a2db7fe06c2e3f5fc1926f478dac66a5f7b3eaeb4a0ffae577e6f3cb3d822cb1ed2ed3798f70f5cb1aa06bc2ad8bcd1f557342f5c425fd83c37a8fc1cfd2ba + languageName: node + linkType: hard + "@babel/plugin-transform-function-name@npm:^7.0.0, @babel/plugin-transform-function-name@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-function-name@npm:7.23.3" @@ -2006,6 +2545,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-function-name@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-function-name@npm:7.25.9" + dependencies: + "@babel/helper-compilation-targets": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/traverse": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/a8d7c8d019a6eb57eab5ca1be3e3236f175557d55b1f3b11f8ad7999e3fbb1cf37905fd8cb3a349bffb4163a558e9f33b63f631597fdc97c858757deac1b2fd7 + languageName: node + linkType: hard + "@babel/plugin-transform-json-strings@npm:^7.23.4": version: 7.23.4 resolution: "@babel/plugin-transform-json-strings@npm:7.23.4" @@ -2030,6 +2582,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-json-strings@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-json-strings@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/e2498d84761cfd05aaea53799933d55af309c9d6204e66b38778792d171e4d1311ad34f334259a3aa3407dd0446f6bd3e390a1fcb8ce2e42fe5aabed0e41bee1 + languageName: node + linkType: hard + "@babel/plugin-transform-literals@npm:^7.0.0, @babel/plugin-transform-literals@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-literals@npm:7.23.3" @@ -2052,6 +2615,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-literals@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-literals@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/3cca75823a38aab599bc151b0fa4d816b5e1b62d6e49c156aa90436deb6e13649f5505973151a10418b64f3f9d1c3da53e38a186402e0ed7ad98e482e70c0c14 + languageName: node + linkType: hard + "@babel/plugin-transform-logical-assignment-operators@npm:^7.23.4": version: 7.23.4 resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.23.4" @@ -2076,6 +2650,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-logical-assignment-operators@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-logical-assignment-operators@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/8c6febb4ac53852314d28b5e2c23d5dbbff7bf1e57d61f9672e0d97531ef7778b3f0ad698dcf1179f5486e626c77127508916a65eb846a89e98a92f70ed3537b + languageName: node + linkType: hard + "@babel/plugin-transform-member-expression-literals@npm:^7.0.0": version: 7.24.1 resolution: "@babel/plugin-transform-member-expression-literals@npm:7.24.1" @@ -2109,6 +2694,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-member-expression-literals@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-member-expression-literals@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/db92041ae87b8f59f98b50359e0bb172480f6ba22e5e76b13bdfe07122cbf0daa9cd8ad2e78dcb47939938fed88ad57ab5989346f64b3a16953fc73dea3a9b1f + languageName: node + linkType: hard + "@babel/plugin-transform-modules-amd@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-modules-amd@npm:7.23.3" @@ -2133,6 +2729,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-amd@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-modules-amd@npm:7.25.9" + dependencies: + "@babel/helper-module-transforms": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/75d34c6e709a23bcfa0e06f722c9a72b1d9ac3e7d72a07ef54a943d32f65f97cbbf0e387d874eb9d9b4c8d33045edfa8e8441d0f8794f3c2b9f1d71b928acf2c + languageName: node + linkType: hard + "@babel/plugin-transform-modules-commonjs@npm:^7.0.0, @babel/plugin-transform-modules-commonjs@npm:^7.13.8, @babel/plugin-transform-modules-commonjs@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-modules-commonjs@npm:7.23.3" @@ -2159,6 +2767,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-commonjs@npm:^7.25.9": + version: 7.26.3 + resolution: "@babel/plugin-transform-modules-commonjs@npm:7.26.3" + dependencies: + "@babel/helper-module-transforms": "npm:^7.26.0" + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/f817f02fa04d13f1578f3026239b57f1003bebcf9f9b8d854714bed76a0e4986c79bd6d2e0ac14282c5d309454a8dab683c179709ca753b0152a69c69f3a78e3 + languageName: node + linkType: hard + "@babel/plugin-transform-modules-systemjs@npm:^7.23.9": version: 7.23.9 resolution: "@babel/plugin-transform-modules-systemjs@npm:7.23.9" @@ -2187,6 +2807,20 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-systemjs@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-modules-systemjs@npm:7.25.9" + dependencies: + "@babel/helper-module-transforms": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-validator-identifier": "npm:^7.25.9" + "@babel/traverse": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/03145aa89b7c867941a03755216cfb503df6d475a78df84849a157fa5f2fcc17ba114a968d0579ae34e7c61403f35d1ba5d188fdfb9ad05f19354eb7605792f9 + languageName: node + linkType: hard + "@babel/plugin-transform-modules-umd@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-modules-umd@npm:7.23.3" @@ -2211,6 +2845,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-modules-umd@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-modules-umd@npm:7.25.9" + dependencies: + "@babel/helper-module-transforms": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/47d03485fedac828832d9fee33b3b982a6db8197e8651ceb5d001890e276150b5a7ee3e9780749e1ba76453c471af907a159108832c24f93453dd45221788e97 + languageName: node + linkType: hard + "@babel/plugin-transform-named-capturing-groups-regex@npm:^7.0.0, @babel/plugin-transform-named-capturing-groups-regex@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.22.5" @@ -2235,6 +2881,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-named-capturing-groups-regex@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-named-capturing-groups-regex@npm:7.25.9" + dependencies: + "@babel/helper-create-regexp-features-plugin": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/434346ba05cf74e3f4704b3bdd439287b95cd2a8676afcdc607810b8c38b6f4798cd69c1419726b2e4c7204e62e4a04d31b0360e91ca57a930521c9211e07789 + languageName: node + linkType: hard + "@babel/plugin-transform-new-target@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-new-target@npm:7.23.3" @@ -2257,6 +2915,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-new-target@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-new-target@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/07bb3a09028ee7b8e8ede6e6390e3b3aecc5cf9adb2fc5475ff58036c552b8a3f8e63d4c43211a60545f3307cdc15919f0e54cb5455d9546daed162dc54ff94e + languageName: node + linkType: hard + "@babel/plugin-transform-nullish-coalescing-operator@npm:^7.23.4": version: 7.23.4 resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.23.4" @@ -2281,6 +2950,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-nullish-coalescing-operator@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-nullish-coalescing-operator@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/26e03b1c2c0408cc300e46d8f8cb639653ff3a7b03456d0d8afbb53c44f33a89323f51d99991dade3a5676921119bbdf869728bb7911799b5ef99ffafa2cdd24 + languageName: node + linkType: hard + "@babel/plugin-transform-numeric-separator@npm:^7.23.4": version: 7.23.4 resolution: "@babel/plugin-transform-numeric-separator@npm:7.23.4" @@ -2305,6 +2985,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-numeric-separator@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-numeric-separator@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/0528ef041ed88e8c3f51624ee87b8182a7f246fe4013f0572788e0727d20795b558f2b82e3989b5dd416cbd339500f0d88857de41b6d3b6fdacb1d5344bcc5b1 + languageName: node + linkType: hard + "@babel/plugin-transform-object-rest-spread@npm:^7.12.13": version: 7.24.1 resolution: "@babel/plugin-transform-object-rest-spread@npm:7.24.1" @@ -2348,6 +3039,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-object-rest-spread@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-object-rest-spread@npm:7.25.9" + dependencies: + "@babel/helper-compilation-targets": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/plugin-transform-parameters": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/a157ac5af2721090150858f301d9c0a3a0efb8ef66b90fce326d6cc0ae45ab97b6219b3e441bf8d72a2287e95eb04dd6c12544da88ea2345e70b3fac2c0ac9e2 + languageName: node + linkType: hard + "@babel/plugin-transform-object-super@npm:^7.0.0": version: 7.24.1 resolution: "@babel/plugin-transform-object-super@npm:7.24.1" @@ -2384,6 +3088,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-object-super@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-object-super@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-replace-supers": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/1817b5d8b80e451ae1ad9080cca884f4f16df75880a158947df76a2ed8ab404d567a7dce71dd8051ef95f90fbe3513154086a32aba55cc76027f6cbabfbd7f98 + languageName: node + linkType: hard + "@babel/plugin-transform-optional-catch-binding@npm:^7.23.4": version: 7.23.4 resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.23.4" @@ -2400,11 +3116,22 @@ __metadata: version: 7.24.7 resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.24.7" dependencies: - "@babel/helper-plugin-utils": "npm:^7.24.7" - "@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3" + "@babel/helper-plugin-utils": "npm:^7.24.7" + "@babel/plugin-syntax-optional-catch-binding": "npm:^7.8.3" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/605ae3764354e83f73c1e6430bac29e308806abcce8d1369cf69e4921771ff3592e8f60ba60c15990070d79b8d8740f0841069d64b466b3ce8a8c43e9743da7e + languageName: node + linkType: hard + +"@babel/plugin-transform-optional-catch-binding@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-optional-catch-binding@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" peerDependencies: "@babel/core": ^7.0.0-0 - checksum: 10/605ae3764354e83f73c1e6430bac29e308806abcce8d1369cf69e4921771ff3592e8f60ba60c15990070d79b8d8740f0841069d64b466b3ce8a8c43e9743da7e + checksum: 10/b46a8d1e91829f3db5c252583eb00d05a779b4660abeea5500fda0f8ffa3584fd18299443c22f7fddf0ed9dfdb73c782c43b445dc468d4f89803f2356963b406 languageName: node linkType: hard @@ -2447,6 +3174,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-optional-chaining@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-optional-chaining@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/bc838a499fd9892e163b8bc9bfbc4bf0b28cc3232ee0a6406ae078257c8096518f871d09b4a32c11f4a2d6953c3bc1984619ef748f7ad45aed0b0d9689a8eb36 + languageName: node + linkType: hard + "@babel/plugin-transform-parameters@npm:^7.0.0, @babel/plugin-transform-parameters@npm:^7.20.7, @babel/plugin-transform-parameters@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-parameters@npm:7.23.3" @@ -2480,6 +3219,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-parameters@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-parameters@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/014009a1763deb41fe9f0dbca2c4489ce0ac83dd87395f488492e8eb52399f6c883d5bd591bae3b8836f2460c3937fcebd07e57dce1e0bfe30cdbc63fdfc9d3a + languageName: node + linkType: hard + "@babel/plugin-transform-private-methods@npm:^7.22.5, @babel/plugin-transform-private-methods@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-private-methods@npm:7.23.3" @@ -2504,6 +3254,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-private-methods@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-private-methods@npm:7.25.9" + dependencies: + "@babel/helper-create-class-features-plugin": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/6e3671b352c267847c53a170a1937210fa8151764d70d25005e711ef9b21969aaf422acc14f9f7fb86bc0e4ec43e7aefcc0ad9196ae02d262ec10f509f126a58 + languageName: node + linkType: hard + "@babel/plugin-transform-private-property-in-object@npm:^7.22.11, @babel/plugin-transform-private-property-in-object@npm:^7.23.4": version: 7.23.4 resolution: "@babel/plugin-transform-private-property-in-object@npm:7.23.4" @@ -2532,6 +3294,19 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-private-property-in-object@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-private-property-in-object@npm:7.25.9" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.25.9" + "@babel/helper-create-class-features-plugin": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/aa45bb5669b610afa763d774a4b5583bb60ce7d38e4fd2dedfd0703e73e25aa560e6c6124e155aa90b101601743b127d9e5d3eb00989a7e4b4ab9c2eb88475ba + languageName: node + linkType: hard + "@babel/plugin-transform-property-literals@npm:^7.0.0": version: 7.24.1 resolution: "@babel/plugin-transform-property-literals@npm:7.24.1" @@ -2565,6 +3340,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-property-literals@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-property-literals@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/436046ab07d54a9b44a384eeffec701d4e959a37a7547dda72e069e751ca7ff753d1782a8339e354b97c78a868b49ea97bf41bf5a44c6d7a3c0a05ad40eeb49c + languageName: node + linkType: hard + "@babel/plugin-transform-react-display-name@npm:^7.0.0": version: 7.23.3 resolution: "@babel/plugin-transform-react-display-name@npm:7.23.3" @@ -2587,6 +3373,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-react-display-name@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-react-display-name@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/dc7affde0ed98e40f629ee92a2fc44fbd8008aabda1ddb3f5bd2632699d3289b08dff65b26cf3b89dab46397ec440f453d19856bbb3a9a83df5b4ac6157c5c39 + languageName: node + linkType: hard + "@babel/plugin-transform-react-jsx-development@npm:^7.22.5": version: 7.22.5 resolution: "@babel/plugin-transform-react-jsx-development@npm:7.22.5" @@ -2598,6 +3395,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-react-jsx-development@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-react-jsx-development@npm:7.25.9" + dependencies: + "@babel/plugin-transform-react-jsx": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/537d38369537f1eb56041c4b770bc0733fde1801a7f5ffef40a1217ea448f33ee2fa8e6098a58a82fd00e432c1b9426a66849496da419020c9eca3b1b1a23779 + languageName: node + linkType: hard + "@babel/plugin-transform-react-jsx-self@npm:^7.0.0": version: 7.23.3 resolution: "@babel/plugin-transform-react-jsx-self@npm:7.23.3" @@ -2635,6 +3443,21 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-react-jsx@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-react-jsx@npm:7.25.9" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.25.9" + "@babel/helper-module-imports": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/plugin-syntax-jsx": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/eb179ecdf0ae19aed254105cf78fbac35f9983f51ed04b7b67c863a4820a70a879bd5da250ac518321f86df20eac010e53e3411c8750c386d51da30e4814bfb6 + languageName: node + linkType: hard + "@babel/plugin-transform-react-pure-annotations@npm:^7.24.1": version: 7.24.1 resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.24.1" @@ -2647,6 +3470,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-react-pure-annotations@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-react-pure-annotations@npm:7.25.9" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/9995c0fc7c25d3aaaa0ce84233de02eab2564ea111d0813ec5baa538eb21520402879cc787ad1ad4c2061b99cebc3beb09910e64c9592e8ccb42ae62d9e4fd9a + languageName: node + linkType: hard + "@babel/plugin-transform-regenerator@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-regenerator@npm:7.23.3" @@ -2671,6 +3506,30 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-regenerator@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-regenerator@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + regenerator-transform: "npm:^0.15.2" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/1c09e8087b476c5967282c9790fb8710e065eda77c60f6cb5da541edd59ded9d003d96f8ef640928faab4a0b35bf997673499a194973da4f0c97f0935807a482 + languageName: node + linkType: hard + +"@babel/plugin-transform-regexp-modifiers@npm:^7.26.0": + version: 7.26.0 + resolution: "@babel/plugin-transform-regexp-modifiers@npm:7.26.0" + dependencies: + "@babel/helper-create-regexp-features-plugin": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/726deca486bbd4b176f8a966eb0f4aabc19d9def3b8dabb8b3a656778eca0df1fda3f3c92b213aa5a184232fdafd5b7bd73b4e24ca4345c498ef6baff2bda4e1 + languageName: node + linkType: hard + "@babel/plugin-transform-reserved-words@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-reserved-words@npm:7.23.3" @@ -2693,6 +3552,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-reserved-words@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-reserved-words@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/8beda04481b25767acbd1f6b9ef7b3a9c12fbd9dcb24df45a6ad120e1dc4b247c073db60ac742f9093657d6d8c050501fc0606af042f81a3bb6a3ff862cddc47 + languageName: node + linkType: hard + "@babel/plugin-transform-runtime@npm:^7.0.0": version: 7.23.9 resolution: "@babel/plugin-transform-runtime@npm:7.23.9" @@ -2731,6 +3601,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-shorthand-properties@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-shorthand-properties@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/f774995d58d4e3a992b732cf3a9b8823552d471040e280264dd15e0735433d51b468fef04d75853d061309389c66bda10ce1b298297ce83999220eb0ad62741d + languageName: node + linkType: hard + "@babel/plugin-transform-spread@npm:^7.0.0, @babel/plugin-transform-spread@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-spread@npm:7.23.3" @@ -2755,6 +3636,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-spread@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-spread@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/fe72c6545267176cdc9b6f32f30f9ced37c1cafa1290e4436b83b8f377b4f1c175dad404228c96e3efdec75da692f15bfb9db2108fcd9ad260bc9968778ee41e + languageName: node + linkType: hard + "@babel/plugin-transform-sticky-regex@npm:^7.0.0, @babel/plugin-transform-sticky-regex@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-sticky-regex@npm:7.23.3" @@ -2777,6 +3670,28 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-sticky-regex@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-sticky-regex@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/7454b00844dbe924030dd15e2b3615b36e196500c4c47e98dabc6b37a054c5b1038ecd437e910aabf0e43bf56b973cb148d3437d50f6e2332d8309568e3e979b + languageName: node + linkType: hard + +"@babel/plugin-transform-strict-mode@npm:^7.24.7": + version: 7.25.9 + resolution: "@babel/plugin-transform-strict-mode@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/87b4a937b7c6f9cc4ed557ce1e2037898ec9c2af18f5523a5200f714cfd4101b0e278c732418b59a779e912cdf5574e35db01714d5b4e6fff2e31584501e4364 + languageName: node + linkType: hard + "@babel/plugin-transform-template-literals@npm:^7.0.0": version: 7.24.1 resolution: "@babel/plugin-transform-template-literals@npm:7.24.1" @@ -2810,6 +3725,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-template-literals@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-template-literals@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/92eb1d6e2d95bd24abbb74fa7640d02b66ff6214e0bb616d7fda298a7821ce15132a4265d576a3502a347a3c9e94b6c69ed265bb0784664592fa076785a3d16a + languageName: node + linkType: hard + "@babel/plugin-transform-typeof-symbol@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-typeof-symbol@npm:7.23.3" @@ -2832,6 +3758,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-typeof-symbol@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-typeof-symbol@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/3ae240358f0b0cd59f8610d6c59d395c216fd1bab407f7de58b86d592f030fb42b4d18e2456a29bee4a2ff014c4c1e3404c8ae64462b1155d1c053b2f9d73438 + languageName: node + linkType: hard + "@babel/plugin-transform-typescript@npm:^7.23.3, @babel/plugin-transform-typescript@npm:^7.5.0": version: 7.23.6 resolution: "@babel/plugin-transform-typescript@npm:7.23.6" @@ -2846,6 +3783,21 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-typescript@npm:^7.25.9": + version: 7.26.3 + resolution: "@babel/plugin-transform-typescript@npm:7.26.3" + dependencies: + "@babel/helper-annotate-as-pure": "npm:^7.25.9" + "@babel/helper-create-class-features-plugin": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-skip-transparent-expression-wrappers": "npm:^7.25.9" + "@babel/plugin-syntax-typescript": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/71e82045fc931112ca6cba1826a7d521a30514ea5e8370c3c083f6ee1ed624d62d91e1415fbc41ce9033c4e78ba638a904c43b2d7e023873f36675844b8a4963 + languageName: node + linkType: hard + "@babel/plugin-transform-unicode-escapes@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-unicode-escapes@npm:7.23.3" @@ -2868,6 +3820,17 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-unicode-escapes@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-unicode-escapes@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/f138cbee539963fb3da13f684e6f33c9f7495220369ae12a682b358f1e25ac68936825562c38eae87f01ac9992b2129208b35ec18533567fc805ce5ed0ffd775 + languageName: node + linkType: hard + "@babel/plugin-transform-unicode-property-regex@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.23.3" @@ -2892,6 +3855,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-unicode-property-regex@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-unicode-property-regex@npm:7.25.9" + dependencies: + "@babel/helper-create-regexp-features-plugin": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/201f6f46c1beb399e79aa208b94c5d54412047511795ce1e790edcd189cef73752e6a099fdfc01b3ad12205f139ae344143b62f21f44bbe02338a95e8506a911 + languageName: node + linkType: hard + "@babel/plugin-transform-unicode-regex@npm:^7.0.0, @babel/plugin-transform-unicode-regex@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-unicode-regex@npm:7.23.3" @@ -2916,6 +3891,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-unicode-regex@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-unicode-regex@npm:7.25.9" + dependencies: + "@babel/helper-create-regexp-features-plugin": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/e8baae867526e179467c6ef5280d70390fa7388f8763a19a27c21302dd59b121032568be080749514b097097ceb9af716bf4b90638f1b3cf689aa837ba20150f + languageName: node + linkType: hard + "@babel/plugin-transform-unicode-sets-regex@npm:^7.23.3": version: 7.23.3 resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.23.3" @@ -2940,6 +3927,18 @@ __metadata: languageName: node linkType: hard +"@babel/plugin-transform-unicode-sets-regex@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/plugin-transform-unicode-sets-regex@npm:7.25.9" + dependencies: + "@babel/helper-create-regexp-features-plugin": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0 + checksum: 10/4445ef20de687cb4dcc95169742a8d9013d680aa5eee9186d8e25875bbfa7ee5e2de26a91177ccf70b1db518e36886abcd44750d28db5d7a9539f0efa6839f4b + languageName: node + linkType: hard + "@babel/preset-env@npm:^7.20.0": version: 7.23.9 resolution: "@babel/preset-env@npm:7.23.9" @@ -3030,6 +4029,85 @@ __metadata: languageName: node linkType: hard +"@babel/preset-env@npm:^7.25.2": + version: 7.26.0 + resolution: "@babel/preset-env@npm:7.26.0" + dependencies: + "@babel/compat-data": "npm:^7.26.0" + "@babel/helper-compilation-targets": "npm:^7.25.9" + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-validator-option": "npm:^7.25.9" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "npm:^7.25.9" + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "npm:^7.25.9" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "npm:^7.25.9" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "npm:^7.25.9" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "npm:^7.25.9" + "@babel/plugin-proposal-private-property-in-object": "npm:7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-import-assertions": "npm:^7.26.0" + "@babel/plugin-syntax-import-attributes": "npm:^7.26.0" + "@babel/plugin-syntax-unicode-sets-regex": "npm:^7.18.6" + "@babel/plugin-transform-arrow-functions": "npm:^7.25.9" + "@babel/plugin-transform-async-generator-functions": "npm:^7.25.9" + "@babel/plugin-transform-async-to-generator": "npm:^7.25.9" + "@babel/plugin-transform-block-scoped-functions": "npm:^7.25.9" + "@babel/plugin-transform-block-scoping": "npm:^7.25.9" + "@babel/plugin-transform-class-properties": "npm:^7.25.9" + "@babel/plugin-transform-class-static-block": "npm:^7.26.0" + "@babel/plugin-transform-classes": "npm:^7.25.9" + "@babel/plugin-transform-computed-properties": "npm:^7.25.9" + "@babel/plugin-transform-destructuring": "npm:^7.25.9" + "@babel/plugin-transform-dotall-regex": "npm:^7.25.9" + "@babel/plugin-transform-duplicate-keys": "npm:^7.25.9" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "npm:^7.25.9" + "@babel/plugin-transform-dynamic-import": "npm:^7.25.9" + "@babel/plugin-transform-exponentiation-operator": "npm:^7.25.9" + "@babel/plugin-transform-export-namespace-from": "npm:^7.25.9" + "@babel/plugin-transform-for-of": "npm:^7.25.9" + "@babel/plugin-transform-function-name": "npm:^7.25.9" + "@babel/plugin-transform-json-strings": "npm:^7.25.9" + "@babel/plugin-transform-literals": "npm:^7.25.9" + "@babel/plugin-transform-logical-assignment-operators": "npm:^7.25.9" + "@babel/plugin-transform-member-expression-literals": "npm:^7.25.9" + "@babel/plugin-transform-modules-amd": "npm:^7.25.9" + "@babel/plugin-transform-modules-commonjs": "npm:^7.25.9" + "@babel/plugin-transform-modules-systemjs": "npm:^7.25.9" + "@babel/plugin-transform-modules-umd": "npm:^7.25.9" + "@babel/plugin-transform-named-capturing-groups-regex": "npm:^7.25.9" + "@babel/plugin-transform-new-target": "npm:^7.25.9" + "@babel/plugin-transform-nullish-coalescing-operator": "npm:^7.25.9" + "@babel/plugin-transform-numeric-separator": "npm:^7.25.9" + "@babel/plugin-transform-object-rest-spread": "npm:^7.25.9" + "@babel/plugin-transform-object-super": "npm:^7.25.9" + "@babel/plugin-transform-optional-catch-binding": "npm:^7.25.9" + "@babel/plugin-transform-optional-chaining": "npm:^7.25.9" + "@babel/plugin-transform-parameters": "npm:^7.25.9" + "@babel/plugin-transform-private-methods": "npm:^7.25.9" + "@babel/plugin-transform-private-property-in-object": "npm:^7.25.9" + "@babel/plugin-transform-property-literals": "npm:^7.25.9" + "@babel/plugin-transform-regenerator": "npm:^7.25.9" + "@babel/plugin-transform-regexp-modifiers": "npm:^7.26.0" + "@babel/plugin-transform-reserved-words": "npm:^7.25.9" + "@babel/plugin-transform-shorthand-properties": "npm:^7.25.9" + "@babel/plugin-transform-spread": "npm:^7.25.9" + "@babel/plugin-transform-sticky-regex": "npm:^7.25.9" + "@babel/plugin-transform-template-literals": "npm:^7.25.9" + "@babel/plugin-transform-typeof-symbol": "npm:^7.25.9" + "@babel/plugin-transform-unicode-escapes": "npm:^7.25.9" + "@babel/plugin-transform-unicode-property-regex": "npm:^7.25.9" + "@babel/plugin-transform-unicode-regex": "npm:^7.25.9" + "@babel/plugin-transform-unicode-sets-regex": "npm:^7.25.9" + "@babel/preset-modules": "npm:0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2: "npm:^0.4.10" + babel-plugin-polyfill-corejs3: "npm:^0.10.6" + babel-plugin-polyfill-regenerator: "npm:^0.6.1" + core-js-compat: "npm:^3.38.1" + semver: "npm:^6.3.1" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/a7a80314f845deea713985a6316361c476621c76cfe5c6c28e8b9558f01634b49bbfdd3581ef94b5d6cff5c2b8830468aa53a73f5b5c1224db2dfea5db7e676f + languageName: node + linkType: hard + "@babel/preset-env@npm:^7.25.4": version: 7.25.4 resolution: "@babel/preset-env@npm:7.25.4" @@ -3136,6 +4214,19 @@ __metadata: languageName: node linkType: hard +"@babel/preset-flow@npm:^7.24.7": + version: 7.25.9 + resolution: "@babel/preset-flow@npm:7.25.9" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-validator-option": "npm:^7.25.9" + "@babel/plugin-transform-flow-strip-types": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/b1591ea63a7ace7e34bcefa6deba9e2814d7f082e3c074e2648efb68a1a49016ccefbea024156ba28bd3042a4e768e3eb8b5ecfe433978144fdaaadd36203ba2 + languageName: node + linkType: hard + "@babel/preset-modules@npm:0.1.6-no-external-plugins": version: 0.1.6-no-external-plugins resolution: "@babel/preset-modules@npm:0.1.6-no-external-plugins" @@ -3165,6 +4256,22 @@ __metadata: languageName: node linkType: hard +"@babel/preset-react@npm:^7.24.7": + version: 7.26.3 + resolution: "@babel/preset-react@npm:7.26.3" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-validator-option": "npm:^7.25.9" + "@babel/plugin-transform-react-display-name": "npm:^7.25.9" + "@babel/plugin-transform-react-jsx": "npm:^7.25.9" + "@babel/plugin-transform-react-jsx-development": "npm:^7.25.9" + "@babel/plugin-transform-react-pure-annotations": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/88cb78c402b79f32389ee06451da51698d5b1da7641d9a47482883f537fe5441a138bd4c077d8533fd6d557406b08911c47b94402cea843db598e020bdd9a373 + languageName: node + linkType: hard + "@babel/preset-typescript@npm:^7.13.0": version: 7.23.3 resolution: "@babel/preset-typescript@npm:7.23.3" @@ -3180,6 +4287,21 @@ __metadata: languageName: node linkType: hard +"@babel/preset-typescript@npm:^7.24.7": + version: 7.26.0 + resolution: "@babel/preset-typescript@npm:7.26.0" + dependencies: + "@babel/helper-plugin-utils": "npm:^7.25.9" + "@babel/helper-validator-option": "npm:^7.25.9" + "@babel/plugin-syntax-jsx": "npm:^7.25.9" + "@babel/plugin-transform-modules-commonjs": "npm:^7.25.9" + "@babel/plugin-transform-typescript": "npm:^7.25.9" + peerDependencies: + "@babel/core": ^7.0.0-0 + checksum: 10/81a60826160163a3daae017709f42147744757b725b50c9024ef3ee5a402ee45fd2e93eaecdaaa22c81be91f7940916249cfb7711366431cfcacc69c95878c03 + languageName: node + linkType: hard + "@babel/register@npm:^7.13.16": version: 7.23.7 resolution: "@babel/register@npm:7.23.7" @@ -3211,6 +4333,15 @@ __metadata: languageName: node linkType: hard +"@babel/runtime@npm:^7.25.0": + version: 7.26.0 + resolution: "@babel/runtime@npm:7.26.0" + dependencies: + regenerator-runtime: "npm:^0.14.0" + checksum: 10/9f4ea1c1d566c497c052d505587554e782e021e6ccd302c2ad7ae8291c8e16e3f19d4a7726fb64469e057779ea2081c28b7dbefec6d813a22f08a35712c0f699 + languageName: node + linkType: hard + "@babel/template@npm:^7.0.0, @babel/template@npm:^7.22.15, @babel/template@npm:^7.23.9, @babel/template@npm:^7.3.3": version: 7.23.9 resolution: "@babel/template@npm:7.23.9" @@ -3244,6 +4375,17 @@ __metadata: languageName: node linkType: hard +"@babel/template@npm:^7.25.9": + version: 7.25.9 + resolution: "@babel/template@npm:7.25.9" + dependencies: + "@babel/code-frame": "npm:^7.25.9" + "@babel/parser": "npm:^7.25.9" + "@babel/types": "npm:^7.25.9" + checksum: 10/e861180881507210150c1335ad94aff80fd9e9be6202e1efa752059c93224e2d5310186ddcdd4c0f0b0fc658ce48cb47823f15142b5c00c8456dde54f5de80b2 + languageName: node + linkType: hard + "@babel/traverse@npm:^7.20.0, @babel/traverse@npm:^7.23.9": version: 7.23.9 resolution: "@babel/traverse@npm:7.23.9" @@ -3295,6 +4437,21 @@ __metadata: languageName: node linkType: hard +"@babel/traverse@npm:^7.25.9": + version: 7.26.4 + resolution: "@babel/traverse@npm:7.26.4" + dependencies: + "@babel/code-frame": "npm:^7.26.2" + "@babel/generator": "npm:^7.26.3" + "@babel/parser": "npm:^7.26.3" + "@babel/template": "npm:^7.25.9" + "@babel/types": "npm:^7.26.3" + debug: "npm:^4.3.1" + globals: "npm:^11.1.0" + checksum: 10/30c81a80d66fc39842814bc2e847f4705d30f3859156f130d90a0334fe1d53aa81eed877320141a528ecbc36448acc0f14f544a7d410fa319d1c3ab63b50b58f + languageName: node + linkType: hard + "@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.22.15, @babel/types@npm:^7.22.19, @babel/types@npm:^7.22.5, @babel/types@npm:^7.23.0, @babel/types@npm:^7.23.4, @babel/types@npm:^7.23.6, @babel/types@npm:^7.23.9, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4, @babel/types@npm:^7.8.3": version: 7.23.9 resolution: "@babel/types@npm:7.23.9" @@ -3339,6 +4496,16 @@ __metadata: languageName: node linkType: hard +"@babel/types@npm:^7.25.9, @babel/types@npm:^7.26.3": + version: 7.26.3 + resolution: "@babel/types@npm:7.26.3" + dependencies: + "@babel/helper-string-parser": "npm:^7.25.9" + "@babel/helper-validator-identifier": "npm:^7.25.9" + checksum: 10/c31d0549630a89abfa11410bf82a318b0c87aa846fbf5f9905e47ba5e2aa44f41cc746442f105d622c519e4dc532d35a8d8080460ff4692f9fc7485fbf3a00eb + languageName: node + linkType: hard + "@bcoe/v8-coverage@npm:^0.2.3": version: 0.2.3 resolution: "@bcoe/v8-coverage@npm:0.2.3" @@ -6402,6 +7569,16 @@ __metadata: languageName: unknown linkType: soft +"@react-native-firebase/vertexai@npm:0.0.1, @react-native-firebase/vertexai@workspace:packages/vertexai": + version: 0.0.0-use.local + resolution: "@react-native-firebase/vertexai@workspace:packages/vertexai" + dependencies: + react-native-builder-bob: "npm:^0.35.2" + peerDependencies: + "@react-native-firebase/app": 21.6.2 + languageName: unknown + linkType: soft + "@react-native-mac/virtualized-lists@npm:0.74.87": version: 0.74.87 resolution: "@react-native-mac/virtualized-lists@npm:0.74.87" @@ -8230,6 +9407,19 @@ __metadata: languageName: node linkType: hard +"babel-plugin-module-resolver@npm:^5.0.2": + version: 5.0.2 + resolution: "babel-plugin-module-resolver@npm:5.0.2" + dependencies: + find-babel-config: "npm:^2.1.1" + glob: "npm:^9.3.3" + pkg-up: "npm:^3.1.0" + reselect: "npm:^4.1.7" + resolve: "npm:^1.22.8" + checksum: 10/8084fa8a4cd96aaa861e5fe765a6cd03accef64d21d4108e314029bcd5f3a7fd96faf0c877c575a6a24d4fe0d87458d49748ca56faa4c77b2b812e4ed6023768 + languageName: node + linkType: hard + "babel-plugin-polyfill-corejs2@npm:^0.4.10": version: 0.4.10 resolution: "babel-plugin-polyfill-corejs2@npm:0.4.10" @@ -8661,6 +9851,20 @@ __metadata: languageName: node linkType: hard +"browserslist@npm:^4.20.4, browserslist@npm:^4.24.0, browserslist@npm:^4.24.3": + version: 4.24.4 + resolution: "browserslist@npm:4.24.4" + dependencies: + caniuse-lite: "npm:^1.0.30001688" + electron-to-chromium: "npm:^1.5.73" + node-releases: "npm:^2.0.19" + update-browserslist-db: "npm:^1.1.1" + bin: + browserslist: cli.js + checksum: 10/11fda105e803d891311a21a1f962d83599319165faf471c2d70e045dff82a12128f5b50b1fcba665a2352ad66147aaa248a9d2355a80aadc3f53375eb3de2e48 + languageName: node + linkType: hard + "browserslist@npm:^4.22.2, browserslist@npm:^4.22.3": version: 4.23.0 resolution: "browserslist@npm:4.23.0" @@ -9063,6 +10267,13 @@ __metadata: languageName: node linkType: hard +"caniuse-lite@npm:^1.0.30001688": + version: 1.0.30001690 + resolution: "caniuse-lite@npm:1.0.30001690" + checksum: 10/9fb4659eb09a298601b9593739072c481e2f5cc524bd0530e5e0f002e66246da5e866669854dfc0d53195ee36b201dab02f7933a7cdf60ccba7adb2d4a304caf + languageName: node + linkType: hard + "ccount@npm:^2.0.0": version: 2.0.1 resolution: "ccount@npm:2.0.1" @@ -10059,6 +11270,15 @@ __metadata: languageName: node linkType: hard +"core-js-compat@npm:^3.38.1": + version: 3.40.0 + resolution: "core-js-compat@npm:3.40.0" + dependencies: + browserslist: "npm:^4.24.3" + checksum: 10/3dd3d717b3d4ae0d9c2930d39c0f2a21ca6f195fcdd5711bda833557996c4d9f90277eab576423478e95689257e2de8d1a2623d6618084416bd224d10d5df9a4 + languageName: node + linkType: hard + "core-util-is@npm:~1.0.0": version: 1.0.3 resolution: "core-util-is@npm:1.0.3" @@ -10450,6 +11670,13 @@ __metadata: languageName: node linkType: hard +"dedent@npm:^0.7.0": + version: 0.7.0 + resolution: "dedent@npm:0.7.0" + checksum: 10/87de191050d9a40dd70cad01159a0bcf05ecb59750951242070b6abf9569088684880d00ba92a955b4058804f16eeaf91d604f283929b4f614d181cd7ae633d2 + languageName: node + linkType: hard + "dedent@npm:^1.0.0": version: 1.5.1 resolution: "dedent@npm:1.5.1" @@ -10568,7 +11795,7 @@ __metadata: languageName: node linkType: hard -"del@npm:^6.0.0": +"del@npm:^6.0.0, del@npm:^6.1.1": version: 6.1.1 resolution: "del@npm:6.1.1" dependencies: @@ -10958,6 +12185,13 @@ __metadata: languageName: node linkType: hard +"electron-to-chromium@npm:^1.5.73": + version: 1.5.79 + resolution: "electron-to-chromium@npm:1.5.79" + checksum: 10/c5b25ba04b4f4b46c4024b96e00e43adcd6c321b48c74c8d2660f69704901da5a6592009cbf96c36c89e3f6b53d7742e2b89514477fddbccf4e5c4caebed9d49 + languageName: node + linkType: hard + "emittery@npm:^0.13.1": version: 0.13.1 resolution: "emittery@npm:0.13.1" @@ -11317,7 +12551,7 @@ __metadata: languageName: node linkType: hard -"escalade@npm:^3.1.2": +"escalade@npm:^3.1.2, escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" checksum: 10/9d7169e3965b2f9ae46971afa392f6e5a25545ea30f2e2dd99c9b0a95a3f52b5653681a84f5b2911a413ddad2d7a93d3514165072f349b5ffc59c75a899970d6 @@ -11666,6 +12900,23 @@ __metadata: languageName: node linkType: hard +"execa@npm:^4.0.3": + version: 4.1.0 + resolution: "execa@npm:4.1.0" + dependencies: + cross-spawn: "npm:^7.0.0" + get-stream: "npm:^5.0.0" + human-signals: "npm:^1.1.1" + is-stream: "npm:^2.0.0" + merge-stream: "npm:^2.0.0" + npm-run-path: "npm:^4.0.0" + onetime: "npm:^5.1.0" + signal-exit: "npm:^3.0.2" + strip-final-newline: "npm:^2.0.0" + checksum: 10/ed58e41fe424797f3d837c8fb622548eeb72fa03324f2676af95f806568904eb55f196127a097f87d4517cab524c169ece13e6c9e201867de57b089584864b8f + languageName: node + linkType: hard + "execa@npm:^5.0.0, execa@npm:^5.1.1": version: 5.1.1 resolution: "execa@npm:5.1.1" @@ -12182,6 +13433,15 @@ __metadata: languageName: node linkType: hard +"find-babel-config@npm:^2.1.1": + version: 2.1.2 + resolution: "find-babel-config@npm:2.1.2" + dependencies: + json5: "npm:^2.2.3" + checksum: 10/f0fae1a9125a379cf660fc1b5ca7c1fc1edac5f47e521a89e4c2b92865c8e57101a9152ee503eef9f33e16f196182f2cff03d7768b7caf5eef81c80f1c124a2f + languageName: node + linkType: hard + "find-cache-dir@npm:^2.0.0": version: 2.1.0 resolution: "find-cache-dir@npm:2.1.0" @@ -12885,6 +14145,15 @@ __metadata: languageName: node linkType: hard +"get-stream@npm:^5.0.0": + version: 5.2.0 + resolution: "get-stream@npm:5.2.0" + dependencies: + pump: "npm:^3.0.0" + checksum: 10/13a73148dca795e41421013da6e3ebff8ccb7fba4d2f023fd0c6da2c166ec4e789bec9774a73a7b49c08daf2cae552f8a3e914042ac23b5f59dd278cc8f9cbfb + languageName: node + linkType: hard + "get-stream@npm:^6.0.0": version: 6.0.1 resolution: "get-stream@npm:6.0.1" @@ -13131,7 +14400,7 @@ __metadata: languageName: node linkType: hard -"glob@npm:^9.2.0": +"glob@npm:^9.2.0, glob@npm:^9.3.3": version: 9.3.5 resolution: "glob@npm:9.3.5" dependencies: @@ -13488,6 +14757,13 @@ __metadata: languageName: node linkType: hard +"hermes-estree@npm:0.23.1": + version: 0.23.1 + resolution: "hermes-estree@npm:0.23.1" + checksum: 10/b7ad78f53044d53ec1c77e93036c16e34f6f0985c895540876301e4791d4db08da828870977140f5cf1ae34532bbb9d9d013a0a1a4a5a0da05177225648d5295 + languageName: node + linkType: hard + "hermes-parser@npm:0.15.0": version: 0.15.0 resolution: "hermes-parser@npm:0.15.0" @@ -13506,6 +14782,15 @@ __metadata: languageName: node linkType: hard +"hermes-parser@npm:0.23.1": + version: 0.23.1 + resolution: "hermes-parser@npm:0.23.1" + dependencies: + hermes-estree: "npm:0.23.1" + checksum: 10/de88df4f23bd8dc2ffa89c8a317445320af8c7705a2aeeb05c4dd171f037a747982be153a0a237b1c9c7337b79bceaeb5052934cb8a25fe2e2473294a5343334 + languageName: node + linkType: hard + "hermes-profile-transformer@npm:^0.0.6": version: 0.0.6 resolution: "hermes-profile-transformer@npm:0.0.6" @@ -13640,6 +14925,13 @@ __metadata: languageName: node linkType: hard +"human-signals@npm:^1.1.1": + version: 1.1.1 + resolution: "human-signals@npm:1.1.1" + checksum: 10/6a58224dffcef5588910b1028bda8623c9a7053460a1fe3367e61921a6b5f6b93aba30f323868a958f968d7de3f5f78421f11d4d9f7e9563b1bd2b00ed9a4deb + languageName: node + linkType: hard + "human-signals@npm:^2.1.0": version: 2.1.0 resolution: "human-signals@npm:2.1.0" @@ -13935,6 +15227,16 @@ __metadata: languageName: node linkType: hard +"is-absolute@npm:^1.0.0": + version: 1.0.0 + resolution: "is-absolute@npm:1.0.0" + dependencies: + is-relative: "npm:^1.0.0" + is-windows: "npm:^1.0.1" + checksum: 10/9d16b2605eda3f3ce755410f1d423e327ad3a898bcb86c9354cf63970ed3f91ba85e9828aa56f5d6a952b9fae43d0477770f78d37409ae8ecc31e59ebc279b27 + languageName: node + linkType: hard + "is-arguments@npm:^1.0.4": version: 1.1.1 resolution: "is-arguments@npm:1.1.1" @@ -14145,6 +15447,26 @@ __metadata: languageName: node linkType: hard +"is-git-dirty@npm:^2.0.1": + version: 2.0.2 + resolution: "is-git-dirty@npm:2.0.2" + dependencies: + execa: "npm:^4.0.3" + is-git-repository: "npm:^2.0.0" + checksum: 10/13c8f58600e1ea0874703c1fa0ca87825119cf05347bb3b0bbbd331eec42b6a0e89519be4dcb173ac8eda84d1ade97fe187df8af10df599f1df8d0267680abdd + languageName: node + linkType: hard + +"is-git-repository@npm:^2.0.0": + version: 2.0.0 + resolution: "is-git-repository@npm:2.0.0" + dependencies: + execa: "npm:^4.0.3" + is-absolute: "npm:^1.0.0" + checksum: 10/9eba76437998b3239adc6e87ceb9b81f8ef00d6209f8700f2ba523e61359d5b068d11f8f94474bc90f92b39fd3c8261c4d60feb3cd62d18e1838480b0b135b88 + languageName: node + linkType: hard + "is-glob@npm:^2.0.0": version: 2.0.1 resolution: "is-glob@npm:2.0.1" @@ -14301,6 +15623,15 @@ __metadata: languageName: node linkType: hard +"is-relative@npm:^1.0.0": + version: 1.0.0 + resolution: "is-relative@npm:1.0.0" + dependencies: + is-unc-path: "npm:^1.0.0" + checksum: 10/3271a0df109302ef5e14a29dcd5d23d9788e15ade91a40b942b035827ffbb59f7ce9ff82d036ea798541a52913cbf9d2d0b66456340887b51f3542d57b5a4c05 + languageName: node + linkType: hard + "is-set@npm:^2.0.1": version: 2.0.2 resolution: "is-set@npm:2.0.2" @@ -14406,6 +15737,15 @@ __metadata: languageName: node linkType: hard +"is-unc-path@npm:^1.0.0": + version: 1.0.0 + resolution: "is-unc-path@npm:1.0.0" + dependencies: + unc-path-regex: "npm:^0.1.2" + checksum: 10/e8abfde203f7409f5b03a5f1f8636e3a41e78b983702ef49d9343eb608cdfe691429398e8815157519b987b739bcfbc73ae7cf4c8582b0ab66add5171088eab6 + languageName: node + linkType: hard + "is-unicode-supported@npm:^0.1.0": version: 0.1.0 resolution: "is-unicode-supported@npm:0.1.0" @@ -14455,7 +15795,7 @@ __metadata: languageName: node linkType: hard -"is-windows@npm:^1.0.2": +"is-windows@npm:^1.0.1, is-windows@npm:^1.0.2": version: 1.0.2 resolution: "is-windows@npm:1.0.2" checksum: 10/438b7e52656fe3b9b293b180defb4e448088e7023a523ec21a91a80b9ff8cdb3377ddb5b6e60f7c7de4fa8b63ab56e121b6705fe081b3cf1b828b0a380009ad7 @@ -15361,6 +16701,15 @@ __metadata: languageName: node linkType: hard +"jsesc@npm:^3.0.2": + version: 3.1.0 + resolution: "jsesc@npm:3.1.0" + bin: + jsesc: bin/jsesc + checksum: 10/20bd37a142eca5d1794f354db8f1c9aeb54d85e1f5c247b371de05d23a9751ecd7bd3a9c4fc5298ea6fa09a100dafb4190fa5c98c6610b75952c3487f3ce7967 + languageName: node + linkType: hard + "jsesc@npm:~0.5.0": version: 0.5.0 resolution: "jsesc@npm:0.5.0" @@ -15370,6 +16719,15 @@ __metadata: languageName: node linkType: hard +"jsesc@npm:~3.0.2": + version: 3.0.2 + resolution: "jsesc@npm:3.0.2" + bin: + jsesc: bin/jsesc + checksum: 10/8e5a7de6b70a8bd71f9cb0b5a7ade6a73ae6ab55e697c74cc997cede97417a3a65ed86c36f7dd6125fe49766e8386c845023d9e213916ca92c9dfdd56e2babf3 + languageName: node + linkType: hard + "json-bigint@npm:^1.0.0": version: 1.0.0 resolution: "json-bigint@npm:1.0.0" @@ -15500,7 +16858,7 @@ __metadata: languageName: node linkType: hard -"json5@npm:^2.2.2, json5@npm:^2.2.3": +"json5@npm:^2.2.1, json5@npm:^2.2.2, json5@npm:^2.2.3": version: 2.2.3 resolution: "json5@npm:2.2.3" bin: @@ -15706,7 +17064,7 @@ __metadata: languageName: node linkType: hard -"kleur@npm:^4.0.3": +"kleur@npm:^4.0.3, kleur@npm:^4.1.4": version: 4.1.5 resolution: "kleur@npm:4.1.5" checksum: 10/44d84cc4eedd4311099402ef6d4acd9b2d16e08e499d6ef3bb92389bd4692d7ef09e35248c26e27f98acac532122acb12a1bfee645994ae3af4f0a37996da7df @@ -16647,6 +18005,18 @@ __metadata: languageName: node linkType: hard +"metro-babel-transformer@npm:0.80.12": + version: 0.80.12 + resolution: "metro-babel-transformer@npm:0.80.12" + dependencies: + "@babel/core": "npm:^7.20.0" + flow-enums-runtime: "npm:^0.0.6" + hermes-parser: "npm:0.23.1" + nullthrows: "npm:^1.1.1" + checksum: 10/3912367e269df3ac697d67541d56fed86ab6fc40ce1aa107b8f332402c7a84a3d0991e536897d4877bab2b1986dd21ec7fad0c76704a27c1c2edce0bcf9037a9 + languageName: node + linkType: hard + "metro-babel-transformer@npm:0.80.6": version: 0.80.6 resolution: "metro-babel-transformer@npm:0.80.6" @@ -16658,6 +18028,15 @@ __metadata: languageName: node linkType: hard +"metro-cache-key@npm:0.80.12": + version: 0.80.12 + resolution: "metro-cache-key@npm:0.80.12" + dependencies: + flow-enums-runtime: "npm:^0.0.6" + checksum: 10/7a06601180604361339d19eb833d61b79cc188a4e6ebe73188cc10fbf3a33e711d74c81d1d19a14b6581bd9dfeebe1b253684360682d033ab55909c9995b6a18 + languageName: node + linkType: hard + "metro-cache-key@npm:0.80.6": version: 0.80.6 resolution: "metro-cache-key@npm:0.80.6" @@ -16665,6 +18044,17 @@ __metadata: languageName: node linkType: hard +"metro-cache@npm:0.80.12": + version: 0.80.12 + resolution: "metro-cache@npm:0.80.12" + dependencies: + exponential-backoff: "npm:^3.1.1" + flow-enums-runtime: "npm:^0.0.6" + metro-core: "npm:0.80.12" + checksum: 10/914b599ad4f8a2538e6f4788b3da722aa855688affef3002fe374a0a1cb7dd36ad9224d1ef83f7c17610ebb290cea3cb545bfd67100a216b7bbb3f26f8982c93 + languageName: node + linkType: hard + "metro-cache@npm:0.80.6": version: 0.80.6 resolution: "metro-cache@npm:0.80.6" @@ -16675,6 +18065,22 @@ __metadata: languageName: node linkType: hard +"metro-config@npm:0.80.12, metro-config@npm:^0.80.9": + version: 0.80.12 + resolution: "metro-config@npm:0.80.12" + dependencies: + connect: "npm:^3.6.5" + cosmiconfig: "npm:^5.0.5" + flow-enums-runtime: "npm:^0.0.6" + jest-validate: "npm:^29.6.3" + metro: "npm:0.80.12" + metro-cache: "npm:0.80.12" + metro-core: "npm:0.80.12" + metro-runtime: "npm:0.80.12" + checksum: 10/2d11745d32e8992b78159c275dc54b08bf258871f274634f9824540f1ec80a9b1a9d7eb5493b52078a5a68cccd4fd688cd846dd0802aea2f065b5588e98eb146 + languageName: node + linkType: hard + "metro-config@npm:0.80.6, metro-config@npm:^0.80.3": version: 0.80.6 resolution: "metro-config@npm:0.80.6" @@ -16690,6 +18096,17 @@ __metadata: languageName: node linkType: hard +"metro-core@npm:0.80.12": + version: 0.80.12 + resolution: "metro-core@npm:0.80.12" + dependencies: + flow-enums-runtime: "npm:^0.0.6" + lodash.throttle: "npm:^4.1.1" + metro-resolver: "npm:0.80.12" + checksum: 10/d29ab20df4d19c1d8c5178f7b182e050c659f022ab2adc669504c7ef7fd5d76cdde02936d1599e6d6137e353cbf4fef6b3cfa6aaf217bca954fc23cbf1b61f18 + languageName: node + linkType: hard + "metro-core@npm:0.80.6, metro-core@npm:^0.80.3": version: 0.80.6 resolution: "metro-core@npm:0.80.6" @@ -16700,6 +18117,29 @@ __metadata: languageName: node linkType: hard +"metro-file-map@npm:0.80.12": + version: 0.80.12 + resolution: "metro-file-map@npm:0.80.12" + dependencies: + anymatch: "npm:^3.0.3" + debug: "npm:^2.2.0" + fb-watchman: "npm:^2.0.0" + flow-enums-runtime: "npm:^0.0.6" + fsevents: "npm:^2.3.2" + graceful-fs: "npm:^4.2.4" + invariant: "npm:^2.2.4" + jest-worker: "npm:^29.6.3" + micromatch: "npm:^4.0.4" + node-abort-controller: "npm:^3.1.1" + nullthrows: "npm:^1.1.1" + walker: "npm:^1.0.7" + dependenciesMeta: + fsevents: + optional: true + checksum: 10/a0c06da7c89bfbbe17adadb46470274e2e1ffee43126a08f665db230d7831c6195410ea7165f94182e18a27359e140fc8272d2271c04bf0286a1ea95106a3758 + languageName: node + linkType: hard + "metro-file-map@npm:0.80.6": version: 0.80.6 resolution: "metro-file-map@npm:0.80.6" @@ -16722,6 +18162,16 @@ __metadata: languageName: node linkType: hard +"metro-minify-terser@npm:0.80.12": + version: 0.80.12 + resolution: "metro-minify-terser@npm:0.80.12" + dependencies: + flow-enums-runtime: "npm:^0.0.6" + terser: "npm:^5.15.0" + checksum: 10/ff527b3f04c5814db139e55ceb7689aaaf0af5c7fbb0eb5d4a6f22044932dfb10bd385d388fa7b352acd03a2d078edaf43a6b5cd11cbc87a7c5502a34fc12735 + languageName: node + linkType: hard + "metro-minify-terser@npm:0.80.6": version: 0.80.6 resolution: "metro-minify-terser@npm:0.80.6" @@ -16731,6 +18181,15 @@ __metadata: languageName: node linkType: hard +"metro-resolver@npm:0.80.12": + version: 0.80.12 + resolution: "metro-resolver@npm:0.80.12" + dependencies: + flow-enums-runtime: "npm:^0.0.6" + checksum: 10/e8609f1b93f1bbe7a9f97dd3fa2a6669c0a51f8360fea9a73e528fc25615f7ef61bd8ad9feb9a52fdbf4405a4065195f053183626f3ab56f54225ebefee574ac + languageName: node + linkType: hard + "metro-resolver@npm:0.80.6": version: 0.80.6 resolution: "metro-resolver@npm:0.80.6" @@ -16738,6 +18197,16 @@ __metadata: languageName: node linkType: hard +"metro-runtime@npm:0.80.12": + version: 0.80.12 + resolution: "metro-runtime@npm:0.80.12" + dependencies: + "@babel/runtime": "npm:^7.25.0" + flow-enums-runtime: "npm:^0.0.6" + checksum: 10/8a09e7001bd54331c50145d02e6a2b67589da4dd0da3ff1cdb83e6ce161b9079e2a52a4722db8f222b46f666e3dbfe1fc59ee7c277325763a162e3d27ba81d38 + languageName: node + linkType: hard + "metro-runtime@npm:0.80.6, metro-runtime@npm:^0.80.3": version: 0.80.6 resolution: "metro-runtime@npm:0.80.6" @@ -16747,6 +18216,23 @@ __metadata: languageName: node linkType: hard +"metro-source-map@npm:0.80.12": + version: 0.80.12 + resolution: "metro-source-map@npm:0.80.12" + dependencies: + "@babel/traverse": "npm:^7.20.0" + "@babel/types": "npm:^7.20.0" + flow-enums-runtime: "npm:^0.0.6" + invariant: "npm:^2.2.4" + metro-symbolicate: "npm:0.80.12" + nullthrows: "npm:^1.1.1" + ob1: "npm:0.80.12" + source-map: "npm:^0.5.6" + vlq: "npm:^1.0.0" + checksum: 10/ad6e0cf7f4d2727ecb45a082b4ab92915df8c574de0a905023a53e501a32f619aaeb0f94645aca048ae322176600867f5f21119349261427a2de27cb27ef0ef1 + languageName: node + linkType: hard + "metro-source-map@npm:0.80.6, metro-source-map@npm:^0.80.3": version: 0.80.6 resolution: "metro-source-map@npm:0.80.6" @@ -16763,6 +18249,23 @@ __metadata: languageName: node linkType: hard +"metro-symbolicate@npm:0.80.12": + version: 0.80.12 + resolution: "metro-symbolicate@npm:0.80.12" + dependencies: + flow-enums-runtime: "npm:^0.0.6" + invariant: "npm:^2.2.4" + metro-source-map: "npm:0.80.12" + nullthrows: "npm:^1.1.1" + source-map: "npm:^0.5.6" + through2: "npm:^2.0.1" + vlq: "npm:^1.0.0" + bin: + metro-symbolicate: src/index.js + checksum: 10/0c1dd055691bd670fb73a146f7e2fa3235a5afa3135996e681384676a439e10c9efe398d5b07d588907adbfbf65228829ceb57dab2c19a61eb79dde60bb7dc31 + languageName: node + linkType: hard + "metro-symbolicate@npm:0.80.6": version: 0.80.6 resolution: "metro-symbolicate@npm:0.80.6" @@ -16779,6 +18282,20 @@ __metadata: languageName: node linkType: hard +"metro-transform-plugins@npm:0.80.12": + version: 0.80.12 + resolution: "metro-transform-plugins@npm:0.80.12" + dependencies: + "@babel/core": "npm:^7.20.0" + "@babel/generator": "npm:^7.20.0" + "@babel/template": "npm:^7.0.0" + "@babel/traverse": "npm:^7.20.0" + flow-enums-runtime: "npm:^0.0.6" + nullthrows: "npm:^1.1.1" + checksum: 10/801510bde9cb70ba47572c3d5d42f98fc2ee173a48ca39893cbdeb689de54d5a1fea5383ba4fe388f334af06ecb651e5634b5e7611223e217668c98f8666c913 + languageName: node + linkType: hard + "metro-transform-plugins@npm:0.80.6": version: 0.80.6 resolution: "metro-transform-plugins@npm:0.80.6" @@ -16792,6 +18309,27 @@ __metadata: languageName: node linkType: hard +"metro-transform-worker@npm:0.80.12": + version: 0.80.12 + resolution: "metro-transform-worker@npm:0.80.12" + dependencies: + "@babel/core": "npm:^7.20.0" + "@babel/generator": "npm:^7.20.0" + "@babel/parser": "npm:^7.20.0" + "@babel/types": "npm:^7.20.0" + flow-enums-runtime: "npm:^0.0.6" + metro: "npm:0.80.12" + metro-babel-transformer: "npm:0.80.12" + metro-cache: "npm:0.80.12" + metro-cache-key: "npm:0.80.12" + metro-minify-terser: "npm:0.80.12" + metro-source-map: "npm:0.80.12" + metro-transform-plugins: "npm:0.80.12" + nullthrows: "npm:^1.1.1" + checksum: 10/a0802ebbc308a3bd6c81f9a1c640c62a8918f4d4e73da2184d24be10014ce6bc1cef53c0ef6a59568ecc0d0d44d43e38ec595d4abda043f93072613261074371 + languageName: node + linkType: hard + "metro-transform-worker@npm:0.80.6": version: 0.80.6 resolution: "metro-transform-worker@npm:0.80.6" @@ -16812,6 +18350,58 @@ __metadata: languageName: node linkType: hard +"metro@npm:0.80.12": + version: 0.80.12 + resolution: "metro@npm:0.80.12" + dependencies: + "@babel/code-frame": "npm:^7.0.0" + "@babel/core": "npm:^7.20.0" + "@babel/generator": "npm:^7.20.0" + "@babel/parser": "npm:^7.20.0" + "@babel/template": "npm:^7.0.0" + "@babel/traverse": "npm:^7.20.0" + "@babel/types": "npm:^7.20.0" + accepts: "npm:^1.3.7" + chalk: "npm:^4.0.0" + ci-info: "npm:^2.0.0" + connect: "npm:^3.6.5" + debug: "npm:^2.2.0" + denodeify: "npm:^1.2.1" + error-stack-parser: "npm:^2.0.6" + flow-enums-runtime: "npm:^0.0.6" + graceful-fs: "npm:^4.2.4" + hermes-parser: "npm:0.23.1" + image-size: "npm:^1.0.2" + invariant: "npm:^2.2.4" + jest-worker: "npm:^29.6.3" + jsc-safe-url: "npm:^0.2.2" + lodash.throttle: "npm:^4.1.1" + metro-babel-transformer: "npm:0.80.12" + metro-cache: "npm:0.80.12" + metro-cache-key: "npm:0.80.12" + metro-config: "npm:0.80.12" + metro-core: "npm:0.80.12" + metro-file-map: "npm:0.80.12" + metro-resolver: "npm:0.80.12" + metro-runtime: "npm:0.80.12" + metro-source-map: "npm:0.80.12" + metro-symbolicate: "npm:0.80.12" + metro-transform-plugins: "npm:0.80.12" + metro-transform-worker: "npm:0.80.12" + mime-types: "npm:^2.1.27" + nullthrows: "npm:^1.1.1" + serialize-error: "npm:^2.1.0" + source-map: "npm:^0.5.6" + strip-ansi: "npm:^6.0.0" + throat: "npm:^5.0.0" + ws: "npm:^7.5.10" + yargs: "npm:^17.6.2" + bin: + metro: src/cli.js + checksum: 10/b44280b16d3671be97d11327a9fe0bb2db014a6dcedaab9e88d58696a8133246ef7f8290e9fac0841534872132bbc0d7132745b02f3584339c0999d9e7a58c10 + languageName: node + linkType: hard + "metro@npm:0.80.6, metro@npm:^0.80.3": version: 0.80.6 resolution: "metro@npm:0.80.6" @@ -17950,6 +19540,13 @@ __metadata: languageName: node linkType: hard +"node-releases@npm:^2.0.19": + version: 2.0.19 + resolution: "node-releases@npm:2.0.19" + checksum: 10/c2b33b4f0c40445aee56141f13ca692fa6805db88510e5bbb3baadb2da13e1293b738e638e15e4a8eb668bb9e97debb08e7a35409b477b5cc18f171d35a83045 + languageName: node + linkType: hard + "node-stream-zip@npm:^1.9.1": version: 1.15.0 resolution: "node-stream-zip@npm:1.15.0" @@ -18171,7 +19768,7 @@ __metadata: languageName: node linkType: hard -"npm-run-path@npm:^4.0.1": +"npm-run-path@npm:^4.0.0, npm-run-path@npm:^4.0.1": version: 4.0.1 resolution: "npm-run-path@npm:4.0.1" dependencies: @@ -18361,6 +19958,15 @@ __metadata: languageName: node linkType: hard +"ob1@npm:0.80.12": + version: 0.80.12 + resolution: "ob1@npm:0.80.12" + dependencies: + flow-enums-runtime: "npm:^0.0.6" + checksum: 10/c78af51d6ecf47ba5198bc7eb27d0456a287589533f1445e6d595e2d067f6f8038da02a98e5faa4a6c3d0c04f77c570bc9b29c652fec55518884c40c73212f17 + languageName: node + linkType: hard + "ob1@npm:0.80.6": version: 0.80.6 resolution: "ob1@npm:0.80.6" @@ -19270,6 +20876,13 @@ __metadata: languageName: node linkType: hard +"picocolors@npm:^1.1.1": + version: 1.1.1 + resolution: "picocolors@npm:1.1.1" + checksum: 10/e1cf46bf84886c79055fdfa9dcb3e4711ad259949e3565154b004b260cd356c5d54b31a1437ce9782624bf766272fe6b0154f5f0c744fb7af5d454d2b60db045 + languageName: node + linkType: hard + "picomatch@npm:^2.0.4, picomatch@npm:^2.2.1, picomatch@npm:^2.2.3, picomatch@npm:^2.3.1": version: 2.3.1 resolution: "picomatch@npm:2.3.1" @@ -19346,6 +20959,15 @@ __metadata: languageName: node linkType: hard +"pkg-up@npm:^3.1.0": + version: 3.1.0 + resolution: "pkg-up@npm:3.1.0" + dependencies: + find-up: "npm:^3.0.0" + checksum: 10/5bac346b7c7c903613c057ae3ab722f320716199d753f4a7d053d38f2b5955460f3e6ab73b4762c62fd3e947f58e04f1343e92089e7bb6091c90877406fcd8c8 + languageName: node + linkType: hard + "plist@npm:^3.0.5, plist@npm:^3.1.0": version: 3.1.0 resolution: "plist@npm:3.1.0" @@ -20003,6 +21625,38 @@ __metadata: languageName: node linkType: hard +"react-native-builder-bob@npm:^0.35.2": + version: 0.35.2 + resolution: "react-native-builder-bob@npm:0.35.2" + dependencies: + "@babel/core": "npm:^7.25.2" + "@babel/plugin-transform-strict-mode": "npm:^7.24.7" + "@babel/preset-env": "npm:^7.25.2" + "@babel/preset-flow": "npm:^7.24.7" + "@babel/preset-react": "npm:^7.24.7" + "@babel/preset-typescript": "npm:^7.24.7" + babel-plugin-module-resolver: "npm:^5.0.2" + browserslist: "npm:^4.20.4" + cosmiconfig: "npm:^9.0.0" + cross-spawn: "npm:^7.0.3" + dedent: "npm:^0.7.0" + del: "npm:^6.1.1" + escape-string-regexp: "npm:^4.0.0" + fs-extra: "npm:^10.1.0" + glob: "npm:^8.0.3" + is-git-dirty: "npm:^2.0.1" + json5: "npm:^2.2.1" + kleur: "npm:^4.1.4" + metro-config: "npm:^0.80.9" + prompts: "npm:^2.4.2" + which: "npm:^2.0.2" + yargs: "npm:^17.5.1" + bin: + bob: bin/bob + checksum: 10/cffafaa3cc21dc716711dd282f1c163c82d5fded75c9a02034b34f54bb678cf0f255514bb773ad0fff39d939c5431f49aa663a4b14febd678d1f596e7774c337 + languageName: node + linkType: hard + "react-native-device-info@npm:^13.0.0": version: 13.0.0 resolution: "react-native-device-info@npm:13.0.0" @@ -20036,6 +21690,7 @@ __metadata: "@react-native-firebase/perf": "npm:21.6.2" "@react-native-firebase/remote-config": "npm:21.6.2" "@react-native-firebase/storage": "npm:21.6.2" + "@react-native-firebase/vertexai": "npm:0.0.1" "@react-native/babel-preset": "npm:^0.74.87" "@react-native/metro-config": "npm:^0.74.87" axios: "npm:^1.7.7" @@ -20516,6 +22171,15 @@ __metadata: languageName: node linkType: hard +"regenerate-unicode-properties@npm:^10.2.0": + version: 10.2.0 + resolution: "regenerate-unicode-properties@npm:10.2.0" + dependencies: + regenerate: "npm:^1.4.2" + checksum: 10/9150eae6fe04a8c4f2ff06077396a86a98e224c8afad8344b1b656448e89e84edcd527e4b03aa5476774129eb6ad328ed684f9c1459794a935ec0cc17ce14329 + languageName: node + linkType: hard + "regenerate@npm:^1.4.2": version: 1.4.2 resolution: "regenerate@npm:1.4.2" @@ -20572,6 +22236,20 @@ __metadata: languageName: node linkType: hard +"regexpu-core@npm:^6.2.0": + version: 6.2.0 + resolution: "regexpu-core@npm:6.2.0" + dependencies: + regenerate: "npm:^1.4.2" + regenerate-unicode-properties: "npm:^10.2.0" + regjsgen: "npm:^0.8.0" + regjsparser: "npm:^0.12.0" + unicode-match-property-ecmascript: "npm:^2.0.0" + unicode-match-property-value-ecmascript: "npm:^2.1.0" + checksum: 10/4d054ffcd98ca4f6ca7bf0df6598ed5e4a124264602553308add41d4fa714a0c5bcfb5bc868ac91f7060a9c09889cc21d3180a3a14c5f9c5838442806129ced3 + languageName: node + linkType: hard + "registry-auth-token@npm:^5.0.1": version: 5.0.2 resolution: "registry-auth-token@npm:5.0.2" @@ -20590,6 +22268,24 @@ __metadata: languageName: node linkType: hard +"regjsgen@npm:^0.8.0": + version: 0.8.0 + resolution: "regjsgen@npm:0.8.0" + checksum: 10/b930f03347e4123c917d7b40436b4f87f625b8dd3e705b447ddd44804e4616c3addb7453f0902d6e914ab0446c30e816e445089bb641a4714237fe8141a0ef9d + languageName: node + linkType: hard + +"regjsparser@npm:^0.12.0": + version: 0.12.0 + resolution: "regjsparser@npm:0.12.0" + dependencies: + jsesc: "npm:~3.0.2" + bin: + regjsparser: bin/parser + checksum: 10/c2d6506b3308679de5223a8916984198e0493649a67b477c66bdb875357e3785abbf3bedf7c5c2cf8967d3b3a7bdf08b7cbd39e65a70f9e1ffad584aecf5f06a + languageName: node + linkType: hard + "regjsparser@npm:^0.9.1": version: 0.9.1 resolution: "regjsparser@npm:0.9.1" @@ -20721,6 +22417,13 @@ __metadata: languageName: node linkType: hard +"reselect@npm:^4.1.7": + version: 4.1.8 + resolution: "reselect@npm:4.1.8" + checksum: 10/199984d9872f71cd207f4aa6e6fd2bd48d95154f7aa9b3aee3398335f39f5491059e732f28c12e9031d5d434adab2c458dc8af5afb6564d0ad37e1644445e09c + languageName: node + linkType: hard + "resolve-cwd@npm:^3.0.0": version: 3.0.0 resolution: "resolve-cwd@npm:3.0.0" @@ -23249,6 +24952,13 @@ __metadata: languageName: node linkType: hard +"unc-path-regex@npm:^0.1.2": + version: 0.1.2 + resolution: "unc-path-regex@npm:0.1.2" + checksum: 10/a05fa2006bf4606051c10fc7968f08ce7b28fa646befafa282813aeb1ac1a56f65cb1b577ca7851af2726198d59475bb49b11776036257b843eaacee2860a4ec + languageName: node + linkType: hard + "undici-types@npm:~5.26.4": version: 5.26.5 resolution: "undici-types@npm:5.26.5" @@ -23579,6 +25289,20 @@ __metadata: languageName: node linkType: hard +"update-browserslist-db@npm:^1.1.1": + version: 1.1.2 + resolution: "update-browserslist-db@npm:1.1.2" + dependencies: + escalade: "npm:^3.2.0" + picocolors: "npm:^1.1.1" + peerDependencies: + browserslist: ">= 4.21.0" + bin: + update-browserslist-db: cli.js + checksum: 10/e7bf8221dfb21eba4a770cd803df94625bb04f65a706aa94c567de9600fe4eb6133fda016ec471dad43b9e7959c1bffb6580b5e20a87808d2e8a13e3892699a9 + languageName: node + linkType: hard + "update-notifier-cjs@npm:^5.1.6": version: 5.1.6 resolution: "update-notifier-cjs@npm:5.1.6" @@ -24048,7 +25772,7 @@ __metadata: languageName: node linkType: hard -"which@npm:^2.0.1": +"which@npm:^2.0.1, which@npm:^2.0.2": version: 2.0.2 resolution: "which@npm:2.0.2" dependencies: @@ -24504,7 +26228,7 @@ __metadata: languageName: node linkType: hard -"yargs@npm:17.7.2, yargs@npm:^17.0.0, yargs@npm:^17.3.1, yargs@npm:^17.6.2, yargs@npm:^17.7.2": +"yargs@npm:17.7.2, yargs@npm:^17.0.0, yargs@npm:^17.3.1, yargs@npm:^17.5.1, yargs@npm:^17.6.2, yargs@npm:^17.7.2": version: 17.7.2 resolution: "yargs@npm:17.7.2" dependencies: From 3bfdcdbb02675dd1876886eeeb6bc4ef187713e5 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 8 Jan 2025 16:22:30 +0000 Subject: [PATCH 007/115] package.json updates --- packages/vertexai/package.json | 3 ++- tests/package.json | 1 + yarn.lock | 21 +++++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/vertexai/package.json b/packages/vertexai/package.json index 5ef0749728..8c0df7b430 100644 --- a/packages/vertexai/package.json +++ b/packages/vertexai/package.json @@ -30,7 +30,8 @@ "access": "public" }, "devDependencies": { - "react-native-builder-bob": "^0.35.2" + "react-native-builder-bob": "^0.35.2", + "typescript": "^5.7.2" }, "source": "./lib/index.ts", "module": "./dist/module/index.js", diff --git a/tests/package.json b/tests/package.json index 0b2819c594..9a2ee5fd19 100644 --- a/tests/package.json +++ b/tests/package.json @@ -27,6 +27,7 @@ "@react-native-firebase/perf": "21.6.2", "@react-native-firebase/remote-config": "21.6.2", "@react-native-firebase/storage": "21.6.2", + "@react-native-firebase/vertexai": "0.0.1", "postinstall-postinstall": "2.1.0", "react": "18.3.1", "react-native": "0.74.5", diff --git a/yarn.lock b/yarn.lock index 675c0562bf..ddf7305504 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7574,6 +7574,7 @@ __metadata: resolution: "@react-native-firebase/vertexai@workspace:packages/vertexai" dependencies: react-native-builder-bob: "npm:^0.35.2" + typescript: "npm:^5.7.2" peerDependencies: "@react-native-firebase/app": 21.6.2 languageName: unknown @@ -24890,6 +24891,16 @@ __metadata: languageName: node linkType: hard +"typescript@npm:^5.7.2": + version: 5.7.2 + resolution: "typescript@npm:5.7.2" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/4caa3904df69db9d4a8bedc31bafc1e19ffb7b24fbde2997a1633ae1398d0de5bdbf8daf602ccf3b23faddf1aeeb9b795223a2ed9c9a4fdcaf07bfde114a401a + languageName: node + linkType: hard + "typescript@patch:typescript@npm%3A>=3 < 6#optional!builtin": version: 5.3.3 resolution: "typescript@patch:typescript@npm%3A5.3.3#optional!builtin::version=5.3.3&hash=e012d7" @@ -24910,6 +24921,16 @@ __metadata: languageName: node linkType: hard +"typescript@patch:typescript@npm%3A^5.7.2#optional!builtin": + version: 5.7.2 + resolution: "typescript@patch:typescript@npm%3A5.7.2#optional!builtin::version=5.7.2&hash=cef18b" + bin: + tsc: bin/tsc + tsserver: bin/tsserver + checksum: 10/ff27fc124bceb8969be722baa38af945b2505767cf794de3e2715e58f61b43780284060287d651fcbbdfb6f917f4653b20f4751991f17e0706db389b9bb3f75d + languageName: node + linkType: hard + "typical@npm:^2.6.1": version: 2.6.1 resolution: "typical@npm:2.6.1" From 91e2091c1c712f694a4ac2953e46a01b65bdbde2 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 9 Jan 2025 11:10:53 +0000 Subject: [PATCH 008/115] fix: format & fix TS type warnings --- packages/vertexai/lib/methods/chat-session.ts | 13 ++-- .../vertexai/lib/requests/request-helpers.ts | 2 + packages/vertexai/lib/requests/request.ts | 49 ++++++--------- .../vertexai/lib/requests/response-helpers.ts | 62 ++++++++----------- .../vertexai/lib/requests/schema-builder.ts | 45 ++++++-------- 5 files changed, 71 insertions(+), 100 deletions(-) diff --git a/packages/vertexai/lib/methods/chat-session.ts b/packages/vertexai/lib/methods/chat-session.ts index d22393d5b7..75b15948b9 100644 --- a/packages/vertexai/lib/methods/chat-session.ts +++ b/packages/vertexai/lib/methods/chat-session.ts @@ -23,6 +23,7 @@ import { Part, RequestOptions, StartChatParams, + EnhancedGenerateContentResponse, } from '../types'; import { formatNewContent } from '../requests/request-helpers'; import { formatBlockErrorMessage } from '../requests/response-helpers'; @@ -91,13 +92,13 @@ export class ChatSession { .then(() => generateContent(this._apiSettings, this.model, generateContentRequest, this.requestOptions), ) - .then(result => { + .then((result: GenerateContentResult) => { if (result.response.candidates && result.response.candidates.length > 0) { this._history.push(newContent); const responseContent: Content = { - parts: result.response.candidates?.[0].content.parts || [], + parts: result.response.candidates?.[0]?.content.parts || [], // Response seems to come back without a role set. - role: result.response.candidates?.[0].content.role || 'model', + role: result.response.candidates?.[0]?.content.role || 'model', }; this._history.push(responseContent); } else { @@ -149,15 +150,15 @@ export class ChatSession { throw new Error(SILENT_ERROR); }) .then(streamResult => streamResult.response) - .then(response => { + .then((response: EnhancedGenerateContentResponse) => { if (response.candidates && response.candidates.length > 0) { this._history.push(newContent); - const responseContent = { ...response.candidates[0].content }; + const responseContent = { ...response.candidates[0]?.content }; // Response seems to come back without a role set. if (!responseContent.role) { responseContent.role = 'model'; } - this._history.push(responseContent); + this._history.push(responseContent as Content); } else { const blockErrorMessage = formatBlockErrorMessage(response); if (blockErrorMessage) { diff --git a/packages/vertexai/lib/requests/request-helpers.ts b/packages/vertexai/lib/requests/request-helpers.ts index 44405cb6f4..77809f1edd 100644 --- a/packages/vertexai/lib/requests/request-helpers.ts +++ b/packages/vertexai/lib/requests/request-helpers.ts @@ -33,6 +33,8 @@ export function formatSystemInstruction(input?: string | Part | Content): Conten return input as Content; } } + + return undefined; } export function formatNewContent(request: string | Array): Content { diff --git a/packages/vertexai/lib/requests/request.ts b/packages/vertexai/lib/requests/request.ts index f81b40635e..058dcb8a3c 100644 --- a/packages/vertexai/lib/requests/request.ts +++ b/packages/vertexai/lib/requests/request.ts @@ -23,14 +23,14 @@ import { DEFAULT_BASE_URL, DEFAULT_FETCH_TIMEOUT_MS, LANGUAGE_TAG, - PACKAGE_VERSION + PACKAGE_VERSION, } from '../constants'; import { logger } from '../logger'; export enum Task { GENERATE_CONTENT = 'generateContent', STREAM_GENERATE_CONTENT = 'streamGenerateContent', - COUNT_TOKENS = 'countTokens' + COUNT_TOKENS = 'countTokens', } export class RequestUrl { @@ -39,7 +39,7 @@ export class RequestUrl { public task: Task, public apiSettings: ApiSettings, public stream: boolean, - public requestOptions?: RequestOptions + public requestOptions?: RequestOptions, ) {} toString(): string { // TODO: allow user-set option if that feature becomes available @@ -88,9 +88,7 @@ export async function getHeaders(url: RequestUrl): Promise { if (appCheckToken) { headers.append('X-Firebase-AppCheck', appCheckToken.token); if (appCheckToken.error) { - logger.warn( - `Unable to obtain a valid App Check token: ${appCheckToken.error.message}` - ); + logger.warn(`Unable to obtain a valid App Check token: ${appCheckToken.error.message}`); } } } @@ -111,7 +109,7 @@ export async function constructRequest( apiSettings: ApiSettings, stream: boolean, body: string, - requestOptions?: RequestOptions + requestOptions?: RequestOptions, ): Promise<{ url: string; fetchOptions: RequestInit }> { const url = new RequestUrl(model, task, apiSettings, stream, requestOptions); return { @@ -119,8 +117,8 @@ export async function constructRequest( fetchOptions: { method: 'POST', headers: await getHeaders(url), - body - } + body, + }, }; } @@ -130,20 +128,13 @@ export async function makeRequest( apiSettings: ApiSettings, stream: boolean, body: string, - requestOptions?: RequestOptions + requestOptions?: RequestOptions, ): Promise { const url = new RequestUrl(model, task, apiSettings, stream, requestOptions); let response; let fetchTimeoutId: string | number | NodeJS.Timeout | undefined; try { - const request = await constructRequest( - model, - task, - apiSettings, - stream, - body, - requestOptions - ); + const request = await constructRequest(model, task, apiSettings, stream, body, requestOptions); // Timeout is 180s by default const timeoutMillis = requestOptions?.timeout != null && requestOptions.timeout >= 0 @@ -169,15 +160,11 @@ export async function makeRequest( } if ( response.status === 403 && - errorDetails.some( - (detail: ErrorDetails) => detail.reason === 'SERVICE_DISABLED' - ) && + errorDetails.some((detail: ErrorDetails) => detail.reason === 'SERVICE_DISABLED') && errorDetails.some((detail: ErrorDetails) => - ( - detail.links as Array> - )?.[0]?.description.includes( - 'Google developers console API activation' - ) + (detail.links as Array>)?.[0]?.description?.includes( + 'Google developers console API activation', + ), ) ) { throw new VertexAIError( @@ -192,8 +179,8 @@ export async function makeRequest( { status: response.status, statusText: response.statusText, - errorDetails - } + errorDetails, + }, ); } throw new VertexAIError( @@ -202,8 +189,8 @@ export async function makeRequest( { status: response.status, statusText: response.statusText, - errorDetails - } + errorDetails, + }, ); } } catch (e) { @@ -215,7 +202,7 @@ export async function makeRequest( ) { err = new VertexAIError( VertexAIErrorCode.ERROR, - `Error fetching from ${url.toString()}: ${e.message}` + `Error fetching from ${url.toString()}: ${e.message}`, ); err.stack = e.stack; } diff --git a/packages/vertexai/lib/requests/response-helpers.ts b/packages/vertexai/lib/requests/response-helpers.ts index 27347d10f0..c7abc9d923 100644 --- a/packages/vertexai/lib/requests/response-helpers.ts +++ b/packages/vertexai/lib/requests/response-helpers.ts @@ -21,7 +21,7 @@ import { FunctionCall, GenerateContentCandidate, GenerateContentResponse, - VertexAIErrorCode + VertexAIErrorCode, } from '../types'; import { VertexAIError } from '../errors'; import { logger } from '../logger'; @@ -31,7 +31,7 @@ import { logger } from '../logger'; * other modifications that improve usability. */ export function createEnhancedContentResponse( - response: GenerateContentResponse + response: GenerateContentResponse, ): EnhancedGenerateContentResponse { /** * The Vertex AI backend omits default values. @@ -39,8 +39,8 @@ export function createEnhancedContentResponse( * response, since it has index 0, and 0 is a default value. * See: https://github.com/firebase/firebase-js-sdk/issues/8566 */ - if (response.candidates && !response.candidates[0].hasOwnProperty('index')) { - response.candidates[0].index = 0; + if (response.candidates && !response.candidates[0]?.hasOwnProperty('index')) { + response.candidates[0]!.index = 0; } const responseWithHelpers = addHelpers(response); @@ -51,27 +51,25 @@ export function createEnhancedContentResponse( * Adds convenience helper methods to a response object, including stream * chunks (as long as each chunk is a complete GenerateContentResponse JSON). */ -export function addHelpers( - response: GenerateContentResponse -): EnhancedGenerateContentResponse { +export function addHelpers(response: GenerateContentResponse): EnhancedGenerateContentResponse { (response as EnhancedGenerateContentResponse).text = () => { if (response.candidates && response.candidates.length > 0) { if (response.candidates.length > 1) { logger.warn( `This response had ${response.candidates.length} ` + `candidates. Returning text from the first candidate only. ` + - `Access response.candidates directly to use the other candidates.` + `Access response.candidates directly to use the other candidates.`, ); } - if (hadBadFinishReason(response.candidates[0])) { + if (hadBadFinishReason(response.candidates[0]!)) { throw new VertexAIError( VertexAIErrorCode.RESPONSE_ERROR, `Response error: ${formatBlockErrorMessage( - response + response, )}. Response body stored in error.response`, { - response - } + response, + }, ); } return getText(response); @@ -80,8 +78,8 @@ export function addHelpers( VertexAIErrorCode.RESPONSE_ERROR, `Text not available. ${formatBlockErrorMessage(response)}`, { - response - } + response, + }, ); } return ''; @@ -92,18 +90,18 @@ export function addHelpers( logger.warn( `This response had ${response.candidates.length} ` + `candidates. Returning function calls from the first candidate only. ` + - `Access response.candidates directly to use the other candidates.` + `Access response.candidates directly to use the other candidates.`, ); } - if (hadBadFinishReason(response.candidates[0])) { + if (hadBadFinishReason(response.candidates[0]!)) { throw new VertexAIError( VertexAIErrorCode.RESPONSE_ERROR, `Response error: ${formatBlockErrorMessage( - response + response, )}. Response body stored in error.response`, { - response - } + response, + }, ); } return getFunctionCalls(response); @@ -112,8 +110,8 @@ export function addHelpers( VertexAIErrorCode.RESPONSE_ERROR, `Function call not available. ${formatBlockErrorMessage(response)}`, { - response - } + response, + }, ); } return undefined; @@ -126,7 +124,7 @@ export function addHelpers( */ export function getText(response: GenerateContentResponse): string { const textStrings = []; - if (response.candidates?.[0].content?.parts) { + if (response.candidates?.[0]?.content?.parts) { for (const part of response.candidates?.[0].content?.parts) { if (part.text) { textStrings.push(part.text); @@ -143,11 +141,9 @@ export function getText(response: GenerateContentResponse): string { /** * Returns {@link FunctionCall}s associated with first candidate. */ -export function getFunctionCalls( - response: GenerateContentResponse -): FunctionCall[] | undefined { +export function getFunctionCalls(response: GenerateContentResponse): FunctionCall[] | undefined { const functionCalls: FunctionCall[] = []; - if (response.candidates?.[0].content?.parts) { + if (response.candidates?.[0]?.content?.parts) { for (const part of response.candidates?.[0].content?.parts) { if (part.functionCall) { functionCalls.push(part.functionCall); @@ -164,20 +160,12 @@ export function getFunctionCalls( const badFinishReasons = [FinishReason.RECITATION, FinishReason.SAFETY]; function hadBadFinishReason(candidate: GenerateContentCandidate): boolean { - return ( - !!candidate.finishReason && - badFinishReasons.includes(candidate.finishReason) - ); + return !!candidate.finishReason && badFinishReasons.includes(candidate.finishReason); } -export function formatBlockErrorMessage( - response: GenerateContentResponse -): string { +export function formatBlockErrorMessage(response: GenerateContentResponse): string { let message = ''; - if ( - (!response.candidates || response.candidates.length === 0) && - response.promptFeedback - ) { + if ((!response.candidates || response.candidates.length === 0) && response.promptFeedback) { message += 'Response was blocked'; if (response.promptFeedback?.blockReason) { message += ` due to ${response.promptFeedback.blockReason}`; diff --git a/packages/vertexai/lib/requests/schema-builder.ts b/packages/vertexai/lib/requests/schema-builder.ts index 3d219d58b1..c7ce1aff66 100644 --- a/packages/vertexai/lib/requests/schema-builder.ts +++ b/packages/vertexai/lib/requests/schema-builder.ts @@ -22,7 +22,7 @@ import { SchemaType, SchemaParams, SchemaRequest, - ObjectSchemaInterface + ObjectSchemaInterface, } from '../types/schema'; /** @@ -66,9 +66,7 @@ export abstract class Schema implements SchemaInterface { } // Ensure these are explicitly set to avoid TS errors. this.type = schemaParams.type; - this.nullable = schemaParams.hasOwnProperty('nullable') - ? !!schemaParams.nullable - : false; + this.nullable = schemaParams.hasOwnProperty('nullable') ? !!schemaParams.nullable : false; } /** @@ -78,7 +76,7 @@ export abstract class Schema implements SchemaInterface { */ toJSON(): SchemaRequest { const obj: { type: SchemaType; [key: string]: unknown } = { - type: this.type + type: this.type, }; for (const prop in this) { if (this.hasOwnProperty(prop) && this[prop] !== undefined) { @@ -100,13 +98,9 @@ export abstract class Schema implements SchemaInterface { [k: string]: Schema; }; optionalProperties?: string[]; - } + }, ): ObjectSchema { - return new ObjectSchema( - objectParams, - objectParams.properties, - objectParams.optionalProperties - ); + return new ObjectSchema(objectParams, objectParams.properties, objectParams.optionalProperties); } // eslint-disable-next-line id-blacklist @@ -114,9 +108,7 @@ export abstract class Schema implements SchemaInterface { return new StringSchema(stringParams); } - static enumString( - stringParams: SchemaParams & { enum: string[] } - ): StringSchema { + static enumString(stringParams: SchemaParams & { enum: string[] }): StringSchema { return new StringSchema(stringParams, stringParams.enum); } @@ -155,7 +147,7 @@ export class IntegerSchema extends Schema { constructor(schemaParams?: SchemaParams) { super({ type: SchemaType.INTEGER, - ...schemaParams + ...schemaParams, }); } } @@ -168,7 +160,7 @@ export class NumberSchema extends Schema { constructor(schemaParams?: SchemaParams) { super({ type: SchemaType.NUMBER, - ...schemaParams + ...schemaParams, }); } } @@ -181,7 +173,7 @@ export class BooleanSchema extends Schema { constructor(schemaParams?: SchemaParams) { super({ type: SchemaType.BOOLEAN, - ...schemaParams + ...schemaParams, }); } } @@ -196,7 +188,7 @@ export class StringSchema extends Schema { constructor(schemaParams?: SchemaParams, enumValues?: string[]) { super({ type: SchemaType.STRING, - ...schemaParams + ...schemaParams, }); this.enum = enumValues; } @@ -220,10 +212,13 @@ export class StringSchema extends Schema { * @public */ export class ArraySchema extends Schema { - constructor(schemaParams: SchemaParams, public items: TypedSchema) { + constructor( + schemaParams: SchemaParams, + public items: TypedSchema, + ) { super({ type: SchemaType.ARRAY, - ...schemaParams + ...schemaParams, }); } @@ -248,11 +243,11 @@ export class ObjectSchema extends Schema { public properties: { [k: string]: TypedSchema; }, - public optionalProperties: string[] = [] + public optionalProperties: string[] = [], ) { super({ type: SchemaType.OBJECT, - ...schemaParams + ...schemaParams, }); } @@ -268,16 +263,14 @@ export class ObjectSchema extends Schema { if (!this.properties.hasOwnProperty(propertyKey)) { throw new VertexAIError( VertexAIErrorCode.INVALID_SCHEMA, - `Property "${propertyKey}" specified in "optionalProperties" does not exist.` + `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; + obj.properties[propertyKey] = this.properties[propertyKey]!.toJSON() as SchemaRequest; if (!this.optionalProperties.includes(propertyKey)) { required.push(propertyKey); } From af1db1fa03c6447f818f7fc3c447e58aa07ef97b Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 9 Jan 2025 11:11:48 +0000 Subject: [PATCH 009/115] chore: format stream-reader file --- .../vertexai/lib/requests/stream-reader.ts | 57 +++++++------------ 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/packages/vertexai/lib/requests/stream-reader.ts b/packages/vertexai/lib/requests/stream-reader.ts index 8162407d90..cdd000d0c6 100644 --- a/packages/vertexai/lib/requests/stream-reader.ts +++ b/packages/vertexai/lib/requests/stream-reader.ts @@ -21,7 +21,7 @@ import { GenerateContentResponse, GenerateContentStreamResult, Part, - VertexAIErrorCode + VertexAIErrorCode, } from '../types'; import { VertexAIError } from '../errors'; import { createEnhancedContentResponse } from './response-helpers'; @@ -37,29 +37,24 @@ const responseLineRE = /^data\: (.*)(?:\n\n|\r\r|\r\n\r\n)/; * @param response - Response from a fetch call */ export function processStream(response: Response): GenerateContentStreamResult { - const inputStream = response.body!.pipeThrough( - new TextDecoderStream('utf8', { fatal: true }) - ); - const responseStream = - getResponseStream(inputStream); + const inputStream = response.body!.pipeThrough(new TextDecoderStream('utf8', { fatal: true })); + const responseStream = getResponseStream(inputStream); const [stream1, stream2] = responseStream.tee(); return { stream: generateResponseSequence(stream1), - response: getResponsePromise(stream2) + response: getResponsePromise(stream2), }; } async function getResponsePromise( - stream: ReadableStream + stream: ReadableStream, ): Promise { const allResponses: GenerateContentResponse[] = []; const reader = stream.getReader(); while (true) { const { done, value } = await reader.read(); if (done) { - const enhancedResponse = createEnhancedContentResponse( - aggregateResponses(allResponses) - ); + const enhancedResponse = createEnhancedContentResponse(aggregateResponses(allResponses)); return enhancedResponse; } allResponses.push(value); @@ -67,7 +62,7 @@ async function getResponsePromise( } async function* generateResponseSequence( - stream: ReadableStream + stream: ReadableStream, ): AsyncGenerator { const reader = stream.getReader(); while (true) { @@ -86,9 +81,7 @@ async function* generateResponseSequence( * chunks, returning a new stream that provides a single complete * GenerateContentResponse in each iteration. */ -export function getResponseStream( - inputStream: ReadableStream -): ReadableStream { +export function getResponseStream(inputStream: ReadableStream): ReadableStream { const reader = inputStream.getReader(); const stream = new ReadableStream({ start(controller) { @@ -99,10 +92,7 @@ export function getResponseStream( if (done) { if (currentText.trim()) { controller.error( - new VertexAIError( - VertexAIErrorCode.PARSE_FAILED, - 'Failed to parse stream' - ) + new VertexAIError(VertexAIErrorCode.PARSE_FAILED, 'Failed to parse stream'), ); return; } @@ -120,8 +110,8 @@ export function getResponseStream( controller.error( new VertexAIError( VertexAIErrorCode.PARSE_FAILED, - `Error parsing JSON response: "${match[1]}` - ) + `Error parsing JSON response: "${match[1]}`, + ), ); return; } @@ -132,7 +122,7 @@ export function getResponseStream( return pump(); }); } - } + }, }); return stream; } @@ -141,12 +131,10 @@ export function getResponseStream( * Aggregates an array of `GenerateContentResponse`s into a single * GenerateContentResponse. */ -export function aggregateResponses( - responses: GenerateContentResponse[] -): GenerateContentResponse { +export function aggregateResponses(responses: GenerateContentResponse[]): GenerateContentResponse { const lastResponse = responses[responses.length - 1]; const aggregatedResponse: GenerateContentResponse = { - promptFeedback: lastResponse?.promptFeedback + promptFeedback: lastResponse?.promptFeedback, }; for (const response of responses) { if (response.candidates) { @@ -159,17 +147,14 @@ export function aggregateResponses( } if (!aggregatedResponse.candidates[i]) { aggregatedResponse.candidates[i] = { - index: candidate.index + index: candidate.index, } as GenerateContentCandidate; } // Keep overwriting, the last one will be final - aggregatedResponse.candidates[i].citationMetadata = - candidate.citationMetadata; + aggregatedResponse.candidates[i].citationMetadata = candidate.citationMetadata; aggregatedResponse.candidates[i].finishReason = candidate.finishReason; - aggregatedResponse.candidates[i].finishMessage = - candidate.finishMessage; - aggregatedResponse.candidates[i].safetyRatings = - candidate.safetyRatings; + aggregatedResponse.candidates[i].finishMessage = candidate.finishMessage; + aggregatedResponse.candidates[i].safetyRatings = candidate.safetyRatings; /** * Candidates should always have content and parts, but this handles @@ -179,7 +164,7 @@ export function aggregateResponses( if (!aggregatedResponse.candidates[i].content) { aggregatedResponse.candidates[i].content = { role: candidate.content.role || 'user', - parts: [] + parts: [], }; } const newPart: Partial = {}; @@ -193,9 +178,7 @@ export function aggregateResponses( if (Object.keys(newPart).length === 0) { newPart.text = ''; } - aggregatedResponse.candidates[i].content.parts.push( - newPart as Part - ); + aggregatedResponse.candidates[i].content.parts.push(newPart as Part); } } } From 0908588b5fac20ba9c579e7cbda0fdbc3ec68ee4 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 9 Jan 2025 12:28:12 +0000 Subject: [PATCH 010/115] initial attempt at polyfilling stream --- packages/vertexai/lib/polyfills.ts | 8 +++++++ .../vertexai/lib/requests/stream-reader.ts | 21 ++++++++++++++++--- packages/vertexai/lib/types/polyfills.d.ts | 15 +++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 packages/vertexai/lib/polyfills.ts create mode 100644 packages/vertexai/lib/types/polyfills.d.ts diff --git a/packages/vertexai/lib/polyfills.ts b/packages/vertexai/lib/polyfills.ts new file mode 100644 index 0000000000..3cc5ef1fac --- /dev/null +++ b/packages/vertexai/lib/polyfills.ts @@ -0,0 +1,8 @@ +// @ts-ignore +import { polyfill } from 'react-native-polyfill-globals/src/fetch'; +polyfill(); + +// @ts-ignore +import { ReadableStream as ReadableStreamPolyfill } from 'web-streams-polyfill/dist/ponyfill'; +// @ts-ignore +globalThis.ReadableStream = ReadableStreamPolyfill; diff --git a/packages/vertexai/lib/requests/stream-reader.ts b/packages/vertexai/lib/requests/stream-reader.ts index cdd000d0c6..2662cb473b 100644 --- a/packages/vertexai/lib/requests/stream-reader.ts +++ b/packages/vertexai/lib/requests/stream-reader.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { ReadableStream } from 'web-streams-polyfill'; import { EnhancedGenerateContentResponse, GenerateContentCandidate, @@ -37,7 +38,21 @@ const responseLineRE = /^data\: (.*)(?:\n\n|\r\r|\r\n\r\n)/; * @param response - Response from a fetch call */ export function processStream(response: Response): GenerateContentStreamResult { - const inputStream = response.body!.pipeThrough(new TextDecoderStream('utf8', { fatal: true })); + const inputStream = new ReadableStream({ + async start(controller) { + const reader = response.body!.getReader(); + const decoder = new TextDecoder('utf-8'); + while (true) { + const { done, value } = await reader.read(); + if (done) { + controller.close(); + break; + } + const decodedValue = decoder.decode(value, { stream: true }); + controller.enqueue(decodedValue); + } + }, + }); const responseStream = getResponseStream(inputStream); const [stream1, stream2] = responseStream.tee(); return { @@ -86,7 +101,7 @@ export function getResponseStream(inputStream: ReadableStream): Reada const stream = new ReadableStream({ start(controller) { let currentText = ''; - return pump(); + return pump().then(() => undefined); function pump(): Promise<(() => Promise) | undefined> { return reader.read().then(({ value, done }) => { if (done) { @@ -105,7 +120,7 @@ export function getResponseStream(inputStream: ReadableStream): Reada let parsedResponse: T; while (match) { try { - parsedResponse = JSON.parse(match[1]); + parsedResponse = JSON.parse(match[1]!); } catch (e) { controller.error( new VertexAIError( diff --git a/packages/vertexai/lib/types/polyfills.d.ts b/packages/vertexai/lib/types/polyfills.d.ts new file mode 100644 index 0000000000..06fdf29b09 --- /dev/null +++ b/packages/vertexai/lib/types/polyfills.d.ts @@ -0,0 +1,15 @@ +declare module 'react-native-fetch-api' { + export function fetch(input: RequestInfo, init?: RequestInit): Promise; +} + +declare global { + interface RequestInit { + /** + * @description Polyfilled to enable text ReadableStream for React Native: + * @link https://github.com/facebook/react-native/issues/27741#issuecomment-2362901032 + */ + reactNative?: { + textStreaming: boolean; + }; + } +} From b12d240c1443b839968758018d2c8bbaca2b3137 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 10 Jan 2025 14:40:04 +0000 Subject: [PATCH 011/115] fix: polyfill stream for react native --- packages/vertexai/lib/index.ts | 3 ++- packages/vertexai/lib/polyfills.ts | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/vertexai/lib/index.ts b/packages/vertexai/lib/index.ts index 20ac5384a1..f6439e6abd 100644 --- a/packages/vertexai/lib/index.ts +++ b/packages/vertexai/lib/index.ts @@ -15,6 +15,7 @@ * */ +import './polyfills'; import { getApp, FirebaseApp } from '@firebase/app'; import { ModelParams, RequestOptions, VertexAIErrorCode } from './types'; import { DEFAULT_LOCATION } from './constants'; @@ -22,7 +23,6 @@ import { VertexAI, VertexAIOptions } from './public-types'; // import { ModelParams, RequestOptions, VertexAIErrorCode } from './types'; import { VertexAIError } from './errors'; import { GenerativeModel } from './models/generative-model'; - export { ChatSession } from './methods/chat-session'; export * from './requests/schema-builder'; @@ -43,6 +43,7 @@ export function getVertexAI(app: FirebaseApp = getApp(), options?: VertexAIOptio // const vertexProvider: Provider<'vertexAI'> = _getProvider(app, VERTEX_TYPE); // TODO - app used to get location and later the projectId + // TODO - get all types from node_modules/@firebase // return vertexProvider.getImmediate({ // identifier: options?.location || DEFAULT_LOCATION, // }); diff --git a/packages/vertexai/lib/polyfills.ts b/packages/vertexai/lib/polyfills.ts index 3cc5ef1fac..db76c9911a 100644 --- a/packages/vertexai/lib/polyfills.ts +++ b/packages/vertexai/lib/polyfills.ts @@ -6,3 +6,5 @@ polyfill(); import { ReadableStream as ReadableStreamPolyfill } from 'web-streams-polyfill/dist/ponyfill'; // @ts-ignore globalThis.ReadableStream = ReadableStreamPolyfill; + +import 'text-encoding'; From a91d3a29fa24a3c97bc92d3935617b6823040288 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 10 Jan 2025 14:40:20 +0000 Subject: [PATCH 012/115] chore: remove verbatimModuleSyntax --- packages/vertexai/tsconfig.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/vertexai/tsconfig.json b/packages/vertexai/tsconfig.json index 356b26154c..b72c829b2f 100644 --- a/packages/vertexai/tsconfig.json +++ b/packages/vertexai/tsconfig.json @@ -21,7 +21,6 @@ "resolveJsonModule": true, "skipLibCheck": true, "strict": true, - "target": "ESNext", - "verbatimModuleSyntax": true + "target": "ESNext" } } From c889d1436d2d42bda84debbcf29386e164c6e536 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 10 Jan 2025 14:41:20 +0000 Subject: [PATCH 013/115] chore: use module dist and update version for TS --- packages/vertexai/package.json | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/vertexai/package.json b/packages/vertexai/package.json index 8c0df7b430..f8c5260bf5 100644 --- a/packages/vertexai/package.json +++ b/packages/vertexai/package.json @@ -3,10 +3,10 @@ "version": "0.0.1", "author": "Invertase (http://invertase.io)", "description": "React Native Firebase - Vertex AI is a fully-managed, unified AI development platform for building and using generative AI", - "main": "./dist/commonjs/index.js", - "types": "./dist/typescript/commonjs/lib/index.d.ts", + "main": "./dist/module/index.js", + "types": "./dist/typescript/module/lib/index.d.ts", "scripts": { - "build": "genversion --semi lib/version.js", + "build": "genversion --semi lib/version.js && genversion --esm --semi lib/version.ts", "build:clean": "rimraf android/build && rimraf ios/build", "prepare": "yarn run build && bob build" }, @@ -30,6 +30,7 @@ "access": "public" }, "devDependencies": { + "@types/text-encoding": "^0", "react-native-builder-bob": "^0.35.2", "typescript": "^5.7.2" }, @@ -81,5 +82,11 @@ "eslintIgnore": [ "node_modules/", "dist/" - ] + ], + "dependencies": { + "react-native-fetch-api": "^3.0.0", + "react-native-polyfill-globals": "^3.1.0", + "text-encoding": "^0.7.0", + "web-streams-polyfill": "^4.1.0" + } } From 8495c61f8023ecca01abecbd396b20e09cbbcc10 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 10 Jan 2025 14:41:33 +0000 Subject: [PATCH 014/115] chore: check in version.ts --- packages/vertexai/lib/version.ts | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 packages/vertexai/lib/version.ts diff --git a/packages/vertexai/lib/version.ts b/packages/vertexai/lib/version.ts new file mode 100644 index 0000000000..997ce3b865 --- /dev/null +++ b/packages/vertexai/lib/version.ts @@ -0,0 +1,2 @@ +// Generated by genversion. +export const version = '0.0.1'; From 200d8603d55ac280e5857f4dbf411dbb6f3ed36c Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 10 Jan 2025 14:41:47 +0000 Subject: [PATCH 015/115] get version from version.ts --- packages/vertexai/lib/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vertexai/lib/constants.ts b/packages/vertexai/lib/constants.ts index 357e6c4e77..a9af794707 100644 --- a/packages/vertexai/lib/constants.ts +++ b/packages/vertexai/lib/constants.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import { version } from '../package.json'; +import { version } from './version'; export const VERTEX_TYPE = 'vertexAI'; From 68e502cdff5797a5a0964f0c24cd613a7f36c39b Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 10 Jan 2025 14:42:03 +0000 Subject: [PATCH 016/115] fix: RN specific stream --- packages/vertexai/lib/requests/request.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/vertexai/lib/requests/request.ts b/packages/vertexai/lib/requests/request.ts index 058dcb8a3c..f7deb54819 100644 --- a/packages/vertexai/lib/requests/request.ts +++ b/packages/vertexai/lib/requests/request.ts @@ -143,8 +143,15 @@ export async function makeRequest( const abortController = new AbortController(); fetchTimeoutId = setTimeout(() => abortController.abort(), timeoutMillis); request.fetchOptions.signal = abortController.signal; - - response = await fetch(request.url, request.fetchOptions); + const fetchOptions = stream + ? { + ...request.fetchOptions, + reactNative: { + textStreaming: true, + }, + } + : request.fetchOptions; + response = await fetch(request.url, fetchOptions); if (!response.ok) { let message = ''; let errorDetails; From 30b673d59688597dabb1ba59ab44c3b5a39c5bb8 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 16 Jan 2025 10:53:05 +0000 Subject: [PATCH 017/115] feat: allow auth and app check to be passed in --- packages/vertexai/lib/index.ts | 26 +++++---- .../vertexai/lib/models/generative-model.ts | 5 +- packages/vertexai/lib/requests/request.ts | 13 +++-- packages/vertexai/lib/service.ts | 18 ++---- packages/vertexai/lib/types/internal.ts | 56 +++++++++++++++++-- 5 files changed, 83 insertions(+), 35 deletions(-) diff --git a/packages/vertexai/lib/index.ts b/packages/vertexai/lib/index.ts index f6439e6abd..850b923d45 100644 --- a/packages/vertexai/lib/index.ts +++ b/packages/vertexai/lib/index.ts @@ -17,12 +17,15 @@ import './polyfills'; import { getApp, FirebaseApp } from '@firebase/app'; +import { AppCheck } from '@firebase/app-check'; +import { Auth } from '@firebase/auth'; import { ModelParams, RequestOptions, VertexAIErrorCode } from './types'; import { DEFAULT_LOCATION } from './constants'; import { VertexAI, VertexAIOptions } from './public-types'; // import { ModelParams, RequestOptions, VertexAIErrorCode } from './types'; import { VertexAIError } from './errors'; import { GenerativeModel } from './models/generative-model'; +import { VertexAIService } from './service'; export { ChatSession } from './methods/chat-session'; export * from './requests/schema-builder'; @@ -36,21 +39,22 @@ export { VertexAIError }; * @public * * @param app - The {@link @firebase/app#FirebaseApp} to use. + * @param options - The {@link VertexAIOptions} to use. + * @param appCheck - The {@link @firebase/app-check#AppCheck} to use. + * @param auth - The {@link @firebase/auth#Auth} to use. */ -export function getVertexAI(app: FirebaseApp = getApp(), options?: VertexAIOptions): VertexAI { - // app = getModularInstance(app); - // Dependencies - // const vertexProvider: Provider<'vertexAI'> = _getProvider(app, VERTEX_TYPE); - - // TODO - app used to get location and later the projectId - // TODO - get all types from node_modules/@firebase - // return vertexProvider.getImmediate({ - // identifier: options?.location || DEFAULT_LOCATION, - // }); +export function getVertexAI( + app: FirebaseApp = getApp(), + options?: VertexAIOptions, + appCheck?: AppCheck, + auth?: Auth, +): VertexAI { return { app, location: options?.location || DEFAULT_LOCATION, - }; + appCheck: appCheck || null, + auth: auth || null, + } as VertexAIService; } /** diff --git a/packages/vertexai/lib/models/generative-model.ts b/packages/vertexai/lib/models/generative-model.ts index 9df5d1c4ed..111cefa427 100644 --- a/packages/vertexai/lib/models/generative-model.ts +++ b/packages/vertexai/lib/models/generative-model.ts @@ -77,8 +77,9 @@ export class GenerativeModel { (vertexAI as VertexAIService).appCheck!.getToken(); } - if ((vertexAI as VertexAIService).auth) { - this._apiSettings.getAuthToken = () => (vertexAI as VertexAIService).auth!.getToken(); + if ((vertexAI as VertexAIService).auth?.currentUser) { + this._apiSettings.getAuthToken = () => + (vertexAI as VertexAIService).auth!.currentUser!.getIdToken(); } } if (modelParams.model.includes('/')) { diff --git a/packages/vertexai/lib/requests/request.ts b/packages/vertexai/lib/requests/request.ts index f7deb54819..82669a5b0f 100644 --- a/packages/vertexai/lib/requests/request.ts +++ b/packages/vertexai/lib/requests/request.ts @@ -84,19 +84,22 @@ export async function getHeaders(url: RequestUrl): Promise { headers.append('x-goog-api-client', getClientHeaders()); headers.append('x-goog-api-key', url.apiSettings.apiKey); if (url.apiSettings.getAppCheckToken) { - const appCheckToken = await url.apiSettings.getAppCheckToken(); + let appCheckToken; + + try { + appCheckToken = await url.apiSettings.getAppCheckToken(); + } catch (e) { + logger.warn(`Unable to obtain a valid App Check token: ${e}`); + } if (appCheckToken) { headers.append('X-Firebase-AppCheck', appCheckToken.token); - if (appCheckToken.error) { - logger.warn(`Unable to obtain a valid App Check token: ${appCheckToken.error.message}`); - } } } if (url.apiSettings.getAuthToken) { const authToken = await url.apiSettings.getAuthToken(); if (authToken) { - headers.append('Authorization', `Firebase ${authToken.accessToken}`); + headers.append('Authorization', `Firebase ${authToken}`); } } diff --git a/packages/vertexai/lib/service.ts b/packages/vertexai/lib/service.ts index 1c1573a926..df4295c0b6 100644 --- a/packages/vertexai/lib/service.ts +++ b/packages/vertexai/lib/service.ts @@ -17,28 +17,20 @@ import { FirebaseApp } from '@firebase/app'; import { VertexAI, VertexAIOptions } from './public-types'; -import { - AppCheckInternalComponentName, - FirebaseAppCheckInternal, -} from '@firebase/app-check-interop-types'; -import { Provider } from '@firebase/component'; -import { FirebaseAuthInternal, FirebaseAuthInternalName } from '@firebase/auth-interop-types'; import { DEFAULT_LOCATION } from './constants'; -import { _FirebaseService } from './types/internal'; +import { _FirebaseService, InternalAppCheck, InternalAuth } from './types/internal'; export class VertexAIService implements VertexAI, _FirebaseService { - auth: FirebaseAuthInternal | null; - appCheck: FirebaseAppCheckInternal | null; + auth: InternalAuth | null; + appCheck: InternalAppCheck | null; location: string; constructor( public app: FirebaseApp, - authProvider?: Provider, - appCheckProvider?: Provider, + auth?: InternalAuth, + appCheck?: InternalAppCheck, public options?: VertexAIOptions, ) { - const appCheck = appCheckProvider?.getImmediate({ optional: true }); - const auth = authProvider?.getImmediate({ optional: true }); this.auth = auth || null; this.appCheck = appCheck || null; this.location = this.options?.location || DEFAULT_LOCATION; diff --git a/packages/vertexai/lib/types/internal.ts b/packages/vertexai/lib/types/internal.ts index 1aa36f783f..68dbc068ac 100644 --- a/packages/vertexai/lib/types/internal.ts +++ b/packages/vertexai/lib/types/internal.ts @@ -14,16 +14,13 @@ * limitations under the License. * */ - -import { AppCheckTokenResult } from '@firebase/app-check-interop-types'; -import { FirebaseAuthTokenData } from '@firebase/auth-interop-types'; import { FirebaseApp } from '@firebase/app'; export interface ApiSettings { apiKey: string; project: string; location: string; - getAuthToken?: () => Promise; + getAuthToken?: () => Promise; getAppCheckToken?: () => Promise; } @@ -39,3 +36,54 @@ export interface _FirebaseService { */ _delete(): Promise; } + +export interface InternalAppCheck { + /** + * Requests Firebase App Check token. + * This method should only be used if you need to authorize requests to a non-Firebase backend. + * Requests to Firebase backend are authorized automatically if configured. + * + * @param forceRefresh - If true, a new Firebase App Check token is requested and the token cache is ignored. + * If false, the cached token is used if it exists and has not expired yet. + * In most cases, false should be used. True should only be used if the server explicitly returns an error, indicating a revoked token. + */ + getToken(forceRefresh?: boolean): Promise; +} + +interface AppCheckTokenResult { + /** + * The token string in JWT format. + */ + readonly token: string; +} + +export interface InternalAuth { + /** + * Returns the currently signed-in user (or null if no user signed in). See the User interface documentation for detailed usage. + * + * #### Example + * + * ```js + * const user = firebase.auth().currentUser; + * ``` + * + * > It is recommended to use {@link auth#onAuthStateChanged} to track whether the user is currently signed in. + */ + currentUser: User | null; +} + +export interface User { + /** + * Returns the users authentication token. + * + * #### Example + * + * ```js + * // Force a token refresh + * const idToken = await firebase.auth().currentUser.getIdToken(true); + * ``` + * + * @param forceRefresh A boolean value which forces Firebase to refresh the token. + */ + getIdToken(forceRefresh?: boolean): Promise; +} From 7d385b017f773f2819fc3323ae8afec07a9dda0b Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 16 Jan 2025 12:44:37 +0000 Subject: [PATCH 018/115] test: write first test file --- .../__tests__/chat-session-helpers.test.ts | 153 ++++++++++++++++++ packages/vertexai/tsconfig.json | 2 +- 2 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 packages/vertexai/__tests__/chat-session-helpers.test.ts diff --git a/packages/vertexai/__tests__/chat-session-helpers.test.ts b/packages/vertexai/__tests__/chat-session-helpers.test.ts new file mode 100644 index 0000000000..f96fa95d33 --- /dev/null +++ b/packages/vertexai/__tests__/chat-session-helpers.test.ts @@ -0,0 +1,153 @@ +/** + * @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 { describe, expect, it } from '@jest/globals'; +import { validateChatHistory } from '../lib/methods/chat-session-helpers'; +import { Content } from '../lib/types'; +import { FirebaseError } from '@firebase/util'; + +describe('chat-session-helpers', () => { + describe('validateChatHistory', () => { + const TCS: Array<{ history: Content[]; isValid: boolean }> = [ + { + history: [{ role: 'user', parts: [{ text: 'hi' }] }], + isValid: true, + }, + { + history: [ + { + role: 'user', + parts: [{ text: 'hi' }, { inlineData: { mimeType: 'image/jpeg', data: 'base64==' } }], + }, + ], + isValid: true, + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { role: 'model', parts: [{ text: 'hi' }, { text: 'hi' }] }, + ], + isValid: true, + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { + role: 'model', + parts: [{ functionCall: { name: 'greet', args: { name: 'user' } } }], + }, + ], + isValid: true, + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { + role: 'model', + parts: [{ functionCall: { name: 'greet', args: { name: 'user' } } }], + }, + { + role: 'function', + parts: [ + { + functionResponse: { name: 'greet', response: { name: 'user' } }, + }, + ], + }, + ], + isValid: true, + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { + role: 'model', + parts: [{ functionCall: { name: 'greet', args: { name: 'user' } } }], + }, + { + role: 'function', + parts: [ + { + functionResponse: { name: 'greet', response: { name: 'user' } }, + }, + ], + }, + { + role: 'model', + parts: [{ text: 'hi name' }], + }, + ], + isValid: true, + }, + { + //@ts-expect-error + history: [{ role: 'user', parts: '' }], + isValid: false, + }, + { + //@ts-expect-error + history: [{ role: 'user' }], + isValid: false, + }, + { + history: [{ role: 'user', parts: [] }], + isValid: false, + }, + { + history: [{ role: 'model', parts: [{ text: 'hi' }] }], + isValid: false, + }, + { + history: [ + { + role: 'function', + parts: [ + { + functionResponse: { name: 'greet', response: { name: 'user' } }, + }, + ], + }, + ], + isValid: false, + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { role: 'user', parts: [{ text: 'hi' }] }, + ], + isValid: false, + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { role: 'model', parts: [{ text: 'hi' }] }, + { role: 'model', parts: [{ text: 'hi' }] }, + ], + isValid: false, + }, + ]; + TCS.forEach((tc, index) => { + it(`case ${index}`, () => { + const fn = (): void => validateChatHistory(tc.history); + if (tc.isValid) { + expect(fn).not.toThrow(); + } else { + expect(fn).toThrow(FirebaseError); + } + }); + }); + }); +}); diff --git a/packages/vertexai/tsconfig.json b/packages/vertexai/tsconfig.json index b72c829b2f..95dd21071e 100644 --- a/packages/vertexai/tsconfig.json +++ b/packages/vertexai/tsconfig.json @@ -10,6 +10,7 @@ "ESNext" ], "module": "ESNext", + "target": "ESNext", "moduleResolution": "Bundler", "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, @@ -21,6 +22,5 @@ "resolveJsonModule": true, "skipLibCheck": true, "strict": true, - "target": "ESNext" } } From 08b3ce194bd947c1a6bfce5bbad8603b2c78247b Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 10:55:19 +0000 Subject: [PATCH 019/115] test: chat session --- .../vertexai/__tests__/chat-session.test.ts | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 packages/vertexai/__tests__/chat-session.test.ts diff --git a/packages/vertexai/__tests__/chat-session.test.ts b/packages/vertexai/__tests__/chat-session.test.ts new file mode 100644 index 0000000000..cd96aa32e6 --- /dev/null +++ b/packages/vertexai/__tests__/chat-session.test.ts @@ -0,0 +1,100 @@ +/** + * @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 { describe, expect, it, afterEach, jest } from '@jest/globals'; + +import * as generateContentMethods from '../lib/methods/generate-content'; +import { GenerateContentStreamResult } from '../lib/types'; +import { ChatSession } from '../lib/methods/chat-session'; +import { ApiSettings } from '../lib/types/internal'; +import { RequestOptions } from '../lib/types/requests'; + +const fakeApiSettings: ApiSettings = { + apiKey: 'key', + project: 'my-project', + location: 'us-central1', +}; + +const requestOptions: RequestOptions = { + timeout: 1000, +}; + +describe('ChatSession', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('sendMessage()', () => { + it('generateContent errors should be catchable', async () => { + const generateContentStub = jest + .spyOn(generateContentMethods, 'generateContent') + .mockRejectedValue('generateContent failed'); + + const chatSession = new ChatSession(fakeApiSettings, 'a-model', {}, requestOptions); + + await expect(chatSession.sendMessage('hello')).rejects.toMatch(/generateContent failed/); + + expect(generateContentStub).toHaveBeenCalledWith( + fakeApiSettings, + 'a-model', + expect.anything(), + requestOptions, + ); + }); + }); + + describe('sendMessageStream()', () => { + it('generateContentStream errors should be catchable', async () => { + jest.useFakeTimers(); + const consoleStub = jest.spyOn(console, 'error').mockImplementation(() => {}); + const generateContentStreamStub = jest + .spyOn(generateContentMethods, 'generateContentStream') + .mockRejectedValue('generateContentStream failed'); + const chatSession = new ChatSession(fakeApiSettings, 'a-model', {}, requestOptions); + await expect(chatSession.sendMessageStream('hello')).rejects.toMatch( + /generateContentStream failed/, + ); + expect(generateContentStreamStub).toHaveBeenCalledWith( + fakeApiSettings, + 'a-model', + expect.anything(), + requestOptions, + ); + jest.runAllTimers(); + expect(consoleStub).not.toHaveBeenCalled(); + jest.useRealTimers(); + }); + + it('downstream sendPromise errors should log but not throw', async () => { + const consoleStub = jest.spyOn(console, 'error').mockImplementation(() => {}); + // make response undefined so that response.candidates errors + const generateContentStreamStub = jest + .spyOn(generateContentMethods, 'generateContentStream') + .mockResolvedValue({} as unknown as GenerateContentStreamResult); + const chatSession = new ChatSession(fakeApiSettings, 'a-model', {}, requestOptions); + await chatSession.sendMessageStream('hello'); + expect(generateContentStreamStub).toHaveBeenCalledWith( + fakeApiSettings, + 'a-model', + expect.anything(), + requestOptions, + ); + // wait for the console.error to be called, due to number of promises in the chain + await new Promise(resolve => setTimeout(resolve, 100)); + expect(consoleStub).toHaveBeenCalledTimes(1); + }); + }); +}); From 788a667eac6c1aad10fb05439b4e4e02d5a6471b Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 10:55:28 +0000 Subject: [PATCH 020/115] initial count token --- .../vertexai/__tests__/count-tokens.test.ts | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 packages/vertexai/__tests__/count-tokens.test.ts diff --git a/packages/vertexai/__tests__/count-tokens.test.ts b/packages/vertexai/__tests__/count-tokens.test.ts new file mode 100644 index 0000000000..fd4b99e1e0 --- /dev/null +++ b/packages/vertexai/__tests__/count-tokens.test.ts @@ -0,0 +1,106 @@ +/** + * @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 { match, restore, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { getMockResponse } from '../../test-utils/mock-response'; +import * as request from '../requests/request'; +import { countTokens } from './count-tokens'; +import { CountTokensRequest } from '../types'; +import { ApiSettings } from '../types/internal'; +import { Task } from '../requests/request'; + +use(sinonChai); +use(chaiAsPromised); + +const fakeApiSettings: ApiSettings = { + apiKey: 'key', + project: 'my-project', + location: 'us-central1' +}; + +const fakeRequestParams: CountTokensRequest = { + contents: [{ parts: [{ text: 'hello' }], role: 'user' }] +}; + +describe('countTokens()', () => { + afterEach(() => { + restore(); + }); + it('total tokens', async () => { + const mockResponse = getMockResponse('unary-success-total-tokens.json'); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await countTokens( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.totalTokens).to.equal(6); + expect(result.totalBillableCharacters).to.equal(16); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.COUNT_TOKENS, + fakeApiSettings, + false, + match((value: string) => { + return value.includes('contents'); + }), + undefined + ); + }); + it('total tokens no billable characters', async () => { + const mockResponse = getMockResponse( + 'unary-success-no-billable-characters.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await countTokens( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.totalTokens).to.equal(258); + expect(result).to.not.have.property('totalBillableCharacters'); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.COUNT_TOKENS, + fakeApiSettings, + false, + match((value: string) => { + return value.includes('contents'); + }), + undefined + ); + }); + it('model not found', async () => { + const mockResponse = getMockResponse('unary-failure-model-not-found.json'); + const mockFetch = stub(globalThis, 'fetch').resolves({ + ok: false, + status: 404, + json: mockResponse.json + } as Response); + await expect( + countTokens(fakeApiSettings, 'model', fakeRequestParams) + ).to.be.rejectedWith(/404.*not found/); + expect(mockFetch).to.be.called; + }); +}); From 76ae3af25dd4611e8d9d8efa0d862e5e3b09cbb3 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 11:44:54 +0000 Subject: [PATCH 021/115] test utils --- .../__tests__/test-utils/base64cat.ts | 19 +++++ .../vertexai/__tests__/test-utils/cat.jpeg | Bin 0 -> 35160 bytes .../vertexai/__tests__/test-utils/cat.png | Bin 0 -> 142387 bytes .../__tests__/test-utils/mock-response.ts | 65 ++++++++++++++++++ 4 files changed, 84 insertions(+) create mode 100644 packages/vertexai/__tests__/test-utils/base64cat.ts create mode 100644 packages/vertexai/__tests__/test-utils/cat.jpeg create mode 100644 packages/vertexai/__tests__/test-utils/cat.png create mode 100644 packages/vertexai/__tests__/test-utils/mock-response.ts diff --git a/packages/vertexai/__tests__/test-utils/base64cat.ts b/packages/vertexai/__tests__/test-utils/base64cat.ts new file mode 100644 index 0000000000..45325a1bf5 --- /dev/null +++ b/packages/vertexai/__tests__/test-utils/base64cat.ts @@ -0,0 +1,19 @@ +/** + * @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. + */ + +export const base64Cat = + 'iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAYAAABccqhmAAABJWlDQ1BrQ0dDb2xvclNwYWNlQWRvYmVSR0IxOTk4AAAokWNgYFJILCjIYRJgYMjNKykKcndSiIiMUmB/zsDNwAnE2gwGicnFBY4BAT4MQACjUcG3awyMIPqyLsgsTHm8gCsltTgZSP8B4uzkgqISBgbGDCBbubykAMTuAbJFkrLB7AUgdhHQgUD2FhA7HcI+AVYDYd8BqwkJcgayPwDZfElgNhPILr50CFsAxIbaCwKCjin5SakKIN9rGFpaWmiS6AeCoCS1ogREO+cXVBZlpmeUKDgCQypVwTMvWU9HwcjAyJiBARTuENWfA8HhySh2BiGGAAixORIMDP5LGRhY/iDETHoZGBboMDDwT0WIqRkyMAjoMzDsm5NcWlQGNYaRCWgnIT4AXxVKdgMmGHwAAAFQZVhJZk1NACoAAAAIAAkBDgACAAAARwAAAHoBEgADAAAAAQABAAABGgAFAAAAAQAAAMIBGwAFAAAAAQAAAMoBKAADAAAAAQACAAABMQACAAAACwAAANIBMgACAAAAFAAAAN6CmAACAAAAEwAAAPKHaQAEAAAAAQAAAQYAAAAAUGhvdG9ncmFwaCBmcm9tIFdhbHRlciBDaGFuZG9oYTogVGhlIENhdCBQaG90b2dyYXBoZXIgKEFwZXJ0dXJlLCAyMDE1KQAAAAABLAAAAAEAAAEsAAAAAVBob3RvU2NhcGUAADIwMTU6MDY6MTggMTE6MTM6NTMAwqkgV2FsdGVyIENoYW5kb2hhAAAABJAAAAcAAAAEMDIyMZAEAAIAAAAUAAABPKACAAQAAAABAAABAKADAAQAAAABAAABAAAAAAAyMDE1OjA0OjAxIDE1OjE3OjAzALUWG8IAAAAJcEhZcwAALiMAAC4jAXilP3YAADtdaVRYdFhNTDpjb20uYWRvYmUueG1wAAAAAAA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJYTVAgQ29yZSA2LjAuMCI+CiAgIDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXBSaWdodHM9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9yaWdodHMvIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgICAgICAgICB4bWxuczpzdEV2dD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlRXZlbnQjIgogICAgICAgICAgICB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIKICAgICAgICAgICAgeG1sbnM6eHdudj0iaHR0cDovL25zLnhpbmV0LmNvbS9ucy94aW5ldHNjaGVtYSMiCiAgICAgICAgICAgIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyI+CiAgICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2UvdGlmZjwvZGM6Zm9ybWF0PgogICAgICAgICA8ZGM6ZGVzY3JpcHRpb24+CiAgICAgICAgICAgIDxyZGY6QWx0PgogICAgICAgICAgICAgICA8cmRmOmxpIHhtbDpsYW5nPSJ4LWRlZmF1bHQiPlBob3RvZ3JhcGggZnJvbSBXYWx0ZXIgQ2hhbmRvaGE6IFRoZSBDYXQgUGhvdG9ncmFwaGVyIChBcGVydHVyZSwgMjAxNSk8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6QWx0PgogICAgICAgICA8L2RjOmRlc2NyaXB0aW9uPgogICAgICAgICA8ZGM6cmlnaHRzPgogICAgICAgICAgICA8cmRmOkFsdD4KICAgICAgICAgICAgICAgPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij7CqSBXYWx0ZXIgQ2hhbmRvaGE8L3JkZjpsaT4KICAgICAgICAgICAgPC9yZGY6QWx0PgogICAgICAgICA8L2RjOnJpZ2h0cz4KICAgICAgICAgPHRpZmY6UmVzb2x1dGlvblVuaXQ+MjwvdGlmZjpSZXNvbHV0aW9uVW5pdD4KICAgICAgICAgPHRpZmY6Q29tcHJlc3Npb24+MTwvdGlmZjpDb21wcmVzc2lvbj4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WVJlc29sdXRpb24+MzAwPC90aWZmOllSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpYUmVzb2x1dGlvbj4zMDA8L3RpZmY6WFJlc29sdXRpb24+CiAgICAgICAgIDxleGlmOlBpeGVsWURpbWVuc2lvbj4zMDU3PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6RXhpZlZlcnNpb24+MDIyMTwvZXhpZjpFeGlmVmVyc2lvbj4KICAgICAgICAgPGV4aWY6Q29sb3JTcGFjZT42NTUzNTwvZXhpZjpDb2xvclNwYWNlPgogICAgICAgICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+MjE3MzwvZXhpZjpQaXhlbFhEaW1lbnNpb24+CiAgICAgICAgIDx4bXBSaWdodHM6TWFya2VkPlRydWU8L3htcFJpZ2h0czpNYXJrZWQ+CiAgICAgICAgIDx4bXA6Q3JlYXRvclRvb2w+UGhvdG9TY2FwZTwveG1wOkNyZWF0b3JUb29sPgogICAgICAgICA8eG1wOlJhdGluZz4xPC94bXA6UmF0aW5nPgogICAgICAgICA8eG1wOk1ldGFkYXRhRGF0ZT4yMDE1LTA2LTE4VDExOjEzOjUzLTA0OjAwPC94bXA6TWV0YWRhdGFEYXRlPgogICAgICAgICA8eG1wOkNyZWF0ZURhdGU+MjAxNS0wNC0wMVQxNToxNzowMy0wNDowMDwveG1wOkNyZWF0ZURhdGU+CiAgICAgICAgIDx4bXA6TGFiZWw+QXBwcm92ZWQ8L3htcDpMYWJlbD4KICAgICAgICAgPHhtcDpNb2RpZnlEYXRlPjIwMTUtMDYtMThUMTE6MTM6NTMtMDQ6MDA8L3htcDpNb2RpZnlEYXRlPgogICAgICAgICA8eG1wTU06SGlzdG9yeT4KICAgICAgICAgICAgPHJkZjpTZXE+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ1M1LjEgTWFjaW50b3NoPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTAxVDE1OjE3OjAzLTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOkQ2NDkzMDk2MzgyNzY4MTE4NzFGRDg0Mjk3MDE2Mjk3PC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPmNyZWF0ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ0MgKE1hY2ludG9zaCk8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpjaGFuZ2VkPi88L3N0RXZ0OmNoYW5nZWQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTUtMDQtMDFUMTg6NTg6MDEtMDQ6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6YTI2NDlkMWMtM2YzNy00YzVlLWFmNGMtN2MxMDg1ZTc4Yjc5PC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5jb252ZXJ0ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnBhcmFtZXRlcnM+ZnJvbSBpbWFnZS90aWZmIHRvIGltYWdlL2Vwc2Y8L3N0RXZ0OnBhcmFtZXRlcnM+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5kZXJpdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpwYXJhbWV0ZXJzPmNvbnZlcnRlZCBmcm9tIGltYWdlL3RpZmYgdG8gaW1hZ2UvZXBzZjwvc3RFdnQ6cGFyYW1ldGVycz4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ0MgKE1hY2ludG9zaCk8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpjaGFuZ2VkPi88L3N0RXZ0OmNoYW5nZWQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTUtMDQtMDFUMTg6NTg6MDEtMDQ6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6OGEwOTVhMjMtNTBlMS00ZDA3LWI0YjctNGNhNDA4YzVlODcyPC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENDIChNYWNpbnRvc2gpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTAxVDE5OjQ0OjQ2LTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjlkYzRhZDAyLTgwMzItNDMyZS05YjhlLTk1YThlMTQ1YzJkMDwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+Y29udmVydGVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpwYXJhbWV0ZXJzPmZyb20gaW1hZ2UvZXBzZiB0byBpbWFnZS90aWZmPC9zdEV2dDpwYXJhbWV0ZXJzPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+ZGVyaXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6cGFyYW1ldGVycz5jb252ZXJ0ZWQgZnJvbSBpbWFnZS9lcHNmIHRvIGltYWdlL3RpZmY8L3N0RXZ0OnBhcmFtZXRlcnM+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENDIChNYWNpbnRvc2gpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTAxVDE5OjQ0OjQ2LTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjRkZTAwN2U3LTk5MWUtNDc5MS1hOTZkLTE5ZmVhMDllNWI4NTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDQyAoTWFjaW50b3NoKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNS0wNC0wMVQyMDo0Njo1Ni0wNDowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDowMjMyZmIxZC1jYjQ0LTQwOGYtYWE2MC04N2U3MDYyMGNhYmM8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+c2F2ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ1M1LjEgV2luZG93czwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNS0wNC0xOVQwMDoxODozMy0wNDowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDpCOUM3OENGNDQ2RTZFNDExOTRFQzkzQjJERjdGRjg2NTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+Y29udmVydGVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpwYXJhbWV0ZXJzPmZyb20gaW1hZ2UvdGlmZiB0byBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wPC9zdEV2dDpwYXJhbWV0ZXJzPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+ZGVyaXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6cGFyYW1ldGVycz5jb252ZXJ0ZWQgZnJvbSBpbWFnZS90aWZmIHRvIGFwcGxpY2F0aW9uL3ZuZC5hZG9iZS5waG90b3Nob3A8L3N0RXZ0OnBhcmFtZXRlcnM+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENTNS4xIFdpbmRvd3M8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpjaGFuZ2VkPi88L3N0RXZ0OmNoYW5nZWQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTUtMDQtMTlUMDA6MTg6MzMtMDQ6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6QkFDNzhDRjQ0NkU2RTQxMTk0RUM5M0IyREY3RkY4NjU8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+c2F2ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ1M1LjEgV2luZG93czwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNS0wNC0xOVQxMjo0Mjo1OC0wNDowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDowNUZDQzI0NUIxRTZFNDExQjVCMEU3QTI0NTYyMUZGNjwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDUzUuMSBXaW5kb3dzPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTE5VDEyOjQzOjMwLTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjA4RkNDMjQ1QjFFNkU0MTFCNUIwRTdBMjQ1NjIxRkY2PC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5jb252ZXJ0ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnBhcmFtZXRlcnM+ZnJvbSBhcHBsaWNhdGlvbi92bmQuYWRvYmUucGhvdG9zaG9wIHRvIGltYWdlL3RpZmY8L3N0RXZ0OnBhcmFtZXRlcnM+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5kZXJpdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpwYXJhbWV0ZXJzPmNvbnZlcnRlZCBmcm9tIGFwcGxpY2F0aW9uL3ZuZC5hZG9iZS5waG90b3Nob3AgdG8gaW1hZ2UvdGlmZjwvc3RFdnQ6cGFyYW1ldGVycz4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ1M1LjEgV2luZG93czwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNS0wNC0xOVQxMjo0MzozMC0wNDowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDowOUZDQzI0NUIxRTZFNDExQjVCMEU3QTI0NTYyMUZGNjwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTIxVDEwOjQ4OjM1LTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOmUzODZlNjQ4LWZmNjYtNDA5NC04NWEyLWY2NjgxZTRiM2I4Mjwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIEJyaWRnZSBDQyAoTWFjaW50b3NoKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+L21ldGFkYXRhPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTIzVDIxOjEwOjQ2LTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOmIwZmJjMzZkLThhMjgtNDU4NC05NTlkLTk5NjFmZDEyMDA5Mjwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTIzVDIyOjIyOjIzLTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOmM1MzNhNjRlLTk0OTktNDMzOS05MjM4LThhOGY0Nzc3NzQ3YTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE0IChNYWNpbnRvc2gpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTIzVDIyOjIzOjQyLTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOmRiMjlmZTVlLWQ2ZDAtNDBjZi04Y2RkLTBkYTU2NDgwMTU2YTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDYW1lcmEgUmF3IDguODwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+L21ldGFkYXRhPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTIzVDIyOjMyOjMwLTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOmRkMzFmNTVlLTUyMzctNDA5ZC1hMTk2LTdhM2Q1ZTIwMTM0Mjwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDYW1lcmEgUmF3IDguOCAoTWFjaW50b3NoKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+L21ldGFkYXRhPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTIzVDIyOjMzOjM3LTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOmUzNjExZWQ3LTI4YmYtNGE5MS1hZDA5LWVhNjc3M2U4Y2YyOTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDQyAoTWFjaW50b3NoKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNS0wNC0yOFQxMDowMTo1MS0wNDowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDo1MDViZDMzYS03ZDJiLTRiNzQtOTU1My0xNjIzYjE4MzExMmM8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+c2F2ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ1M2IChXaW5kb3dzKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+Lzwvc3RFdnQ6Y2hhbmdlZD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OndoZW4+MjAxNS0wNi0xOFQxMDowMjo1OS0wNDowMDwvc3RFdnQ6d2hlbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDpFMzBDRTVCOUMyMTVFNTExQkVDQkU3RTMxNDVCODA4NTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDYW1lcmEgUmF3IDguMjwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+L21ldGFkYXRhPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA2LTE4VDEwOjA0OjQ1LTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOkE4MUY0NUYxQzIxNUU1MTE4OTE4RkI0OTg3NjBDNzI5PC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENhbWVyYSBSYXcgOC4yIChXaW5kb3dzKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmNoYW5nZWQ+L21ldGFkYXRhPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA2LTE4VDEwOjA1OjU1LTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjMzNTk0ZDkyLWNlMmYtNGE0Ny1iNTFmLTRmOTUzNjVmNWJkNTwvc3RFdnQ6aW5zdGFuY2VJRD4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OmFjdGlvbj5zYXZlZDwvc3RFdnQ6YWN0aW9uPgogICAgICAgICAgICAgICA8L3JkZjpsaT4KICAgICAgICAgICAgICAgPHJkZjpsaSByZGY6cGFyc2VUeXBlPSJSZXNvdXJjZSI+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDUzYgKFdpbmRvd3MpPC9zdEV2dDpzb2Z0d2FyZUFnZW50PgogICAgICAgICAgICAgICAgICA8c3RFdnQ6Y2hhbmdlZD4vPC9zdEV2dDpjaGFuZ2VkPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA2LTE4VDExOjEyLTA0OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6aW5zdGFuY2VJRD54bXAuaWlkOjA3QUE3MDVFQ0MxNUU1MTE5NjEwQjRCN0U1NjM5OEUwPC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICAgICA8cmRmOmxpIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0OnNvZnR3YXJlQWdlbnQ+QWRvYmUgUGhvdG9zaG9wIENTNiAoV2luZG93cyk8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpjaGFuZ2VkPi88L3N0RXZ0OmNoYW5nZWQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTUtMDYtMThUMTE6MTM6NTMtMDQ6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6MTFBQTcwNUVDQzE1RTUxMTk2MTBCNEI3RTU2Mzk4RTA8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDphY3Rpb24+c2F2ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgIDwvcmRmOlNlcT4KICAgICAgICAgPC94bXBNTTpIaXN0b3J5PgogICAgICAgICA8eG1wTU06T3JpZ2luYWxEb2N1bWVudElEPnhtcC5kaWQ6RDY0OTMwOTYzODI3NjgxMTg3MUZEODQyOTcwMTYyOTc8L3htcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOkRlcml2ZWRGcm9tIHJkZjpwYXJzZVR5cGU9IlJlc291cmNlIj4KICAgICAgICAgICAgPHN0UmVmOm9yaWdpbmFsRG9jdW1lbnRJRD54bXAuZGlkOkQ2NDkzMDk2MzgyNzY4MTE4NzFGRDg0Mjk3MDE2Mjk3PC9zdFJlZjpvcmlnaW5hbERvY3VtZW50SUQ+CiAgICAgICAgICAgIDxzdFJlZjppbnN0YW5jZUlEPnhtcC5paWQ6MDhGQ0MyNDVCMUU2RTQxMUI1QjBFN0EyNDU2MjFGRjY8L3N0UmVmOmluc3RhbmNlSUQ+CiAgICAgICAgICAgIDxzdFJlZjpkb2N1bWVudElEPnhtcC5kaWQ6RDY0OTMwOTYzODI3NjgxMTg3MUZEODQyOTcwMTYyOTc8L3N0UmVmOmRvY3VtZW50SUQ+CiAgICAgICAgIDwveG1wTU06RGVyaXZlZEZyb20+CiAgICAgICAg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'; diff --git a/packages/vertexai/__tests__/test-utils/cat.jpeg b/packages/vertexai/__tests__/test-utils/cat.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..2c21763bad2b0bf81dd3b4c2220e8595d5ee667f GIT binary patch literal 35160 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⁣*j_xe#Q2p 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�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

oHVPob#2|C>H_4WI7;XA?cC0%{_C4G{cOod=0JA z2qirQukMXhMw-vYph*jGELcs)G@BX(Aa9ba*{=Gi8 zxM4Sb4WZ2g_59Bc=Z5cWe7j*`6odF7;;6c^`|9YX>iiAj^q5Hz7E$4slaSw1rPGE# zlQYt%$pAD5MDr*64DupE$nc~!(j-2juo%cBY-(b_f%C)`-rIKO+3z`B9fIk-wskHv4iX6hKB*6wkFnm*+;N?1YivuOm(*o& zat*Un*O1z8?tC1~4A-aqIU@<_R&W>ju;A!K&+(#jv@Y|7ph;vJ!vD+KeKzTtrH6fg zfCjo7jhy4mV)E{CcPY^#siI`rF2^dplU?=;e;IxwRmm>9TuQd8BvK-mD{kJ&Gr$aT zMxz_(c57Yc3JsxV_{wqP!qXMG{Z?qQ;xl-$pp8~Ka{P{ z9L0=Q&gM8h*7bG%+-wbAqy9CVq zoawn#(5?|>aklJ#Yp&y=DHUL?VjC(NGJ(J^Q_&aQhh z!skdcMvUfQX2GNTcZXC#oSyB~^Vs4T0ckqV7a@}z#(^L?ybzo35RLO7u4{(pxo1dx zBAuNB=`k1*(OB5QPeWt}XPlN*r0jDHD0SF4RS_Z{c@QJ%r~AVV&)VLb@)9^qF$?fQ zG?pZxv1^=-_b$%x*e3&=PQ6xivNgm>mw7$oV zoNTQVnuQWo59*_6x1wW1@GyTxyz~@E@UZm(vHy*^|NoPYiO#1>GXc68D0fV+FF%aW z5%>7``l}1(bG@~|GqdPZj6kDu1GM6UU?Px#xR4;VcVALxk`fRmVvbpCZcZ6@5=5q% zMGz%VK&TO)E`oqSEFPyKsdwaPVV8(&XahaYZxB&sBKwZiV4t%2Ew}}c%BgB=*juZo z#v+I?019OFyh-(LFJK3h-_vcv<1&6&9)oY z%^X(_kUO3C_eN525PtVC$NeF+=VB`EwG{KD9U1>HTheL})Jg=VBw4|av4N8i9>fw$ zMhxb4cu)9rW!Tn}`7q@pXsBA~dDQ%CpZ&D>|Dm>|qPg6BaIZq2(~Ad{5M=i= zMKAbyTi<_5C>jwsH7FFTn<9Dc%mV@?SS2Zm&g zjfYcQ---k6nzgc)<`~>D3waqA&Yx}EFofwQCnbLZW+jr)nlS+;4B{aEcORbf?>kyY zH*&-2#QAkX-breXpT+{t=e)Wlq=fj+r$SBxL-;)FIlJ=i9?Whgc@fP1owDK1*1{R# zhv=vs^5W9-fukFTMc{3=_%;`@$bB)M#iMGgZ&g!??}0>AH`0;nMJ3i1*K^#P6A|%I zxt~8*nn^=gTU?Hdy9U8on91gb0?lT%vBrT5}I64-BeHeRd*-~MWv|dLB zV{kQxPd};nhXn1~p{DjcU1-4_w}-@cD$ddR9Gppp{q3OSnZ}5qAp}H+$Os<-BO0pp zqttj~1RKv_w>#&29%P@X$1{>MtLJfbS|kXrTOI5s{;<6K@!UZyj&>KI1xO; z^Bhco?LHB>XonVVd>ol(o$Fe2z<8M|#>L^tu{Sg2+1d6y?ZY_u8K(PB#Y6}iF`qtt zrhBE$wJtt8z!aR|4u8vF7CqNF#s?->o6Ceh+avS|USK)j12gpdi^jRH5_aRXHmj|x z`_LrV>v~Qa^H(tfua_>rG3F!l^GaQkRiWO#{dMK;_o@LnHqzd;t(OKb1Cm2w2lUoPIS`HSSb|{rP^MIP#%}>$VCs)g&8vBj?|l9L)3NmT|KROE{%!a2 zFhCj${c<1^Q)mcS(0t%VA=E_plWpSjlH7n04&kVbO3^P$kmRihXlt*KM?KTnpu#v7 z2jFpTga|mkgM9XY=gqg+xG7G2D$n$loJAPGYJckfg=W~65RXI2waPLrI&EYM%}ll_ z1oTHDEi4}=VnF&T4~FEC7>h(ngy zuz~x{oWSlvAuEL0hcSPIDP-t*h`|^jGl^~z2O zn#|JAv(*tmAPG5$fRl{Vlz)S!1_o*{Zq;Z6qmT&Xx~mb?-R5mw5IlPq(=@M>sp_

(|HFU1yzm=q1mXoA2C%N#Ne#;}t_QzhQ^>>Rk83B>cID zF^N`SG+^Tzzd8JdS-O`wgU8Gy6;WNS-@d_}&6GGU-o|fose=s$T#D(Nh^5u-Ncc{>76saZ{eF+7i6C|aR^c#aFQ*%cy+Q9kJR&w zjfnH!yL)FW7^!FAkj{Z~lLELV2ab#_(g+eXiV^COIAKvAgm@!Cs8zBMh@Z`h8=HcA zuD2lwMl;*h5LEvHzxjiJU@?=*Fuf4dEYqG$2xzC7})2Hs%B9zO^kzg zc&N)g!)N!E|4)sVHHX-rJ*n4UM2Fa|C2fjH;WMNK69{9D#_k@F(I2>8yLN4y56LhA z2N+Vljny^27Zq_WhBb!+=VGMQ=8gG~gzLdk($K}KI5lTw6U!U$XWaZN#>D4?$T3pu zXwDpqG(GqAJ=h(O$Q(2Seg-%sw_sWa?Xjn(sOD;25w7){56lGN8M|}Ljf8gH&~7>r zY<#wU#yk5);1RSD(VzYNmy0to<5{B}8HPDaF%_-L_9Fh1$Ip+% zAak+1mi!fFg|1ip1$+bP$YV?+Dv6zU( zJ-s4)QU*b=2T5S_@Eg0X?KX<kMKv`M$fZDG??;(V2-W)V8-%0Y_TP= zhuPsUXZHq^tu1vIVAS3TF$o0iIrBj)6k_B=UddUC@Se6a&xzD=D)<*)UK{rzmEo*0 zh|zO!QtuTh9cW(*W~4cp-^U++G|Z$gzPvs(SNly0Puuq|WxhF1*{T@TiFOCx+JeVX zO?WTrCBu}tt3nXCcBTIE6SPi?-Dsd*0DEnwGui*aVIt3YO8?QX-!K4I2DfocuhR#> zv}*Hr)90B1w3_tm{$wNdW-PEPNL>rIS9oqG)m_f?-ZD-+jTpz}5y!dbKlhIA15%8m z^TzKX5cLc0h3?y~GH!$|xoTClt zaCpdoFd)!Oz!nMelU8hBL}XsZ>Cd?jALe}@%*p48kx(DafjVpJCvagRU{D%hI1wY^ z6Y&gVb1!4T1TZImmpL^nKoas+2=r2mko-b}1h=L? z;L6sw_H)RW3U3Zn>RF0fPiui`N@;eD@%VoK{wYc`5qh8u?=U9gLgX`{yVp$Yh#9l; zSvVSV@VjTw64Y{#>IhwJhgYFza6fs{xTd}UGX32D%=y#dEE7(`*3`91=OhA0j|!#b z$BR~oT5OgKr)}E(d*xUJFF4`=e~!%M7!S|UlqpJ}q-m^ZKuQa;MvZkP|5v>Nx3|Ix zxYxDaV9?sj;5y-u=7V0V7_qrVd9|Uy9pQOp9j+W@ant}B_Ir<7$e~boPM9j zD10=|Z{j%jwD1vnArHW?D5T)v@Gk0nAtFe^KBZDaUTlN2L@^Jktx=;K@tvGFqz)u{ znAGuVkw!5~2;=i?k0Vr)aU4DB*igG8&K8=AYMeuQMHC!{h#&G!607I4i@RnZV&|Is z+CH3`cEZM=V0SE{L-e)}lZEE{;n9x}Avy%4awAO3>{U`oPZ~td$ma1N2clOJ3&V(ge#4M3-?e*FCg;(kq&%%_$^jVgQsgxlgjC$B zMmrFs-^|V5@j6P4rkmOI7|=}Y=Hyy%wN4JWWNb4$dy|h-i`tAoG3=ISfKP-*;Z~ws`4o%B;a$~op z=Tij6dOdnrE=JXZV;Aklj5+IeET!dOnSb7&I$afMt@P}&{i1#WgxKD4jK06EHq90B z+8;iJz8x>Q$$05dJ8LmDI)g*M`c8%Uf^`KT?Y05F^n2Wo`)K8Io#F7OFa5Hkt*x zar4@cL{w8Ptf^;`R}ia317uDwTvG`okzmH4@LOXV9GB5%@_~!5p3j z7v_=BpzEABYa@MZT#|%x@+FnQ+r5|(m>32C?nA-_SVW`#-gsBa+I(08OgQLw%xtHS z?L@Rx^vML9=kI&t=UecHC_T&lJ==Y{WDv4(ZneQA^Jr!O;D*Tk4d&YU%+0Zg6aP@Vw)5R(=D!b=*g8i+e0 zQLU-57`LP$>%v)V0=fmU`;VPEzPNDtQh6ia8cg6|5`Sq4+|!uS0`?awMD{e{>1a&} zoR^>eIxjQb&abZd#srwZ9IxZ96Egr<$E)9cni)mEb*_aNf8(d=eAl^@U1ZnO-I{|Q z#sIQ$sP>!r1b8qZti|!i6rcieypXCwZU;>`#^M@Mk>uBDg_@;bJR-SYGR>3JU&$-- z^X<#_;<Musl-omJaX`UwqCM1uz``taM#O5GEVABjH0O{y=bDcDS zskVWA&XMQd#+jZoV1$@S3x12ey~+M3aWDde0|}Nx7|#J;4AZlR*3owe3L#FgoJxbu zWj-I_ON61OLoN&yJlzA#NGrtTFh6l-U41bK4lE2qC7>}mj|cx`w(WN2nyNz%EJVOY zcUT{c+PMf3!5nWr@7}%BZ!!Y&xW{ZUISxP4(=%lHF(a%kc#zZ+0W+H>=EUFO{rsD` z81uDjzly;jV$R)3OM@%sW&@fiuaQkoL-&N6n3Z##M=KE(F&67aD=|I{=~#^T_x|v^ zN#0tBg=WkE;lHDe9c)J)L|T%$S__2R&0_>42DCr$z(_q^uc2Bofz~+BZGR;lcqUh1Zm*K0N)$E3&YZ?EQ1+FO8mmb0Np=J9ig9`RPvw zmxg5aO9$-xIRQEBwnAb=^~!NI#m(8tr2a}k)BT5iI@H~gQu0erM3e$A$o_9Ug{NXjOc#wY4jhz_Vi((@45iO# zIQckF@K*akoys9O!lohYY#ty6y#tsS!h=sUNrEDfhVOO4Ns6wdl6!yWRz*T-88ZWb zoGR2D8hcDkXi9#;ER5+!QfcI)hvW!+ZOt6*=JVB9ON8Toy3$&E2u_l7c4AJLjEq4< z_@ItV9G~X?=)O9JCqrwZ;UFl*I@hmXZ`0DR2QiSw2zMh!Vt(x8M`<#gwoI0DCEkQK z61%4#KQyauKD{bRkoz$+AvG4KS()ZIzBWF!kUf!SePy z@22gp4UK~ad-nXrOj_;HIK|~TB&Zte@uNk$V#cRVovHO#1x#^x+0wq| zvA_C&<+^8uNBh&FQVfENxlg{4vDkmje1)wRv zen^b&G1ah7CIJ-^u&&2lNeCR%qKLmgcu)_&qzlK5+Iz^a51~*KkFoKo{L$UU0#VbL z1kX0^49?E(#winB)b!1+E-0{9cS`PUpP~1?{k+H7fxR6 z`?&PsntztVZ7=xs2wIPWc-rD|s_T=+bv?wl;p3CX?Mr`K?}3`5>~F)zI=}AJ2Kv^G zJI(!Uix~q6vE!nXO4t{)3MHu|mV?Q_hJN$Px5n#Ab~h8{amX|XHYsJ^)OpgHra^5* zH1C9vny22VMb?Rv^#d%6(t+8~aA-8_<1gER#&;M6Z3_{Iuuz5V{`1KvpAM!%qEhGW zW&a|;-z_1k$4pWxq&)u-`pFa%#t9KaaoQ!E^h@CefQA-)nV)c&}IN|LV~J6W@k(@Ejf$nYHDNn<^G z?!w@@5DfRFHHcx4aGH5V=uCo%b4&Ye!XceWP=(glmq*A-b{)-wA7C(mxV{52OwOa6 zTP?okiz6$G&es(rBxl`Teb%XF7tWqu+)vB+^3xA{bk$)OPuBA5Izrqb`%%6KKu{dV1XGjWa|UyBK((PY;hJ9Q#;xLtz3eK~GVN~|w#eiZ?JmGkm` zS!<7r$rm14j!=1uReuG(yvP!vG7pwdOpc~TiK*SYbFUWAshX+p_KPR|ML-5a>~|`v z%C?ah6dVvW=O)jz`GPI`5#gRIZB-Q|6&oBO{oOWiK{R&6IhAz3)_Vx*d}*%iUeb=| zpK2_hfZtzm!S`^J=_(_t&%p4M1D=m>BrQBEExKiH1g~7zW zieb_ibo4>=Jn$N|=oyLVrvt;n%>6D&r#=(;H#%Vu6b%3z_Q#YRkK2~*z_YZLq%1gT z*Znk=oCdNR%xDtSbNJ*xxf;8>PX+@!6(gg?5E|yix%#3FKW8m#`Ct)|d=m|D@n%j_ z8kIEx+iCMq8KaV~rmer$Uc&su={96dNNhAuyE@T0w_n8c8;53}E!x2K)fn8q=Dxd^ zzo>1+VsDKwT7&I_?k8gR49DxlJ-+Cl-~4UfZx1u{_N!WJ-T+xp8*RIO~*@C z;A4Nz8LC+e$SxKGmq(58H);mLdSQoCov6MRjrugQ^FlH(+ZboTOH>MHQlFKO8z+tO@L}q_oPlF8-zxnOb8uk>wHPt0 z=Z(3ow{G3boVeTjuNTL5bx5`$iipsp-h-jBi5w}*RRb^*a9ZGNUuL?TITwR0^qQ8$ z?87Kk`N6LO`>%)fv`&a&Gy#sWw4RayZp`S&QQkd0&g;fZRfRfHV?-x`oL>I(9nZ zhkTr2!@lY_Dyr|OxYxe?Vo1B$QTH^}Y6{))peh}RutNq+K;jTnfv1*T#wm5=*r|rh znN>vL+MUJYdli73I=1-1@BD6Uy}z|Mf4MNtqV7BUq?}4w&7Ka?_hBkAlm{eLbH!j( zEAHj3WuHymjs1CJ4fZJe*1n&NLDjTxeV1MdF*ZBo*+qCSDOMBF6x%>in5>Cz(iG zlFRjW9Qw;2{b)SHJspUb-M`+mT;nqY_P!kolf<;D2GQMRR`Si8Mm5L@ND*M-O7Wbqne~l5G@}9jqI!aQmSuIjQ^bL|Bl(;8mpsk1}2MXVxy)>TdCTGBs#Kv`Uc?eiDq2$;MOwd)ntPNm>`5 z;o(+e1yA@5?id5@c{e`SWkl0HDaLn@%5QEnt3q`Fk|KpXyKQH$l=hp3`8b?=S&g;hh#z3#6@d|EY3s z^6DoBvXh!%=fg~)x{(u~bnuL9dMF8ao!n2-Gc9xUE__sZgX-+pWHJKuhg)=gr0@2EZA3pm^|?i!aNfTZ^fD(7lf2DBPF1uw7#Z9RApe z5};x}=Pt1AzPtGGk3U>|`stS;&rV2qDTLBvZ;EJ8J0anS!zYm-3>6h@vknI_PaTi) zJ;V^$t|Sb?M)0r04-&Z!Gr$D+31lNuixK;J*F#KjcMp=xeW?D3J>U;XX1N^@en{T5 zJWI&FW|(jc(+;g6sfdZY=LqjMZ->vcwB==cHr~nsc`KWL>Th_k?+zbXjYx|awH?-> zL(IU{DfRZ$`4|J|Z!lcT2H%&|-7h*rgW)F$4vm=>!*}$e!mrh}vOiD0n`7?E;^#m6 zq`!|^ug8mbTF>LBuY?;ynJ>~5+9)M|(Ea=w`_t%_mgFXstP^p+_x?LYM!snNlDydM z?TB3C87=Sm8o+|aix$AwoTm!Ww1r>`u3&CGnJ3`N&c`5`Y0SFEkJ=k?F z0V3e(l5ZB@z<9n7$4O6}y%6ynOKO$$QJF7Wo;2Q06VN4xRFStmAq+yBvfKb|m;eD# z(uee1lAlUNZMBfi0DC}$zg@Oz z3%!}zxRKhPqLVbez4*uf^iLK){n>{Rcq(P;+qb`cwK{gvw7GY7_F%0 zN_7PLNmGqk)?y&}kQRqJx10cd7!o+kty5^!eKQ#``3RDfMi<9VmIM}))?fcvn%9{# z4*;8l z_saF-*dg5TG$7af;K^na$;&$8@!bPZmiAE4X_so?RO8F=6^Revj zU;cD&@q-_HP?~IsO3SJ8+a>HA)x0#-KlO7p(`9+7@KDcVy}WpyqzRGN$AS*D$R6Ha z{N+zSTYUU!#ZArg!Gqn!kN@(sq;{%hV}0>l6rjqzV-`Czrx1{k;G1vO({Aazseb#~ zTr|&cMN`6-+l3ve(;FK_8uFivWSG=TNKOhNCVl`gfnc*_!9+2D0o$0!p|$26y!wAI zho}1GCvk-7be$OygRWtwc+Z(9TnC1jfW!6RixCWMVMsYeIIEp^JA#}_n=vX?lQamp zF=+*~r_|)`O;R!qIDikUfoVxs?lo&WGXcG zC#gUG^y|f+|C?Xr6s>tlHv1p_y}z+I9TOTRQPPw5eJ}C2RZmLR8BOa{V_bh)wdsS+ zwroF>nR9;e!0sUiC9Ps)6h|vX_Vd)GL^X0xr7*) z@3wo;Sm*Gc5b$>}4TwNPYyCRb=Qp(g|GoRAf?MT$+DZ0LyNVxLAK?YtUQbWp-K>fSi&Sjy7xWRlVeE+ z9H~$G0|+M&^-1QPuD*Bg+#1L_)T96mNpW3_MADEQg?Cb)ss5w1=%y&Ka?>TYg^Y$3{?sK9|^#n-=B z{P_=m*?x1X{mvy7HWxqt#r0ZP{h%tbl46qbtE*HGD!52WG*M zcjipF^hy8w_bR7tx0_R^4lmw+?}O32ln~e-ypI%ZSxF<~16Wy|^7o$x+N+ zFaLaT~pxj~k7@p;b`dU)dJk4;!+DG~w~k@s2c> z0YJ(`kP*&Ok}T35$sXJLbJCm$a7QjgDD44YQ_$3d=N6}l*cb3d>ZfE7jv?{dSzSW> zkRLLdJT=W9iH9I90<8eUfYc<+Ac8)#|E6xSseCaPt(*{m$~26LgGaPLx7C0Br~hp6 zd*6R^apBy`;{54yvfFkAQTF%}KC+3MXBy#%^{0F{Zk=07>`5coDCD=V*Y#)n7ysg4 z{Alsfhjp@DJ{1C~1t`NzLPw145B}gkTfFnu)xo&r&o8BokP-*u?wcD=k^qq0UUJ7% z#a|>n9wiaqjp=D|y;XxtPK0|uayZ|n}76o+vBgeJA?8ca6$JKtLAmP|sUQ} z`iK84#!(os%F>;x<8Buo<<&oX=0ek$nzhOc5E-yDPpdZOmvufg5_2T1bfMBw>Pazm zyG#A;zx_weGhy&ZBi~W!002M$Nkl<%--ZU@7Z;6J^xg6DDBv@tPtmlZS*}!Ml0%fH%@Inl(XzuA(T_6&xI(pf-a63+&aeqdE_PNurX7y zSJ|^NspPJw(l)W~IuLJexG~Iu?k$~@_Z5TS<+qJD1S6?QZhKM8vG_Vs?N1T+b8BvqMxPxU?!GqY>Vh$xs@NHIXuqU^Sp8!7Ubt8izUi^Z;*BLaS-WSlD{K%HNF6b7#6aNI7$bvUhHEu=pfyufj@ z>a#P&h&SpKvzw_T)_o$shM2jMUzvlnq_d@tlEfG!hKK<|OsY77H9y~TCfUe@hA?~Z zIsKSjUhmw#F$lu>qvpT!5Vv)n5tnD1NdPf1n-OF0Sa|!l}*e_j6T!rMcPb3 z#NC~P5#f_GKs_h;F6uOOfD>3_m?UQFf8xZnvk<8|szp<;CrO)-ih;cQ-kXJNh4>cb zd%V#)AIfw~lZvs+iI^fDlZRd(!u9Y=Le{zP`dnJWjT;eUKn~bC@rWpKF zQY~u1EK)`2yU_u;#4DMi_8<&|#xoq3}C*Hr`)Y);&(F1Z)qI!4V^##;Xa7(K!ia8$xPBL(tX%Nxa^VI zb{#r&bn*0g(mPLv$wE%j0kV=La^n#`BBPfit``NFHr1%k!`lvC$=8) zWnOIML)f9iCOP`2S;;aBd79#Oj=z$a`$vnv_={i0gfeN=K$O+R zu{7o#*XEGIcVE;IX=JlyW{d=v%!{QQrbly7oja?!V zgT;p*{#k2G%E(I~NYrM8qu~^7jFv$Q;1>atix>CT@bhsw2*0e8&--Ie7=%PAy2F7O z@UeU~&qr_msxMp15wdIjMKwnKLm}I*?euh}<~6~WKvTGcK_X;A4Ko-t5j-Vi`Tor5 zTAl?rxQbT#I=?1IZ32PT6F!hSdoj#W^%(5O+I5~+LJex7!I>Vd|M=~-HTK<1i|sUCg1MWzl@T}ia%d_RwcDeZyNkl4aLsdy>UTVm5s|`^0es;g{r*)(PBl@)v zek;eI{)x{k`f%?9#idUiKb)O^EGBnz5VNh;``tGz1Cb7;YVp%?Rwm}@WY8fRoV4(_h^5{d^{)8x<2~odIbDNzZIH{AcyAG?>jLP3~eQiZ@p`FV;b6V`JwTi3OE11zNp z?gV4sL10=ykL)i%(*VrHZ?7G%GlBW*`5dqF`yM%Q4=2s{n17sqd!6`SU5t_@;-|V& z$2lhU0!o3AxOjvdQs{_+2~epfVUfz(rSjZO94EB%iL5P1OkMO`ZMemW@7~R${*;Mu za3pK6MU98&pJbxSy8mbe-UGbH6d(|LA96xwnOjAZdN$jfy4=DA$koQY6LVS1{#rh0 z$Dj~C?{u1C@zz2%si3iq-k10juo0;+k+f0WT+f`kPz?7%5rWc*yN~i(*=iU`9*#JB zvEH*q54Ik*A7DuyCGK}BWJQIk&<3j6Y!eu+g$f)4^x zQ_$>&gNd(0hzb%RPE27ihM)(@{LVzUbougdGWm`}&}Zi{2QUEFV;UF<@ z1~A45Xk^E`#(CDs{lHdjf`pcf7tgkA;XzD^w39yT846$2S=_mIr?o9*Hn<(Hmg8g$ z&N|bk*t^zsbxm|eWu*{t_X;=OeB(Pw?j!jhc53%sMd6`89ge!CJpKDJA%F3!pD+H& zKm8}&>sjZv$6?Duz=+fDt`>B{5Bu+E-2k?-bOzrzhX`Z!O* zS(seMA2vR`6vA7MK^X6P41Te=H^xg##{A$U-vM71a}3;+UcdF$J29El;~a3);RuYj zTc2ma*cvcb)-oFpmQoI0Dge>dOndYCZa04Spov=d(Da+*72O5{fasXNd;N8O@Aq%= z0Ol{}*ZE@HZGJz$Vifav2%=#QWZA8C@t&lN99xoQ5TZpSt%KvR^ zG!##tV&YTUtQ~MR(m?h?ZX1_2gh7DpMfV%Ng_qKPKR7*~rP@suewA!Ku9 zsw}0t>Lb7ae6~^5SX$NP%O?l3qbYF|@ifzpo>k;T#YemtDhjY3h?*xFk}w@4WFcxy zf!P4TA(i)Y)BsG~M^+v69b&q-*8%gcb=?%gn^LVwdLb+hO#T-QEl!_~7$SbmTf~D} zj}bguFIgtSJrpxnac5)MJ%wb!lFBdjcgYSJJ;Tn@HZNUU%Y0nDn3lB~lleS?JyPXo zOeaZx=!i2Rt=;GtFD#e~CK3}CED z_p%qDCLc5r(H(5UoQ|g*Y;9GUS%VJuA`EP6FUe}X4P$TJksWBg4h`n1$*kHEW0%^? z=J%PVDyaboihdrI0xC`p(I78`Lc}DG?=F?-bMwa6!=npf)%)|LLUKU*K-d(qjBfxfScqTrbfi{IIh0c{j?B^9wXR%7(;re za-}j-6M><(rT+$OUd-{8aLj>Hk-rMD|7`K|pMSFWvp@fFHt1(9aw>Y+kK4n!xL90$ zyQGj9!OEc+UdY1tpnrTy_hL0Bc_8H2my`-U2NT@khv=d9Hy8*Uw^s@)3r$VLf8c*!Z87O3h^0N-FD;!a4O*2kiw4LSQKoye1fs`JIOXzzgdlsF zt0GJ#Cbs_FFkj8n{2K5KtX(IcLWZPH!I*$vjA~BKoT*g$W&Q>K%E{BEg!gBy2usWo z9t=j)J^ZK2)_Oa~n(PGk-FhAFOKWua4JFPJe|-7!pBYbH|L!;6SAIO*$PX@cx1qQ`5QHhN-U<$CJG86e~MO>aW5W}HFv1kbn4iCX_^6%eXan8XImEG zPU_YDR76D)R3=g`ZLLM&aGJJ#+Mkz|U49Q@^gK2fN7LCB>2`%0yx&TB_hLj;T@Fd- zi5M^eNW+na*%;M&YtOWoHyKb><~=U|{_x=lGOs+z?@%>15HS=o*C3H^2_wrf*nit; z2gid`5={@J-^zqh zNJK)6#Ewc}N7t7K&JzzM?RWEss1o$I62X|qikylhkp>eo>JTBs(eaixrF$)BDIdtu zq!E9Cp7oqfkWD@W_}t1*LYn}GDQYrp{f$|OjZcw*o||YTh3v8 zGj-LmThEKuSm^KFcPl~8L3!`qtw9KP?%d6cn7rW}y|P4CVwM=DQ193;AriG~?(;&U zj~}-oX`8n^eylyXEC7I94&2T_-EZbB>XAA4y}ZBB*y@opixcOrmfCqK zjvW%Wu#cWr6jBbqklb#-|MK%sCutu-L1OA5Vij7kl@pazl!1zlAqblwaFXau>OUem zQF!cmY@d32`iWS6^ zPCZ7c$=yER~WbpPXO^uLNRMexsc zKHi9#9xQwBQJT(~wlFJHS+2l~t(>ln_jrDj-Tl>v?6od?Pcx@#UzSb1l&>ZgY&h=t zXP5xOY6;wM%9ztM(h#3l&#_&^VYyxnv_Go-ll68A3ct3C+PwAl2aC6_E*77B{9&f* z^&G#pFFRBgW1EB=%_r4%Ug$)c`CD(jli8WGv#q{2OHk5J;KGGV!Brhb;mLd;IxyY8 zCq$UmcBq~g;lzP*We$y=88y1eyqoFoz=6z%qd7Ly2%c4nzOnOJ4Q0wwMU$`7ZCLjo zI;`EyFn&z~aN)!;x+7}uAo&s5bdZ4)7sVfZ;_NWUz%^+JNiN8=T0+awlX=H0Zjl1K zy-zJaq97fq&zMW8mZwh+l<%e8MZx@6sI1!{}I0|Dz+9CQy5SgutPPP zW*ruUbA@E1j!|EQYA_LF^gUIVIl%PD{9l|Rky@nNcb_cYe)m!fUJtucgO3)vBxoA4S!P99+87f-{>}`)X=2lk8TpX|Qw-dtaQMnkk3f0bV4WJ@+tPn_&1|-MsIt zOJp?|0L^7PrmK8e7%-H{33xJTc|SPUG^FC1?aZ&WBp{U^GlGCUVtBm|rFC_SeuEvo z_FHj=KEz}*lt%e*%+h&pzkO!$$wyzLx!iBu+2@%wa!xK?I@5FcqO{NwQe`rzg3KA1 zpJ_FwEFGDjLDb1!d(xEoa_p|eq3cfr5@v^x^MSm^YeySzE$lHk3>(Mgvr>ShZ^DW$ zJ^iOSt-Airp{Z%+nue9R#@vf@OM6oI`EjYmI$G%(+;DOxTPuE=G~Q9&Apu0Un+gv+ zB0=12N(e)(gk<8d0L2~^Uxti|Tp*n!6h%Oga^wi~8@m?rABmv!Ruhk<_7D4&gQaVI zb`GY30a4kg%g?Ih(riKxfcM^gGnGrINPR(l3!m3zIK+fl!xzx>cd3(k>~}FUDZx36 z^lLk`a%Az*uRh5s_@D)e5R>fx&42xO7vKHvTWK{RbPY!iCRKz=NglD}Lm?z4L*hSq zQbJRUEP-V=-@@~{1-<#^JEI`wMWv^rD2PW@pe{8%VlcC3&p1ORK~44ccG_WIA}~mY z@cC09qPc@Tq?q841_6OOE_nOg3#q zAp!xuHu1c9vo!6-b~J75kXUO(Ev~-Wv#!2%v2_nuN&=husYxk)Rk%p5JfHVgTLmC0QEgUpe5xMAw#frse6?$=^f)rYkMLs|%*@{OkMa zc>N~6We3V)6L!O;4&=$Kd~QEQh9TuxPEFjMA3$t>N&@)M>w&8 zLq0a-R5nUYn|A6^8-Njz>^E`{a!8Tx7{cOVlBH+*3>mex!c;H;2n?W11q|e2QnSG| zi^5cQA9t_zem{35fVMSMk^sY@F1m(KB6b{M#YmdxJ{&m}ajWJeOSpBmi1|{6q5z34J%Dcfaf2OG9IBfrqu= zKT&64o<3tB!hSmcXfZ|ybT0>|p~w!?!ShBGpz%0Iq~K9$!M061U#BP0oF|!J%$KDS zf@qVRmCx4e36Q-GclOta1+nwUyH`U9Q9UWxdyV<-Mi3$en&Dl%aANVj-?_T@#m|40 z1m)|ASupud9;yFdLStWPwlPaxbXut9ns251i2sj}DxXX1D6Cwdda?avf7*mnUPRpI z?0dK(?0WJf{H3WBR!m)dP<_D0(ds;6W*NVWCz*6K9=#|LEC=3dM5|j6I>BUVGSq+C z56x*iZS9x8s+j5i?ZE&Zl-sgetp9WjMJljxx*dx=6YTfL939r?KzKzX9X=x_bTf#Q z`|lNfj+0;eXIAxRCh*!%V~nu><$>_MogO&e^+m$q<#!(P>bGxx?aU4&o(Sn_NWnp+z~xCH9-CM|M#PPC4gf{i zuEBAC@Av-ZAZQYa+BK=H+j)d<4DTz(0tk?v#KJ5*-}#b;)@loVumxJG2LH!D`6Mmi zQj1X?`HPZ>T8KSrRxoHi#^1iAvjsRCaLtBeDz-jy^r?FiQ^1 zLycQ50Y=7QMQTIbSsOA>+ScjPY#|~9!Vu42IA54j+41J6FW~GW!AK?=l5v2O%ri)X zNWHf{7y}02TK)mp;J&m2SfB;=dc=W|(Qr6wIjYnU+`E0N@qq*FjbEqUN->_3C*1{e z!9)-$Pk7kW?@PUc)mG)>2u$G%E$E%M&n`ar;QZoW|C^tVOD|qLzPNJXWC=6fyIcv5 z<%6On&548AUI{{{gQ#lPr2U$4KTF%Xd86KZ_vK}%MQCl)4lr0wNjSCtNW;a(D&j)u5ekH0l-XZM7&A{7PRPbMGizutd(H3v%aI1~S1*}TEb|;mwg^r6;_Y%~pZ!v-Hw3K85`sX1xg1dRMXYD6B zq>;WTFa6TuFMjy{L_~S0L(Jt6ZX%qi5r@{(t0CORV2oOSW$#C%npepFs3li>eSb?hP+T7uBvlO!lVfVzC8-)nVAp2CKC>Vpjpiv(?81HTaeFix^{#_u94N#mWE z=o|G3_Ny7G4Pf5OC3PK4LtBbjGXoqnfW3O` zt(RW>;ji*NJwG_w!ZrN9&jp-WHW93Rf_7&PH{bu@-2_4=Pv#hN$=sP1dMhBVg3ZxVR?PkWr4+j(2ixUQszC&wp=(iB?nC0}(OslVb_08KM7^hF&z2h6gA0MXg zJoeYV@AG{4e%OtvCjRr;O?h0YpliiJd9KG+-Z=sHl@+%e!ri%JkGiR#=ZjB2UR-J$ zYEoazClzbTBV`J5=qZDxSu7u{xne6M{gXz}qXyyg8z78%-#ZeE4`H+y3u8(nuuC70OuVBmSPBgE%PDP_!J3l8>eQ|@_k zCkp+o9?xUWLtLn|vSTgOWC2d9ewi*I>F`T54n&49XDP3QcMEVY?doDtiPe?5D%}iL zNq-?ouvN#QTN4dM0*rzznSfGkRX2Ja>9)bhTEa6vp#kP#<2&}SiZ{tp>NpN%UeYx+ zWBtMRztwa9D6{RW{4Bqdk0AiYoR)ILiooQ!@4QT(`~w_x=FZ&_sw|drv{v=I`0D!l z;`1-=H|9L-Izu%s%RBb;w{PAZJ^VtXv5$h^j_!&a$hDX{ zEJ<$`;aEMylsMg(>Rudd2{|~(Pelv-#m|2}a{f=By4bbH!~bBPd9qw7GW`v(#cxB=M)GLw%$X`2V+MQnqeZD;7B?I^^SI2!F?){s`e53h$Hz6f#ps^Q+ z-|n+ct0`QA_=WpML(UvI)EH8Tl>hY8PqJZavXIS)phq zw^@hIERuml0Y;O_m#RIjV)~mLh$C4gWK z#cU%U4xz2!^Sm(Kc4`yn-)@@Gqg2bIg#qtX=E|vf{`{#V$B)V{C_ETa*d+$s-G@d7 zkvJu#t=_mfRr|>^cMcU@SeC@+5X2ZTIt2gTd+!ZN%pAH~J%Cs}Vty9FJHVfi-!5WRW=YqOCzT?QT^;sN)YtT>XEGxg!4G?F7B zdAO0@)N?pGu^H*x`7UCl+rgZs@T9D}rVv3Oq9P~V=Lg^W*5Y@*qnpoa8=2lrir3sF zvuL|i>%E*#D`{R^F=F`u8S^oBMlaI9gpq3kR@blJ4=*LH)!x2}Kz^$mwK%(Y>5bO! zL>gHgrqVLizkKxZoyE;6Fq=sYV(-)%zVKtPc>HK1rhQ{{LLz$Hr zncRZF(t!!eFindIJ`nLYY0_E$p<`wQOaJqK`N1E%!F&^l?LB)5rA$)5|UDsnlqL0lk!A@TQ zD!qg03xQ1W2Phz**IE+4NIaoVp|zs)S$XYyY*hzeJiM zF#6|tzMCr77X6{w`F@z014rRzj?{21<1;*rDV1%xSBkaH&RK=6Md7>hG>#l4fO687 zPjL9CPCJ#3Z<=p;2%Co&Kl|Cu#h?AzXW?bpe#g%Z7-M2euBqwz9DM6rMTNp?PR*fx zH7*ekyh@MDlWD88$_x2b;1X@pqN540RWHEwqybG8hLfg-k(2cL7@`3_6kCO#Z}X7L zmoE>-t=qIrMC?#u(IG`a-J4cgj3!t<&L>gQ9KTXb(J_SaOOE{wJ-?*w*J<3j^Jn_$ zYX@YU)UVfXU^25GfaDx2KduUCT6jcB>Nt-yJbci$*X5hXvi1FcT*1r394_nGwj>U- z;Z!kf8!(&*nMoTdn1BTNq^3IFeDjS#_>g5n&?cS~VuK-a3r6xG3TmyfK|l^IYA6YR zIt0~~MuE$2e*jy&e#Du?2Lp2jkN8*;FE-ma zUZmZ~MW=Ofz{)wN6<~zc8R3&k?n`AQ{hzL9_r`E&2}}V_N9!}90gausrPeYe;+= zXCrM=)_-tR;Up%|&=xW^ivn=;nJ~N>&bc8N?e5Ed&-_VhZ?<+j+r{M{Z!f<5{ASvf z9Q^YItZK@cl*Zsmf4-;FXKh{+5x1sK!iytSB%UbBAlZl0m}bIJs~XkrJooQ|cXoSP z!1EH8Hiw3)Zh2|(y(^PHV0|RNO$A%#Voksa19vc;)Gx@lkSz3~#5(yEqoY;HKU?`Q z_<@v|=ivwxX$b45YuQ}5Hf;~?`x|f?cl_)9^NkbGg%izmCfT((-W>QrSh9p#Nw*Rf$cMD~lYv3(JYd0ZkRb6>lFJ}AGC9Yq)HdK0b(+`Uo z0}QT7@;m26HXECgM7JM3Rg^R#B{Zv;?EXnJk*Ff`4+Eg4kzfdf1SJVayaF8>)ZkFN@nDi&A0yJsZ7WqNJ*2YbQlmr6Q>Wip{Hs6t;o|czZwzyU^G~ig zZK$b4yh)s#38ae5H?SU!Q+gI+wk{81a3nn@!Bg)bS;0c67F?KQdi={%=Px*OCSr)e zV8CEOWv7CZc6Yl5+=t{3E>jMHwrwF^pYNvbGXZEDRAo&n?XNeo2-V{6&kSfD2f_(G z7B+KwK8z_y-nnzPh8!jKNHdlat@$b@phFTVU(c(T>o_q0xH$SvvLSL92H6-uc$t;~ zF0s7UI#YoJPm*ix{ir;T-Ca2($79xO`HG${epTJTS6}C&*wJmLMzu+E8bt=|VEQr- zWo6#EU0Arh64ibBNPK+vqrq@5UU(x%-<6^`@<-b1AfLs92RS6e#T$iz5w5Jici;P9 z+}pZf)EKZD2sIJcuYXzeAuX%9h86iRPGxq=xtK!Q1eE)oODiDpw@QKDZoN4|#gp|i zKwmrE+x=fZUS|To`P=+?J||u|5yNzki3!Xnd40tIJZO3h699q?l|4e11Q8!zJPRox zr|pZyRBZ&ZTUzCli1tyLK+j$}vM>#5yKXeQP1G|6akj9YAvb_T_d1yy%#T_{=MOl@ zT^zSn3Fot|rP!PR#_LZF-rJj)i19f_p-Iw<=XGccoi}kyAxa+M7}G{tg{{3Be-F(D zECn~U@zXEc;yTkJs?Xt*d6KgTvSith*PX(CN=EgwN2sHNO*cez5KH@NCCE}@Rr)1| znu|_7MMBnt_x^T}y-UqLS$5vz#gBjbs}}uoYAdN<)?+XLpY_y#QknYrI7X>aiH566 zhn4WsY!EScag1Wn9HZ*ZG3Ma}4IYEh(?UAWIp8LYMcVVPFsWz&2%MCX7vMnPq%tN4 z4bD{%!(;vXjxR;zVL9z;=|DRk zKUu0c>|RA%H)FV&k~yW9(y}m#$N6rSs?J=?e<1Pa5pA$AFMIpvB4~r#IewvrgL8c?4){syPUuLSVh1@}~uHielsvl=cU2_nmNhrsu!P7`n+f$4ui( z|I2^%$_Fri=@lV*oizgs;Fy1#5Ga*2;LE3nn9ov6<)R~=wPR;$X_QC5yyd05#H$gG z3@c47Z{Kc5-fVB1J)8^}07Z3Y^Fes_s^?0_B#o%1wwzLvz1g|SHre`+nw>N>kH(7N zT}M4$4|&CIXMX@DfVkPU%`V_YoN-hYTSFaBo1lVmykKY?bO5YB9BH7d?ABIxeyZyJ z7HhA*fVORhoTMSjMQd=2`ov+K-`=XINg)!1?CRSN?JBm z-5hCJIxTG}nz!%JewBU4a(FH()vlXx4M0D?_Mp6gwsAXkwiN8+nI^4&%;V_smEc~I zRnisnx_JJLdPH6xj21jL%LjN``mWv#v^-j$CNpvw_>e@4rk<8%g~kMEuonuQvfrnO zKz^*oOIR@#H9gg3*c>GZ9kX>_ui(g;da8;^xGkDSE7fp^QgooYlNDjj!t8rnnY+)r z-^imFAQ}HmH2Uvm0I&Z(7{IGbX9Dj@9ZodpUOFbuG6Nr>;nK3d5Do;Dfp@fpJ|=_h zMD|q-;9xe^Mh!9_mFh}uWglbPc*({q5vjlB zyuWtam9ZEG4bx?#s>Zu_o+M>sa%GBP1`?Sz`<}`s75G3*0g*=|i7$$k_`BTwmM7bD zt9}MAUKF?AP@%XTVXfCB<9Rk;`@?Ne-6tQw$zUhRgC@n^hfLJ!Q8^i*8#9x}*bsmU zG6;zvG38@9*yI2h*QiYonXTiH^uY(5m$PXnp-){TV*5SHRb8YMPd~Rqu%!xEv{3 zKuzzqtyg1Dt2kKR|IRMcMl1GqASyA3_ICB^JHeqC|E(`GXTR!q8`j3mE8K}X2DALS zkgKTHxVH7sI|95tK!+i7#|%)=wQFC7lM!xn6n(j!CQQ3JUL61?sXl}@MU%o%hPKmv z^w$Ut1_M|vU0r9X{W&=Io;@fZWpdEJPT2E%@S2a`_T4wBz!Oj>!gqm_=U2bYK%c)K z5+Gz88F>wiD!m-C9n4|EVMV1!a1TSQy=0sPiLHmW9v69n~KOi5A+f)~vAA?z z_~&HICW%rm!M?U~W})!+X~ZK@w-13ylUeAe%)*?v+wX-Ai@ zT+N2O8oxPhOLF2+Kwb}wOA(H6;9!nHj5m@q~#V4XwnLED1sPAf%tJsa2k znJad+;g8uY?1@-tR5J2JWN2iM8{hNP@CWtlKXx2pk>17o577py8K?+~|8!@l`$_(I z0o9-Vuk$p<&8H$dg1La%{+fC|ee!(qt6zS$_}MRRDV-ZKT?Ox4%&$fvo1!u zu!UM}!ges0On_Y9fgnap(;5PLqScvBz0R~d-u#w-W;)KLFr6GUd-w8v7r3R9Nc`y! zr<^nb4>gJYZ*hfxrjQ*~>0u$07q_1*POMkyw|i`H^xOwU0a9~Yr2E-O5SglaX!&A` z%=QU!0w8bb{c6}HN-zN+w3>I4!5?p{kfGK|ka^lC;K_)cPF;ui2&QJ0Y*%&LGOuVA z7!fBKVz9t$Fy}@WJ@au~`P$;+&+H^qp~s0bu(HFbwX*mgCG}5ayPhak{5%JfE;iLM zCn?Kx3u!+7JjQTur~Gabru!H7m#6Rk;{F7|lRU=jbqrGE!vs)&aNyvHkg||dL;`VF zl9pc;%RbtEfHpI+6sODRlfTdddoHP6Iqyq*-W(9!k?i)P<*kqWfEHxAHtg1C2+pVn zvm=oaBuUi=}Mt1H%Ms3RTQy!3W0RoA$J?iacT2Pd~r4_}S0@a>V5~H+7f- z*SZeZmj3kF%j}rN?II_3qTup|dSFGw4VN@d<^PD^QZfAmMc-#Ye; zrOhHX^=PbyfqK8#xwm*0qj+4=kQ{x*S`w#@6bZm+X%TP^9GOoXe;m|uL!M_&{OCtN z96b$0WeBtVMU$4Rp&1QTYB|5w+z7r&@N(d`1mOXbA@_e<(>+=|k7kxbu%%4!NjT=z z9t&@oG00wB_8Z3_{+B<*046WLbH+uWJy^lS3^2J)&0jNlu~`!z-rRj#P%pa#H8U~YV%4du?Tc0j&7$Lm0`Tf%Mlv~ z+iFaNjsN)p#7oW|b+~`iVHubo3L1SMj)!d3fvIgZxVUmDr>*@5NT8*rND-LSv=Esl!K3#gr@N zd6u&>IPTzZD0w za4ani6DIH+gXjn3C+ePA!^i_5`M==`-RLEA=*(83-yHLI(~YL@{DzQ7kH(ht2yoeB z)Jq#ANTOJe>pe^Lf1KHBG7=!}ZsuuCx_p&Av=v|=rP_7?TTi0M45LCzpb;wj>iUfk zl&Uv^uJg-y#F&hZCnss3JNtdacpvNio?ytp!(m&995jpl`C z^Rl<(d!Ia>NfD#?#hqV_d$ZXqCJ{++b2cuvE(e^Pekr}S4?B^QX!t+U4yfpN@7|$l5C4h| zOY2GXvE_ExVKk~4F;q;k8x92ZUaNEC@Lti&#hi{Dd-y)Pn3jPdiLRJ~P%CC*i!AGX zH=Fg#OcSlwWJ>ZGFgHXT5CN&D0ju>G;Ydk8Nw0Gk>J3rv2Wl*-c68-fYg(~T_grn= z&!$Zn1177_VQe;aeQoipYP zA#>_TFf6vdeskc$>6q-&(y1}tFK&Kb_)SvPv&DS`8nL{HiRsgy#nN;uSi9?59fPWS zXg%EX-kk?y-n6neu3Q{AJvv@9R3i;pi;v6h+*`C!NDP1$Bt&W(vC;OcU+#rpkLyFQ zUcJD{Q`K#xVZ>ir)2XF6ilgQ2WKPmb;$3M8MFnEC`}mt)WEyPN3NF7yv7x~rS9;Fy zg>)@#+}}+2=oi(_ue}`yaWzhI;`p1sA1C(3%TK@lGM_s_GR@e6NYh_H!%Trxq(>z5gxFeqjTGCih5Vky24yzv1R-`PB6A>o zDywTDG{Uq15Dd|gVm#6o)}Mv+z4H;NWDmcKD)^4dOADYWoH;Ya-`Q~H!mJ=2q%V7p z@BGcu7RusjzGXE@Hx?QqeCq1MNUbOLhR>6zxuF~>hJ z!h(a_PtWl@uxE0SK>P~k%lXRdKij|NKPM|;G#r(f&1Q^hv^UHDLduKu$ZvofD=|n# zNVG{B9EnTX5g4O;bQ8i{)aXk@0C+%$zZ#lWJ^|rZ5hUTd8!v7RSQv*fj1=VvexrL0 zKSYcQp}0T%Fh;PRa49Af^O>19m<}fKa{Z2{p%O?KHm19guS1(UbsHR?qXs6^Za8IQ zT!YcHZeM)$n(O|X-?)a9Pv@Eb)uo6o&V4=7{Gkebb8vr{6(+hCgWzl#6w;?EC5c~(xWjCtC0|VxS@SAnRw01L} z3XI-MR2;p-b;5A(_TTVDk3KAs~gpl{>T{4Cmny_+^f zOVRQ8P~DUR%Ji3jMSY{~eeY-~G;L#hkr1H8jg)IA^_)S&z0QZP9rNq-DSut)I zS~xbYr23@lA|Rn68GutP^}>+450gTpYfKVQ9Ge{u$v*qy^Kp#@0Z^r;5RrBO$vI>! z>P!e~3jiOoYqGJ~E9WEi&cg7*dL3$CBULIZ1m<2;Z_h+9A{YJ;zXOp%68AP1&zl*7 zYjJerf{dgK0^om{B^qZcm?;z@Q6MkLi+ON@nh%L3-@@1sJ*j35%mt&EGyv;F#pm#w z#w+K+rlWqR+G1wLW-eZ>rNj8m!=HIL@ABo#ImvE@z@jRJwYo;to?Zsdk=jh%huEIX z^NlG?sog!NdrQL}_l{smL-3W+Ir29=zxmoRdNaw*cL836!-I$QAIP7xJDjEo7kf75 zOOa23*B8W15TuD<7S(Bx_~6zwIusdA%oyxE-*e3wE@Ot6h&+SM=D~h9Z_l=N<_;{@ z$#oNMO;G~#c3?)||Ni#}lgD&s24Ou0BZ)zdtI30XZbq&zPs)nhNij$~46me~b@a%89_7w*!#Z z0gA?IgGhHX1xl`V|%`G;owA$M!A)_63 zR|2>=FB|Md-E+2DD7R5ugXo{0Z3E5q2iefYs6)&Hg=ci2Nuz2q_O73Z7Scl+Hm49A zZ>oveT9Q? zlSx2`fLIa8EVXCCfJhQXCP^`cY#;<;Kn_ zNG4jt7z`y|G^ERj(8P$`1AYhxV*M|A?>`J|Hk%gva(ZR1F$GA(p%Jv^VCFjW&`t|u z!rU-`%@TKlJZ#5662-jyB9o?gD^9`!5{}-PYG_A1lt`0m>K#!PS-GFH>)3jbNp9!za9h` z!#aKD{7VKsnW*r=T437myz|b`2yT}NyOED;TH{aAgu`{L_AFyM(3~(y zxO$*4;F9{2-t$9Y%1wQmL-Qek%?q*d$-1SZlI{*eO+;pf-5uEl(2#hxv17O?UD*H@$1t4^`F?rXF28~GrOh_l0 ztu%ud-P7+gF)=-!#!zSlp659T+j|IupoVSRJf@iLRIxTOo5sf$%|rn+jL>+GHa4+c zi#o2FjMH!QALzXjUx+2fkIN%&&NL~$Lwjn$UGEPfGe2Y>f;*;Y`DC!cWr)&{ z2Mmne;hu;Xyd>#>$*lG|2SSdJYH)*Oe)m1198D>@)=jD@IRF4a07*naR6RHWX;CuQ zsQ!1#&EP9x`Prz&Gsd$TBRC8jItGA&yB{XxSzzP7AAb1ZAf`zZs)DZ=_+C*I4mI=K z&eZUp<4^hbhPlV2v||^BoFq7BE1$u^#kDU!A8YGc{)s+q_PzkrW9TvU~$8`L$w#K9C(s!6Y;iiq6 zWFe)+PfATbb2dNI=#E@eA#+e-8Rqt)d#u#{?MM}YakMC@=Z7fQ?F0QXvNdWhP++#_oB>iI_%3UYUf5-!8N?hj~sA*jz9UYVSO@6A_5s3&M0@ z49Pi>ATJnTQo@oD3-lnqu$J%mM!*3g`yOGqFJ?fRW9Bf0R>gis=xp<+{qdQ&x9m5* z1kT(+Jbm{?<54q@*`DY+{vPv1q!P21!X?|ZnbWh@OA7e&LX*PF#=Tc=hJ%(S()7;T z?~P~zEo6q5b%YZnvsX-+8HM5UHDGq;1s|-T1H-^9t(RvbYUYm&QRjh|-UseC2tGW3 z`}#k4u7oGeM>q!Gdh6}xcrLh=>k$6B9;26r?pjjXe2>(%=(q~PVE15Cw1YpRwC>J> z%f?8X^+vh=mk$KOx1rzW@%(se)h-EtT87_y>645h2x>M5Ey# zTp=-%3Zftw$t2)8Z2#0+2gc-}{`#{>vi6)e2)OacT690-1!Lp-^2;xV?Jrb%@#6Uq zG)9y}<_Ewy5GNl3>5XBSm-|Srf*A53Xi8_#o=IY?4QHJBd;X~P@A}YB^ zxP!yg4`6Z*d$#Z?hG$JY3lo$)WkU}P9V4u8NJ3G0LS zi~(N56KjD$edY^#`|Y;}qjL@|bJmXF5BLn+Z#-b(GqaK-kD4xwr@lZnCR`<4X05|I z@HJqV?)9`HBs(Qf>C9S^7g7z6zV+7CHoUD^$mk=EgQR{Mk9O&!scaplo;|%=w&JzL z&PHZrc`ESGoCj@&2L$^})8;jfmoL+YiNX)K$l)Y^6b|Q47m@J3dC44lo$iHEG`wD( z*e*F&vMx9rM9XC`9SkT(lG2Cm374}^>*nop<0Vpf9%469Koi`!Q#^R7q?h9*m7F`T ztICCeEVwdh3DL9-$Hj$$YzuB&^57-SkdPPxiA72PZ~x(ijLuQY62D(r?>F9 zmoZHm!{i(A9MS**oJn=#mBl!mPZC2gNWrTuOf@8E_fQ2#Lio*}MX*_@_mJJ*dxI+n zIBVftt+W;R+%K8M_}K8~X7N1#r$7B^Ol^ND%Wnh&b@Q!Jf0NUS<7x8Xn+K%A0K}vv zpFnm8#PgeHn>#{uf6qq@Os5;A0s9{OZj>2!;bNOubp2o;?$MYY_nCTbTsAVrK;#Ut z`^Eg{XMiOpVr-b@gw&AhHM-XAGlg8vT;b0xi!{HvrBgfvjtvTW`H!wli{!|$H z4y^ovA=}F50)Q;pT zU&^t4WhbSJR5^(&pQJxS5?4NtZOJOhntEtSvu=Ad5Obz0QZV?DtM{+qe7dZdKruBDWabK(03h zXdU#8YhR6|r|kg9I{}R}mwiV-Ow@TKEx-KnXkrN}SFc_fv)mM#thg?YDGCsy`2z&4 zMN_jN_;O!|C;>!FOj93hs)j~I2#5k;wTHnW7SjF3_=JILK8V9J2-`j8fjtXW2+TML z2yH^1a|7tEdq#WCVXU?C5z~CaNSO#2v+01?0?oS5D;pJ!+*=DRq}mfC7Sa}EufaT(mz9_xd};5=d`{Gkz(XDRl1v3!f3x^;{+? z7=XpX^vhHrxWc{4L6~u7GNM0gI?Ra4#so{rN(9@)0kXi39y=Ufnm#O+^8Wkp4NaLY z-&P8@wIoN6zEG`!S2tdKxhg|VtHT4UcxO|gJA(DLQl4!x(<-3O)wlKsuxu3My zTHk#ztRH$at-v^?KNh)?b+Y`TAAN>bl*F~&=;S~Gz43&n0C=w zfiwOKsoP?d+7=Ask6OqgNrGFjdv^>J@+==Uk7}hDqLEZ#mEdcyof$KT1XmQqm`o4K zsJhoV@d4){2LbX?5jv**=%dTyV`Ds;PlQ$$AwdJl5Cegt)|hRT^sf2s@GOGnMraoU zBW(B6N~{m_zP?F#gu{2#AJ6)B5GrGmBGm3-7AjIgplM6{%z5t?b3oj*Jj(}O2qS?w z<6o!zoYy`AfhKN%woL(IXjOOwJh@)}CZsSuW+j|FOL%$LA^puvb{<@u(=TIq(zU&N z7hf5DfQRkDR^dQdEP|tAr#(YRYR9`}XA;WBWQK)3sf?Ih0|{84C7>-Ht7|g`mIJrK zN;uZSwca!EFW&+6efs%Q?j1hu)rH;73lzk)uoe1WxY> zL>B;5`B;G2_-#-2+AG$?G!xZ+2G$-}t$OTeu39+wDkb?+o8u~RF5kuY^y`Vhb9lpV zXsD?A2mn^*S|j0^q6UDc2Tor9Tz_vDOj?=V)>rEdumQpTUe7Ug9m~H-xq8Ih++w~z zVIZl*tU&Y-k!6x5z#g}7X10MY%ZVUCbQp)ZFVcJK*0s(_%CTJZ$;O2X=PTFr>BgC} zuW$U(&;Exwhp;V=eD$?gYI|!YB~U~LMPP;1m_^m!GC8; zBYG|Xrn>N}3xjw*G2}X!bp(j}AVP}euI6N!R(mti%`6}a1WwXpZmx>)X%}N+aPK1& zu3Wh?uyhaRgG2&@BY;o@%CraG-lyN%I)40Em1{0f``!UgHYAyfz=51lglNyg3MOC7 zPW9xU3?m|H0>EKBK8V@3aeRK|>clq0DkRnD1A^T=b9Sr`_hDw|z=Wpd2DlI1pdUgJ zGhlrE2N!*TYyPclzgJGbGUvH!uT?gi_7g`rbM9Q$z*7I-d&VLQ$6Pn-Othz6Gajb3 zcGs@84~H7JBA|+LSR%bGSK;Lr+R)^mS8w80iDvhw8k5|R#abaKtzE$Q;4W*=I~|La zaAY(!n0Y7HfiQqG{qSwv;7y|XZa|>I)QYBz8k+VYSag!Ajy;(fT zH!F%D`1IqCqN{cyK2n-_+ndQ~q^Ne4T5cVT>QNG|zY<_{3jRS&7XVNGmdFQLw%-|r{`Rr7rWPgb3b2Hpsb4#g>9A*%Iz4o>D}h| zi?SX&nO{=$RZ)$C9EfFr8{pMbAE{UaBF{e3In7~1kO;jpSb#7<>@^TVa|q~22hDgN z(@)e!Cw7cm|QJ437D|m%w>BKii&vOm)vNDc$eAOVEneTIw4dPZ*eLM9d0M zuqKR)ufGuSGBxG>gdsP8Rx&OYmcupcGkhDvT6~k)y{xAv&;^szto4Bvz^ulo2#!h4 z9LE^W8xyzAG$Udq3}{9;GDakF<%=t0)_Vuo4uSR({W4a|qM6KQ9jsk|^Q-y4H?ryk zh%a0$0Gb(oB&~Zg%S!Hrq9eO{_SmuG1p?dEr}xwH+zz$)F+Yuu&R9|2!@?4n6BK3v zU@|x|2Kc5Vd{!akH#lj>@1g6)Zwwfhr7B75qmM34yH-dpbM+~Q#-jEG<|k~qR%&Lb zA$Z}!`N{q>ohmWt+}Se;;$jf{4kw&msEx%EV^(tk2tBME%g#U|D2oai-2)& zy$yNY!{_1UPve>MG1;#CtlceO+1(z2PukPXc3OAybtHNq5Yu8zHA{kR@L@WPgsw{k zH>70-kWwiTF%uop+3p%r5g(+P0^F59#YaV~3o~BbxOnm61hrTW5Q+#y*}d2E5RVa= z>dG%6U<{{dQLFBnTA;X>P+MZipcbFsjJ z>2L0yU}hN^oA+3L3um*f_K|7=mW;DESRBcH^DUKJDKkk^-cD z5t2(fvrRe$@x*8pP7$<)8}1+>y!2_QgJ@A0SnDIWuoMX^_(C6Gie^^2L3iK<#$e`f z55>UzXT4AWEUq8^=*JtIXU@*fOP}WY5bl(ROiZ{l|1U04Yr#HzMN@~ObyIr^$lh#) zp{?x1Y^)Qo;VOFLX0xy(_+fqpK^|2N=+2F%C2xJvuG7+hmqa_$qhL#9U`PG_N5}B! z(=+|v?uPH)G>qIu10O-qx8KvEZ!&EfCLDT!Q++1$x1U*LEzeFGHLq)J`_|S|(<=Kj zYwjFc{G-P))xFY)A6M4-@+X%fj<3@qr#D_dcXr|fhNC9>{ThQDQ%v@2>%}v*Mq>uU z09z(nh!-rQK@ls)6R-m4nKp`8Fx%~jA7UXIL0V$Wmv!)`_NErlHbI+@IH0h`T z_|u5+vtEiZFd%~G^GRFAqzFqiodD1VYlEeN=xHDf3XY!T7JQiYFcTsWC)mYjC-C$S zL5uRz^x6Sni(UvOmXEUKG$xuFmYU0gAvg#>aSV)33pUu+;lD79 z!U*BhfA96zM<9O|oK%m(AlcJB(+ft#t;0N(Q1iwibZ8TS=@P)we(L z(wU8Y$4-~A-KGeY%aSy5ET%@F5`oTt_)!478bN&%<7NWa-%pw{!UXPFcDcE`@q@SD zt`f`1m@z=b$dJPKwiaG~^u5eJSylI*n!O5gD8E$?8;go9Mc|unZm=82i>B}1eR?Fg z;0<4VjpSp@V=)VJ7$M1Xr{%UZF83htdod1E$GzLz7!(nR=wA8a!1%V7t3G^CrJEQ6 z;!mA=c{Kdg;;%gk9i#cuJcwR2{Ke`H+=yV$fAB%p&W`rId$Z-UFAk$(UPS&`aSz&P z-F+A{AnIMs1YlrR6ULJ|EAlL-oaszJF{_>9Q!C5O#fo!B2wqJaO9?&{5pyR5V-IAx zUHxjAtw4APd20LB->M9S9|C67RD?~pFOpKbaJ%^W=-~LV z_6|rGgEyB4eVJ7t_%>sBu>U5_4R$kkUwd3vG_X55wwm!|%b^b@JZ_iH{=s|iYN<%WRC zcIA4odZSBymq6tJi}{WR=GSQXSo`1o=^Hz*lPhk)tYkuAY$cgprxg)`Of^D@w$L7phiHwF z*7jb>JzN9l7GHhwt39XkEheSCnZ4e1B27m4&>9F=9|enid!9K+TX~*Fu)aZ{2oZaa z;2gsDycmTjuxUK2(cDKnnoSTaW3pQ5XbO`SK-9*;s$ohZc!(uT5TQUU;VTk_iOtO~ zGaaTYrKT3d?7A6%B?co&DZB;C2MIVyOoSaDmc^#(F!zZTa~~^;WhGv7y?(K{cQe4!XB#M)4{ZnW&`q20zSL$;Xdo(H?s_3BXl2sI{y=av>D0?cJG1%yEyMspxc$hZ3tKx0T+-Ld$_%XMa>52VsA2-Cd+J?>}Pe_9Yn zW*B7iFA+56<^Qg~=UqL^VwgT7j_xBcnaAL1d|-#r5SFP)nuUNNP%t)MU9=q0f;|Qy zj1b_Osc5Q<1u`om69kx^Ny!|qiD5ihWEfw@;V_~Lp;tfr$CRJ=g1P84#{K$jZwA%` zp+L*JcZ$CTZ;X#InV0&xFT9+HDZ#;*PZkU`PK<~iFyBm9vD#`!-wF2@gDX7M_`FN) zL2eLu6S(MvP+!ZCyWcsk+pmB9>-om;o12@##I{-=k88-aB0%&RE?%l{gg%;;lr=&k zcoMSfyWm~h@Z!*CIH5t&19w0h=*9B{va({}sURu00{(29#?m`pJd2W2Y*oM->@Jnf z`Kw?2qUFP%jY~-odEddt3NGF!`DX-nbiTEDe+!-j1zFCs4kAx*dwBpmEwQ)DbQ{#% zzV=Dh&9cx4Y>j&n0N8>f{Vgxg;GX{JKF7BI^ zIWwA1o8xa~67qefE`HA=aFzgI-fMve|8E#QBeW2(b{w8&J}UD`QqY>V=@TE z=*)TnFN6iz4v3+#5u$dDpXTD0a2Z$!h=!1HFF}C_5y%^FyfH*lH|8CL9l^siUm0T+ zGTI)KRj{mf1U@m2wgrFm-Ma}O@1NqSX(=ZEqBZ&2;s9bYgx#WT{l>h$O*MMAk9NJs z^exSZQD}c~z7p};*O3YTRckT?;@l7CG@}SRoO!oCN&99I_KFyN$)g`37K{lc%q3#a zg0Vd~7_k`Oo&{#?%a}8L)0}2D-~${9cLJ7ms4n7KvyebQlN7@%uPn1YtUfT-KIJrV zg2u>=@IKRyp1~9>L2&~#0tbYJ#4v)@yZsHP-}~P8<{fepE?v5`ak{Bvv`H{%OMeAW zF?cPHfUtJsRMs0;m=H#v=#TaE@#Ray^n~1d@4ho}rxPUw5tsz+%P*fAVJ|Ix3YAtB zdT%Q_6^Aul?w;blF?8_H1+W{^j)Lbqw?#kKKkZsH8Xi_=XCVz@8t1cf^?Q!KjA1u0Eht7tV97&q#**Np=e-Qjuv@TKEb0YmBsH|b_AL|@RDXS{M^f2 znfihSM7Sdu7Qyq`1f&oiX8NRZPFJp88TY_0HuhUHS_BqPWLAAV(h(8!-Ly^eK{Cdp z84$Wvv-fNtg4$)&u!ojnv>4(t~kn^inLbN^z*whfM3_i^L z(K-<&6B1E)kM?|KjyHy3qHCYf;UBXAFp1~AOTY~ggOiN5L1Ku;^aK-gTVDKZ&y{}< z@qr;t9T7Ap1j#}oEVbu3{kWeNGls9KDF}aw`Y~U*IpBa{;a4s`vQZ0wnNApimCC;e zo&ZO8h}u|;!*vWss9{WQ7YpWOcm@k@%}19$8vI~l3`QWXuyq}b%_z7IXBbfh9ru7A zcw>B4mbDIGjrv&~U||dd2my+=xKR3|Z^ljGfGfAkv?zMk4>UtSff3>0e!pEqLu1uN z&*07qCj36m4ZL)@{QLlK21y1gA#3lzrJ%}_b~XNC47`2vAdKg$ET<9d^;0`9pE@-u z#iw80EO=WXnydjd0{?}wngvKf6`y#N%W(HrS*>@z$QoE?Dh5A;bc`Q8*Jt1BF*MUy z*Tdh>%7V|uuB_i~@>?fIvX+VXp``8J5b@KW{&bk{zxgnRCX>e`^E-1H|f@ zs62>TiM6;Iho|2(2cjS}5DJ&T)@X=xB~pZfIlvT=>WBBRMwscQPBDo9CqZa!zBUmY z*ob!^W=OqP;L~p!h``0<5>m7;a1_w{%^W0HtdVcJuXe$j;Ju%q5MV`AYixx7?|#gI z$uI>$s!w$@_+*ZTS?Q?mm3vM=?MdiiB=F##8Rse>xd3O*oGJhPv&M38<8%R0bKhb> zm=PY%Y;I0m$T-1+5QSS~XI*`n23J{VfAG*ZyE&s1Rs<#_pwXmfeQ0^J6ar$}Fa}kd z2octscN(9Mu~9ZQ0VTlJ9hS7qS~Q(%(Go=hZ|*TR7UQGfYqjGa{m~z_+wT`M@Bhkc zFHb_77=UrIfB;YWJDgm-+RnfwcD+z`=b!!ApA8?_JCTsIBayg{m;npzS_1j*gI3FC zNfX{1`%B->x=?y-TlE9)6su9H{&6r*?9O|P4Gg@K0vP^%O2c!0D1P~8xn};O!yn&0 z)^olAo{5H`5V#isE+S+OtJ5y9LI5a=uw!pkO>7F%w9KBCJ8mr^|M+g2CnmF3y8}}( z{~tX#(LxvjE~&Zdz$Xm$ z_`s|AdL}`I*$^g;?b^QZ0+HTL_+TFI<0Brc-^WS25bT6^*PX+BmApwHTpE>38G)hH)QDdFoLK~o7u7A>)2CbnH zV5p5h_=7)~=f12T`U$^Ze7*$lt|wgNlibJ(G9Iiaf5p`BB1ktj{_9`=Ri(-=#S>DE zOKb}spQ-uRm@ZzpH18Hj=XPrMW+mhg9H~gikrUx|QNqu7E@40WBn%}iw44+enkPVT zGT~iE2A2QW2I%5=5H@4ox)09;=Y+_Q0XKjy~>K9FZ-13}UV0+4Fn zYm>m3T>hT-zRk_eAtpZR2*n8K&fTj+l;DbZt*&#QJ_so4pZCi#poxqh@qssD9POI6 z;Rfl;o!-j~lJT}Ic3H}dfdj1&8iG&3+K5^d9`n!CUw@+tcruodFoJD=m5hzuF~X>C zJJmdjIm}Lg0rTEE3xPsGNWni-YG!9x0kp4b!Aw`$&dNx{Z>%gUW-+*5O3TuIU{0A_ zEvkMEv-EyUsO@nxqKo5A`SJY)ZCN`6vxDI0erS)dK^wHU_AoSfB7A1Qb4wD|lL;Wh zt1%k?GUn*E`?XEL`PL8G9sE5n@OR_JGDZAf|N5^dsRte2NxR=}A&$22!*5dpC-dO} zS>lqh%QH~D7n}&>pZ)BQn!Px&@n8Pp&o_Sc%U=v{oGw@@6ZFiPGwoV*KKOh&a6h4z zV6=R=cO5xaEyZZ&~BsHb&gpo`A|2KPeONQJF%D zB+zQsxWE5?I|McNOK6BHkIxt5o1W1Q07DK204S@sMo@Ik6pBF5u@Wj)lk>fpo6ir4 zp2c`fQ)V`axSIU{Olu$tKy=>cTjE$WB0?}m8o?B#zfE)Lo9CF@vkH+NVe$)@UGpQDR=Iogn_kx2INHAyNDFH4EP(aWJy^K(d9`^5}ypGOTxK-XS z%Ta|Pc?EV#!XLYva&t?HuL^!$J?AQ9+!_WU-8grjn059b86GbBonvdoR?8>*_fA~akfwl!}Y4EM) z0~P)>6JVe;siX=_PKq=4neW;|JR}%G6ht9?O}#8Zy(IHf!Z(#jqau`Y5~ zsP6N(58~xhy8rm`@+M-8HNYzUmHGT);}!E5;_rF)Vi<&g*$5p10C9VtX+G~}JsG<> zeD9Hujw|8+*z|cVuaUtQ8wPV%?6B5Lp({F?-I7o2lJI1+u`Ep%e@TN~Q)^Oq=)Wrs* zmct=Skf1d#<2#*M?i;KqjYZIDYjIzVL(!JmE3)UkpX-M{ydRB-0kA;9(DOY!?Q`p1 ztgmJRPV~(>6~kEs$dadF%mkV0P4fQVfA8IyrXpcpH=BJ> z9r;M@A07NykQSdkY1*-3AbZONkPooC9gXjm3Y-HtG={D9bIkte2hBSNcF&IIemXZX z06!3^8`qov@i*rFPD4-s1~jnoj^+8pK)$7_zjk3!z~f=tVQp_-lOJ2=?hvt#^0CI8 zHxC{W2pT#TlTJWO^m^A=3+KV08%FjXWAyPi zq|(m&_pe1iD^Z@sz-4gHfryk~5Fp~uA_xS?cXXXEM;KtX)2C0*y`IOk%akACCYL|+ zJ%U<%)Nc!M7$^T&V9Y&yM>8Z=y*V$KW{emfY~*-zc?d0pW+xyFTBp*+D%>vl|Fp*4 zYQ&j3varemlp)~SiTpdiloH2~J{K=s95@G=ES-X72@O89DKtbHWz@4w^_XY`RWN5( zGI24q)Z($Qx)bc-3=UMKO`tL9315jf;Ppay#oPQeflCUak;U8hs|%$BU+UV#0ntFDtJiyNDpuWh_}?zN2*N3-NgtKagl;xxt0?tF7$<6wfI zjt#Ak4sHw)(YOG{Lz%9Jza7uM`~6G+%$2AA80aAWxrbfy;Bsxb(ch%JwfVt!VY-;y z6d-Fr=9v2ZPuj@9mPWgC10JgHzAH0k2y{={BwsHeV>UrUdjLMpGC+I~EQm*H%QmB_ zkfXWk`yoQvDAL$<%x8HkEx>BPC@cflG3BkKJwie#2-b%tG6oAsv_Uc>Jm*Ms0)z(9 zH|G%n0u_lyc-_|fj*N@oyn~7O>Z`96{QhjlMHvK{AHa7mUoTroCP`k(JXl zmT`)ve^ZY6!&ZB-OuUN#knP3XtBC7#7k1veX4_20^Hb87wAx z^W<4gy?iVROhz#2tAn8TSp?Uz2E^a+iVg(Lz+OMVmSyK%qVVpS`S->Fx9~+s2>x0l zVe0dZx4u6~Q=eZo3zW55Ti{~%qzmUSjH^T-a8<=HUT$swsz98-8H4Z7sG6#rai5hCk;zr+PGIarmYEsXmLsy zDP)MD0T?lp;7VKVYeyJX0&RBdW(hNye%4HznzLBP$}^^%>xhdFj=|(#^UdFR_jiLB z8s9cg&e154Az{P;nFz`Fo+l{CbleuK?ArcTFzOd#Gfn1L36~EmS`BfKgeYi#CL&A5 zAsv|D!H5EIVgf7^?Gajt1_7>d074GIaE0vjqo5{Y@E+qM%x3znzImU`Eieqh=Z%<_ zMnw1v(Pk#5sWFc)f&d)NE*veObm)kbUBtX(Gm2E-x^XM5`9)0LYz`dN7gMyit5?Sl zv1sezQ(=r2SOU^gGsp)Pd3$5FGKpV0x#UNfB@lPOFvbh!1T6FZemK!LL12QFmIgET zxqpP+QvpWkx(0p(rGrK2If4{!SfKmT#560{gm7>Tyufb+TkrNXxD!I7Y|_G*M(%`f z@5eA9a}>y!F3PeBq+Eal%LtF7YqIwuk|8?n=!FG z(Z==?etz-GcPsFd1(SujvzdSslAJcrl#v2U((OLG60MHZ5-eGQbf~0x{e^3VO#rqWy&u;H%BX>qd;7NCZ;wa0pN&DrCl09 z6r0J7u|>K0RUe!`U(|Ui>xD>GjUkw1T-x#;(i>5bkiMPsj$27jnuG-~#Froq~Iq+~9FjdQNeGS7%fEQ4S|2ng8XA&g-uEXJ{| zl^lGGpdU+#ujc)82?R+^n<6x!W8nY}S6#+}T+gqBp^WuH26hw}Op0Q^+%FlF^= zDcH1-lxF7nnb*#arn0np_4W1HY^}fj-jSfr)}MFT5afEAIcZ*;M?Jp9XO+bC+UAlO z2}Z`uI&@vY&3MrcmFCQU=+GS+5&t;eBC!AQU!7|K+~>JJ7dJjhA?-}SohyA=7NC`Z z`f#{eu0xdw-?z81Z!ML*?AP00f4Xt=tBdU-oY02<8AM~LuUn!$@9`lNos(B{L3-%MZ$vtxWjDyd4kD)`>cB6;|9R%Wpxavy6$@M!h2LZjf|org z*``H8;nzET2orrX-qqTXNvE%#1#{o%Wp#VJhsy-!TsQ*DvRYQrD1h+y?z`_!8~UTq z(~r)>Eu3cLPoKetu<#x@!+h>#+M{1EgcnwqX=YZa{!VsebjXtRagHnCSyP?IjxCF^ zC=;JOS%L&V7w2J&wzuE6gU7{cP>5%sDT0X78a{}jDJHos-mQO&aF4lr&+gsZt4Z-% z%chTI;hBYKdHm(e8^>}9l}uMm^=Oux<-*&xX6ZE+t6C{tGXweC)24O@_kHI9O#hwp z-*=Da`8bykixC`8eS~RfX%yflv&;>{uzvjG<3VQd76jF_#B9@4QqZHM&ffMq-@PLS z+85L3`#yXWu?2v8ZGI4*M|NQcP>#r#nFPQiGWAQ6h(pj0{3Z%LrtPwzM=%gUU=RYpx(388!p(i)qZ{kKh{Zx6 zmJR=3{{@QNZ>nz%_DM|A76MW$&obYzHlKz%nvP4tl5l9-e6XFESAolqe^_7cBk+FT zO4wRzx~K7CFvwYEb}~UL>!bvl1A}{qKqc*KRVTyqSW)inS!Uje*~MJ8W-aNbIepJz zIR5*NLl?E&E zMYUIvTYsz2;z|jE6>mg<3N=>r zp-y~!xuPIh8T!f(RLRH+I0S?M1PTcngtZg{IXJ{AqwSR}6#{puigYtUkgpv9V^r3J zez9;c1cDXGHoxp!Tgqz@UWnXEK(Pa3U8btMx1^#N)bj+Lq#Zbt?(2Kz@O#IjKD5M> z&g&1eetw6=e8Q|Y!K0X>_C(4FCSxTE?WJhTcN=={H0E$@0hkMKjohM)u) z=2yv=fH5n9_*o?{J0g~9c<#O_*0Y(GWm*^!UG?t(UqI*=%`uHRF(p2{> z$Os_KU2ZM9tSyrI&h-dC06C<*A{bgyYOw39gKZ(|KwC(iBs>s!{|PdgLq}91zoaxI zLNx7;37un(`8emD>u#|alr~^`YlBJc8HhJN@7b|q*=S-kLic+fF~~E&vt_Bm!^ftD zz1y=io_GO`#w2AGoH=u5f{WY}%u87KWDK^(d*4)&xqj*LQ=(f|k$D%m3qER>Cgdyk zs%6eNA{A&<8}DYZ3HOg#92|{Re`H1y7&SC81l9ma%KYUUKIs`&*+lChpy&LS`KSMc z2rFi1@9zc54TR$a1O?FZwpTw~5{vu1A9Jv#XmWS}R}pc25DfDzhM~5CVC!#w^P3oA znd)P48KZuqHB2&vFVb$OTV)5XG-sPn&NJRA(!N*Vxi|{kX#>tFDFReK;Lcba=*h=B z915l|>lb`4Wz8*!QhyRG#dl7Yt@kDKz4ysq@!fA!XOOH0OS#jPr4&Y+L6GZVQ%PLpOs-n zc|~$DwXR`Q3oopfo++^00BvLHk1vbDE2p$f>*B1CoW48Ev>V-7+~VPh#Bz+7V%@XtsU<{n8wA7Ht$P- z2nv}o!0g_=+=Bp5_S~|_$(X&Zf2bfBO^+r7tiWrb2l&2Q|CgAFu`X7Ov|@YT zS&VaQBJ7^|yvk2tVLgTXdcsm)F^I$_$w6SDKhF8Rq>sz>kBPc2uJlbiOmPe4FMv6> zjrHbTV8l8yZZtM+^ep^fNH~-{W-#ty4albSPBT5+KW-MhfWa!fjmtZLodWL1;ewzSoy=~S*Keo zBmTG*w-2(SmCU1Lhs7I>YvqjDLF{YtNnd9sf((TSOu1?r`(--}6WR7=UZO z9Xju`)bn3V>28Qf>OhPwZ5TjJ$CX~vVxY{-Ek(z79nHM4qAezm@b)#232^%`eNvqE zr%@+;@@n(XSpnwqFI~EnNwpNIFe(gTe*8=c5|WO@4w#()u3rm++HCPDqhVSMk0@Lh z^kY8LsE7dJ(HzEySypZ0IZTfr9AH3b&@`SgRXFtxx-NM|oB(lagQkIeM54crm9a6Z z`)HdR^@+);J@3&!*A|UiNBzE7yqhopdt;{gS0MldSlq3{ z>-~5p0OrJc;XB_3=Hqw<%=xQ^ME)1?Gz~lL#Y45%H-oygp=W6zhv_*bgeCU}DRMXhxh5ShZgZGds8dUt{_2%UyfQ3%t}6vjac zd%q>H_R#aZdz%5C`;^z7KG$OiTLlLqrMXsEXv26B{=zWJuEE-aKz^b8_nVLz!K?sZ z+w(jtK=vKr``fb^X0&o6VuteDF#@L2u6GeW2y$^7md!B`)M}mZ8?BP_PHDBrbDuC2 zVcEd(hm}I&#`02Bih$y zjRaFxf4!GLMJr5rCNfxfujyPG=(7T0ECRI-jny=net^ApwIhB+fYFd(uYcO$)3XjJ z7KgD*#PW>aK7{bJ+w;Z&zs_qz0u{kY*kg3=>?p0q2p*HV{Iu=GGK-EA@^V;y|NGz1 zJuF7F$Fe(h`qUKn5U@sj#y~)TgLe{$`r@~9aOVIMbg(6w*H3{Qcqh zL1i*gikVA z_(+c;c2#$G?5dhh<{K01T8zG$PD23sS84)$Rh>W903;pD?A%sfG{6Bq32eReg$w5c zs)+uQg(X;qdHjuVMA7vV@z5rO2qwZf{+4+sxCA*q-j7gxAkDMdK`h{~BG7fn7QkC_ zx0ghsg9U3mw;~+@und@s&Wk@_e#GK$pL7Ty)+XpPH zdZZEE_etY6)4`&0@01gZNgd7!IxVnSToL!INi%a(|1b%;0V*)Fl1uvdzK27 z$5PNIAB8V4*xrOX@M-MeG#jXR zU+?kISODGc{o3f~;CY3?2mlx7O8@%3{Qd23H?IuREzj<8em*_EG`IkE?uk&CWCvo3 z;`VJ0QpfAyAWEHFk0I`^X= z{iw0N@>H7?W9a@V9{SY6025=;PedkCPO~yO9Y=c~!LykPAsF8M{X4_Bh+a7pdg-7%zcxcLwdxV)DJ|u9L2K- z&Vd~qg2y6E_}2>!EHGHAIuW|&_tzD#tR9vf)7y%_H{W_|gs;AVJ-0yHraZYV#;1L_ zK_@FLSY+Y`?gd{Tzn=-E_+wYDNC*lii(q~^i)POr@sMIdFPw$3$|CtA#E>KB?*`vkPFbe#Mffw+GJO3!*-7p6gfAxKkxB%$ey9xaNRd6qr0ZD-N;1}Lq>Mv6~5r;@Ec zU}E0_D>Q^|DOsho_De;PnT6(t^@T}^@kMO)3D5x_k}#9LIv{NAGxL-dg#g;iA(028 zARg@^bRWb_t7&%tS9^pI4UVWBHupqaG~g;I5Hi9L*mBK#d}~|(g6t%BxW|SOhYGBD z{>?Yv9AT$#+#w~*1j76crZl~1GiD_y2|MY(@FS+73JxY1B7Q2sKKS5+3FLVOV-vIl zlEV^!Aszq#KmbWZK~%lk04MncVC0-Y7`*Az(LUT8TF`ke074%|&kDmJ>H>0cMra3X zt^|Q-Zl1~R8leX+`f5C2Xx<*#m>zZBJHeZEff2a+xW{?- z8H;o1)^GS+X#+z(k84(rcI9Fi|6(CluOM2nq?@HjAZZ8Q_3_M^GvVv4akD*{iOjHV0nRo`Z$Q=-Puo@ug56p>&O5cKpZbD$&0G7eUBn|BP@lA;O+rb2`6?v9 zn>IHF_tBP?o&MnaKbUjOLU{!GB9P_!`SV;Mi>DS%<5^K`@dM)|1brhg>kW*_^ZEq0 zyKUW8AJ+56dnfJt3bP+vinGv=#^F7fP;d~loIKSmMb8p4J7aQ!0I~afcLK|d!;?&G z&tWtHKG#f9uFME7gfJWuWC>m538s}^z)&0T zx#AzqItT9F39f{>bsrYTDbv2U6`cDIsCyxxxmeh&o(bZ{+@k-pndk&OgKhZ$+jd3c z2~2T|yR8>UT90p!A93=Qf}er2G1LE=UD|CJ8*L6M!Yz$=(EO#Qc!+sKA7h;CCOU$x3jrOn9S;Cfmz+N<8&Uh^kHii4QoCgJ>YrG!H@G z{o1ljlU8&Do z;OXDsv;G$J+8WUy5FdhW6_W5XL|tFNSP;()!13e9hNxZFSLVO|GV9S6f@j4LQs4=5_-Jw6^xHd(C}(*gaN!dMgTAc0ma7waL<1{?(c=^SLUy^ zkPOiy0G=Y!z#(3C8N?xO9d(~M=k2W;Z2e-A%ruRpAk%+Y4oTl5`CBpuZJT7^raKTh zjp2E1k2yNuf_00J56HBMW3*GqK!`q=Ze5v%03Z&6Cu^=UUpJa^S&?sim<1gC_Kjc= zybnUsrhBw|Bk6thYKu*t5*FgqM|~Ig6LkhR$wo`I-nR)$i2q{KS=;K1_5=$p8`VeV zx3*S-mGx(8RK%OB;2q3+Z6lmIFm2KJ%M|CbdoAXYbi()F9xmWxJ!W+#7>6BWW8ynl zZe#AV>_Qdcg5bf(x(#jWFU#g!=KJnFJ2&i?U=2K<7jw!6taIbK5wxrwWyra0;AM$7 zX0i?d^T9D8&c$&UA6I6khOH+rf48JUqb{#zeoa>Gv>q z0>&}1q8Q0+#r^ONKAwa7Jqb5kwSE7s?~k7^(Z*_1`xDGQ3(W!$P+U1{8z?W<7$HW0 zffpRIZhTk?+U3GtxNu>FgY@YKz4Lf&o;&+`*_5w@zp6eL3_RZI&D~|CZX3V9&Pv@3 zA267Wo-l}(TXB28INFZ(36RUNr?kGB+<17WG2JQsx{$!^%-#6W2=4c_20P(*76T!) zmFNM%2Y|nOIA;((o*4lUadu`h&F-}y9RNbsIu)Uf$<%AUc}L}sc11910X|7OJ&E}R zdq{T3ge*b8`z@uE*m0+16&3;$h!42{b}>u6N1nWNT6y)OMU%J~&xb%*C9ttUym27{ zfBQXtc1Sdyb-a+KFAn3-C)a#D<8Z%2at&g05A#$YlbPrsV8BRQGzG2g`Y=MKEseUo z)70X@`g~(nmk}1M8e_#!s?MH2e?Bc^itO+(lQGaVw%HP_`?T%7^oiCaJh%z>gNeR8 z>uX(Jar~rlIrIA!2;^K}XJe9{Rj%S2F-&Co)lez{AJ*xf$~MfARN!KjTNE?ln7M9l@0w zSEjF&)8js11&-e9Sm^~Fa#1cO?BQl}b93rE5)|)$a6SdFBUj+&#(VF*U;f3tgzWYN zr;Ttd-Cp7}$)2zxT)fa7p^>~Sn4TbbqUe)0&9WvLJwSq}z2 zG?y}U__th?#awg301%gNK<)qeo`*c(#MAHj?Vj2tK|P0IeCzH6E&2wS32@}gZ%LZF zhRL@ko_4hV+pZQv@S#obSoV1Pb^)zhf-m-TyPZ_Ejna}`we|~OAVUV&y^<*~oA>G` zWJ4m8Q?yndziZY7U@C%xwBA};;=YqhmK?%x4)Q$jK!i+OjJh7)@9?c1-=gTA1xNS$ ztsg!Mkt)5FR*j&*0&|<9qXAhE-^3K)#zaKWXU?3N`^;w}Qs>PK{Nf*eIRQ?lqx<;B z>hLL(=G|5qGOxdAZ9fJONx~>#aWNEA!HS{x`K}-0<>cR)t{a(WX=n zGzDvn+DD*@rp_J?3Fz^M>l^I|-tWKv{s=DZE$P1e-lk6bFp0rVzcH@yfG1{wBV%JN zXva(gn6kLmvhSE~q1%GL#g%4VN{nP27~Yg&@%}#kUX+OUJZ_6RZH`Y}pSNcJFJ;9R zp&=II7=OFAwl_n>D!rX0cR5!8ZG7*I?`@oE_1>d!YE6io75fno)^NO<`(O>n&iO`@ zWZ-OBo-*cuZDYwXB;n;D_@TOQm)_2n3 z$&<#ORsEzC`_9bwlVS)ta)W<4FEoFs=w2iI_So{5|Ih!@DFplN9KXw}09bBao>~Qf z!7Kt`X;d~Ez|tPhkGbCqiHk!mV%QS%Juc96?@5bfsz$rxKpO5yd*dBwh1JH-o(jSipmzoCkP^zumtwCxUU^ z2a!Rh-!z(_<9bg2AsgA@Nv;0hjm)_~u79=xn& zFfIbj;XXO_nBD~ILiU~OTWz#pIgVd=eg#eUwrH5CK63yp^08&u6oCoWJ6dADb6YE9w^m8$aqCd-r37w%-ddrixw8hgC>RnR zym$S~VEX6j_TT`pbPQm89E5_-JJxf)pKnCObP*4WNg{#7>L~M%fH=XcUJfzp&_7J` zK8|U2=Ku4-b`}@}Bx#vGew#}svG2Fs5i@GTS;)~phc-me zX-x>wmUb`?L@2jJa?=Q&MTj&%q8tVZ!HCGUwSDcZMC$kDk1s7_YkVuQdVUo!h+JDt zK3e_MsZ-Nd^gM*=I$_45(LeA;nBo@l14eaAAnr-g4^kGxxzs zsc9~NaZdlzI<%7Dpnj}l0U!4?ct594;N}^tG3`z)@kbJnASSKr9f*$aeygclE8Nsz zFcTmkcm9L-hxutlV__Y9+RjdIzx{UY?u>aYlxAwt-Cb*ALUTM5bDRHngHjEF3+M(&4N2?C!T=O>?G1d6vpZ;X%j>~i8Nc&)J zo|_l|p~Y?7*R;8T8;49$V^A|el9E-cg#fyq(i0E-@&e*rjsaUT;`1pDmSGs7Ui|NZZ0J^#L!Uy86wxaoZ~)L;JP zUzVtIs$p72s&Y^{?-60>?ch<0k}uZW)(8cyV5*Zb|H({LaE3pHH8JbQSz@=U-XtOC z#F)J&CPJ-*n7$r8rri2610oP8(dwrWxKv>W;WECd7%@9!#j$yXWyS~z@^Kj}O{jk*hQJPc?$v7X@KDtOQ zX*YY7?xX>br;xk_fQ+yn2%sC|7UXnnjNT{#c8?(sJqNj>H&oqYLp z_!dW^3<=}~FBI3k++|}Ml@@(0DZKGxX9kZk{#e|(3|m_p!nJ;qHQ@8Ovf)n@;K??( zK*+TExC1>sQ-ggc02Tl*FGeFvunUl#(_z1dxpnR7_4(F`q>}6W5rL95@c`j7&u@p6 z?E}Qj$@YuizPn_f9i{5twPd!Lhn73WKHXESU|)f!JH-QzG=H2=v$deco(87)$L=&oPArKkCtqkAt>#Xd)BMHr=RX*!?){J=0=}A=i;y@G6WCg8+57MP_uore zE~%}8aS#JYHa1bE_}<;cO<+P}I)Xqz5&=q3vb)jaM+KOY){u>hPFui{zwGa|I4GYD zkw%hF|^1_Alxdn@xrq8T| z=T~K#6;Ur@!P&A-MBPU)Va>Q7Gw+Sj&@;w$uPwe0Jhc+%e9S8jvOWB<7%(uaXp*MD zB_ZWmozyq5(mxCjW3wKYYB>@ZH>9P1{34hXdjN~c}L5nced30?ybwgs5K*xau4dm)&$mry9tR0 zXdrc4$ zd+*%_w`{WbRVJpsNghHFViR|J-WKB%)C$=I!c*TgAd}kJ_=P)wbcaC89TdV-nABdK@0;~0mt_Trh z(+(jc{$X7D#@boy9b#MW<-$2f8Q8eRIvBLOQ*0~R9)4%Z_q=zrunGE|G5_PogkmwA zl|HwJU> zn9yk^#V?n869b;BihLM9?%1|QAKuT8KB_IdB;5wBvN z$BrH!!a5b$pvq6~cL1L8FX#C$7lfN2EN4jn4s z7lR^J)tbP-0p5S`)1v&BE=@$9pAAL`^}|ef_%k)-zE3~{XUvCb)Z#WdX3NXW!2aL&Jo!ckfM_qjcjYRS$P6B5P4Lo{Mr?V`$RMr@P>BWSDap~X71z0h+^Q;DcV?I8u`!SYh&0JtW zJL%NJ!5^XrXAEQdkp(~q=>tadJ#JM2Wo6=;@SD`#>0@-ppGON^H4N=<+E*1NW#9;& zPfB3=&wue3G5M<#*p%@|2>9^#eJt0;prZ9)hK983u#N*f2^xp%m>2yy=bC&LmJ0am z%PIsZAJv=`f?ZRNS;}DNgF|EkqF=(=A!Yz36pY{7A1IDe?BQWlvv>jVLe5<{dz1o< zv1&WEd#F%O8lMorgt-qai$^KsE&u8N{5MYw5Wo#!W&D--eLMgBXT9c~51-BgJmvS1 zE||PW=2*`Xhpcy{BZSKi2aIkNH9%~s3Zs+@F55hVO4 zms%-ztu4I2Znu=oOjADh0`A=`3X=X_nsgxq6?WPwI0T(N+nVisJw7q>TT~lT6xL9f z5n>Q1$xPz16dWuBtJWZCrbQz%{;>mM5!a+R=Z}-@%-1Dhgl1Ugvk4?oV~b2|lt~lO zezN(%Le}KIgbk$50+dpvxf%$BdFmbdN+T0$$BrGZpQV;|-ZENkIP@EQzGzB~tH7oD z=+fnJZxA<}GIJf4JYyi!bXHMXlkZze@>fnPQNB!Tp|g9;#0cm}%%VUkSi!x%^4smC z!{WL6B_F!ofU`ivUNA6$Lfh)2nGFJn@L@5a8APvNs}C<>hIN(Ntj~OjAC3n9Ib+}Oiuy+iU2?-rD-x+NG*Xs{^LI$#)nWQE$Msn$`TVm01zTK zo7;jQPZbS^cnB9gUZlM>|7|aw5L5i|!A@zXHkVYzs31f4yv!5$+JJ~oO4Yl0GP!5c0H=DCI} zgjwa-In#$r;#X623G0(DzmlL_BJSWykoblNV*>+i``h8&mo8tPnIVFhYYIPq_(wk* zW=AJ7EA{h-Kl<^c_l_oSPTfqw_O@Mft+3o!7KEW(1g;^PpcwQUF5M>v1h&ScFWMqh ze0E2x3aU!D(r;E6p+84L1>CivG7+UID--{Lv)Q-pv}vr)-Q_As{7D?e_*%fX1(kPY z*=}i8;C_j9?I-ZKsdIT6J8PRDc_sinqH`;QuNt2KSkHa;+cV1naUC%>qJI8k5SX8~ zepFHVjkN;#MMTov zuWX!pxv8|YhqTih^#QSPGj=q$8x73gmky3cfXzY z+>Gf>{V^#86t(rq$7!AD2rcZ~aWE}DN0o0=R?urtXCHhV|x?iv>f9;~z@ zq}*>DU`q2D8^O7{M_jV6a;^5pY-oT$(dMdYMcBnh)~G(g@S~snWEhKZU^${o*_7vV zbNJ=|?4SShd9SI}U;XM=8;66j_bS?PG#Xp1!^M@d=7kF-IJ^i&dqr4}uq21Aib%g! z-onuO3WtKOtP4xpwF{RlxxM8U*z%n)Bp|sjJ9d;m5p3k$P@^gmJ$_u^xG1E3H$!1} zBa^OY0Z6))Sy)Z}#$OIYGXjq)H$L?_n-v<1g%I(K-zQ74V~ z{Lh>I^>OYY0X4-`5!k9%GmzOcuqmV`MKTW^JGJrB>876!oosbeK2gs;NIUHh5H~6* zCIMpzyy+x4?zM2(41ClDZom^;>7_$y6gDZJo$AI!p4aetO{5$=UC8}71VA&FE=6KDAh!8dj*8(AZkWZNPe69xY1kA6+*?*u8| z*)$k4+p{b@%EWp202=~aZT+1wO|KrNfYWW;J{ovxmH?p~Xn8b2fU?#uUAjEN#7a&~ z2QLD|@4x@SxC(#xhkvy3r+@k{TD|$(N#7Mi0#g!0SU)>=E;DZn z^K-4u&;^j5c~sDE$M%BPdn(Cht&Yr_3W$I;!8@6N00lUJwH~WLU@AXrv!!tSs z2>eX*%kw?gNy=r)O!d{}-o>=>TmJcu=FzuR+2rQ6N-DRt*q#FosCi^dj4W_?@x^0J zZ=_xNym0*Jm^a%*GV@`2mH&tEE~ZItG_}O1^s#%c#Va*4+C#sYju;&Bj0+(EIyYg6 zEaWl85SPs~X!>9O`qv9FH6BEA^r(6IC8ZY-)5XOsnwI+d>aaQTA*Scnt;ffjLWqvV z&$f7KSrcJTI`^_L?nK0QBVxFs$?e6sXLlA;!uIfy>J}uR2{GDyEVazqz&6u#(vYoK z+}zxpz@%(Hfl$Qg-2|);Cc*gDJNO%Co_TF^#t42a0B|ry*^lT#Z2++vu@i8?*ar{L zQU%dE4?d&$>l-Dq2!aGuKJ1P%%lOKKv-@smDZ;gPzy-moXs8@}2i$`Zp;pAsO``N@;{H_0RAxMbcl=HfqfHq|b4)=3eN07&;-a`lwqJ*cufh`>S z5EP^F3vwTSK}KUUU-i}4jFoG;t=+a{{c%$kZY$>Ylpj5hPHA!hzDK!APhsX$jpW|m}YiITR zpPm_0hB@O96xAn|3j@4%S z0@kTad{bjrBQn9S_doa`CS0n=danaAWNRt!1o-+%Ai0h>>_hOzn{SQ@sqX?mmUKdx z6<>l}f?86O7we$D)e7w`V$X`1L?D5c%xQZJUb(Vlvq8`c_1iZjX`641fY!EXo_)DH zmiZD|AAIosn6L=%ddzk6leBUS!CgX7=I2>47|ZMmSibS*vO}$iy3|#gg{I(_voInD z7#~f{db*!LdXO-%bvpq}FzhJ%k_G3Z_8*~is|-to@>x@j;7Dk2Ma%{q%j`E*M^Jp6 zV8R$IJW+n*!(cw#C5d6itqmH>*u^{CM;L&u%|ZmUX5mc&r0`*5*gx7{+A}F+tTZ$Y zN+TNQsbVBdTQ0%Mz!(H$U1~bRG=#VS)7rxo1W8W6bGmOzE4Tb02|1ApG6m z{q3AbAKHK$L04Ir#(yO@@9XGALfLFh-neFLwZW3Jj9YvF&8?anlY5u_AP+~w2U~E2 zHYaH*3k75EZjS)kovUY&5+UII3eh?U6fVKGcC?b{H9>3NEQj8=tvU4_r3F8lYC$2k zl=+8Ena(oe-fx508sFIRpZ~9aTcJ>74bIE={Lw!PLw|eD-_IW^jM^xIe}+Ij&W4~} ze3+OzO`^FtI|nr&(`5W|!HBzMGYMWCJowVaiIe9x4jg`E+<@C<*F9(f(XI+4eBHFw zr!fe^--3Y?V^<5*SfM3bjkLUxa4-u1`Nm-uKrqhKAj#^WS)|w^9%FH@z|O^_IjK%E zpE{L0lF25?2eCljM0>km)SCG@QP{3CxugM0g~f0qaAF3z1dyyBG+HlP+=vJYbBd<( zy9vRgM_*`zjNcBN5SHMd_lizS=VhXrQRr3EXV(zswqg&W^j}vlolqlayi*|y8kx49 zdV7sqJcFr?@g)J-?)y>S_Eb=x!M=1D3|<6X1r%ut@fYv1G?=R*8xf(&iX7&S2<_A8 zy#xUJ4-AbFf%+}@r_VGlrlR%v>6locaH;mx%Fb}HFivo;kC)~O~)K>!F0(-RDQWMjji7?%l0A~RVL3Fdaq zIm?z!16kh8k6--j0yeeheeR>Fy<;T`1b|Q`5EnwhV^*EjM@aU0{q^q+5ngD9!?V`> zGkxJ^#gF-E{_ck#T^UO5dsKVfXfzch@23Y=fFLXlVB!?@68g@=R2kP!ZTq5wws%qBV51@?dcna!PKk) zOfJC33c!&1vZy5DHxjW^yHOAuZ7n?msX`S(8v`i2Pr056W2!x#pF#v`b z1#gshVnXUWLPRwG^q>B0d~I5pxw&Yv7$&LB6@Wm=IHu$FcR!b9Neo){suPrYkgy>K zcQ$v+q_>|PhUHtz$Rmtw?<62*?K-nqWPTw|i*xc>LohflRzMkm2`cYo_R=bd(ijOI z`2Yk3^BF-RT!PR2^2E)->yQ|Q85hYR%CY-a>OFO^J$sz@I{{`hrpHwn#eK+bG#ZoK`2 zAI!7d5%*awsZRsLdM0rRUMK;<$`7*=*Y_BcAnc#~$sZ40{pL5n8JN8N_7A4-{$_2k zSkam3X)7Wr8L%RRM(D)I(t0UJ!VT;QVf3sX0C|qT#HzRflj!Cx%&i&|*Ylp_b z#GxHigK@?Pe2vDM{S}>-ck=jAO73awX{G@&>U#LCf%SVm|J`r*)ZzfDw+-Vv*Qp^Q z+DNmes8|X!?H7Y=53zik;x`fHan#nPCP^>&ykqS1p7A1Z?qC8SEVPXKuU@?}Oo=uy$5jxivFlILzARGxBwGq4*6RKxGkpAkI zznpK(`BHSi@Bi_Se{2|I@nBFi;2X28E{pg1fQL5!_HX{Cm76~tONS5>JcL8B79UOs5cnvMMMSTN;_ z=}jNc{#42gWsz8iyUv&rsR{b!AP4R79%b5%W@77pIUjWF##&mgbgO*D1oZ>W~=mw2|;biUz94nWNBJdmAPK~ z0#lO{5i{z`Yvn8ua@sW3Lj}-iVzGnBZv+6CV1Dr82EhZXfCgTz#Ia!DCq{A4gNPV2 z5zfqgI|g03QdzN><8WDVm|*Rju@m^#J+KHc6@0Vq2%0zFd~+Drw4(svBJ`Ki@RWl4 zUw^&YdojH0?!%zy*BHTl6&8fBcm3iQzZiHL3mOM!LP1|7f^pvnE%hADjCcl*5FqEz zpHGmghP((+Fd)pe%^ElmP9MYzcTxz81yqhpu%tv338|_1n%$~Aj( z+BB7@a3 ziFrtsel4{dM{Db@WOyDE*m_vFUoU8gG{{%Z4>FO2}8rLTO^;*&2wEsM@x?QN$NAxhZ6 zJWNeyHfS}h$%(hI!@1Yb4RVxZ5+PO}fN5_r1%N@S1M;i}6a>Wl2!$qOmeMM;A9x{h zNZQpb$NkEZLAd#J8kg^gQ3wJAM|jXiN}C_5!Vw>E2^8kuw3<+MBZlCT$ZkYjc2^pq z5W(=R`NqzJD`Z=rPVnv8*@lu4qI(>l7Pa@Auu{lU5Q?wAqvsIC!OUq^g;)&%$;}c_ z?2pMf6Hj3Qh^#~Qv8*40N7538W&VrWVFrodW2WyinsxJ>y zH1NIfko5+~Hr`4Q>4R9$pa1!v_v{j&W95O3{Q!K^-g^u7;%{)4I=(W!@9WnssA3&R zr8kvF2w0}S)J;@u>if(HqCJnzueqxwYZwp}hrGx7B6>;C=zY zN7Xyn_V54o+dl^s4F~7>2Fy8}D@^2HbGzO@GO6z|A?wI!a~i!-U~=u4Z>PEVZU9PJ z`N&PM?tE{6d7Mvr>qfr(-L^*BmY=uR4!cXopD1;_v+clkA8*O^gMQ}Ln_4fDQ-uS~m_`{7izW2sxr{8Cu{o)_~ zVdI?wK%ac7&Ro&$BB-9jAhen(FZt^)2pB~G_A;;^ zUrhn|*$SBm3i{hFI9D>0ALMgg&t*{%^~~AL{JwVbDR00!dZzi~gogL)2gdaLjsiKR zkdVU_gSFxAbdQGY3|10=z+E4 zTcjV;^S@ati$Gi=)r5}c(a6M&`kSVvIHYVdi3v1B?z8y&vfbp1$hs?Qk`n2DFkn^r ztghWKewv**eCpKcp^4dTI9JeE%;1>G5ZwH%Rg7PLc_mtTtGWjTQEMCg$NlNL8V`i` z`3vXAMYO=m(&*P-+Z@41`=f=K4NFWB0xShCC2NUAxw*MH0uKEcCo6|vZ!?@5S(;bA zj0eKoSW=Bo;a7s+-vs{9U3ADQy_0aJ;3VG}lVfjMpF2p1`=HPJjf21>pa?NaPGC^J z$M)Gs#DcbFQc^lw{^Ni6v-(XklCz71T+IB8kr4pS$JbA#C#j9_2!vEc%XpdeY3;6K z2!n)hnPC8lDOZ64c>*pF;T>D+*z6dV@4qc`g&&hmy79Pu&$jNZB-OrC8(a6kvhieh zLSS13CkqDhgKu67d6zG=5av=0_US~utq6Scdv7JJ&kgZ^@ZJYSd*2y>Wa>mpu=;KK zqR`^PsJR>UmC1~7Y&yD3C$%g#(+v?D1Bp*F3PRCr#%a6)MxKLc#LOqwzJeE~+xW}~ z^J+{7zOv`Q4@{Wf=HTzAIYs4z;AkFj5e*mB1$Y5D_j@0|cvfH5nmOS6_f*@JHVe^< znJxVnd=_fNt+S$>Fn1Y5wI5fM1)}s*- zl5f#(;|C`>1zQt%@*(tFAAHMQN5C|BC#F5x87x^Ep2y^b?}3D#*b0p*KH=S-VS(^> z*ByFIc@E5jL;nXys~i#hVsg`x0A@#*b9=tcwR21t+u2LY5sh$ba!NaCu{ z%p;^quJ|H%xp7%`l8d|qpCWG(S6(3(_FWPyMM72)9072`3^aiJe{1&v`}4u0aPnqGX^N<#TdX|?@8q3#-kBWLjq)I-q*bmG zwJ&enY*nj07`-9rNAvN1Nn>OAZ4@fU<9qPe-|z;{c+v4~dk0Wxb{8HmDy1Zspp6oI zO$CO__60bsTJY#ho8+9z8#}I&^8T&btPtyWzy0Nh-PiFig&zXf7!uU}ve;k}PZ0@$ zXfc9sWgdZ_DTxRIxsYJ47z1isTMi1qR0bvBluYZB7|3D|8ck)FolW=8RB3mZJ_%a7>)_z(XuHRvp+vRrzFgHR zx?lk6u38Z^~21;(l<>wXVY<1%lBjJ2|6n7@Fuu2*Bea(ApA1 z6VmVUXe8rg&TXB(DNk`XI3;!|APUiV7&*_o-{vQl4s+41q=cs%38|@0vodCU2%mEw zee_X=aI0qoYuE zc;f^nbR}aphgBak;Y0UPsQ8Un(?AAi;KKv(#<-!z{v-h_C-yGEKFtFrpOi^F`@7%1 z_aU^x?R@$hQr5YJaGKd1yM$kNErJdqi-jW~CIN)O&Qwa*oI4kAEEWw*+nUCJ6ob(h zH-+Xm=+RhhU}5#rnWJ`xIeK-JfbUZ&fZg^JloYS_@j>_i2dfi zZ;&$4F-rZ#r@?HRgbOHxO_3B+_0NB-nZzCj(GItMv$R{>O<*9F1}6kyCjb-y_e{kh zhQh?$Kru1v?_So%^Mnua;AwMp8~HG=K&z&^MnF=8oi}4}uPpdC-`LM!t=7ALkDx{T z1j0FZQ>ZfosSR!^%SmX!2xbCnobayfnIcp1hv_7ZFlToReLZ9Ow9rR#PzGH11`inB zb?p#XZE{QXWkD5Rk&7=;MKMvNaO7YN{0$cTfbMYT1v0J?RPL^JC$W}5qX&HZxGoN+ z2*9Hs%ui|IJ;E?vL`oE3l*-T{xX~8=e_AD_uz`!d=%5(M3rPwwgIC^!@}&sS=of7k zVu`M6_(d(l(5=2bKT09K{<_4g+KUH;rmw#G%8t5#OXU{On{qKu9>^!x+a4``P?rLB z8+pv?%V5n2k6f-nOlxw#b6t$AJ;hBw@Yu4ZC*bDJ#bYaL6G;$iN8zY_cuK&8d& zB_fn}9G7hD{q0|-03zhS^Z-I3v)O^65zc4RkwXKzb z-%fwV!sWF|rwSPJ`BM-RGBArw@7WTNHbej$%OF`p8{Y`&zK2hlt!+{|)t-zjH;If|?0ddjPiD8cq2$dm=)C9LWP@DN_W7X%NjC=fh@HW*l& zgn?p13vEzNO98C$j5p9Z^mh-%OKGr_yh+an)Pk+QJ}p^hv5xy?-xc<-z(PBG8j2>o zGA1E}b_k%vvawPG+e=yG9)zmKq%F#a5Xybry?cAYz3(e+_xsYNSGwo*8Rr!7q@)-Z zMuxoo=*=rbYp(=jEGA^?it*tk`45Rt)6A6V8k_3U*32YhoAT?CJm>HcFBrq7XnEQ( zdjGYqAX$j?^WhQ1Wy2eLfBV~Ce2AG5V?O;o1DtabPJ|UhE;npVL#!+@b#|?ggB<&d7g4H^#_mooSlT~$TI}o#t#UNk@XK3tM)(F zxAXoM53dYOm6H)3P!tas?fb_vj(Zv0=p=ciE%>$IMU4fxWws>&F`Fqc3@66sHu45A z9c6-%C@i$z6byHmRn@2GoJTt_5{B`N8av^JU(do9`!jUveu@!(C;_-Nedb(0X57ng zu;xpFP=?xe=vSYVlW{rF#Ec4I<&G<6qD&|ya8R(?!j~U^{PDmqgtJqW`FqCT`$7f^ zg97zUS)dCg1ecV=XKl|8Kcnl6@cCYfF|$-c6gpJG@g*tFgoSTJAq3i}%b88cz-8?lgb?6-7U}H8l8hy3+RN;T zg$%(k&({W_=E6__2+hjs+vb&4<~?k8RlLIk*o1j`4=hZcK(UuOXNeoa=P-MK2{E-X zgO7VJ^V;a2w*%oUJgeqyw_~YdYH+Z&rhQx|Oc>DpE9^*_5~|<^lku>~7>$J(&7LJh za34YiZ@erAT!C-1qP&jt6-vQ_^S0N*`1)WSw9NwfjXrW97}~vDS-8{BxcM=vSA7{X zIKb>V3dHzKrBMn$y8NT*Pt_%XT#UG4dICfljR(~=EB(-ROC}q`rcCjM>u_c~-~h9J zg%9?Q)V{Vhp+|#mm_An6$>OcO9z4fFil2Ew#s(KYU?e<}y^3;-M_(U&@b6}t*xrEZ z3DE1+1d>7|do*0g3e9-=zR*Z5tf+Gzk#no~w4CU4b6=^27bULu;_G$R|o;^ z!^5?`mMJ^PyKmEjln*c9Nv@CGZtHli(LG$eOKDup{OM<(&Upf9FTiD8jb+vUgV}w{ z>KA8+7_l%0a*d}%kT8q!AiQ>Dhu0X_VidCi@TJ_;#vurYnC77|CtTC!2W6xsTE%V( zNbaOu3E;e8H#~w<`7VJXK$qJk_n>XP6ck;#a%GgyovareztJ|_n4Y5Hc5+Am5c5k$ zf}H{e115hn>*R0B!Ly#hWa9+{pKAohECGuPA83jGrrX@-5ZAJlEW4v&jnaaDe4uS( zXDBf4D8K|2Jc}>#qU^-7hf@7YE z9>$|*FjC}rkS9u@WaQCBEUuP8O$dH?=iPTFF8{i{3OoY_N(3Fb;5;|Ln5|%BC{^2; zn>DlFz9}}>CBHF=4O4j{V8i3W6H49=Mik!9-hO*ttg6^*|8(KbNfH-s-~s$_YWzOv zvUxeqyU%#hNce+qw+cb$7>e<{!kwvQ8`kiE!8r^_xy1YI&oHu-07|E#IFPsURL=WDM?%Gs^By$NzZCH3?x zbYW=-+R!d|5t-1srmwF`L=l3|s(0X)f}7G(y(m9GDBzKjpyVV|-Lq;DDC`wRSqjVb zyqmHymN5h=7|Ky?y>H1q%&$Zl&9-OJgYf&k;&#?-c|pTjW^K&Z3v~ga|+N{|M&kfWAGr${Pxb(t5;Y1@r4-% z2M><|@Vr7W=O@YBwRhh6v)WSzok6HSZK@?eYk4PS%3OFteYV*gNxxNyc743NmtVQm z7~V)u*6zX9KHzc6!#K19AK-`QrS#TfH}3x6COW&vl3)io;K;!v1V2t-FlwT);B^eY z_(kRs??gAcXN4Yuya7Fu!<_ zK(HR3AyAeF%l;d)*px6}AZ;L!Z;WKo6E~99#++&+#ERT|Zl!kamuF$c-e_MwML?3A z6r5dxxc~a0jO7>PLnr}#`BtsRT6r@sykG>=k;X|;Vi+|6eye9UodtKMuC_K6__zWm|;zeV$js(qwfOtgT_=Gs{TqIk)EQhO( zcOR6*z1OUcVj{SpEGTjOC+9N8C;nmOd1>$FsqpHIh43$aUU=j^vhw)B*cjsRT#|^% z1%$f^x8ob^i~mhE@BK|7z+_FG?I4`BGB+qj!O#W{**XU3BGhts%*SVQ#zGpbEUE7m zzll3maspAb4JoX#?5-QV{)+mgo##HK1`6Dv%Q$m zrax;_;n13v^9)9{;tftI91N=vNpgzI`g!iIO+3JFD|BzQr3mJ&Kb->`rQw`yz-OV( zC@))>rN}Y_Y8%|cvgwNfsH6EX9T#I%rP^TkW@jwLEjIoYwqUwNKi_CNVO=-+4W~A| zARG%9c!qJ2FKIjT@VYYPD-W=&-FnTJ>P2*fUy9g=;uaFD&~b%XL)#M=w9V>XN$}C< z+O=zgpZDMYwNLQMh(iHVdHadewfIZd-~@zZ4{D34?{PQF0Mc}$FlrYeWLF`CcQI2x6{dKN)M)qK1rRBBp6r#>Y8GHWMX-NRp+=j6 z>{c9Pl@l07Sp_^ep5!`MNHGKt4oF}nAOzej6e7P+EGL9n%0LOQSqBwsF;oO`eX7n< zOyGmS5vUj(QzD*m%~yY6A`Gc*i1rLc$%7Cc5kj#(rGc@x+6eH$r8eOOlh+cIPuqTP z<*pdgNrLWMfyv$4X0^Q^VR->P-8F5p%t9B!%3@P=g!Jmww}x&kx?IBL-XAD0LOJ~S z%`3IHEWhWhmg8|y3ibjd073=rSt?D5yiy49>GkU|zKnL(HJU6VP~tKEn8in~g;5kF zxb4a$4};e%3xBmjD+NB38u%qAE$mYFZ}X4)55VS{aVrHbteY8$wZ7xpAO0{xP01LS zB)7L9Euk;Do9Aqt;{l}X9r(_@46QvD$tDF)fuOf>Qb)$K_q+e_n-5L5U?hS_!-}5C%{i+e4jovpHSIpNj;&b9Dl ze=Le~+!}u1M~3&7arG42&F~9@r@6IVa%g!69nfZ;1~{!N_Yskc$Zt=UHe;LFCt+PYVZ8tYWmPO5!<&#g_~k)HFY>pyai-tOzUF7ZSb;N#EuESfIlj(B24dLIh@( zS@v^?6C9R(iftk4!Nan`?UC-ja5b^lBsN1Bf<0es5>6o52XhJ+-~lrIrT{RWSj{*I zmG)h;AO=qK$vTT2`}zFYr3AX(m}qf@NvuC@Eshqy5gJJ)-x-?Onj1}Q7RnW6@m1>~ z_~v$E49vnEWz~fa+;DVJ?{Mi-3v{yZaF5Zwl+v^4Dn?vec&)fy7<2RHjbUtpO)+r) zCkzOmPY6Tfq6qMUDOceNIuQI#X%N0mAmN0kIbKWf$A!bFm=#`yhVTl9?qku%2*o!9 zKz>Alj&|yD@Q(jHE4-vEc$c1^7`$tQi}izZaG*On_4p+9rr&7f8L&>QTwbQ1;Y}>P zimkZegHK91T)Ad54lkUUaH#ei`h^4VGIp%}Yq|CpFP7MC3VOU);|>5j0P_lri!!F* zu77fUybv6MD!u z0I|10*_QZRF9nVLy02Wpfz?$dzp4g&`m`dG_BV#a8mk*f$6Pi-Q0(IEKp`e~m|5I! zRHk2EYS6Y3zgWL#LM-O^{L_Mqw62)iOGZqyS&?UP$bD}i%F6*a>%rZ7oN%!k=bDv3 z6q389mmqFgcS!C+xLa|%@ffSa7%>~oOaTxM?XleJyagzdWth0}7venX+QZyI-c=+M z>d%HL=UQ$|0q>t(I~@&UB^0b43wb*Q%B8f~hqwBu9y>ZLX%`KJXounANw6B<)l}P4~jF?>ZR8!b+a49o_|? zm%@qqqm(Gfg%>;A4sXQt1dagvzz0|-j0+}jbDrX-ti3}VOHlEi`T%&=4h8?L@zDcH z!ztoL16z45zCo+O^R&e3>zAkGdP&(71zJ-yu2~--uJ^K1VG;%Q#g{4EcuMFa>1rzN zkAM8}xh&7EgOxkk2(_ACocfc3+|nLm}0cb))JdY0*8 zaF&(pSJM+-*o>6&0;DAHc2SXUQ@sA?x8cSz=1FK;DJ9scAcNhN9!M!Xs zcrmlCr)R?0oci%pKgw|Nh z3a}i1Y<-3CaQ|6$`31~w#V3o8;lUrW!W$Nm86O`~I5z^=Z($v! z<~f9OAB9Iy7y>j@up|4;ic0hvpXoKiMi91XK(h!0$yj_w5l1lJLI*QHJk>?#=vW-@ z?Yy(YZajd!cw0hL3Sk61I>?83CnFEbG%wyE5BgK}CuwYI)^hSjF&+)tDvu^B_7?!x zmCIKus%jIFwtp8M@IZ&&(N`V?t|er7Z{Wwr_)_l0lr&lNfme!5SON}3Yxrm^fApqY zL-z0jeuZ`a*B4ss{kQ-0{)ZU+IE2t>XA*on$Vl!zZZQfAz%kuEAn`C_Op-64PAP=w zJXe$k_f4s+7}MOi0uDqF@8`lK#Dqc-i%muEJjk7C7vO_)&HtajoHFU9iIahjTqrbFBVnP8JplZ`hW zx364s)=f*C@OE;OvxxQ&@FGUbN4*jpR4>LbR?Xc>5Y5fIA4BoiF`9NKLmVLp;GjRe z!JkA=9N4?*U#!|OuO@sSq%16|5#rboghj)sgnt!!EJcKn6Q{P3iZ}CEQ8`{h_}~We zY$SyUc%u!{{?!L7snpfC<-(MoSW(#LIxk0_VHVBQ*5rH|H>HKS1|EuP48cO8O(~5X zFHla}-pw)*SPJfbV-O&i{<@z+mhYLJq2kNCDd}EJ8PBl%=-SA_iQj}~EdgHBx0vSMN2nOXDe$kaczId@M;%1Aqyu4h=Gx3~!7KI%rhX^olmh81xhZg>S5s6`tq% z^El8{A(Ieg!o^@e_U-_@(!XVV@-;^&YhZ96yTl1;k!~3xORD9@hgcerHK1whdjxZc z5d(0ecn*^-?OrkdQ7EJ9O?@GrTF3AyNmSRs~OmoUBr?J!Wpf6)|Tty4Do5SADZ1|@)C zMHBa{Dzv}ea+AfmR=BN1Cwkb<3bTWYmDLV-9zDWn?QxiE%<`TKhmvlRGLLNtsL$8AT97#uj0>fAJ8j@8z!d`|zXeRbL!5)(hQv2E0aY{uIQdR46Bw z9*x-pOD3Ql-)@zu%$J%+>RvKQ=Lm_a}Yy>B0e$wx^nXYyRvmZ!BgNEIqQi$CF$ zV8P+%jZREW`0VZkx4-(;uTnC-bff|+bXU@=U286`r)bp`NCcbzUvjg;9&Y})_|8}Q zzSBN^)CSssgEwdTm(Uw8IL51s&SqT*_8ZME2$i&Np8~uAAHrB|*mu(6Him`*7{e>& z5}%%~@(_L}HzULBAlweq81t??3ZQ{wSZ0%UL14u0D}p9)Sv>b}1-T<*bwdp8=HP}7 zVM02uz;l?upe8JczhP-X@~;(DP#GWj*O2xd3q^ zfzICUJgLn98h8zOw0@S_whGZ`jHimlt~JLH-icz;0YhW z^W54WfEUK{=T2s&5m^z+qjq_H_IPP@Ww`_fGk}%*edDW}-D|zV+R(&HJ!TPC9*Ms8 zsyzgh$iZv2;C$;+rrmf-yAA@+fbg~! zE^HCr>*VrAS#MK|XQB<5DS%0OXSiI`CO*Ln#xjae{NJF_6uzWwOWv6_KbUd%W!hrp6$kTXWLsZFW@P} z5d0W9$tnXbdA3|h!mmBTlGmCw2&R}~7B-}ahPT@Fl$GE<$q z28&Kf%shI}vGkn{w&F#eMPt2nE6*zYTyE;`zPHAvd=8J~E7lG|H8V^Ck5&^BHvM2O z3gk-Gji(^@1JB(1-8P$$s6!)+=%pRE+q1ymTmVJ`p}IU{c72p$)?fRSr`>FpkXiU?WTvKllIQi!YDmzjo~pJAd)P2fab)A7?BIc)Zy2i!Z(!#mT^co4c=; zh4DCQXKU6QuO6Iig9nH947hgfupKf{miXn}tKirbMmeJU=-p z&&;-Ajgv8f10ngy^2zx0hsOJ-2eH9eYh&>Ve+Qx1gmsE;G}J)2-_B3sW(cv0S|lRW z1V9?blDN>`A5iSE}9#?h98I|e5dgFM8!G&Ux6&$FgH z9gox0@PpCB<8Zl@XxHjXQ!}HHKzUijH{afxTm*%%V)+zPc%#H+$5}rDC{%g)fO20# z!wWJ!X$$dlwvNs6s+1%kl3Wy#bpvoCJUK`(mD$4Yyk zVr&ABR=fk}i0&x%dk!MxF>Cr6N3G#^HV z5RYpcz2KH7pnW{cs;W;=s3T880C>EnmmfT^5NL%E?j=NPI#C8+ao2*=CN1_*u#Ees zKfO9rfPeo#{%1 zZ-n9gV`_0Mw};iBSr0F~JcU1JFBH$T4fsxlKT3d~oiYVTv#mrJ_jq1s9rMK}!#o&_ z_4mRHjLz!G`pdLWf~fmOXhWXw&3a1Q@LERM#eEpUcrh79u+0{2O3NamZ*d-rgK6D| z5r4S8T-K%RgiY|W5^gLo;=v2;ivfeaXYHK3UZEv1*{KoQzGBGm)OD4J5^8YhSz&-a zSrXfXtvLMFFphS;cR=#T*j<|?%;9CD6GmbM>tO^ikh7pbU6Z)wo)Z$`oy4Jh3mD;q zXM`3)nFTjbOBjil%8zvTjV}p0n($mG2S$cK!xdrKtUvEZ+vtN2@aZP#=p>8JeKy^Q zxA6x0f>)R-`H077bCmwJE?=#Jx_xvohJS9Bgtubn=})d-A2=2Cz=`~hs#=DFqP&|o zak~XpCo#TfroV{n(5K|1PHD3F zt3Fvw?y1Eul)zZGnBEKlq?$St!?_O=HRO4o;-J`g9GhTsLow^NyUp6+#ys;XM4CC> z2rzM91m>!*-C7B6aMm_PQ-C2W>)US@dIU5g`P(yy?E$z8py!^K3@;@B4)mBAml%;3 zLZBp;j75F_g(~yLY9x3Qb~GmFhBw-uK=2|=X;RMmqX4Gw@awv37{=5hWo7u9S+Hn#Zq|Ey|e36IAILoju*2I z@Ehnd0PqM!vmuCv9FVqsHsQwv%Y9#Q@CZ-$QwrXo%Qa-NSboBfnP?l0`)!JffT?tv z%`#}(E*eO01lzg)H018JXy%}pw!Hv^0uKutXiJR5mB~wqb<)Y_j5hxZ-mB(gb7?c0!%Sw zs#z+yA^c$TypMD6K+yH)fFteM77RVsy!%?=IR#;i{yb5f;jSNJ-Rci6$FTJ*{-Izf3~lnv^ovG>oRV9!7Aw?asJ8qQUX@+1 z!S(v~cn7@3!TZwYTW`HJ^ktcU`RiW~9)yxFUaWYixc^ccjZg@7{}GZUhT|CocsxaM zHzN>?c#$V&N}E!i>EhtWGkC|?@vl;1+mus`_A~G*Jb4ZDm$ZHJ%9|6ynYq|zbT*vA zokKgj|Led1N4!6Sb8?UtW+;I0Gl-V$_qPWS$AEnx;Xw@G@pZSRxXOR>TQN>s_pv-jf9clOR-X%E1cQUr%P532{b-}H?|5|8>wW__QQwIr6isu1XGYvFBy zMQa{+og0lwys38ftgF9C-h?!XF^pm}hnqKVOud7ZaNhirzrAzue6RdULC~ni!!l!( z&63jm5VpCXn_@u-n)+e|B&(EmSsFNk=q>j0to8{1W_8>He+Uz6hTElp!+vr?ftt=) zai63b&8|J`<4ya3LAyer^M|d`Xo`w`wyJp>MZ!Pv>(u>8_&{cVQAUXSp1c=*C70~!0} zMU|w@gCP%VigvBDxn0uNVlOjkij(vM25rmvDB?nknFT0xdF7Q}TUVQY!z+IE%u6XA z;R>FBO0Ta6FJnn=@ZQv@Kr<$A6IvMbc6h)A=eF}3fjngT&pjIv7b9=aPiw=tI-BXd z?qw04MPPB&e&vu)ssgMGFlLc3%4*2|_c?)P25#_(g_*ov1VJntRZ4uE_P!TWa>cd; ziA5J<4f_V4Fn||=xu1kgR$>@3z`y^xmvAcyg+#xtR%|;P6ZTghlvfG3fGojLQMe!FBk6 z!zHwvz=0K$qfeWH-Vuv0iwP&JsIgO+R(Yz?M<4BBd_pqeSnYz@w%%ZaBRmNP_oAO~ z$__jm-tnNZ`xuMoEdHT1c@`|Xl3nA!)LgyS&&&EB^cJkCZ0tF+Ux(+*e`r7DEqQnC zE%O z&C5YG;>Qx%H$IVyZV$cK*%p%~24IXovdgJaZfMHQEBZO_jtLTLr3x>sz%MyXd z?;0Fph<_2|-~DeY@(uNR95u`s{uXibSo$_#VIEZXPpyZ;9Tz>SU;aW@6PqOowSD&^@} z3dP|ZcqsrDU!oK)<#PZG{}cf04o_>`OSw_ldnL`BCyYJNSg7s_WcDqi?VSM+^NQ}R z{LQ_lGWEYa1;Ux48h&DkC{z7p-SIR979V;o7M>HfP+D+<2R0h$%Xo~z0Y?NCeSP4i zQ{AuRTg77VcJDSO&+tfp`qtIv`t96YGB!nUSiOrkN}o>&<54vjSFc{3qOa?PdRAxR z|5ekm>ijna0UslmC$NBlXJ}@~I4CgJht4Hjm-1b5<(%i?LVI!z@Z?Ql#5ELmG$4Pw zX}G$^AV8x!#-`o7-vtfU4A)3ol6R*gszHn2#i{^&5CcE)+T!OA zrTuGvs{R6+kRh15b=7_MTFSfM3c|w+Yku=$<-^=q_12UPrNz@g9dE+no|#Vvkhn^9 zo!4Wd{+)>TSsuirhpY0-SScpo5!Qt9! zpHMhgvon4>E7$DKZm?6dkF$(-?=4G+ew2Whvr4Q{NDosKH4{83DJ>-Ud8l(Vycrh; zwA+$d4Dq%x+|4UFj44@9^X$em$yDQ=ekiyt%%KP=uGMhE9bAG3Vo$eY)Ksk%`e9-& zvZ+7t+Z=_Wa-aqU4#_re3O65h7+R<1Tfh`;Ofx=t`hC24p%CHKc(1A9 zXRpp$1KaCaaHe*JoSDKD9vFu{eRJC&0MIZLn#*CI@T0=Xu?^mB2FL36M=XIYmS$hf4V^z6EENqQ;pDZ&d%4ZAQ!D?L zW>@3?X!q-#y%$0VcdYXW&bluJ;5(+Qzug#5at>m5AFcTM``e3QtSvfUF2UYz%2Zbq;18)`pgHFsF`6 za@8$QfXl5wiLm(`S0-!k!e2GhAAdv2%2=z&M1wifMyuRV1kMIJHjUU}8Ksdp$%CoJ< z&>mEdQK>T?o$!WRPZ3N%C&B>d>;A?`aWpV|#F!Yj>*z2FwQmYzDHym}N?@a@-#+l> z@T~C?AltrymomT$j=PQDl<0-7nNBqYeR%Y3G-=Zh#XV>hoh%-+xQx+M=%)XLC)1-X zRnI_eWAN?cz|-T=)Q7mAff?^AJ{cntZrzIxla~n3Q;Rb^EjJ@1R=vstI2jboVopbR zLf;|K0Ms?Y2$2X!@H@4bVd0AG&NG;QF?m00@0A4eOoGP_SOkKpAi!V?mT+$V^Yp4` z-S(A>-yfR>uE0BBBr|6b@Ri;5YRJJd?DdHI8>rifId zcOwR(gWuS^2k+5C?*mi-)Pk9iM&Jk&rhsh7WKAEIKs!EO?%&2Fva;}kS)9|R54>&+ zG&t;<-_CD}VtspdStHE96u=TJIHE`~kha&$EzM7hW37o-It|ZRKUC@4hw$9^HkLB-9o|fOj0QVv|xZsR+fP&z{f8*rX$cI3sX z^t}6z;KPj&V0mcf^VjxfUw!#qN-5!vrt%hODvpOIISz2^PncmWYbp4`ePFM(0nu7s zic(P21WudGD6(4Pi6#}+83%kiHa=Vmz#!*z|4T^Ljq3!$5&m&&3C0OBcH=z2NoE0u z=q_Z{L~Y1!ZwdusE%#IzW*n+O_-yc6x4jh6C0a#_k zust(Nb*8DqGcmb$6xx)62E@#3L(W{Nl2I$ZPxHqpU*}R`O8kXzU^v?zgtO8s4Zal% z(Soi)zT^y{Ljd*en~;M8f_)$qLrL&B?_T|`0-m*208=?nKsN$3^ue3Hw&_IIHf5ph z+pSH|r=_fD$ZEswg$su}_r^_MUc)Ao6iVogKCGE!84rlXqL|PXLsJmSVY}YRKnm6< zSqG+DH>Ko0kAR02nWd&^-uVy;{85}N{wRVN56&pzS-T!CC<0S+8@@DXjN{?qEjR?@ z@k-VbEY_(G8Z_Q{71Ni(@ilrXFmqBA_M||5(c;>qdXP;;?wOK_z9O| zyXF~xy9eGEUoZX!$DjU2W80@ufW~}q@T|Q6decvaZ>GNMYrK;dJtdFjrBYDk0FL?y zM=dx1`qqtkHNEZj@CWNp&cN+}PX{F;gisGa*vXu6(wsw&eh2nEXZw5J0lpq { + const encoder = new TextEncoder(); + let currentChunkStart = 0; + + const stream = new ReadableStream({ + start(controller) { + while (currentChunkStart < input.length) { + const substring = input.slice( + currentChunkStart, + currentChunkStart + chunkLength + ); + currentChunkStart += chunkLength; + const chunk = encoder.encode(substring); + controller.enqueue(chunk); + } + controller.close(); + } + }); + + return stream; +} +export function getMockResponseStreaming( + filename: string, + chunkLength: number = 20 +): Partial { + const fullText = mocksLookup[filename]; + + return { + body: getChunkedStream(fullText, chunkLength) + }; +} + +export function getMockResponse(filename: string): Partial { + const fullText = mocksLookup[filename]; + return { + ok: true, + json: () => Promise.resolve(JSON.parse(fullText)) + }; +} From b4a868b591535ca1f867a1713dd00dc3268573ef Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 11:46:20 +0000 Subject: [PATCH 022/115] ignore vertex test data --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 4af8534f30..755403b091 100644 --- a/.gitignore +++ b/.gitignore @@ -579,3 +579,7 @@ website/public # Typescript items *.tsbuildinfo + +# vertexai test data +vertexai-sdk-test-data +mocks-lookup.ts From 1c5a1f8c59a61a2fa78f9bfc857f12c751b2d09e Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 11:46:38 +0000 Subject: [PATCH 023/115] convert mocks --- .../__tests__/test-utils/convert-mocks.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 packages/vertexai/__tests__/test-utils/convert-mocks.ts diff --git a/packages/vertexai/__tests__/test-utils/convert-mocks.ts b/packages/vertexai/__tests__/test-utils/convert-mocks.ts new file mode 100644 index 0000000000..bbca9aa4cf --- /dev/null +++ b/packages/vertexai/__tests__/test-utils/convert-mocks.ts @@ -0,0 +1,53 @@ +/** + * @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. + */ + +/** + * Converts mock text files into a js file that karma can read without + * using fs. + */ + +// eslint-disable-next-line @typescript-eslint/no-require-imports +const fs = require('fs'); +// eslint-disable-next-line @typescript-eslint/no-require-imports +const { join } = require('path'); + +const mockResponseDir = join(__dirname, 'vertexai-sdk-test-data/mock-responses'); + +async function main(): Promise { + const list = fs.readdirSync(mockResponseDir); + const lookup: Record = {}; + // eslint-disable-next-line guard-for-in + for (const fileName of list) { + const fullText = fs.readFileSync(join(mockResponseDir, fileName), 'utf-8'); + lookup[fileName] = fullText; + } + let fileText = `// Generated from mocks text files.`; + + fileText += '\n\n'; + fileText += `export const mocksLookup: Record = ${JSON.stringify( + lookup, + null, + 2, + )}`; + fileText += ';\n'; + fs.writeFileSync(join(__dirname, 'mocks-lookup.ts'), fileText, 'utf-8'); +} + +main().catch(e => { + console.error(e); + process.exit(1); +}); From 8c0bc5ee806f21ea8c49e01617e9758b17753c80 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 11:52:46 +0000 Subject: [PATCH 024/115] test: grab vertex mock responses --- package.json | 8 +++++--- vertex_mock_responses.sh | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) create mode 100755 vertex_mock_responses.sh diff --git a/package.json b/package.json index 6a6e82bca9..8751bd5535 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,10 @@ "lint:spellcheck": "spellchecker --quiet --files=\"docs/**/*.md\" --dictionaries=\"./.spellcheck.dict.txt\" --reports=\"spelling.json\" --plugins spell indefinite-article repeated-words syntax-mentions syntax-urls frontmatter", "tsc:compile": "tsc --project .", "lint:all": "yarn lint && yarn lint:markdown && yarn lint:spellcheck && yarn tsc:compile", - "tests:jest": "jest", - "tests:jest-watch": "jest --watch", - "tests:jest-coverage": "jest --coverage", + "tests:vertex:mocks": "./vertex_mock_responses.sh && yarn ts-node ./test-utils/convert-mocks.ts", + "tests:jest": "yarn tests:vertex:mocks && jest", + "tests:jest-watch": "tests:vertex:mocks && jest --watch", + "tests:jest-coverage": "tests:vertex:mocks && jest --coverage", "tests:packager:chrome": "cd tests && yarn react-native start --reset-cache", "tests:packager:jet": "cd tests && cross-env REACT_DEBUGGER=\"echo nope\" yarn react-native start", "tests:packager:jet-ci": "cd tests && (mkdir $HOME/.metro || true) && cross-env TMPDIR=$HOME/.metro REACT_DEBUGGER=\"echo nope\" yarn react-native start", @@ -89,6 +90,7 @@ "shelljs": "^0.8.5", "spellchecker-cli": "^6.2.0", "ts-jest": "^29.2.5", + "ts-node": "^10.9.2", "typescript": "^5.6.2" }, "resolutions": { diff --git a/vertex_mock_responses.sh b/vertex_mock_responses.sh new file mode 100755 index 0000000000..0d58789e4f --- /dev/null +++ b/vertex_mock_responses.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +# 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. + +# This script replaces mock response files for Vertex AI unit tests with a fresh +# clone of the shared repository of Vertex AI test data. + +RESPONSES_VERSION='v5.*' # The major version of mock responses to use +REPO_NAME="vertexai-sdk-test-data" +REPO_LINK="https://github.com/FirebaseExtended/$REPO_NAME.git" + +cd "$(dirname "$0")/packages/vertexai/__tests__/test-utils" || exit +rm -rf "$REPO_NAME" +git clone "$REPO_LINK" --quiet || exit +cd "$REPO_NAME" || exit + +# Find and checkout latest tag matching major version +TAG=$(git tag -l "$RESPONSES_VERSION" --sort=v:refname | tail -n1) +if [ -z "$TAG" ]; then + echo "Error: No tag matching '$RESPONSES_VERSION' found in $REPO_NAME" + exit +fi +git checkout "$TAG" --quiet From 4c205b729e23474e1b9fb99e7a6d6a4949850c33 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 12:04:41 +0000 Subject: [PATCH 025/115] setup jest for vertex --- jest.config.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/jest.config.js b/jest.config.js index 8c2c2459c8..40efbfead5 100644 --- a/jest.config.js +++ b/jest.config.js @@ -11,4 +11,7 @@ module.exports = { testPathIgnorePatterns: ['./packages/template'], moduleDirectories: ['node_modules', './tests/node_modules'], moduleFileExtensions: ['ts', 'tsx', 'js'], + transformIgnorePatterns: [ + 'node_modules/(?!((jest-)?react-native|@firebase|@react-native(-community)?))', + ], }; From 99e6179acecab597e5f5389a6e7dd02426858fbf Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 12:05:05 +0000 Subject: [PATCH 026/115] fix vertex script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8751bd5535..466ff52bd9 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "lint:spellcheck": "spellchecker --quiet --files=\"docs/**/*.md\" --dictionaries=\"./.spellcheck.dict.txt\" --reports=\"spelling.json\" --plugins spell indefinite-article repeated-words syntax-mentions syntax-urls frontmatter", "tsc:compile": "tsc --project .", "lint:all": "yarn lint && yarn lint:markdown && yarn lint:spellcheck && yarn tsc:compile", - "tests:vertex:mocks": "./vertex_mock_responses.sh && yarn ts-node ./test-utils/convert-mocks.ts", + "tests:vertex:mocks": "./vertex_mock_responses.sh && yarn ts-node ./packages/vertexai/__tests__/test-utils/convert-mocks.ts", "tests:jest": "yarn tests:vertex:mocks && jest", "tests:jest-watch": "tests:vertex:mocks && jest --watch", "tests:jest-coverage": "tests:vertex:mocks && jest --coverage", From 4df40ac9cbf1cfb32f2dff5d2775ba08f3032396 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 12:05:28 +0000 Subject: [PATCH 027/115] test: fix count token to use jest --- .../vertexai/__tests__/count-tokens.test.ts | 94 ++++++++----------- 1 file changed, 38 insertions(+), 56 deletions(-) diff --git a/packages/vertexai/__tests__/count-tokens.test.ts b/packages/vertexai/__tests__/count-tokens.test.ts index fd4b99e1e0..3cd7b78970 100644 --- a/packages/vertexai/__tests__/count-tokens.test.ts +++ b/packages/vertexai/__tests__/count-tokens.test.ts @@ -14,93 +14,75 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -import { expect, use } from 'chai'; -import { match, restore, stub } from 'sinon'; -import sinonChai from 'sinon-chai'; -import chaiAsPromised from 'chai-as-promised'; -import { getMockResponse } from '../../test-utils/mock-response'; -import * as request from '../requests/request'; -import { countTokens } from './count-tokens'; -import { CountTokensRequest } from '../types'; -import { ApiSettings } from '../types/internal'; -import { Task } from '../requests/request'; - -use(sinonChai); -use(chaiAsPromised); +import { describe, expect, it, afterEach, jest } from '@jest/globals'; +import { getMockResponse } from './test-utils/mock-response'; +import * as request from '../lib/requests/request'; +import { countTokens } from '../lib/methods/count-tokens'; +import { CountTokensRequest } from '../lib/types'; +import { ApiSettings } from '../lib/types/internal'; +import { Task } from '../lib/requests/request'; const fakeApiSettings: ApiSettings = { apiKey: 'key', project: 'my-project', - location: 'us-central1' + location: 'us-central1', }; const fakeRequestParams: CountTokensRequest = { - contents: [{ parts: [{ text: 'hello' }], role: 'user' }] + contents: [{ parts: [{ text: 'hello' }], role: 'user' }], }; describe('countTokens()', () => { afterEach(() => { - restore(); + jest.restoreAllMocks(); }); + it('total tokens', async () => { const mockResponse = getMockResponse('unary-success-total-tokens.json'); - const makeRequestStub = stub(request, 'makeRequest').resolves( - mockResponse as Response - ); - const result = await countTokens( - fakeApiSettings, - 'model', - fakeRequestParams - ); - expect(result.totalTokens).to.equal(6); - expect(result.totalBillableCharacters).to.equal(16); - expect(makeRequestStub).to.be.calledWith( + const makeRequestStub = jest + .spyOn(request, 'makeRequest') + .mockResolvedValue(mockResponse as Response); + const result = await countTokens(fakeApiSettings, 'model', fakeRequestParams); + expect(result.totalTokens).toBe(6); + expect(result.totalBillableCharacters).toBe(16); + expect(makeRequestStub).toHaveBeenCalledWith( 'model', Task.COUNT_TOKENS, fakeApiSettings, false, - match((value: string) => { - return value.includes('contents'); - }), - undefined + expect.stringContaining('contents'), + undefined, ); }); + it('total tokens no billable characters', async () => { - const mockResponse = getMockResponse( - 'unary-success-no-billable-characters.json' - ); - const makeRequestStub = stub(request, 'makeRequest').resolves( - mockResponse as Response - ); - const result = await countTokens( - fakeApiSettings, - 'model', - fakeRequestParams - ); - expect(result.totalTokens).to.equal(258); - expect(result).to.not.have.property('totalBillableCharacters'); - expect(makeRequestStub).to.be.calledWith( + const mockResponse = getMockResponse('unary-success-no-billable-characters.json'); + const makeRequestStub = jest + .spyOn(request, 'makeRequest') + .mockResolvedValue(mockResponse as Response); + const result = await countTokens(fakeApiSettings, 'model', fakeRequestParams); + expect(result.totalTokens).toBe(258); + expect(result).not.toHaveProperty('totalBillableCharacters'); + expect(makeRequestStub).toHaveBeenCalledWith( 'model', Task.COUNT_TOKENS, fakeApiSettings, false, - match((value: string) => { - return value.includes('contents'); - }), - undefined + expect.stringContaining('contents'), + undefined, ); }); + it('model not found', async () => { const mockResponse = getMockResponse('unary-failure-model-not-found.json'); - const mockFetch = stub(globalThis, 'fetch').resolves({ + const mockFetch = jest.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: false, status: 404, - json: mockResponse.json + json: mockResponse.json, } as Response); - await expect( - countTokens(fakeApiSettings, 'model', fakeRequestParams) - ).to.be.rejectedWith(/404.*not found/); - expect(mockFetch).to.be.called; + await expect(countTokens(fakeApiSettings, 'model', fakeRequestParams)).rejects.toThrow( + /404.*not found/, + ); + expect(mockFetch).toHaveBeenCalled(); }); }); From e76e4c4512097c5eb0eca5799349343eec958c7e Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 12:05:51 +0000 Subject: [PATCH 028/115] yarn.lock --- jest.config.js | 2 +- yarn.lock | 192 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 191 insertions(+), 3 deletions(-) diff --git a/jest.config.js b/jest.config.js index 40efbfead5..b618b1c0d8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,7 +6,7 @@ module.exports = { '\\.(ts|tsx)$': 'ts-jest', }, setupFiles: ['./jest.setup.ts'], - testMatch: ['**/packages/**/__tests__/**/*.test.(ts|js)'], + testMatch: ['**/packages/vertexai/__tests__/**/*.test.(ts|js)'], modulePaths: ['node_modules', './tests/node_modules'], testPathIgnorePatterns: ['./packages/template'], moduleDirectories: ['node_modules', './tests/node_modules'], diff --git a/yarn.lock b/yarn.lock index ddf7305504..da67db9998 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4527,6 +4527,15 @@ __metadata: languageName: node linkType: hard +"@cspotcode/source-map-support@npm:^0.8.0": + version: 0.8.1 + resolution: "@cspotcode/source-map-support@npm:0.8.1" + dependencies: + "@jridgewell/trace-mapping": "npm:0.3.9" + checksum: 10/b6e38a1712fab242c86a241c229cf562195aad985d0564bd352ac404be583029e89e93028ffd2c251d2c407ecac5fb0cbdca94a2d5c10f29ac806ede0508b3ff + languageName: node + linkType: hard + "@dabh/diagnostics@npm:^2.0.2": version: 2.0.3 resolution: "@dabh/diagnostics@npm:2.0.3" @@ -6081,7 +6090,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/resolve-uri@npm:^3.1.0": +"@jridgewell/resolve-uri@npm:^3.0.3, @jridgewell/resolve-uri@npm:^3.1.0": version: 3.1.2 resolution: "@jridgewell/resolve-uri@npm:3.1.2" checksum: 10/97106439d750a409c22c8bff822d648f6a71f3aa9bc8e5129efdc36343cd3096ddc4eeb1c62d2fe48e9bdd4db37b05d4646a17114ecebd3bbcacfa2de51c3c1d @@ -6119,6 +6128,16 @@ __metadata: languageName: node linkType: hard +"@jridgewell/trace-mapping@npm:0.3.9": + version: 0.3.9 + resolution: "@jridgewell/trace-mapping@npm:0.3.9" + dependencies: + "@jridgewell/resolve-uri": "npm:^3.0.3" + "@jridgewell/sourcemap-codec": "npm:^1.4.10" + checksum: 10/83deafb8e7a5ca98993c2c6eeaa93c270f6f647a4c0dc00deb38c9cf9b2d3b7bf15e8839540155247ef034a052c0ec4466f980bf0c9e2ab63b97d16c0cedd3ff + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.9": version: 0.3.22 resolution: "@jridgewell/trace-mapping@npm:0.3.22" @@ -7573,8 +7592,13 @@ __metadata: version: 0.0.0-use.local resolution: "@react-native-firebase/vertexai@workspace:packages/vertexai" dependencies: + "@types/text-encoding": "npm:^0" react-native-builder-bob: "npm:^0.35.2" + react-native-fetch-api: "npm:^3.0.0" + react-native-polyfill-globals: "npm:^3.1.0" + text-encoding: "npm:^0.7.0" typescript: "npm:^5.7.2" + web-streams-polyfill: "npm:^4.1.0" peerDependencies: "@react-native-firebase/app": 21.6.2 languageName: unknown @@ -8189,6 +8213,34 @@ __metadata: languageName: node linkType: hard +"@tsconfig/node10@npm:^1.0.7": + version: 1.0.11 + resolution: "@tsconfig/node10@npm:1.0.11" + checksum: 10/51fe47d55fe1b80ec35e6e5ed30a13665fd3a531945350aa74a14a1e82875fb60b350c2f2a5e72a64831b1b6bc02acb6760c30b3738b54954ec2dea82db7a267 + languageName: node + linkType: hard + +"@tsconfig/node12@npm:^1.0.7": + version: 1.0.11 + resolution: "@tsconfig/node12@npm:1.0.11" + checksum: 10/5ce29a41b13e7897a58b8e2df11269c5395999e588b9a467386f99d1d26f6c77d1af2719e407621412520ea30517d718d5192a32403b8dfcc163bf33e40a338a + languageName: node + linkType: hard + +"@tsconfig/node14@npm:^1.0.0": + version: 1.0.3 + resolution: "@tsconfig/node14@npm:1.0.3" + checksum: 10/19275fe80c4c8d0ad0abed6a96dbf00642e88b220b090418609c4376e1cef81bf16237bf170ad1b341452feddb8115d8dd2e5acdfdea1b27422071163dc9ba9d + languageName: node + linkType: hard + +"@tsconfig/node16@npm:^1.0.2": + version: 1.0.4 + resolution: "@tsconfig/node16@npm:1.0.4" + checksum: 10/202319785901f942a6e1e476b872d421baec20cf09f4b266a1854060efbf78cde16a4d256e8bc949d31e6cd9a90f1e8ef8fb06af96a65e98338a2b6b0de0a0ff + languageName: node + linkType: hard + "@tufjs/canonical-json@npm:2.0.0": version: 2.0.0 resolution: "@tufjs/canonical-json@npm:2.0.0" @@ -8456,6 +8508,13 @@ __metadata: languageName: node linkType: hard +"@types/text-encoding@npm:^0": + version: 0.0.40 + resolution: "@types/text-encoding@npm:0.0.40" + checksum: 10/7eef99107cdf5c1827b51b830a3b65cfd3cc34f9aa760bc2be2f5547cec28fef5c4104473f0d1fdd739eebd7ac9f1d743902af7e1c31852a4a3e748900001e51 + languageName: node + linkType: hard + "@types/tough-cookie@npm:*": version: 4.0.5 resolution: "@types/tough-cookie@npm:4.0.5" @@ -8750,6 +8809,24 @@ __metadata: languageName: node linkType: hard +"acorn-walk@npm:^8.1.1": + version: 8.3.4 + resolution: "acorn-walk@npm:8.3.4" + dependencies: + acorn: "npm:^8.11.0" + checksum: 10/871386764e1451c637bb8ab9f76f4995d408057e9909be6fb5ad68537ae3375d85e6a6f170b98989f44ab3ff6c74ad120bc2779a3d577606e7a0cd2b4efcaf77 + languageName: node + linkType: hard + +"acorn@npm:^8.11.0, acorn@npm:^8.4.1": + version: 8.14.0 + resolution: "acorn@npm:8.14.0" + bin: + acorn: bin/acorn + checksum: 10/6df29c35556782ca9e632db461a7f97947772c6c1d5438a81f0c873a3da3a792487e83e404d1c6c25f70513e91aa18745f6eafb1fcc3a43ecd1920b21dd173d2 + languageName: node + linkType: hard + "acorn@npm:^8.8.2": version: 8.11.3 resolution: "acorn@npm:8.11.3" @@ -9044,6 +9121,13 @@ __metadata: languageName: node linkType: hard +"arg@npm:^4.1.0": + version: 4.1.3 + resolution: "arg@npm:4.1.3" + checksum: 10/969b491082f20cad166649fa4d2073ea9e974a4e5ac36247ca23d2e5a8b3cb12d60e9ff70a8acfe26d76566c71fd351ee5e6a9a6595157eb36f92b1fd64e1599 + languageName: node + linkType: hard + "argparse@npm:^1.0.7": version: 1.0.10 resolution: "argparse@npm:1.0.10" @@ -11379,6 +11463,13 @@ __metadata: languageName: node linkType: hard +"create-require@npm:^1.1.0": + version: 1.1.1 + resolution: "create-require@npm:1.1.1" + checksum: 10/a9a1503d4390d8b59ad86f4607de7870b39cad43d929813599a23714831e81c520bddf61bcdd1f8e30f05fd3a2b71ae8538e946eb2786dc65c2bbc520f692eff + languageName: node + linkType: hard + "cross-env@npm:^5.1.3": version: 5.2.1 resolution: "cross-env@npm:5.2.1" @@ -12002,6 +12093,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:^4.0.1": + version: 4.0.2 + resolution: "diff@npm:4.0.2" + checksum: 10/ec09ec2101934ca5966355a229d77afcad5911c92e2a77413efda5455636c4cf2ce84057e2d7715227a2eeeda04255b849bd3ae3a4dd22eb22e86e76456df069 + languageName: node + linkType: hard + "diff@npm:^5.0.0, diff@npm:^5.1.0, diff@npm:^5.2.0": version: 5.2.0 resolution: "diff@npm:5.2.0" @@ -17720,7 +17818,7 @@ __metadata: languageName: node linkType: hard -"make-error@npm:^1.3.6": +"make-error@npm:^1.1.1, make-error@npm:^1.3.6": version: 1.3.6 resolution: "make-error@npm:1.3.6" checksum: 10/b86e5e0e25f7f777b77fabd8e2cbf15737972869d852a22b7e73c17623928fccb826d8e46b9951501d3f20e51ad74ba8c59ed584f610526a48f8ccf88aaec402 @@ -21667,6 +21765,15 @@ __metadata: languageName: node linkType: hard +"react-native-fetch-api@npm:^3.0.0": + version: 3.0.0 + resolution: "react-native-fetch-api@npm:3.0.0" + dependencies: + p-defer: "npm:^3.0.0" + checksum: 10/e1e612d402615b439eb996b1fcc677944841a3ae51a31a2b0527e03a8e3afe00c0504ade4e88de0a36f6d11df45b2a543224e7f845c5763e68f585b1108937e7 + languageName: node + linkType: hard + "react-native-firebase-tests@workspace:tests": version: 0.0.0-use.local resolution: "react-native-firebase-tests@workspace:tests" @@ -21756,6 +21863,7 @@ __metadata: shelljs: "npm:^0.8.5" spellchecker-cli: "npm:^6.2.0" ts-jest: "npm:^29.2.5" + ts-node: "npm:^10.9.2" typescript: "npm:^5.6.2" languageName: unknown linkType: soft @@ -21813,6 +21921,20 @@ __metadata: languageName: node linkType: hard +"react-native-polyfill-globals@npm:^3.1.0": + version: 3.1.0 + resolution: "react-native-polyfill-globals@npm:3.1.0" + peerDependencies: + base-64: "*" + react-native-fetch-api: "*" + react-native-get-random-values: "*" + react-native-url-polyfill: "*" + text-encoding: "*" + web-streams-polyfill: "*" + checksum: 10/1385de6de67bf65842a3c4549f22a91ba39de576f8ccdd3e3ad79d67defc9953f8d6a2a53aedfbcd04344b8c8359b1c92242de4624066d680f40ca0310f2f7d6 + languageName: node + linkType: hard + "react-native@npm:*": version: 0.73.4 resolution: "react-native@npm:0.73.4" @@ -24396,6 +24518,13 @@ __metadata: languageName: node linkType: hard +"text-encoding@npm:^0.7.0": + version: 0.7.0 + resolution: "text-encoding@npm:0.7.0" + checksum: 10/c61b7a59a54c58f0714da0ef4c1f65732821bb1f761e6f21c3e681e51c6dd56fdefa4fcfccd3254bf47132d87420fbc9898da21a804c89e87861f4e1478d5f18 + languageName: node + linkType: hard + "text-extensions@npm:^1.0.0": version: 1.9.0 resolution: "text-extensions@npm:1.9.0" @@ -24650,6 +24779,44 @@ __metadata: languageName: node linkType: hard +"ts-node@npm:^10.9.2": + version: 10.9.2 + resolution: "ts-node@npm:10.9.2" + dependencies: + "@cspotcode/source-map-support": "npm:^0.8.0" + "@tsconfig/node10": "npm:^1.0.7" + "@tsconfig/node12": "npm:^1.0.7" + "@tsconfig/node14": "npm:^1.0.0" + "@tsconfig/node16": "npm:^1.0.2" + acorn: "npm:^8.4.1" + acorn-walk: "npm:^8.1.1" + arg: "npm:^4.1.0" + create-require: "npm:^1.1.0" + diff: "npm:^4.0.1" + make-error: "npm:^1.1.1" + v8-compile-cache-lib: "npm:^3.0.1" + yn: "npm:3.1.1" + peerDependencies: + "@swc/core": ">=1.2.50" + "@swc/wasm": ">=1.2.50" + "@types/node": "*" + typescript: ">=2.7" + peerDependenciesMeta: + "@swc/core": + optional: true + "@swc/wasm": + optional: true + bin: + ts-node: dist/bin.js + ts-node-cwd: dist/bin-cwd.js + ts-node-esm: dist/bin-esm.js + ts-node-script: dist/bin-script.js + ts-node-transpile-only: dist/bin-transpile.js + ts-script: dist/bin-script-deprecated.js + checksum: 10/a91a15b3c9f76ac462f006fa88b6bfa528130dcfb849dd7ef7f9d640832ab681e235b8a2bc58ecde42f72851cc1d5d4e22c901b0c11aa51001ea1d395074b794 + languageName: node + linkType: hard + "tsconfig-paths@npm:^4.1.2": version: 4.2.0 resolution: "tsconfig-paths@npm:4.2.0" @@ -25471,6 +25638,13 @@ __metadata: languageName: node linkType: hard +"v8-compile-cache-lib@npm:^3.0.1": + version: 3.0.1 + resolution: "v8-compile-cache-lib@npm:3.0.1" + checksum: 10/88d3423a52b6aaf1836be779cab12f7016d47ad8430dffba6edf766695e6d90ad4adaa3d8eeb512cc05924f3e246c4a4ca51e089dccf4402caa536b5e5be8961 + languageName: node + linkType: hard + "v8-to-istanbul@npm:^9.0.1": version: 9.2.0 resolution: "v8-to-istanbul@npm:9.2.0" @@ -25644,6 +25818,13 @@ __metadata: languageName: node linkType: hard +"web-streams-polyfill@npm:^4.1.0": + version: 4.1.0 + resolution: "web-streams-polyfill@npm:4.1.0" + checksum: 10/c6e802e6b83a8dc72acee8ec14b3a4af46d5ea4a15d0260303d7e4cab3c600716ff998b70457bc3ac0998073c0e73344c89400bfcc5f49a9bc0b10bb263c2f5f + languageName: node + linkType: hard + "webidl-conversions@npm:^3.0.0": version: 3.0.1 resolution: "webidl-conversions@npm:3.0.1" @@ -26283,6 +26464,13 @@ __metadata: languageName: node linkType: hard +"yn@npm:3.1.1": + version: 3.1.1 + resolution: "yn@npm:3.1.1" + checksum: 10/2c487b0e149e746ef48cda9f8bad10fc83693cd69d7f9dcd8be4214e985de33a29c9e24f3c0d6bcf2288427040a8947406ab27f7af67ee9456e6b84854f02dd6 + languageName: node + linkType: hard + "yocto-queue@npm:^0.1.0": version: 0.1.0 resolution: "yocto-queue@npm:0.1.0" From 5740233970443c312afc75be56b2a5ecdb3643cb Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 12:06:21 +0000 Subject: [PATCH 029/115] revert jest config --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index b618b1c0d8..40efbfead5 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,7 +6,7 @@ module.exports = { '\\.(ts|tsx)$': 'ts-jest', }, setupFiles: ['./jest.setup.ts'], - testMatch: ['**/packages/vertexai/__tests__/**/*.test.(ts|js)'], + testMatch: ['**/packages/**/__tests__/**/*.test.(ts|js)'], modulePaths: ['node_modules', './tests/node_modules'], testPathIgnorePatterns: ['./packages/template'], moduleDirectories: ['node_modules', './tests/node_modules'], From f29f85b2ecf55f98d7da5337c9572b23503318d5 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 12:07:58 +0000 Subject: [PATCH 030/115] test: port over generate content test --- .../__tests__/generate-content.test.ts | 239 ++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 packages/vertexai/__tests__/generate-content.test.ts diff --git a/packages/vertexai/__tests__/generate-content.test.ts b/packages/vertexai/__tests__/generate-content.test.ts new file mode 100644 index 0000000000..c5a1d9e1e9 --- /dev/null +++ b/packages/vertexai/__tests__/generate-content.test.ts @@ -0,0 +1,239 @@ +/** + * @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 { match, restore, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { getMockResponse } from '../../test-utils/mock-response'; +import * as request from '../requests/request'; +import { generateContent } from './generate-content'; +import { + GenerateContentRequest, + HarmBlockMethod, + HarmBlockThreshold, + HarmCategory +} from '../types'; +import { ApiSettings } from '../types/internal'; +import { Task } from '../requests/request'; + +use(sinonChai); +use(chaiAsPromised); + +const fakeApiSettings: ApiSettings = { + apiKey: 'key', + project: 'my-project', + location: 'us-central1' +}; + +const fakeRequestParams: GenerateContentRequest = { + contents: [{ parts: [{ text: 'hello' }], role: 'user' }], + generationConfig: { + topK: 16 + }, + safetySettings: [ + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE, + method: HarmBlockMethod.SEVERITY + } + ] +}; + +describe('generateContent()', () => { + afterEach(() => { + restore(); + }); + it('short response', async () => { + const mockResponse = getMockResponse( + 'unary-success-basic-reply-short.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text()).to.include('Mountain View, California'); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + match((value: string) => { + return value.includes('contents'); + }), + undefined + ); + }); + it('long response', async () => { + const mockResponse = getMockResponse('unary-success-basic-reply-long.json'); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text()).to.include('Use Freshly Ground Coffee'); + expect(result.response.text()).to.include('30 minutes of brewing'); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + match.any + ); + }); + it('citations', async () => { + const mockResponse = getMockResponse('unary-success-citations.json'); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text()).to.include( + 'Some information cited from an external source' + ); + expect( + result.response.candidates?.[0].citationMetadata?.citations.length + ).to.equal(3); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + match.any + ); + }); + it('blocked prompt', async () => { + const mockResponse = getMockResponse( + 'unary-failure-prompt-blocked-safety.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text).to.throw('SAFETY'); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + match.any + ); + }); + it('finishReason safety', async () => { + const mockResponse = getMockResponse( + 'unary-failure-finish-reason-safety.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text).to.throw('SAFETY'); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + match.any + ); + }); + it('empty content', async () => { + const mockResponse = getMockResponse('unary-failure-empty-content.json'); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text()).to.equal(''); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + match.any + ); + }); + it('unknown enum - should ignore', async () => { + const mockResponse = getMockResponse( + 'unary-success-unknown-enum-safety-ratings.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + const result = await generateContent( + fakeApiSettings, + 'model', + fakeRequestParams + ); + expect(result.response.text()).to.include('Some text'); + expect(makeRequestStub).to.be.calledWith( + 'model', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + match.any + ); + }); + it('image rejected (400)', async () => { + const mockResponse = getMockResponse('unary-failure-image-rejected.json'); + const mockFetch = stub(globalThis, 'fetch').resolves({ + ok: false, + status: 400, + json: mockResponse.json + } as Response); + await expect( + generateContent(fakeApiSettings, 'model', fakeRequestParams) + ).to.be.rejectedWith(/400.*invalid argument/); + expect(mockFetch).to.be.called; + }); + it('api not enabled (403)', async () => { + const mockResponse = getMockResponse( + 'unary-failure-firebasevertexai-api-not-enabled.json' + ); + const mockFetch = stub(globalThis, 'fetch').resolves({ + ok: false, + status: 403, + json: mockResponse.json + } as Response); + await expect( + generateContent(fakeApiSettings, 'model', fakeRequestParams) + ).to.be.rejectedWith( + /firebasevertexai\.googleapis[\s\S]*my-project[\s\S]*api-not-enabled/ + ); + expect(mockFetch).to.be.called; + }); +}); From b754196cba55f220b851e05ae3ee559c1abcbf16 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 13:40:39 +0000 Subject: [PATCH 031/115] test: fix generate content --- .../__tests__/generate-content.test.ts | 249 ++++++++---------- 1 file changed, 110 insertions(+), 139 deletions(-) diff --git a/packages/vertexai/__tests__/generate-content.test.ts b/packages/vertexai/__tests__/generate-content.test.ts index c5a1d9e1e9..c9ae29b9c2 100644 --- a/packages/vertexai/__tests__/generate-content.test.ts +++ b/packages/vertexai/__tests__/generate-content.test.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,227 +12,199 @@ * 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 { match, restore, stub } from 'sinon'; -import sinonChai from 'sinon-chai'; -import chaiAsPromised from 'chai-as-promised'; -import { getMockResponse } from '../../test-utils/mock-response'; -import * as request from '../requests/request'; -import { generateContent } from './generate-content'; +import { describe, expect, it, afterEach, jest } from '@jest/globals'; +import { getMockResponse } from './test-utils/mock-response'; +import * as request from '../lib/requests/request'; +import { generateContent } from '../lib/methods/generate-content'; import { GenerateContentRequest, HarmBlockMethod, HarmBlockThreshold, - HarmCategory -} from '../types'; -import { ApiSettings } from '../types/internal'; -import { Task } from '../requests/request'; - -use(sinonChai); -use(chaiAsPromised); + HarmCategory, + // RequestOptions, +} from '../lib/types'; +import { ApiSettings } from '../lib/types/internal'; +import { Task } from '../lib/requests/request'; const fakeApiSettings: ApiSettings = { apiKey: 'key', project: 'my-project', - location: 'us-central1' + location: 'us-central1', }; +// const requestOptions: RequestOptions = { +// timeout: 1000, +// }; + const fakeRequestParams: GenerateContentRequest = { contents: [{ parts: [{ text: 'hello' }], role: 'user' }], generationConfig: { - topK: 16 + topK: 16, }, safetySettings: [ { category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold: HarmBlockThreshold.BLOCK_LOW_AND_ABOVE, - method: HarmBlockMethod.SEVERITY - } - ] + method: HarmBlockMethod.SEVERITY, + }, + ], }; describe('generateContent()', () => { afterEach(() => { - restore(); + jest.restoreAllMocks(); }); + it('short response', async () => { - const mockResponse = getMockResponse( - 'unary-success-basic-reply-short.json' - ); - const makeRequestStub = stub(request, 'makeRequest').resolves( - mockResponse as Response - ); - const result = await generateContent( - fakeApiSettings, - 'model', - fakeRequestParams - ); - expect(result.response.text()).to.include('Mountain View, California'); - expect(makeRequestStub).to.be.calledWith( + const mockResponse = getMockResponse('unary-success-basic-reply-short.json'); + const makeRequestStub = jest + .spyOn(request, 'makeRequest') + .mockResolvedValue(mockResponse as Response); + const result = await generateContent(fakeApiSettings, 'model', fakeRequestParams); + expect(await result.response.text()).toContain('Mountain View, California'); + expect(makeRequestStub).toHaveBeenCalledWith( 'model', Task.GENERATE_CONTENT, fakeApiSettings, false, - match((value: string) => { - return value.includes('contents'); - }), - undefined + expect.stringContaining('contents'), + undefined, ); }); + it('long response', async () => { const mockResponse = getMockResponse('unary-success-basic-reply-long.json'); - const makeRequestStub = stub(request, 'makeRequest').resolves( - mockResponse as Response - ); - const result = await generateContent( - fakeApiSettings, - 'model', - fakeRequestParams - ); - expect(result.response.text()).to.include('Use Freshly Ground Coffee'); - expect(result.response.text()).to.include('30 minutes of brewing'); - expect(makeRequestStub).to.be.calledWith( + const makeRequestStub = jest + .spyOn(request, 'makeRequest') + .mockResolvedValue(mockResponse as Response); + const result = await generateContent(fakeApiSettings, 'model', fakeRequestParams); + expect(await result.response.text()).toContain('Use Freshly Ground Coffee'); + expect(await result.response.text()).toContain('30 minutes of brewing'); + expect(makeRequestStub).toHaveBeenCalledWith( 'model', Task.GENERATE_CONTENT, fakeApiSettings, false, - match.any + expect.anything(), + undefined, ); }); + it('citations', async () => { const mockResponse = getMockResponse('unary-success-citations.json'); - const makeRequestStub = stub(request, 'makeRequest').resolves( - mockResponse as Response - ); - const result = await generateContent( - fakeApiSettings, - 'model', - fakeRequestParams - ); - expect(result.response.text()).to.include( - 'Some information cited from an external source' - ); - expect( - result.response.candidates?.[0].citationMetadata?.citations.length - ).to.equal(3); - expect(makeRequestStub).to.be.calledWith( + const makeRequestStub = jest + .spyOn(request, 'makeRequest') + .mockResolvedValue(mockResponse as Response); + const result = await generateContent(fakeApiSettings, 'model', fakeRequestParams); + expect(await result.response.text()).toContain( + 'Some information cited from an external source', + ); + expect(result.response.candidates?.[0]!.citationMetadata?.citations.length).toBe(3); + expect(makeRequestStub).toHaveBeenCalledWith( 'model', Task.GENERATE_CONTENT, fakeApiSettings, false, - match.any + expect.anything(), + undefined, ); }); + it('blocked prompt', async () => { - const mockResponse = getMockResponse( - 'unary-failure-prompt-blocked-safety.json' - ); - const makeRequestStub = stub(request, 'makeRequest').resolves( - mockResponse as Response - ); - const result = await generateContent( - fakeApiSettings, - 'model', - fakeRequestParams - ); - expect(result.response.text).to.throw('SAFETY'); - expect(makeRequestStub).to.be.calledWith( + const mockResponse = getMockResponse('unary-failure-prompt-blocked-safety.json'); + const makeRequestStub = jest + .spyOn(request, 'makeRequest') + .mockResolvedValue(mockResponse as Response); + + const result = await generateContent(fakeApiSettings, 'model', fakeRequestParams); + + expect(() => result.response.text()).toThrowError('SAFETY'); + expect(makeRequestStub).toHaveBeenCalledWith( 'model', Task.GENERATE_CONTENT, fakeApiSettings, false, - match.any + expect.anything(), + undefined, ); }); + it('finishReason safety', async () => { - const mockResponse = getMockResponse( - 'unary-failure-finish-reason-safety.json' - ); - const makeRequestStub = stub(request, 'makeRequest').resolves( - mockResponse as Response - ); - const result = await generateContent( - fakeApiSettings, - 'model', - fakeRequestParams - ); - expect(result.response.text).to.throw('SAFETY'); - expect(makeRequestStub).to.be.calledWith( + const mockResponse = getMockResponse('unary-failure-finish-reason-safety.json'); + const makeRequestStub = jest + .spyOn(request, 'makeRequest') + .mockResolvedValue(mockResponse as Response); + const result = await generateContent(fakeApiSettings, 'model', fakeRequestParams); + expect(() => result.response.text()).toThrow('SAFETY'); + expect(makeRequestStub).toHaveBeenCalledWith( 'model', Task.GENERATE_CONTENT, fakeApiSettings, false, - match.any + expect.anything(), + undefined, ); }); + it('empty content', async () => { const mockResponse = getMockResponse('unary-failure-empty-content.json'); - const makeRequestStub = stub(request, 'makeRequest').resolves( - mockResponse as Response - ); - const result = await generateContent( - fakeApiSettings, - 'model', - fakeRequestParams - ); - expect(result.response.text()).to.equal(''); - expect(makeRequestStub).to.be.calledWith( + const makeRequestStub = jest + .spyOn(request, 'makeRequest') + .mockResolvedValue(mockResponse as Response); + const result = await generateContent(fakeApiSettings, 'model', fakeRequestParams); + expect(await result.response.text()).toBe(''); + expect(makeRequestStub).toHaveBeenCalledWith( 'model', Task.GENERATE_CONTENT, fakeApiSettings, false, - match.any + expect.anything(), + undefined, ); }); + it('unknown enum - should ignore', async () => { - const mockResponse = getMockResponse( - 'unary-success-unknown-enum-safety-ratings.json' - ); - const makeRequestStub = stub(request, 'makeRequest').resolves( - mockResponse as Response - ); - const result = await generateContent( - fakeApiSettings, - 'model', - fakeRequestParams - ); - expect(result.response.text()).to.include('Some text'); - expect(makeRequestStub).to.be.calledWith( + const mockResponse = getMockResponse('unary-success-unknown-enum-safety-ratings.json'); + const makeRequestStub = jest + .spyOn(request, 'makeRequest') + .mockResolvedValue(mockResponse as Response); + const result = await generateContent(fakeApiSettings, 'model', fakeRequestParams); + expect(await result.response.text()).toContain('Some text'); + expect(makeRequestStub).toHaveBeenCalledWith( 'model', Task.GENERATE_CONTENT, fakeApiSettings, false, - match.any + expect.anything(), + undefined, ); }); + it('image rejected (400)', async () => { const mockResponse = getMockResponse('unary-failure-image-rejected.json'); - const mockFetch = stub(globalThis, 'fetch').resolves({ + const mockFetch = jest.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: false, status: 400, - json: mockResponse.json + json: mockResponse.json, } as Response); - await expect( - generateContent(fakeApiSettings, 'model', fakeRequestParams) - ).to.be.rejectedWith(/400.*invalid argument/); - expect(mockFetch).to.be.called; + await expect(generateContent(fakeApiSettings, 'model', fakeRequestParams)).rejects.toThrow( + /400.*invalid argument/, + ); + expect(mockFetch).toHaveBeenCalled(); }); + it('api not enabled (403)', async () => { - const mockResponse = getMockResponse( - 'unary-failure-firebasevertexai-api-not-enabled.json' - ); - const mockFetch = stub(globalThis, 'fetch').resolves({ + const mockResponse = getMockResponse('unary-failure-firebasevertexai-api-not-enabled.json'); + const mockFetch = jest.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: false, status: 403, - json: mockResponse.json + json: mockResponse.json, } as Response); - await expect( - generateContent(fakeApiSettings, 'model', fakeRequestParams) - ).to.be.rejectedWith( - /firebasevertexai\.googleapis[\s\S]*my-project[\s\S]*api-not-enabled/ + await expect(generateContent(fakeApiSettings, 'model', fakeRequestParams)).rejects.toThrow( + /firebasevertexai\.googleapis[\s\S]*my-project[\s\S]*api-not-enabled/, ); - expect(mockFetch).to.be.called; + expect(mockFetch).toHaveBeenCalled(); }); }); From 6d50eca4d42400428602e4902f5dcbfe9ea99997 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 13:41:13 +0000 Subject: [PATCH 032/115] format mock response --- .../__tests__/test-utils/mock-response.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/packages/vertexai/__tests__/test-utils/mock-response.ts b/packages/vertexai/__tests__/test-utils/mock-response.ts index 8332d9eb36..bc95f3e359 100644 --- a/packages/vertexai/__tests__/test-utils/mock-response.ts +++ b/packages/vertexai/__tests__/test-utils/mock-response.ts @@ -21,38 +21,32 @@ import { mocksLookup } from './mocks-lookup'; * Mock native Response.body * Streams contents of json file in 20 character chunks */ -export function getChunkedStream( - input: string, - chunkLength = 20 -): ReadableStream { +export function getChunkedStream(input: string, chunkLength = 20): ReadableStream { const encoder = new TextEncoder(); let currentChunkStart = 0; const stream = new ReadableStream({ start(controller) { while (currentChunkStart < input.length) { - const substring = input.slice( - currentChunkStart, - currentChunkStart + chunkLength - ); + const substring = input.slice(currentChunkStart, currentChunkStart + chunkLength); currentChunkStart += chunkLength; const chunk = encoder.encode(substring); controller.enqueue(chunk); } controller.close(); - } + }, }); return stream; } export function getMockResponseStreaming( filename: string, - chunkLength: number = 20 + chunkLength: number = 20, ): Partial { const fullText = mocksLookup[filename]; return { - body: getChunkedStream(fullText, chunkLength) + body: getChunkedStream(fullText, chunkLength), }; } @@ -60,6 +54,6 @@ export function getMockResponse(filename: string): Partial { const fullText = mocksLookup[filename]; return { ok: true, - json: () => Promise.resolve(JSON.parse(fullText)) + json: () => Promise.resolve(JSON.parse(fullText)), }; } From 13a85bebb2fc56a9b62f9fbbd7a79d5f34c712f5 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 13:43:35 +0000 Subject: [PATCH 033/115] port over generative model test --- .../__tests__/generative-model.test.ts | 317 ++++++++++++++++++ 1 file changed, 317 insertions(+) create mode 100644 packages/vertexai/__tests__/generative-model.test.ts diff --git a/packages/vertexai/__tests__/generative-model.test.ts b/packages/vertexai/__tests__/generative-model.test.ts new file mode 100644 index 0000000000..e03f39e8a8 --- /dev/null +++ b/packages/vertexai/__tests__/generative-model.test.ts @@ -0,0 +1,317 @@ +/** + * @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 { use, expect } from 'chai'; +import { GenerativeModel } from './generative-model'; +import { FunctionCallingMode, VertexAI } from '../public-types'; +import * as request from '../requests/request'; +import { match, restore, stub } from 'sinon'; +import { getMockResponse } from '../../test-utils/mock-response'; +import sinonChai from 'sinon-chai'; + +use(sinonChai); + +const fakeVertexAI: VertexAI = { + app: { + name: 'DEFAULT', + automaticDataCollectionEnabled: true, + options: { + apiKey: 'key', + projectId: 'my-project' + } + }, + location: 'us-central1' +}; + +describe('GenerativeModel', () => { + it('handles plain model name', () => { + const genModel = new GenerativeModel(fakeVertexAI, { model: 'my-model' }); + expect(genModel.model).to.equal('publishers/google/models/my-model'); + }); + it('handles models/ prefixed model name', () => { + const genModel = new GenerativeModel(fakeVertexAI, { + model: 'models/my-model' + }); + expect(genModel.model).to.equal('publishers/google/models/my-model'); + }); + it('handles full model name', () => { + const genModel = new GenerativeModel(fakeVertexAI, { + model: 'publishers/google/models/my-model' + }); + expect(genModel.model).to.equal('publishers/google/models/my-model'); + }); + it('handles prefixed tuned model name', () => { + const genModel = new GenerativeModel(fakeVertexAI, { + model: 'tunedModels/my-model' + }); + expect(genModel.model).to.equal('tunedModels/my-model'); + }); + it('passes params through to generateContent', async () => { + const genModel = new GenerativeModel(fakeVertexAI, { + model: 'my-model', + tools: [ + { + functionDeclarations: [ + { + name: 'myfunc', + description: 'mydesc' + } + ] + } + ], + toolConfig: { functionCallingConfig: { mode: FunctionCallingMode.NONE } }, + systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] } + }); + expect(genModel.tools?.length).to.equal(1); + expect(genModel.toolConfig?.functionCallingConfig?.mode).to.equal( + FunctionCallingMode.NONE + ); + expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); + const mockResponse = getMockResponse( + 'unary-success-basic-reply-short.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + await genModel.generateContent('hello'); + expect(makeRequestStub).to.be.calledWith( + 'publishers/google/models/my-model', + request.Task.GENERATE_CONTENT, + match.any, + false, + match((value: string) => { + return ( + value.includes('myfunc') && + value.includes(FunctionCallingMode.NONE) && + value.includes('be friendly') + ); + }), + {} + ); + restore(); + }); + it('passes text-only systemInstruction through to generateContent', async () => { + const genModel = new GenerativeModel(fakeVertexAI, { + model: 'my-model', + systemInstruction: 'be friendly' + }); + expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); + const mockResponse = getMockResponse( + 'unary-success-basic-reply-short.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + await genModel.generateContent('hello'); + expect(makeRequestStub).to.be.calledWith( + 'publishers/google/models/my-model', + request.Task.GENERATE_CONTENT, + match.any, + false, + match((value: string) => { + return value.includes('be friendly'); + }), + {} + ); + restore(); + }); + it('generateContent overrides model values', async () => { + const genModel = new GenerativeModel(fakeVertexAI, { + model: 'my-model', + tools: [ + { + functionDeclarations: [ + { + name: 'myfunc', + description: 'mydesc' + } + ] + } + ], + toolConfig: { functionCallingConfig: { mode: FunctionCallingMode.NONE } }, + systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] } + }); + expect(genModel.tools?.length).to.equal(1); + expect(genModel.toolConfig?.functionCallingConfig?.mode).to.equal( + FunctionCallingMode.NONE + ); + expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); + const mockResponse = getMockResponse( + 'unary-success-basic-reply-short.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + await genModel.generateContent({ + contents: [{ role: 'user', parts: [{ text: 'hello' }] }], + tools: [ + { + functionDeclarations: [ + { name: 'otherfunc', description: 'otherdesc' } + ] + } + ], + toolConfig: { functionCallingConfig: { mode: FunctionCallingMode.AUTO } }, + systemInstruction: { role: 'system', parts: [{ text: 'be formal' }] } + }); + expect(makeRequestStub).to.be.calledWith( + 'publishers/google/models/my-model', + request.Task.GENERATE_CONTENT, + match.any, + false, + match((value: string) => { + return ( + value.includes('otherfunc') && + value.includes(FunctionCallingMode.AUTO) && + value.includes('be formal') + ); + }), + {} + ); + restore(); + }); + it('passes params through to chat.sendMessage', async () => { + const genModel = new GenerativeModel(fakeVertexAI, { + model: 'my-model', + tools: [ + { functionDeclarations: [{ name: 'myfunc', description: 'mydesc' }] } + ], + toolConfig: { functionCallingConfig: { mode: FunctionCallingMode.NONE } }, + systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] } + }); + expect(genModel.tools?.length).to.equal(1); + expect(genModel.toolConfig?.functionCallingConfig?.mode).to.equal( + FunctionCallingMode.NONE + ); + expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); + const mockResponse = getMockResponse( + 'unary-success-basic-reply-short.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + await genModel.startChat().sendMessage('hello'); + expect(makeRequestStub).to.be.calledWith( + 'publishers/google/models/my-model', + request.Task.GENERATE_CONTENT, + match.any, + false, + match((value: string) => { + return ( + value.includes('myfunc') && + value.includes(FunctionCallingMode.NONE) && + value.includes('be friendly') + ); + }), + {} + ); + restore(); + }); + it('passes text-only systemInstruction through to chat.sendMessage', async () => { + const genModel = new GenerativeModel(fakeVertexAI, { + model: 'my-model', + systemInstruction: 'be friendly' + }); + expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); + const mockResponse = getMockResponse( + 'unary-success-basic-reply-short.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + await genModel.startChat().sendMessage('hello'); + expect(makeRequestStub).to.be.calledWith( + 'publishers/google/models/my-model', + request.Task.GENERATE_CONTENT, + match.any, + false, + match((value: string) => { + return value.includes('be friendly'); + }), + {} + ); + restore(); + }); + it('startChat overrides model values', async () => { + const genModel = new GenerativeModel(fakeVertexAI, { + model: 'my-model', + tools: [ + { functionDeclarations: [{ name: 'myfunc', description: 'mydesc' }] } + ], + toolConfig: { functionCallingConfig: { mode: FunctionCallingMode.NONE } }, + systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] } + }); + expect(genModel.tools?.length).to.equal(1); + expect(genModel.toolConfig?.functionCallingConfig?.mode).to.equal( + FunctionCallingMode.NONE + ); + expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); + const mockResponse = getMockResponse( + 'unary-success-basic-reply-short.json' + ); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + await genModel + .startChat({ + tools: [ + { + functionDeclarations: [ + { name: 'otherfunc', description: 'otherdesc' } + ] + } + ], + toolConfig: { + functionCallingConfig: { mode: FunctionCallingMode.AUTO } + }, + systemInstruction: { role: 'system', parts: [{ text: 'be formal' }] } + }) + .sendMessage('hello'); + expect(makeRequestStub).to.be.calledWith( + 'publishers/google/models/my-model', + request.Task.GENERATE_CONTENT, + match.any, + false, + match((value: string) => { + return ( + value.includes('otherfunc') && + value.includes(FunctionCallingMode.AUTO) && + value.includes('be formal') + ); + }), + {} + ); + restore(); + }); + it('calls countTokens', async () => { + const genModel = new GenerativeModel(fakeVertexAI, { model: 'my-model' }); + const mockResponse = getMockResponse('unary-success-total-tokens.json'); + const makeRequestStub = stub(request, 'makeRequest').resolves( + mockResponse as Response + ); + await genModel.countTokens('hello'); + expect(makeRequestStub).to.be.calledWith( + 'publishers/google/models/my-model', + request.Task.COUNT_TOKENS, + match.any, + false, + match((value: string) => { + return value.includes('hello'); + }) + ); + restore(); + }); +}); From 128d8409e5e2e4cf670aa5f0b860c8c19e9c5a60 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 15:59:09 +0000 Subject: [PATCH 034/115] test: generative model --- .../__tests__/generative-model.test.ts | 307 ++++++++---------- 1 file changed, 128 insertions(+), 179 deletions(-) diff --git a/packages/vertexai/__tests__/generative-model.test.ts b/packages/vertexai/__tests__/generative-model.test.ts index e03f39e8a8..63f6fd521b 100644 --- a/packages/vertexai/__tests__/generative-model.test.ts +++ b/packages/vertexai/__tests__/generative-model.test.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,16 +12,13 @@ * 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 { use, expect } from 'chai'; -import { GenerativeModel } from './generative-model'; -import { FunctionCallingMode, VertexAI } from '../public-types'; -import * as request from '../requests/request'; -import { match, restore, stub } from 'sinon'; -import { getMockResponse } from '../../test-utils/mock-response'; -import sinonChai from 'sinon-chai'; - -use(sinonChai); +import { describe, expect, it, jest } from '@jest/globals'; +import { GenerativeModel } from '../lib/models/generative-model'; +import { FunctionCallingMode, VertexAI } from '../lib/public-types'; +import * as request from '../lib/requests/request'; +import { getMockResponse } from './test-utils/mock-response'; const fakeVertexAI: VertexAI = { app: { @@ -30,35 +26,39 @@ const fakeVertexAI: VertexAI = { automaticDataCollectionEnabled: true, options: { apiKey: 'key', - projectId: 'my-project' - } + projectId: 'my-project', + }, }, - location: 'us-central1' + location: 'us-central1', }; describe('GenerativeModel', () => { it('handles plain model name', () => { const genModel = new GenerativeModel(fakeVertexAI, { model: 'my-model' }); - expect(genModel.model).to.equal('publishers/google/models/my-model'); + expect(genModel.model).toBe('publishers/google/models/my-model'); }); + it('handles models/ prefixed model name', () => { const genModel = new GenerativeModel(fakeVertexAI, { - model: 'models/my-model' + model: 'models/my-model', }); - expect(genModel.model).to.equal('publishers/google/models/my-model'); + expect(genModel.model).toBe('publishers/google/models/my-model'); }); + it('handles full model name', () => { const genModel = new GenerativeModel(fakeVertexAI, { - model: 'publishers/google/models/my-model' + model: 'publishers/google/models/my-model', }); - expect(genModel.model).to.equal('publishers/google/models/my-model'); + expect(genModel.model).toBe('publishers/google/models/my-model'); }); + it('handles prefixed tuned model name', () => { const genModel = new GenerativeModel(fakeVertexAI, { - model: 'tunedModels/my-model' + model: 'tunedModels/my-model', }); - expect(genModel.model).to.equal('tunedModels/my-model'); + expect(genModel.model).toBe('tunedModels/my-model'); }); + it('passes params through to generateContent', async () => { const genModel = new GenerativeModel(fakeVertexAI, { model: 'my-model', @@ -67,67 +67,55 @@ describe('GenerativeModel', () => { functionDeclarations: [ { name: 'myfunc', - description: 'mydesc' - } - ] - } + description: 'mydesc', + }, + ], + }, ], toolConfig: { functionCallingConfig: { mode: FunctionCallingMode.NONE } }, - systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] } + systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] }, }); - expect(genModel.tools?.length).to.equal(1); - expect(genModel.toolConfig?.functionCallingConfig?.mode).to.equal( - FunctionCallingMode.NONE - ); - expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); - const mockResponse = getMockResponse( - 'unary-success-basic-reply-short.json' - ); - const makeRequestStub = stub(request, 'makeRequest').resolves( - mockResponse as Response - ); + expect(genModel.tools?.length).toBe(1); + expect(genModel.toolConfig?.functionCallingConfig?.mode).toBe(FunctionCallingMode.NONE); + expect(genModel.systemInstruction?.parts[0]!.text).toBe('be friendly'); + const mockResponse = getMockResponse('unary-success-basic-reply-short.json'); + const makeRequestStub = jest + .spyOn(request, 'makeRequest') + .mockResolvedValue(mockResponse as Response); await genModel.generateContent('hello'); - expect(makeRequestStub).to.be.calledWith( + expect(makeRequestStub).toHaveBeenCalledWith( 'publishers/google/models/my-model', request.Task.GENERATE_CONTENT, - match.any, + expect.anything(), false, - match((value: string) => { - return ( - value.includes('myfunc') && - value.includes(FunctionCallingMode.NONE) && - value.includes('be friendly') - ); - }), - {} + expect.stringMatching(new RegExp(`myfunc|be friendly|${FunctionCallingMode.NONE}`)), + {}, ); - restore(); + makeRequestStub.mockRestore(); }); + it('passes text-only systemInstruction through to generateContent', async () => { const genModel = new GenerativeModel(fakeVertexAI, { model: 'my-model', - systemInstruction: 'be friendly' + systemInstruction: 'be friendly', }); - expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); - const mockResponse = getMockResponse( - 'unary-success-basic-reply-short.json' - ); - const makeRequestStub = stub(request, 'makeRequest').resolves( - mockResponse as Response - ); + expect(genModel.systemInstruction?.parts[0]!.text).toBe('be friendly'); + const mockResponse = getMockResponse('unary-success-basic-reply-short.json'); + const makeRequestStub = jest + .spyOn(request, 'makeRequest') + .mockResolvedValue(mockResponse as Response); await genModel.generateContent('hello'); - expect(makeRequestStub).to.be.calledWith( + expect(makeRequestStub).toHaveBeenCalledWith( 'publishers/google/models/my-model', request.Task.GENERATE_CONTENT, - match.any, + expect.anything(), false, - match((value: string) => { - return value.includes('be friendly'); - }), - {} + expect.stringContaining('be friendly'), + {}, ); - restore(); + makeRequestStub.mockRestore(); }); + it('generateContent overrides model values', async () => { const genModel = new GenerativeModel(fakeVertexAI, { model: 'my-model', @@ -136,182 +124,143 @@ describe('GenerativeModel', () => { functionDeclarations: [ { name: 'myfunc', - description: 'mydesc' - } - ] - } + description: 'mydesc', + }, + ], + }, ], toolConfig: { functionCallingConfig: { mode: FunctionCallingMode.NONE } }, - systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] } + systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] }, }); - expect(genModel.tools?.length).to.equal(1); - expect(genModel.toolConfig?.functionCallingConfig?.mode).to.equal( - FunctionCallingMode.NONE - ); - expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); - const mockResponse = getMockResponse( - 'unary-success-basic-reply-short.json' - ); - const makeRequestStub = stub(request, 'makeRequest').resolves( - mockResponse as Response - ); + expect(genModel.tools?.length).toBe(1); + expect(genModel.toolConfig?.functionCallingConfig?.mode).toBe(FunctionCallingMode.NONE); + expect(genModel.systemInstruction?.parts[0]!.text).toBe('be friendly'); + const mockResponse = getMockResponse('unary-success-basic-reply-short.json'); + const makeRequestStub = jest + .spyOn(request, 'makeRequest') + .mockResolvedValue(mockResponse as Response); await genModel.generateContent({ contents: [{ role: 'user', parts: [{ text: 'hello' }] }], tools: [ { - functionDeclarations: [ - { name: 'otherfunc', description: 'otherdesc' } - ] - } + functionDeclarations: [{ name: 'otherfunc', description: 'otherdesc' }], + }, ], toolConfig: { functionCallingConfig: { mode: FunctionCallingMode.AUTO } }, - systemInstruction: { role: 'system', parts: [{ text: 'be formal' }] } + systemInstruction: { role: 'system', parts: [{ text: 'be formal' }] }, }); - expect(makeRequestStub).to.be.calledWith( + expect(makeRequestStub).toHaveBeenCalledWith( 'publishers/google/models/my-model', request.Task.GENERATE_CONTENT, - match.any, + expect.anything(), false, - match((value: string) => { - return ( - value.includes('otherfunc') && - value.includes(FunctionCallingMode.AUTO) && - value.includes('be formal') - ); - }), - {} + expect.stringMatching(new RegExp(`be formal|otherfunc|${FunctionCallingMode.AUTO}`)), + {}, ); - restore(); + makeRequestStub.mockRestore(); }); + it('passes params through to chat.sendMessage', async () => { const genModel = new GenerativeModel(fakeVertexAI, { model: 'my-model', - tools: [ - { functionDeclarations: [{ name: 'myfunc', description: 'mydesc' }] } - ], + tools: [{ functionDeclarations: [{ name: 'myfunc', description: 'mydesc' }] }], toolConfig: { functionCallingConfig: { mode: FunctionCallingMode.NONE } }, - systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] } + systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] }, }); - expect(genModel.tools?.length).to.equal(1); - expect(genModel.toolConfig?.functionCallingConfig?.mode).to.equal( - FunctionCallingMode.NONE - ); - expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); - const mockResponse = getMockResponse( - 'unary-success-basic-reply-short.json' - ); - const makeRequestStub = stub(request, 'makeRequest').resolves( - mockResponse as Response - ); + expect(genModel.tools?.length).toBe(1); + expect(genModel.toolConfig?.functionCallingConfig?.mode).toBe(FunctionCallingMode.NONE); + expect(genModel.systemInstruction?.parts[0]!.text).toBe('be friendly'); + const mockResponse = getMockResponse('unary-success-basic-reply-short.json'); + const makeRequestStub = jest + .spyOn(request, 'makeRequest') + .mockResolvedValue(mockResponse as Response); await genModel.startChat().sendMessage('hello'); - expect(makeRequestStub).to.be.calledWith( + expect(makeRequestStub).toHaveBeenCalledWith( 'publishers/google/models/my-model', request.Task.GENERATE_CONTENT, - match.any, + expect.anything(), false, - match((value: string) => { - return ( - value.includes('myfunc') && - value.includes(FunctionCallingMode.NONE) && - value.includes('be friendly') - ); - }), - {} + expect.stringMatching(new RegExp(`myfunc|be friendly|${FunctionCallingMode.NONE}`)), + {}, ); - restore(); + makeRequestStub.mockRestore(); }); + it('passes text-only systemInstruction through to chat.sendMessage', async () => { const genModel = new GenerativeModel(fakeVertexAI, { model: 'my-model', - systemInstruction: 'be friendly' + systemInstruction: 'be friendly', }); - expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); - const mockResponse = getMockResponse( - 'unary-success-basic-reply-short.json' - ); - const makeRequestStub = stub(request, 'makeRequest').resolves( - mockResponse as Response - ); + expect(genModel.systemInstruction?.parts[0]!.text).toBe('be friendly'); + const mockResponse = getMockResponse('unary-success-basic-reply-short.json'); + const makeRequestStub = jest + .spyOn(request, 'makeRequest') + .mockResolvedValue(mockResponse as Response); await genModel.startChat().sendMessage('hello'); - expect(makeRequestStub).to.be.calledWith( + expect(makeRequestStub).toHaveBeenCalledWith( 'publishers/google/models/my-model', request.Task.GENERATE_CONTENT, - match.any, + expect.anything(), false, - match((value: string) => { - return value.includes('be friendly'); - }), - {} + expect.stringContaining('be friendly'), + {}, ); - restore(); + makeRequestStub.mockRestore(); }); + it('startChat overrides model values', async () => { const genModel = new GenerativeModel(fakeVertexAI, { model: 'my-model', - tools: [ - { functionDeclarations: [{ name: 'myfunc', description: 'mydesc' }] } - ], + tools: [{ functionDeclarations: [{ name: 'myfunc', description: 'mydesc' }] }], toolConfig: { functionCallingConfig: { mode: FunctionCallingMode.NONE } }, - systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] } + systemInstruction: { role: 'system', parts: [{ text: 'be friendly' }] }, }); - expect(genModel.tools?.length).to.equal(1); - expect(genModel.toolConfig?.functionCallingConfig?.mode).to.equal( - FunctionCallingMode.NONE - ); - expect(genModel.systemInstruction?.parts[0].text).to.equal('be friendly'); - const mockResponse = getMockResponse( - 'unary-success-basic-reply-short.json' - ); - const makeRequestStub = stub(request, 'makeRequest').resolves( - mockResponse as Response - ); + expect(genModel.tools?.length).toBe(1); + expect(genModel.toolConfig?.functionCallingConfig?.mode).toBe(FunctionCallingMode.NONE); + expect(genModel.systemInstruction?.parts[0]!.text).toBe('be friendly'); + const mockResponse = getMockResponse('unary-success-basic-reply-short.json'); + const makeRequestStub = jest + .spyOn(request, 'makeRequest') + .mockResolvedValue(mockResponse as Response); await genModel .startChat({ tools: [ { - functionDeclarations: [ - { name: 'otherfunc', description: 'otherdesc' } - ] - } + functionDeclarations: [{ name: 'otherfunc', description: 'otherdesc' }], + }, ], toolConfig: { - functionCallingConfig: { mode: FunctionCallingMode.AUTO } + functionCallingConfig: { mode: FunctionCallingMode.AUTO }, }, - systemInstruction: { role: 'system', parts: [{ text: 'be formal' }] } + systemInstruction: { role: 'system', parts: [{ text: 'be formal' }] }, }) .sendMessage('hello'); - expect(makeRequestStub).to.be.calledWith( + expect(makeRequestStub).toHaveBeenCalledWith( 'publishers/google/models/my-model', request.Task.GENERATE_CONTENT, - match.any, + expect.anything(), false, - match((value: string) => { - return ( - value.includes('otherfunc') && - value.includes(FunctionCallingMode.AUTO) && - value.includes('be formal') - ); - }), - {} + expect.stringMatching(new RegExp(`otherfunc|be formal|${FunctionCallingMode.AUTO}`)), + {}, ); - restore(); + makeRequestStub.mockRestore(); }); + it('calls countTokens', async () => { const genModel = new GenerativeModel(fakeVertexAI, { model: 'my-model' }); const mockResponse = getMockResponse('unary-success-total-tokens.json'); - const makeRequestStub = stub(request, 'makeRequest').resolves( - mockResponse as Response - ); + const makeRequestStub = jest + .spyOn(request, 'makeRequest') + .mockResolvedValue(mockResponse as Response); await genModel.countTokens('hello'); - expect(makeRequestStub).to.be.calledWith( + expect(makeRequestStub).toHaveBeenCalledWith( 'publishers/google/models/my-model', request.Task.COUNT_TOKENS, - match.any, + expect.anything(), false, - match((value: string) => { - return value.includes('hello'); - }) + expect.stringContaining('hello'), + undefined, ); - restore(); + makeRequestStub.mockRestore(); }); }); From 419c3db2da8fdea8d5960550ec9fded432849912 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 16:02:16 +0000 Subject: [PATCH 035/115] port over request helpers tests --- .../__tests__/request-helpers.test.ts | 202 ++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 packages/vertexai/__tests__/request-helpers.test.ts diff --git a/packages/vertexai/__tests__/request-helpers.test.ts b/packages/vertexai/__tests__/request-helpers.test.ts new file mode 100644 index 0000000000..76b2f0ca1b --- /dev/null +++ b/packages/vertexai/__tests__/request-helpers.test.ts @@ -0,0 +1,202 @@ +/** + * @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 { Content } from '../types'; +import { formatGenerateContentInput } from './request-helpers'; + +use(sinonChai); + +describe('request formatting methods', () => { + describe('formatGenerateContentInput', () => { + it('formats a text string into a request', () => { + const result = formatGenerateContentInput('some text content'); + expect(result).to.deep.equal({ + contents: [ + { + role: 'user', + parts: [{ text: 'some text content' }] + } + ] + }); + }); + it('formats an array of strings into a request', () => { + const result = formatGenerateContentInput(['txt1', 'txt2']); + expect(result).to.deep.equal({ + contents: [ + { + role: 'user', + parts: [{ text: 'txt1' }, { text: 'txt2' }] + } + ] + }); + }); + it('formats an array of Parts into a request', () => { + const result = formatGenerateContentInput([ + { text: 'txt1' }, + { text: 'txtB' } + ]); + expect(result).to.deep.equal({ + contents: [ + { + role: 'user', + parts: [{ text: 'txt1' }, { text: 'txtB' }] + } + ] + }); + }); + it('formats a mixed array into a request', () => { + const result = formatGenerateContentInput(['txtA', { text: 'txtB' }]); + expect(result).to.deep.equal({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }, { text: 'txtB' }] + } + ] + }); + }); + it('preserves other properties of request', () => { + const result = formatGenerateContentInput({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + generationConfig: { topK: 100 } + }); + expect(result).to.deep.equal({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + generationConfig: { topK: 100 } + }); + }); + it('formats systemInstructions if provided as text', () => { + const result = formatGenerateContentInput({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + systemInstruction: 'be excited' + }); + expect(result).to.deep.equal({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] } + }); + }); + it('formats systemInstructions if provided as Part', () => { + const result = formatGenerateContentInput({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + systemInstruction: { text: 'be excited' } + }); + expect(result).to.deep.equal({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] } + }); + }); + it('formats systemInstructions if provided as Content (no role)', () => { + const result = formatGenerateContentInput({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + systemInstruction: { parts: [{ text: 'be excited' }] } as Content + }); + expect(result).to.deep.equal({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] } + }); + }); + it('passes thru systemInstructions if provided as Content', () => { + const result = formatGenerateContentInput({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] } + }); + expect(result).to.deep.equal({ + contents: [ + { + role: 'user', + parts: [{ text: 'txtA' }] + } + ], + systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] } + }); + }), + it('formats fileData as part if provided as part', () => { + const result = formatGenerateContentInput([ + 'What is this?', + { + fileData: { + mimeType: 'image/jpeg', + fileUri: 'gs://sample.appspot.com/image.jpeg' + } + } + ]); + expect(result).to.be.deep.equal({ + contents: [ + { + role: 'user', + parts: [ + { text: 'What is this?' }, + { + fileData: { + mimeType: 'image/jpeg', + fileUri: 'gs://sample.appspot.com/image.jpeg' + } + } + ] + } + ] + }); + }); + }); +}); From 09bc95931b189653c2740f45166c36a2ee4d5ebe Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 16:07:05 +0000 Subject: [PATCH 036/115] test: request helpers using jest --- .../__tests__/request-helpers.test.ts | 186 +++++++++--------- 1 file changed, 94 insertions(+), 92 deletions(-) diff --git a/packages/vertexai/__tests__/request-helpers.test.ts b/packages/vertexai/__tests__/request-helpers.test.ts index 76b2f0ca1b..da98948ac6 100644 --- a/packages/vertexai/__tests__/request-helpers.test.ts +++ b/packages/vertexai/__tests__/request-helpers.test.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,190 +12,193 @@ * 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 { Content } from '../types'; -import { formatGenerateContentInput } from './request-helpers'; - -use(sinonChai); +import { describe, expect, it } from '@jest/globals'; +import { Content } from '../lib/types'; +import { formatGenerateContentInput } from '../lib/requests/request-helpers'; describe('request formatting methods', () => { describe('formatGenerateContentInput', () => { it('formats a text string into a request', () => { const result = formatGenerateContentInput('some text content'); - expect(result).to.deep.equal({ + expect(result).toEqual({ contents: [ { role: 'user', - parts: [{ text: 'some text content' }] - } - ] + parts: [{ text: 'some text content' }], + }, + ], }); }); + it('formats an array of strings into a request', () => { const result = formatGenerateContentInput(['txt1', 'txt2']); - expect(result).to.deep.equal({ + expect(result).toEqual({ contents: [ { role: 'user', - parts: [{ text: 'txt1' }, { text: 'txt2' }] - } - ] + parts: [{ text: 'txt1' }, { text: 'txt2' }], + }, + ], }); }); + it('formats an array of Parts into a request', () => { - const result = formatGenerateContentInput([ - { text: 'txt1' }, - { text: 'txtB' } - ]); - expect(result).to.deep.equal({ + const result = formatGenerateContentInput([{ text: 'txt1' }, { text: 'txtB' }]); + expect(result).toEqual({ contents: [ { role: 'user', - parts: [{ text: 'txt1' }, { text: 'txtB' }] - } - ] + parts: [{ text: 'txt1' }, { text: 'txtB' }], + }, + ], }); }); + it('formats a mixed array into a request', () => { const result = formatGenerateContentInput(['txtA', { text: 'txtB' }]); - expect(result).to.deep.equal({ + expect(result).toEqual({ contents: [ { role: 'user', - parts: [{ text: 'txtA' }, { text: 'txtB' }] - } - ] + parts: [{ text: 'txtA' }, { text: 'txtB' }], + }, + ], }); }); + it('preserves other properties of request', () => { const result = formatGenerateContentInput({ contents: [ { role: 'user', - parts: [{ text: 'txtA' }] - } + parts: [{ text: 'txtA' }], + }, ], - generationConfig: { topK: 100 } + generationConfig: { topK: 100 }, }); - expect(result).to.deep.equal({ + expect(result).toEqual({ contents: [ { role: 'user', - parts: [{ text: 'txtA' }] - } + parts: [{ text: 'txtA' }], + }, ], - generationConfig: { topK: 100 } + generationConfig: { topK: 100 }, }); }); + it('formats systemInstructions if provided as text', () => { const result = formatGenerateContentInput({ contents: [ { role: 'user', - parts: [{ text: 'txtA' }] - } + parts: [{ text: 'txtA' }], + }, ], - systemInstruction: 'be excited' + systemInstruction: 'be excited', }); - expect(result).to.deep.equal({ + expect(result).toEqual({ contents: [ { role: 'user', - parts: [{ text: 'txtA' }] - } + parts: [{ text: 'txtA' }], + }, ], - systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] } + systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] }, }); }); + it('formats systemInstructions if provided as Part', () => { const result = formatGenerateContentInput({ contents: [ { role: 'user', - parts: [{ text: 'txtA' }] - } + parts: [{ text: 'txtA' }], + }, ], - systemInstruction: { text: 'be excited' } + systemInstruction: { text: 'be excited' }, }); - expect(result).to.deep.equal({ + expect(result).toEqual({ contents: [ { role: 'user', - parts: [{ text: 'txtA' }] - } + parts: [{ text: 'txtA' }], + }, ], - systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] } + systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] }, }); }); + it('formats systemInstructions if provided as Content (no role)', () => { const result = formatGenerateContentInput({ contents: [ { role: 'user', - parts: [{ text: 'txtA' }] - } + parts: [{ text: 'txtA' }], + }, ], - systemInstruction: { parts: [{ text: 'be excited' }] } as Content + systemInstruction: { parts: [{ text: 'be excited' }] } as Content, }); - expect(result).to.deep.equal({ + expect(result).toEqual({ contents: [ { role: 'user', - parts: [{ text: 'txtA' }] - } + parts: [{ text: 'txtA' }], + }, ], - systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] } + systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] }, }); }); + it('passes thru systemInstructions if provided as Content', () => { const result = formatGenerateContentInput({ contents: [ { role: 'user', - parts: [{ text: 'txtA' }] - } + parts: [{ text: 'txtA' }], + }, ], - systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] } + systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] }, }); - expect(result).to.deep.equal({ + expect(result).toEqual({ contents: [ { role: 'user', - parts: [{ text: 'txtA' }] - } + parts: [{ text: 'txtA' }], + }, ], - systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] } + systemInstruction: { role: 'system', parts: [{ text: 'be excited' }] }, }); - }), - it('formats fileData as part if provided as part', () => { - const result = formatGenerateContentInput([ - 'What is this?', + }); + + it('formats fileData as part if provided as part', () => { + const result = formatGenerateContentInput([ + 'What is this?', + { + fileData: { + mimeType: 'image/jpeg', + fileUri: 'gs://sample.appspot.com/image.jpeg', + }, + }, + ]); + expect(result).toEqual({ + contents: [ { - fileData: { - mimeType: 'image/jpeg', - fileUri: 'gs://sample.appspot.com/image.jpeg' - } - } - ]); - expect(result).to.be.deep.equal({ - contents: [ - { - role: 'user', - parts: [ - { text: 'What is this?' }, - { - fileData: { - mimeType: 'image/jpeg', - fileUri: 'gs://sample.appspot.com/image.jpeg' - } - } - ] - } - ] - }); + role: 'user', + parts: [ + { text: 'What is this?' }, + { + fileData: { + mimeType: 'image/jpeg', + fileUri: 'gs://sample.appspot.com/image.jpeg', + }, + }, + ], + }, + ], }); + }); }); }); From 852b44ea24972db1f36527b9c2435c8c680d47b4 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 16:07:15 +0000 Subject: [PATCH 037/115] port over request tests --- packages/vertexai/__tests__/request.test.ts | 390 ++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 packages/vertexai/__tests__/request.test.ts diff --git a/packages/vertexai/__tests__/request.test.ts b/packages/vertexai/__tests__/request.test.ts new file mode 100644 index 0000000000..b6d0ecb9b7 --- /dev/null +++ b/packages/vertexai/__tests__/request.test.ts @@ -0,0 +1,390 @@ +/** + * @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 { match, restore, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; +import chaiAsPromised from 'chai-as-promised'; +import { RequestUrl, Task, getHeaders, makeRequest } from './request'; +import { ApiSettings } from '../types/internal'; +import { DEFAULT_API_VERSION } from '../constants'; +import { VertexAIErrorCode } from '../types'; +import { VertexAIError } from '../errors'; +import { getMockResponse } from '../../test-utils/mock-response'; + +use(sinonChai); +use(chaiAsPromised); + +const fakeApiSettings: ApiSettings = { + apiKey: 'key', + project: 'my-project', + location: 'us-central1' +}; + +describe('request methods', () => { + afterEach(() => { + restore(); + }); + describe('RequestUrl', () => { + it('stream', async () => { + const url = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + true, + {} + ); + expect(url.toString()).to.include('models/model-name:generateContent'); + expect(url.toString()).to.not.include(fakeApiSettings); + expect(url.toString()).to.include('alt=sse'); + }); + it('non-stream', async () => { + const url = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + {} + ); + expect(url.toString()).to.include('models/model-name:generateContent'); + expect(url.toString()).to.not.include(fakeApiSettings); + expect(url.toString()).to.not.include('alt=sse'); + }); + it('default apiVersion', async () => { + const url = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + {} + ); + expect(url.toString()).to.include(DEFAULT_API_VERSION); + }); + it('custom baseUrl', async () => { + const url = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + { baseUrl: 'https://my.special.endpoint' } + ); + expect(url.toString()).to.include('https://my.special.endpoint'); + }); + it('non-stream - tunedModels/', async () => { + const url = new RequestUrl( + 'tunedModels/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + {} + ); + expect(url.toString()).to.include( + 'tunedModels/model-name:generateContent' + ); + expect(url.toString()).to.not.include(fakeApiSettings); + expect(url.toString()).to.not.include('alt=sse'); + }); + }); + describe('getHeaders', () => { + const fakeApiSettings: ApiSettings = { + apiKey: 'key', + project: 'myproject', + location: 'moon', + getAuthToken: () => Promise.resolve({ accessToken: 'authtoken' }), + getAppCheckToken: () => Promise.resolve({ token: 'appchecktoken' }) + }; + const fakeUrl = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + true, + {} + ); + it('adds client headers', async () => { + const headers = await getHeaders(fakeUrl); + expect(headers.get('x-goog-api-client')).to.match( + /gl-js\/[0-9\.]+ fire\/[0-9\.]+/ + ); + }); + it('adds api key', async () => { + const headers = await getHeaders(fakeUrl); + expect(headers.get('x-goog-api-key')).to.equal('key'); + }); + it('adds app check token if it exists', async () => { + const headers = await getHeaders(fakeUrl); + expect(headers.get('X-Firebase-AppCheck')).to.equal('appchecktoken'); + }); + it('ignores app check token header if no appcheck service', async () => { + const fakeUrl = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + { + apiKey: 'key', + project: 'myproject', + location: 'moon' + }, + true, + {} + ); + const headers = await getHeaders(fakeUrl); + expect(headers.has('X-Firebase-AppCheck')).to.be.false; + }); + it('ignores app check token header if returned token was undefined', async () => { + const fakeUrl = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + { + apiKey: 'key', + project: 'myproject', + location: 'moon', + //@ts-ignore + getAppCheckToken: () => Promise.resolve() + }, + true, + {} + ); + const headers = await getHeaders(fakeUrl); + expect(headers.has('X-Firebase-AppCheck')).to.be.false; + }); + it('ignores app check token header if returned token had error', async () => { + const fakeUrl = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + { + apiKey: 'key', + project: 'myproject', + location: 'moon', + getAppCheckToken: () => + Promise.resolve({ token: 'dummytoken', error: Error('oops') }) + }, + true, + {} + ); + const warnStub = stub(console, 'warn'); + const headers = await getHeaders(fakeUrl); + expect(headers.get('X-Firebase-AppCheck')).to.equal('dummytoken'); + expect(warnStub).to.be.calledWith( + match(/vertexai/), + match(/App Check.*oops/) + ); + }); + it('adds auth token if it exists', async () => { + const headers = await getHeaders(fakeUrl); + expect(headers.get('Authorization')).to.equal('Firebase authtoken'); + }); + it('ignores auth token header if no auth service', async () => { + const fakeUrl = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + { + apiKey: 'key', + project: 'myproject', + location: 'moon' + }, + true, + {} + ); + const headers = await getHeaders(fakeUrl); + expect(headers.has('Authorization')).to.be.false; + }); + it('ignores auth token header if returned token was undefined', async () => { + const fakeUrl = new RequestUrl( + 'models/model-name', + Task.GENERATE_CONTENT, + { + apiKey: 'key', + project: 'myproject', + location: 'moon', + //@ts-ignore + getAppCheckToken: () => Promise.resolve() + }, + true, + {} + ); + const headers = await getHeaders(fakeUrl); + expect(headers.has('Authorization')).to.be.false; + }); + }); + describe('makeRequest', () => { + it('no error', async () => { + const fetchStub = stub(globalThis, 'fetch').resolves({ + ok: true + } as Response); + const response = await makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '' + ); + expect(fetchStub).to.be.calledOnce; + expect(response.ok).to.be.true; + }); + it('error with timeout', async () => { + const fetchStub = stub(globalThis, 'fetch').resolves({ + ok: false, + status: 500, + statusText: 'AbortError' + } as Response); + + try { + await makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '', + { + timeout: 180000 + } + ); + } catch (e) { + expect((e as VertexAIError).code).to.equal( + VertexAIErrorCode.FETCH_ERROR + ); + expect((e as VertexAIError).customErrorData?.status).to.equal(500); + expect((e as VertexAIError).customErrorData?.statusText).to.equal( + 'AbortError' + ); + expect((e as VertexAIError).message).to.include('500 AbortError'); + } + + expect(fetchStub).to.be.calledOnce; + }); + it('Network error, no response.json()', async () => { + const fetchStub = stub(globalThis, 'fetch').resolves({ + ok: false, + status: 500, + statusText: 'Server Error' + } as Response); + try { + await makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '' + ); + } catch (e) { + expect((e as VertexAIError).code).to.equal( + VertexAIErrorCode.FETCH_ERROR + ); + expect((e as VertexAIError).customErrorData?.status).to.equal(500); + expect((e as VertexAIError).customErrorData?.statusText).to.equal( + 'Server Error' + ); + expect((e as VertexAIError).message).to.include('500 Server Error'); + } + expect(fetchStub).to.be.calledOnce; + }); + it('Network error, includes response.json()', async () => { + const fetchStub = stub(globalThis, 'fetch').resolves({ + ok: false, + status: 500, + statusText: 'Server Error', + json: () => Promise.resolve({ error: { message: 'extra info' } }) + } as Response); + try { + await makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '' + ); + } catch (e) { + expect((e as VertexAIError).code).to.equal( + VertexAIErrorCode.FETCH_ERROR + ); + expect((e as VertexAIError).customErrorData?.status).to.equal(500); + expect((e as VertexAIError).customErrorData?.statusText).to.equal( + 'Server Error' + ); + expect((e as VertexAIError).message).to.include('500 Server Error'); + expect((e as VertexAIError).message).to.include('extra info'); + } + expect(fetchStub).to.be.calledOnce; + }); + it('Network error, includes response.json() and details', async () => { + const fetchStub = stub(globalThis, 'fetch').resolves({ + ok: false, + status: 500, + statusText: 'Server Error', + json: () => + Promise.resolve({ + error: { + message: 'extra info', + details: [ + { + '@type': 'type.googleapis.com/google.rpc.DebugInfo', + detail: + '[ORIGINAL ERROR] generic::invalid_argument: invalid status photos.thumbnailer.Status.Code::5: Source image 0 too short' + } + ] + } + }) + } as Response); + try { + await makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '' + ); + } catch (e) { + expect((e as VertexAIError).code).to.equal( + VertexAIErrorCode.FETCH_ERROR + ); + expect((e as VertexAIError).customErrorData?.status).to.equal(500); + expect((e as VertexAIError).customErrorData?.statusText).to.equal( + 'Server Error' + ); + expect((e as VertexAIError).message).to.include('500 Server Error'); + expect((e as VertexAIError).message).to.include('extra info'); + expect((e as VertexAIError).message).to.include( + 'generic::invalid_argument' + ); + } + expect(fetchStub).to.be.calledOnce; + }); + }); + it('Network error, API not enabled', async () => { + const mockResponse = getMockResponse( + 'unary-failure-firebasevertexai-api-not-enabled.json' + ); + const fetchStub = stub(globalThis, 'fetch').resolves( + mockResponse as Response + ); + try { + await makeRequest( + 'models/model-name', + Task.GENERATE_CONTENT, + fakeApiSettings, + false, + '' + ); + } catch (e) { + expect((e as VertexAIError).code).to.equal( + VertexAIErrorCode.API_NOT_ENABLED + ); + expect((e as VertexAIError).message).to.include('my-project'); + expect((e as VertexAIError).message).to.include('googleapis.com'); + } + expect(fetchStub).to.be.calledOnce; + }); +}); From ab460e74d050612c8181b8a31fc189914c9cfc46 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 17 Jan 2025 18:05:21 +0000 Subject: [PATCH 038/115] test: request tests --- packages/vertexai/__tests__/request.test.ts | 311 +++++++++----------- 1 file changed, 136 insertions(+), 175 deletions(-) diff --git a/packages/vertexai/__tests__/request.test.ts b/packages/vertexai/__tests__/request.test.ts index b6d0ecb9b7..3e211b8dbd 100644 --- a/packages/vertexai/__tests__/request.test.ts +++ b/packages/vertexai/__tests__/request.test.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,32 +12,27 @@ * 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 { match, restore, stub } from 'sinon'; -import sinonChai from 'sinon-chai'; -import chaiAsPromised from 'chai-as-promised'; -import { RequestUrl, Task, getHeaders, makeRequest } from './request'; -import { ApiSettings } from '../types/internal'; -import { DEFAULT_API_VERSION } from '../constants'; -import { VertexAIErrorCode } from '../types'; -import { VertexAIError } from '../errors'; -import { getMockResponse } from '../../test-utils/mock-response'; - -use(sinonChai); -use(chaiAsPromised); +import { describe, expect, it, jest, afterEach } from '@jest/globals'; +import { RequestUrl, Task, getHeaders, makeRequest } from '../lib/requests/request'; +import { ApiSettings } from '../lib/types/internal'; +import { DEFAULT_API_VERSION } from '../lib/constants'; +import { VertexAIErrorCode } from '../lib/types'; +import { VertexAIError } from '../lib/errors'; +import { getMockResponse } from './test-utils/mock-response'; const fakeApiSettings: ApiSettings = { apiKey: 'key', project: 'my-project', - location: 'us-central1' + location: 'us-central1', }; describe('request methods', () => { afterEach(() => { - restore(); + jest.restoreAllMocks(); // Use Jest's restoreAllMocks }); + describe('RequestUrl', () => { it('stream', async () => { const url = new RequestUrl( @@ -46,88 +40,99 @@ describe('request methods', () => { Task.GENERATE_CONTENT, fakeApiSettings, true, - {} + {}, ); - expect(url.toString()).to.include('models/model-name:generateContent'); - expect(url.toString()).to.not.include(fakeApiSettings); - expect(url.toString()).to.include('alt=sse'); + const urlStr = url.toString(); + expect(urlStr).toContain('models/model-name:generateContent'); + expect(urlStr).toContain(fakeApiSettings.project); + expect(urlStr).toContain(fakeApiSettings.location); + expect(urlStr).toContain('alt=sse'); }); + it('non-stream', async () => { const url = new RequestUrl( 'models/model-name', Task.GENERATE_CONTENT, fakeApiSettings, false, - {} + {}, ); - expect(url.toString()).to.include('models/model-name:generateContent'); - expect(url.toString()).to.not.include(fakeApiSettings); - expect(url.toString()).to.not.include('alt=sse'); + const urlStr = url.toString(); + expect(urlStr).toContain('models/model-name:generateContent'); + expect(urlStr).toContain(fakeApiSettings.project); + expect(urlStr).toContain(fakeApiSettings.location); + expect(urlStr).not.toContain('alt=sse'); }); + it('default apiVersion', async () => { const url = new RequestUrl( 'models/model-name', Task.GENERATE_CONTENT, fakeApiSettings, false, - {} + {}, ); - expect(url.toString()).to.include(DEFAULT_API_VERSION); + expect(url.toString()).toContain(DEFAULT_API_VERSION); }); + it('custom baseUrl', async () => { const url = new RequestUrl( 'models/model-name', Task.GENERATE_CONTENT, fakeApiSettings, false, - { baseUrl: 'https://my.special.endpoint' } + { baseUrl: 'https://my.special.endpoint' }, ); - expect(url.toString()).to.include('https://my.special.endpoint'); + expect(url.toString()).toContain('https://my.special.endpoint'); }); + it('non-stream - tunedModels/', async () => { const url = new RequestUrl( 'tunedModels/model-name', Task.GENERATE_CONTENT, fakeApiSettings, false, - {} + {}, ); - expect(url.toString()).to.include( - 'tunedModels/model-name:generateContent' - ); - expect(url.toString()).to.not.include(fakeApiSettings); - expect(url.toString()).to.not.include('alt=sse'); + const urlStr = url.toString(); + expect(urlStr).toContain('tunedModels/model-name:generateContent'); + expect(urlStr).toContain(fakeApiSettings.location); + expect(urlStr).toContain(fakeApiSettings.project); + expect(urlStr).not.toContain('alt=sse'); }); }); + describe('getHeaders', () => { const fakeApiSettings: ApiSettings = { apiKey: 'key', project: 'myproject', location: 'moon', - getAuthToken: () => Promise.resolve({ accessToken: 'authtoken' }), - getAppCheckToken: () => Promise.resolve({ token: 'appchecktoken' }) + getAuthToken: () => Promise.resolve('authtoken'), + getAppCheckToken: () => Promise.resolve({ token: 'appchecktoken' }), }; const fakeUrl = new RequestUrl( 'models/model-name', Task.GENERATE_CONTENT, fakeApiSettings, true, - {} + {}, ); + it('adds client headers', async () => { const headers = await getHeaders(fakeUrl); - expect(headers.get('x-goog-api-client')).to.match( - /gl-js\/[0-9\.]+ fire\/[0-9\.]+/ - ); + expect(headers.get('x-goog-api-client')).toMatch(/gl-js\/[0-9\.]+ fire\/[0-9\.]+/); }); + it('adds api key', async () => { const headers = await getHeaders(fakeUrl); - expect(headers.get('x-goog-api-key')).to.equal('key'); + expect(headers.get('x-goog-api-key')).toBe('key'); }); + it('adds app check token if it exists', async () => { const headers = await getHeaders(fakeUrl); - expect(headers.get('X-Firebase-AppCheck')).to.equal('appchecktoken'); + expect(headers.get('X-Firebase-AppCheck')).toBe('appchecktoken'); }); + it('ignores app check token header if no appcheck service', async () => { const fakeUrl = new RequestUrl( 'models/model-name', @@ -135,14 +140,15 @@ describe('request methods', () => { { apiKey: 'key', project: 'myproject', - location: 'moon' + location: 'moon', }, true, - {} + {}, ); const headers = await getHeaders(fakeUrl); - expect(headers.has('X-Firebase-AppCheck')).to.be.false; + expect(headers.has('X-Firebase-AppCheck')).toBe(false); }); + it('ignores app check token header if returned token was undefined', async () => { const fakeUrl = new RequestUrl( 'models/model-name', @@ -152,14 +158,15 @@ describe('request methods', () => { project: 'myproject', location: 'moon', //@ts-ignore - getAppCheckToken: () => Promise.resolve() + getAppCheckToken: () => Promise.resolve(), }, true, - {} + {}, ); const headers = await getHeaders(fakeUrl); - expect(headers.has('X-Firebase-AppCheck')).to.be.false; + expect(headers.has('X-Firebase-AppCheck')).toBe(false); }); + it('ignores app check token header if returned token had error', async () => { const fakeUrl = new RequestUrl( 'models/model-name', @@ -168,24 +175,26 @@ describe('request methods', () => { apiKey: 'key', project: 'myproject', location: 'moon', - getAppCheckToken: () => - Promise.resolve({ token: 'dummytoken', error: Error('oops') }) + getAppCheckToken: () => Promise.resolve({ token: 'dummytoken', error: Error('oops') }), }, true, - {} + {}, ); - const warnStub = stub(console, 'warn'); + + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); const headers = await getHeaders(fakeUrl); - expect(headers.get('X-Firebase-AppCheck')).to.equal('dummytoken'); - expect(warnStub).to.be.calledWith( - match(/vertexai/), - match(/App Check.*oops/) + expect(headers.get('X-Firebase-AppCheck')).toBe('dummytoken'); + expect(warnSpy).toHaveBeenCalledWith( + expect.stringMatching(/vertexai/), + expect.stringMatching(/App Check.*oops/), ); }); + it('adds auth token if it exists', async () => { const headers = await getHeaders(fakeUrl); - expect(headers.get('Authorization')).to.equal('Firebase authtoken'); + expect(headers.get('Authorization')).toBe('Firebase authtoken'); }); + it('ignores auth token header if no auth service', async () => { const fakeUrl = new RequestUrl( 'models/model-name', @@ -193,14 +202,15 @@ describe('request methods', () => { { apiKey: 'key', project: 'myproject', - location: 'moon' + location: 'moon', }, true, - {} + {}, ); const headers = await getHeaders(fakeUrl); - expect(headers.has('Authorization')).to.be.false; + expect(headers.has('Authorization')).toBe(false); }); + it('ignores auth token header if returned token was undefined', async () => { const fakeUrl = new RequestUrl( 'models/model-name', @@ -210,117 +220,91 @@ describe('request methods', () => { project: 'myproject', location: 'moon', //@ts-ignore - getAppCheckToken: () => Promise.resolve() + getAppCheckToken: () => Promise.resolve(), }, true, - {} + {}, ); const headers = await getHeaders(fakeUrl); - expect(headers.has('Authorization')).to.be.false; + expect(headers.has('Authorization')).toBe(false); }); }); + describe('makeRequest', () => { it('no error', async () => { - const fetchStub = stub(globalThis, 'fetch').resolves({ - ok: true + const fetchMock = jest.spyOn(globalThis, 'fetch').mockResolvedValue({ + ok: true, } as Response); const response = await makeRequest( 'models/model-name', Task.GENERATE_CONTENT, fakeApiSettings, false, - '' + '', ); - expect(fetchStub).to.be.calledOnce; - expect(response.ok).to.be.true; + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(response.ok).toBe(true); }); + it('error with timeout', async () => { - const fetchStub = stub(globalThis, 'fetch').resolves({ + const fetchMock = jest.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: false, status: 500, - statusText: 'AbortError' + statusText: 'AbortError', } as Response); try { - await makeRequest( - 'models/model-name', - Task.GENERATE_CONTENT, - fakeApiSettings, - false, - '', - { - timeout: 180000 - } - ); + await makeRequest('models/model-name', Task.GENERATE_CONTENT, fakeApiSettings, false, '', { + timeout: 180000, + }); } catch (e) { - expect((e as VertexAIError).code).to.equal( - VertexAIErrorCode.FETCH_ERROR - ); - expect((e as VertexAIError).customErrorData?.status).to.equal(500); - expect((e as VertexAIError).customErrorData?.statusText).to.equal( - 'AbortError' - ); - expect((e as VertexAIError).message).to.include('500 AbortError'); + expect((e as VertexAIError).code).toBe(VertexAIErrorCode.FETCH_ERROR); + expect((e as VertexAIError).customErrorData?.status).toBe(500); + expect((e as VertexAIError).customErrorData?.statusText).toBe('AbortError'); + expect((e as VertexAIError).message).toContain('500 AbortError'); } - expect(fetchStub).to.be.calledOnce; + expect(fetchMock).toHaveBeenCalledTimes(1); }); + it('Network error, no response.json()', async () => { - const fetchStub = stub(globalThis, 'fetch').resolves({ + const fetchMock = jest.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: false, status: 500, - statusText: 'Server Error' + statusText: 'Server Error', } as Response); try { - await makeRequest( - 'models/model-name', - Task.GENERATE_CONTENT, - fakeApiSettings, - false, - '' - ); + await makeRequest('models/model-name', Task.GENERATE_CONTENT, fakeApiSettings, false, ''); } catch (e) { - expect((e as VertexAIError).code).to.equal( - VertexAIErrorCode.FETCH_ERROR - ); - expect((e as VertexAIError).customErrorData?.status).to.equal(500); - expect((e as VertexAIError).customErrorData?.statusText).to.equal( - 'Server Error' - ); - expect((e as VertexAIError).message).to.include('500 Server Error'); + expect((e as VertexAIError).code).toBe(VertexAIErrorCode.FETCH_ERROR); + expect((e as VertexAIError).customErrorData?.status).toBe(500); + expect((e as VertexAIError).customErrorData?.statusText).toBe('Server Error'); + expect((e as VertexAIError).message).toContain('500 Server Error'); } - expect(fetchStub).to.be.calledOnce; + expect(fetchMock).toHaveBeenCalledTimes(1); }); + it('Network error, includes response.json()', async () => { - const fetchStub = stub(globalThis, 'fetch').resolves({ + const fetchMock = jest.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: false, status: 500, statusText: 'Server Error', - json: () => Promise.resolve({ error: { message: 'extra info' } }) + json: () => Promise.resolve({ error: { message: 'extra info' } }), } as Response); try { - await makeRequest( - 'models/model-name', - Task.GENERATE_CONTENT, - fakeApiSettings, - false, - '' - ); + await makeRequest('models/model-name', Task.GENERATE_CONTENT, fakeApiSettings, false, ''); } catch (e) { - expect((e as VertexAIError).code).to.equal( - VertexAIErrorCode.FETCH_ERROR - ); - expect((e as VertexAIError).customErrorData?.status).to.equal(500); - expect((e as VertexAIError).customErrorData?.statusText).to.equal( - 'Server Error' - ); - expect((e as VertexAIError).message).to.include('500 Server Error'); - expect((e as VertexAIError).message).to.include('extra info'); + expect((e as VertexAIError).code).toBe(VertexAIErrorCode.FETCH_ERROR); + expect((e as VertexAIError).customErrorData?.status).toBe(500); + expect((e as VertexAIError).customErrorData?.statusText).toBe('Server Error'); + expect((e as VertexAIError).message).toContain('500 Server Error'); + expect((e as VertexAIError).message).toContain('extra info'); } - expect(fetchStub).to.be.calledOnce; + expect(fetchMock).toHaveBeenCalledTimes(1); }); + it('Network error, includes response.json() and details', async () => { - const fetchStub = stub(globalThis, 'fetch').resolves({ + const fetchMock = jest.spyOn(globalThis, 'fetch').mockResolvedValue({ ok: false, status: 500, statusText: 'Server Error', @@ -332,59 +316,36 @@ describe('request methods', () => { { '@type': 'type.googleapis.com/google.rpc.DebugInfo', detail: - '[ORIGINAL ERROR] generic::invalid_argument: invalid status photos.thumbnailer.Status.Code::5: Source image 0 too short' - } - ] - } - }) + '[ORIGINAL ERROR] generic::invalid_argument: invalid status photos.thumbnailer.Status.Code::5: Source image 0 too short', + }, + ], + }, + }), } as Response); try { - await makeRequest( - 'models/model-name', - Task.GENERATE_CONTENT, - fakeApiSettings, - false, - '' - ); + await makeRequest('models/model-name', Task.GENERATE_CONTENT, fakeApiSettings, false, ''); } catch (e) { - expect((e as VertexAIError).code).to.equal( - VertexAIErrorCode.FETCH_ERROR - ); - expect((e as VertexAIError).customErrorData?.status).to.equal(500); - expect((e as VertexAIError).customErrorData?.statusText).to.equal( - 'Server Error' - ); - expect((e as VertexAIError).message).to.include('500 Server Error'); - expect((e as VertexAIError).message).to.include('extra info'); - expect((e as VertexAIError).message).to.include( - 'generic::invalid_argument' - ); + expect((e as VertexAIError).code).toBe(VertexAIErrorCode.FETCH_ERROR); + expect((e as VertexAIError).customErrorData?.status).toBe(500); + expect((e as VertexAIError).customErrorData?.statusText).toBe('Server Error'); + expect((e as VertexAIError).message).toContain('500 Server Error'); + expect((e as VertexAIError).message).toContain('extra info'); + expect((e as VertexAIError).message).toContain('generic::invalid_argument'); } - expect(fetchStub).to.be.calledOnce; + expect(fetchMock).toHaveBeenCalledTimes(1); }); }); + it('Network error, API not enabled', async () => { - const mockResponse = getMockResponse( - 'unary-failure-firebasevertexai-api-not-enabled.json' - ); - const fetchStub = stub(globalThis, 'fetch').resolves( - mockResponse as Response - ); + const mockResponse = getMockResponse('unary-failure-firebasevertexai-api-not-enabled.json'); + const fetchMock = jest.spyOn(globalThis, 'fetch').mockResolvedValue(mockResponse as Response); try { - await makeRequest( - 'models/model-name', - Task.GENERATE_CONTENT, - fakeApiSettings, - false, - '' - ); + await makeRequest('models/model-name', Task.GENERATE_CONTENT, fakeApiSettings, false, ''); } catch (e) { - expect((e as VertexAIError).code).to.equal( - VertexAIErrorCode.API_NOT_ENABLED - ); - expect((e as VertexAIError).message).to.include('my-project'); - expect((e as VertexAIError).message).to.include('googleapis.com'); + expect((e as VertexAIError).code).toBe(VertexAIErrorCode.API_NOT_ENABLED); + expect((e as VertexAIError).message).toContain('my-project'); + expect((e as VertexAIError).message).toContain('googleapis.com'); } - expect(fetchStub).to.be.calledOnce; + expect(fetchMock).toHaveBeenCalledTimes(1); }); }); From 58e19c56a2120fc98f69d44d346fcbe78d9e31b9 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 20 Jan 2025 10:54:32 +0000 Subject: [PATCH 039/115] test: request tests --- packages/vertexai/__tests__/request.test.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/vertexai/__tests__/request.test.ts b/packages/vertexai/__tests__/request.test.ts index 3e211b8dbd..c9e241457e 100644 --- a/packages/vertexai/__tests__/request.test.ts +++ b/packages/vertexai/__tests__/request.test.ts @@ -175,15 +175,17 @@ describe('request methods', () => { apiKey: 'key', project: 'myproject', location: 'moon', - getAppCheckToken: () => Promise.resolve({ token: 'dummytoken', error: Error('oops') }), + getAppCheckToken: () => Promise.reject(new Error('oops')), }, true, {}, ); - + const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); - const headers = await getHeaders(fakeUrl); - expect(headers.get('X-Firebase-AppCheck')).toBe('dummytoken'); + await getHeaders(fakeUrl); + // NOTE - no app check header if there is no token, this is different to firebase-js-sdk + // See: https://github.com/firebase/firebase-js-sdk/blob/main/packages/vertexai/src/requests/request.test.ts#L172 + // expect(headers.get('X-Firebase-AppCheck')).toBe('dummytoken'); expect(warnSpy).toHaveBeenCalledWith( expect.stringMatching(/vertexai/), expect.stringMatching(/App Check.*oops/), From ce69d52c77c87e04ca215b3a6e11a20037b1a55e Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 20 Jan 2025 10:57:10 +0000 Subject: [PATCH 040/115] port over response helpers --- .../__tests__/response-helpers.test.ts | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 packages/vertexai/__tests__/response-helpers.test.ts diff --git a/packages/vertexai/__tests__/response-helpers.test.ts b/packages/vertexai/__tests__/response-helpers.test.ts new file mode 100644 index 0000000000..91a60d2cfc --- /dev/null +++ b/packages/vertexai/__tests__/response-helpers.test.ts @@ -0,0 +1,249 @@ +/** + * @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 { addHelpers, formatBlockErrorMessage } from './response-helpers'; +import { expect, use } from 'chai'; +import { restore } from 'sinon'; +import sinonChai from 'sinon-chai'; +import { + BlockReason, + Content, + FinishReason, + GenerateContentResponse +} from '../types'; + +use(sinonChai); + +const fakeResponseText: GenerateContentResponse = { + candidates: [ + { + index: 0, + content: { + role: 'model', + parts: [{ text: 'Some text' }, { text: ' and some more text' }] + } + } + ] +}; + +const functionCallPart1 = { + functionCall: { + name: 'find_theaters', + args: { + location: 'Mountain View, CA', + movie: 'Barbie' + } + } +}; + +const functionCallPart2 = { + functionCall: { + name: 'find_times', + args: { + location: 'Mountain View, CA', + movie: 'Barbie', + time: '20:00' + } + } +}; + +const fakeResponseFunctionCall: GenerateContentResponse = { + candidates: [ + { + index: 0, + content: { + role: 'model', + parts: [functionCallPart1] + } + } + ] +}; + +const fakeResponseFunctionCalls: GenerateContentResponse = { + candidates: [ + { + index: 0, + content: { + role: 'model', + parts: [functionCallPart1, functionCallPart2] + } + } + ] +}; + +const fakeResponseMixed1: GenerateContentResponse = { + candidates: [ + { + index: 0, + content: { + role: 'model', + parts: [{ text: 'some text' }, functionCallPart2] + } + } + ] +}; + +const fakeResponseMixed2: GenerateContentResponse = { + candidates: [ + { + index: 0, + content: { + role: 'model', + parts: [functionCallPart1, { text: 'some text' }] + } + } + ] +}; + +const fakeResponseMixed3: GenerateContentResponse = { + candidates: [ + { + index: 0, + content: { + role: 'model', + parts: [ + { text: 'some text' }, + functionCallPart1, + { text: ' and more text' } + ] + } + } + ] +}; + +const badFakeResponse: GenerateContentResponse = { + promptFeedback: { + blockReason: BlockReason.SAFETY, + safetyRatings: [] + } +}; + +describe('response-helpers methods', () => { + afterEach(() => { + restore(); + }); + describe('addHelpers', () => { + it('good response text', async () => { + const enhancedResponse = addHelpers(fakeResponseText); + expect(enhancedResponse.text()).to.equal('Some text and some more text'); + expect(enhancedResponse.functionCalls()).to.be.undefined; + }); + it('good response functionCall', async () => { + const enhancedResponse = addHelpers(fakeResponseFunctionCall); + expect(enhancedResponse.text()).to.equal(''); + expect(enhancedResponse.functionCalls()).to.deep.equal([ + functionCallPart1.functionCall + ]); + }); + it('good response functionCalls', async () => { + const enhancedResponse = addHelpers(fakeResponseFunctionCalls); + expect(enhancedResponse.text()).to.equal(''); + expect(enhancedResponse.functionCalls()).to.deep.equal([ + functionCallPart1.functionCall, + functionCallPart2.functionCall + ]); + }); + it('good response text/functionCall', async () => { + const enhancedResponse = addHelpers(fakeResponseMixed1); + expect(enhancedResponse.functionCalls()).to.deep.equal([ + functionCallPart2.functionCall + ]); + expect(enhancedResponse.text()).to.equal('some text'); + }); + it('good response functionCall/text', async () => { + const enhancedResponse = addHelpers(fakeResponseMixed2); + expect(enhancedResponse.functionCalls()).to.deep.equal([ + functionCallPart1.functionCall + ]); + expect(enhancedResponse.text()).to.equal('some text'); + }); + it('good response text/functionCall/text', async () => { + const enhancedResponse = addHelpers(fakeResponseMixed3); + expect(enhancedResponse.functionCalls()).to.deep.equal([ + functionCallPart1.functionCall + ]); + expect(enhancedResponse.text()).to.equal('some text and more text'); + }); + it('bad response safety', async () => { + const enhancedResponse = addHelpers(badFakeResponse); + expect(enhancedResponse.text).to.throw('SAFETY'); + }); + }); + describe('getBlockString', () => { + it('has no promptFeedback or bad finishReason', async () => { + const message = formatBlockErrorMessage({ + candidates: [ + { + index: 0, + finishReason: FinishReason.STOP, + finishMessage: 'this was fine', + content: {} as Content + } + ] + }); + expect(message).to.equal(''); + }); + it('has promptFeedback and blockReason only', async () => { + const message = formatBlockErrorMessage({ + promptFeedback: { + blockReason: BlockReason.SAFETY, + safetyRatings: [] + } + }); + expect(message).to.include('Response was blocked due to SAFETY'); + }); + it('has promptFeedback with blockReason and blockMessage', async () => { + const message = formatBlockErrorMessage({ + promptFeedback: { + blockReason: BlockReason.SAFETY, + blockReasonMessage: 'safety reasons', + safetyRatings: [] + } + }); + expect(message).to.include( + 'Response was blocked due to SAFETY: safety reasons' + ); + }); + it('has bad finishReason only', async () => { + const message = formatBlockErrorMessage({ + candidates: [ + { + index: 0, + finishReason: FinishReason.SAFETY, + content: {} as Content + } + ] + }); + expect(message).to.include('Candidate was blocked due to SAFETY'); + }); + it('has finishReason and finishMessage', async () => { + const message = formatBlockErrorMessage({ + candidates: [ + { + index: 0, + finishReason: FinishReason.SAFETY, + finishMessage: 'unsafe candidate', + content: {} as Content + } + ] + }); + expect(message).to.include( + 'Candidate was blocked due to SAFETY: unsafe candidate' + ); + }); + }); +}); From 22816ec0d0f6cfac2ada07f1bffb78351a36d92f Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 20 Jan 2025 11:57:49 +0000 Subject: [PATCH 041/115] test: response headers --- .../__tests__/response-helpers.test.ts | 205 ++++++++---------- 1 file changed, 96 insertions(+), 109 deletions(-) diff --git a/packages/vertexai/__tests__/response-helpers.test.ts b/packages/vertexai/__tests__/response-helpers.test.ts index 91a60d2cfc..d10c377b47 100644 --- a/packages/vertexai/__tests__/response-helpers.test.ts +++ b/packages/vertexai/__tests__/response-helpers.test.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,20 +12,12 @@ * 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 { describe, expect, it, jest, afterEach } from '@jest/globals'; +import { addHelpers, formatBlockErrorMessage } from '../lib/requests/response-helpers'; -import { addHelpers, formatBlockErrorMessage } from './response-helpers'; -import { expect, use } from 'chai'; -import { restore } from 'sinon'; -import sinonChai from 'sinon-chai'; -import { - BlockReason, - Content, - FinishReason, - GenerateContentResponse -} from '../types'; - -use(sinonChai); +import { BlockReason, Content, FinishReason, GenerateContentResponse } from '../lib/types'; const fakeResponseText: GenerateContentResponse = { candidates: [ @@ -34,10 +25,10 @@ const fakeResponseText: GenerateContentResponse = { index: 0, content: { role: 'model', - parts: [{ text: 'Some text' }, { text: ' and some more text' }] - } - } - ] + parts: [{ text: 'Some text' }, { text: ' and some more text' }], + }, + }, + ], }; const functionCallPart1 = { @@ -45,9 +36,9 @@ const functionCallPart1 = { name: 'find_theaters', args: { location: 'Mountain View, CA', - movie: 'Barbie' - } - } + movie: 'Barbie', + }, + }, }; const functionCallPart2 = { @@ -56,9 +47,9 @@ const functionCallPart2 = { args: { location: 'Mountain View, CA', movie: 'Barbie', - time: '20:00' - } - } + time: '20:00', + }, + }, }; const fakeResponseFunctionCall: GenerateContentResponse = { @@ -67,10 +58,10 @@ const fakeResponseFunctionCall: GenerateContentResponse = { index: 0, content: { role: 'model', - parts: [functionCallPart1] - } - } - ] + parts: [functionCallPart1], + }, + }, + ], }; const fakeResponseFunctionCalls: GenerateContentResponse = { @@ -79,10 +70,10 @@ const fakeResponseFunctionCalls: GenerateContentResponse = { index: 0, content: { role: 'model', - parts: [functionCallPart1, functionCallPart2] - } - } - ] + parts: [functionCallPart1, functionCallPart2], + }, + }, + ], }; const fakeResponseMixed1: GenerateContentResponse = { @@ -91,10 +82,10 @@ const fakeResponseMixed1: GenerateContentResponse = { index: 0, content: { role: 'model', - parts: [{ text: 'some text' }, functionCallPart2] - } - } - ] + parts: [{ text: 'some text' }, functionCallPart2], + }, + }, + ], }; const fakeResponseMixed2: GenerateContentResponse = { @@ -103,10 +94,10 @@ const fakeResponseMixed2: GenerateContentResponse = { index: 0, content: { role: 'model', - parts: [functionCallPart1, { text: 'some text' }] - } - } - ] + parts: [functionCallPart1, { text: 'some text' }], + }, + }, + ], }; const fakeResponseMixed3: GenerateContentResponse = { @@ -115,135 +106,131 @@ const fakeResponseMixed3: GenerateContentResponse = { index: 0, content: { role: 'model', - parts: [ - { text: 'some text' }, - functionCallPart1, - { text: ' and more text' } - ] - } - } - ] + parts: [{ text: 'some text' }, functionCallPart1, { text: ' and more text' }], + }, + }, + ], }; const badFakeResponse: GenerateContentResponse = { promptFeedback: { blockReason: BlockReason.SAFETY, - safetyRatings: [] - } + safetyRatings: [], + }, }; describe('response-helpers methods', () => { afterEach(() => { - restore(); + jest.restoreAllMocks(); // Use Jest's restore function }); + describe('addHelpers', () => { - it('good response text', async () => { + it('good response text', () => { const enhancedResponse = addHelpers(fakeResponseText); - expect(enhancedResponse.text()).to.equal('Some text and some more text'); - expect(enhancedResponse.functionCalls()).to.be.undefined; + expect(enhancedResponse.text()).toBe('Some text and some more text'); + expect(enhancedResponse.functionCalls()).toBeUndefined(); }); - it('good response functionCall', async () => { + + it('good response functionCall', () => { const enhancedResponse = addHelpers(fakeResponseFunctionCall); - expect(enhancedResponse.text()).to.equal(''); - expect(enhancedResponse.functionCalls()).to.deep.equal([ - functionCallPart1.functionCall - ]); + expect(enhancedResponse.text()).toBe(''); + expect(enhancedResponse.functionCalls()).toEqual([functionCallPart1.functionCall]); }); - it('good response functionCalls', async () => { + + it('good response functionCalls', () => { const enhancedResponse = addHelpers(fakeResponseFunctionCalls); - expect(enhancedResponse.text()).to.equal(''); - expect(enhancedResponse.functionCalls()).to.deep.equal([ + expect(enhancedResponse.text()).toBe(''); + expect(enhancedResponse.functionCalls()).toEqual([ functionCallPart1.functionCall, - functionCallPart2.functionCall + functionCallPart2.functionCall, ]); }); - it('good response text/functionCall', async () => { + + it('good response text/functionCall', () => { const enhancedResponse = addHelpers(fakeResponseMixed1); - expect(enhancedResponse.functionCalls()).to.deep.equal([ - functionCallPart2.functionCall - ]); - expect(enhancedResponse.text()).to.equal('some text'); + expect(enhancedResponse.functionCalls()).toEqual([functionCallPart2.functionCall]); + expect(enhancedResponse.text()).toBe('some text'); }); - it('good response functionCall/text', async () => { + + it('good response functionCall/text', () => { const enhancedResponse = addHelpers(fakeResponseMixed2); - expect(enhancedResponse.functionCalls()).to.deep.equal([ - functionCallPart1.functionCall - ]); - expect(enhancedResponse.text()).to.equal('some text'); + expect(enhancedResponse.functionCalls()).toEqual([functionCallPart1.functionCall]); + expect(enhancedResponse.text()).toBe('some text'); }); - it('good response text/functionCall/text', async () => { + + it('good response text/functionCall/text', () => { const enhancedResponse = addHelpers(fakeResponseMixed3); - expect(enhancedResponse.functionCalls()).to.deep.equal([ - functionCallPart1.functionCall - ]); - expect(enhancedResponse.text()).to.equal('some text and more text'); + expect(enhancedResponse.functionCalls()).toEqual([functionCallPart1.functionCall]); + expect(enhancedResponse.text()).toBe('some text and more text'); }); - it('bad response safety', async () => { + + it('bad response safety', () => { const enhancedResponse = addHelpers(badFakeResponse); - expect(enhancedResponse.text).to.throw('SAFETY'); + expect(() => enhancedResponse.text()).toThrow('SAFETY'); }); }); + describe('getBlockString', () => { - it('has no promptFeedback or bad finishReason', async () => { + it('has no promptFeedback or bad finishReason', () => { const message = formatBlockErrorMessage({ candidates: [ { index: 0, finishReason: FinishReason.STOP, finishMessage: 'this was fine', - content: {} as Content - } - ] + content: {} as Content, + }, + ], }); - expect(message).to.equal(''); + expect(message).toBe(''); }); - it('has promptFeedback and blockReason only', async () => { + + it('has promptFeedback and blockReason only', () => { const message = formatBlockErrorMessage({ promptFeedback: { blockReason: BlockReason.SAFETY, - safetyRatings: [] - } + safetyRatings: [], + }, }); - expect(message).to.include('Response was blocked due to SAFETY'); + expect(message).toContain('Response was blocked due to SAFETY'); }); - it('has promptFeedback with blockReason and blockMessage', async () => { + + it('has promptFeedback with blockReason and blockMessage', () => { const message = formatBlockErrorMessage({ promptFeedback: { blockReason: BlockReason.SAFETY, blockReasonMessage: 'safety reasons', - safetyRatings: [] - } + safetyRatings: [], + }, }); - expect(message).to.include( - 'Response was blocked due to SAFETY: safety reasons' - ); + expect(message).toContain('Response was blocked due to SAFETY: safety reasons'); }); - it('has bad finishReason only', async () => { + + it('has bad finishReason only', () => { const message = formatBlockErrorMessage({ candidates: [ { index: 0, finishReason: FinishReason.SAFETY, - content: {} as Content - } - ] + content: {} as Content, + }, + ], }); - expect(message).to.include('Candidate was blocked due to SAFETY'); + expect(message).toContain('Candidate was blocked due to SAFETY'); }); - it('has finishReason and finishMessage', async () => { + + it('has finishReason and finishMessage', () => { const message = formatBlockErrorMessage({ candidates: [ { index: 0, finishReason: FinishReason.SAFETY, finishMessage: 'unsafe candidate', - content: {} as Content - } - ] + content: {} as Content, + }, + ], }); - expect(message).to.include( - 'Candidate was blocked due to SAFETY: unsafe candidate' - ); + expect(message).toContain('Candidate was blocked due to SAFETY: unsafe candidate'); }); }); }); From a37c29befac9bef1c5ac49658451269d1250c66c Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 20 Jan 2025 11:58:00 +0000 Subject: [PATCH 042/115] port over schema builder --- .../vertexai/__tests__/schema-builder.test.ts | 393 ++++++++++++++++++ 1 file changed, 393 insertions(+) create mode 100644 packages/vertexai/__tests__/schema-builder.test.ts diff --git a/packages/vertexai/__tests__/schema-builder.test.ts b/packages/vertexai/__tests__/schema-builder.test.ts new file mode 100644 index 0000000000..b95acaae9f --- /dev/null +++ b/packages/vertexai/__tests__/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 + } + } + } +}; From 90b5324e22124ca5ccf1fa6cbcd1a11f51525ee3 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 20 Jan 2025 12:47:34 +0000 Subject: [PATCH 043/115] test: scheme builder --- .../vertexai/__tests__/schema-builder.test.ts | 414 +++++++++--------- 1 file changed, 205 insertions(+), 209 deletions(-) diff --git a/packages/vertexai/__tests__/schema-builder.test.ts b/packages/vertexai/__tests__/schema-builder.test.ts index b95acaae9f..2b10f06d2d 100644 --- a/packages/vertexai/__tests__/schema-builder.test.ts +++ b/packages/vertexai/__tests__/schema-builder.test.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,142 +12,150 @@ * 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); +import { describe, expect, it } from '@jest/globals'; +import { Schema } from '../lib/requests/schema-builder'; +import { VertexAIErrorCode } from '../lib/types'; describe('Schema builder', () => { it('builds integer schema', () => { const schema = Schema.integer(); - expect(schema.toJSON()).to.eql({ + expect(schema.toJSON()).toEqual({ type: 'integer', - nullable: false + nullable: false, }); }); + it('builds integer schema with options and overrides', () => { const schema = Schema.integer({ nullable: true, format: 'int32' }); - expect(schema.toJSON()).to.eql({ + expect(schema.toJSON()).toEqual({ type: 'integer', format: 'int32', - nullable: true + nullable: true, }); }); + it('builds number schema', () => { const schema = Schema.number(); - expect(schema.toJSON()).to.eql({ + expect(schema.toJSON()).toEqual({ type: 'number', - nullable: false + nullable: false, }); }); + it('builds number schema with options and unknown options', () => { const schema = Schema.number({ format: 'float', futureOption: 'test' }); - expect(schema.toJSON()).to.eql({ + expect(schema.toJSON()).toEqual({ type: 'number', format: 'float', futureOption: 'test', - nullable: false + nullable: false, }); }); + it('builds boolean schema', () => { const schema = Schema.boolean(); - expect(schema.toJSON()).to.eql({ + expect(schema.toJSON()).toEqual({ type: 'boolean', - nullable: false + nullable: false, }); }); + it('builds string schema', () => { const schema = Schema.string({ description: 'hey' }); - expect(schema.toJSON()).to.eql({ + expect(schema.toJSON()).toEqual({ type: 'string', description: 'hey', - nullable: false + nullable: false, }); }); + it('builds enumString schema', () => { const schema = Schema.enumString({ example: 'east', - enum: ['east', 'west'] + enum: ['east', 'west'], }); - expect(schema.toJSON()).to.eql({ + expect(schema.toJSON()).toEqual({ type: 'string', example: 'east', enum: ['east', 'west'], - nullable: false + nullable: false, }); }); + it('builds an object schema', () => { const schema = Schema.object({ properties: { - 'someInput': Schema.string() - } + someInput: Schema.string(), + }, }); - expect(schema.toJSON()).to.eql({ + + expect(schema.toJSON()).toEqual({ type: 'object', nullable: false, properties: { - 'someInput': { + someInput: { type: 'string', - nullable: false - } + nullable: false, + }, }, - required: ['someInput'] + required: ['someInput'], }); }); + it('builds an object schema with optional properties', () => { const schema = Schema.object({ properties: { - 'someInput': Schema.string(), - 'someBool': Schema.boolean() + someInput: Schema.string(), + someBool: Schema.boolean(), }, - optionalProperties: ['someBool'] + optionalProperties: ['someBool'], }); - expect(schema.toJSON()).to.eql({ + expect(schema.toJSON()).toEqual({ type: 'object', nullable: false, properties: { - 'someInput': { + someInput: { type: 'string', - nullable: false + nullable: false, }, - 'someBool': { + someBool: { type: 'boolean', - nullable: false - } + nullable: false, + }, }, - required: ['someInput'] + required: ['someInput'], }); }); + it('builds layered schema - partially filled out', () => { const schema = Schema.array({ items: Schema.object({ properties: { country: Schema.string({ - description: 'A country name' + description: 'A country name', }), population: Schema.integer(), coordinates: Schema.object({ properties: { latitude: Schema.number({ format: 'float' }), - longitude: Schema.number({ format: 'double' }) - } + longitude: Schema.number({ format: 'double' }), + }, }), hemisphere: Schema.object({ properties: { latitudinal: Schema.enumString({ enum: ['N', 'S'] }), - longitudinal: Schema.enumString({ enum: ['E', 'W'] }) - } + longitudinal: Schema.enumString({ enum: ['E', 'W'] }), + }, }), - isCapital: Schema.boolean() - } - }) + isCapital: Schema.boolean(), + }, + }), }); - expect(schema.toJSON()).to.eql(layeredSchemaOutputPartial); + expect(schema.toJSON()).toEqual(layeredSchemaOutputPartial); }); + it('builds layered schema - fully filled out', () => { const schema = Schema.array({ items: Schema.object({ @@ -158,12 +165,12 @@ describe('Schema builder', () => { country: Schema.string({ nullable: false, description: 'Country name', - format: undefined + format: undefined, }), population: Schema.integer({ nullable: false, description: 'Number of people in country', - format: 'int64' + format: 'int64', }), coordinates: Schema.object({ nullable: false, @@ -172,222 +179,211 @@ describe('Schema builder', () => { latitude: Schema.number({ nullable: false, description: 'Latitude of capital', - format: 'float' + format: 'float', }), longitude: Schema.number({ nullable: false, description: 'Longitude of capital', - format: 'double' - }) - } + 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'] }) - } + 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" + description: "This doesn't make a lot of sense but it's a demo", }), elevation: Schema.integer({ nullable: false, description: 'Average elevation', - format: 'float' - }) + format: 'float', + }), }, - optionalProperties: [] - }) + optionalProperties: [], + }), }); - expect(schema.toJSON()).to.eql(layeredSchemaOutput); + expect(schema.toJSON()).toEqual(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 }) + population: Schema.integer({ nullable: true }), }, - optionalProperties: ['elevation'] + optionalProperties: ['elevation'], }); - expect(schema.toJSON()).to.eql({ - 'type': 'object', - 'nullable': false, - 'properties': { - 'country': { - 'type': 'string', - 'nullable': false + expect(schema.toJSON()).toEqual({ + type: 'object', + nullable: false, + properties: { + country: { + type: 'string', + nullable: false, + }, + elevation: { + type: 'number', + nullable: false, }, - 'elevation': { - 'type': 'number', - 'nullable': false + population: { + type: 'integer', + nullable: true, }, - 'population': { - 'type': 'integer', - 'nullable': true - } }, - 'required': ['country', 'population'] + 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 }) + population: Schema.integer({ nullable: true }), }, - optionalProperties: ['cat'] + optionalProperties: ['cat'], }); - expect(() => schema.toJSON()).to.throw(VertexAIErrorCode.INVALID_SCHEMA); + expect(() => schema.toJSON()).toThrow(VertexAIErrorCode.INVALID_SCHEMA); }); }); const layeredSchemaOutputPartial = { - 'type': 'array', - 'nullable': false, - 'items': { - 'type': 'object', - 'nullable': false, - 'properties': { - 'country': { - 'type': 'string', - 'description': 'A country name', - 'nullable': false + type: 'array', + nullable: false, + items: { + type: 'object', + nullable: false, + properties: { + country: { + type: 'string', + description: 'A country name', + nullable: false, }, - 'population': { - 'type': 'integer', - 'nullable': false + population: { + type: 'integer', + nullable: false, }, - 'coordinates': { - 'type': 'object', - 'nullable': false, - 'properties': { - 'latitude': { - 'type': 'number', - 'format': 'float', - 'nullable': false + coordinates: { + type: 'object', + nullable: false, + properties: { + latitude: { + type: 'number', + format: 'float', + nullable: false, + }, + longitude: { + type: 'number', + format: 'double', + nullable: false, }, - 'longitude': { - 'type': 'number', - 'format': 'double', - 'nullable': false - } }, - 'required': ['latitude', 'longitude'] + required: ['latitude', 'longitude'], }, - 'hemisphere': { - 'type': 'object', - 'nullable': false, - 'properties': { - 'latitudinal': { - 'type': 'string', - 'nullable': false, - 'enum': ['N', 'S'] + hemisphere: { + type: 'object', + nullable: false, + properties: { + latitudinal: { + type: 'string', + nullable: false, + enum: ['N', 'S'], + }, + longitudinal: { + type: 'string', + nullable: false, + enum: ['E', 'W'], }, - 'longitudinal': { - 'type': 'string', - 'nullable': false, - 'enum': ['E', 'W'] - } }, - 'required': ['latitudinal', 'longitudinal'] + required: ['latitudinal', 'longitudinal'], + }, + isCapital: { + type: 'boolean', + nullable: false, }, - 'isCapital': { - 'type': 'boolean', - 'nullable': false - } }, - 'required': [ - 'country', - 'population', - 'coordinates', - 'hemisphere', - 'isCapital' - ] - } + 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 + 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 + 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 + 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, }, - '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'] + 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'], }, - '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 + 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, }, - 'elevation': { - 'type': 'integer', - 'format': 'float', - 'description': 'Average elevation', - 'nullable': false - } - } - } + }, + }, }; From 8a84f82781b0d3cc3bfa56121f823bbe55f34a64 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 20 Jan 2025 12:47:45 +0000 Subject: [PATCH 044/115] test: stream reader --- .../vertexai/__tests__/stream-reader.test.ts | 309 ++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 packages/vertexai/__tests__/stream-reader.test.ts diff --git a/packages/vertexai/__tests__/stream-reader.test.ts b/packages/vertexai/__tests__/stream-reader.test.ts new file mode 100644 index 0000000000..b2871ec4d4 --- /dev/null +++ b/packages/vertexai/__tests__/stream-reader.test.ts @@ -0,0 +1,309 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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 { describe, expect, it, jest, afterEach, beforeAll } from '@jest/globals'; +import { ReadableStream } from 'web-streams-polyfill'; +import { + aggregateResponses, + getResponseStream, + processStream, +} from '../lib/requests/stream-reader'; + +import { getChunkedStream, getMockResponseStreaming } from './test-utils/mock-response'; +import { + BlockReason, + FinishReason, + GenerateContentResponse, + HarmCategory, + HarmProbability, + SafetyRating, +} from '../lib/types'; + +describe('stream-reader', () => { + describe('getResponseStream', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('two lines', async () => { + const src = [{ text: 'A' }, { text: 'B' }]; + const inputStream = getChunkedStream( + src + .map(v => JSON.stringify(v)) + .map(v => 'data: ' + v + '\r\n\r\n') + .join(''), + ); + + const decodeStream = new ReadableStream({ + async start(controller) { + const reader = inputStream.getReader(); + const decoder = new TextDecoder('utf-8'); + while (true) { + const { done, value } = await reader.read(); + if (done) { + controller.close(); + break; + } + const decodedValue = decoder.decode(value, { stream: true }); + controller.enqueue(decodedValue); + } + }, + }); + + const responseStream = getResponseStream<{ text: string }>(decodeStream); + const reader = responseStream.getReader(); + const responses: Array<{ text: string }> = []; + while (true) { + const { done, value } = await reader.read(); + if (done) { + break; + } + responses.push(value); + } + expect(responses).toEqual(src); + }); + }); + + describe('processStream', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('streaming response - short', async () => { + const fakeResponse = getMockResponseStreaming('streaming-success-basic-reply-short.txt'); + const result = processStream(fakeResponse as Response); + for await (const response of result.stream) { + expect(response.text()).not.toBe(''); + } + const aggregatedResponse = await result.response; + expect(aggregatedResponse.text()).toContain('Cheyenne'); + }); + + it('streaming response - functioncall', async () => { + const fakeResponse = getMockResponseStreaming('streaming-success-function-call-short.txt'); + const result = processStream(fakeResponse as Response); + for await (const response of result.stream) { + expect(response.text()).toBe(''); + expect(response.functionCalls()).toEqual([ + { + name: 'getTemperature', + args: { city: 'San Jose' }, + }, + ]); + } + const aggregatedResponse = await result.response; + expect(aggregatedResponse.text()).toBe(''); + expect(aggregatedResponse.functionCalls()).toEqual([ + { + name: 'getTemperature', + args: { city: 'San Jose' }, + }, + ]); + }); + + it('handles citations', async () => { + const fakeResponse = getMockResponseStreaming('streaming-success-citations.txt'); + const result = processStream(fakeResponse as Response); + const aggregatedResponse = await result.response; + expect(aggregatedResponse.text()).toContain('Quantum mechanics is'); + expect(aggregatedResponse.candidates?.[0]!.citationMetadata?.citations.length).toBe(3); + let foundCitationMetadata = false; + for await (const response of result.stream) { + expect(response.text()).not.toBe(''); + if (response.candidates?.[0]?.citationMetadata) { + foundCitationMetadata = true; + } + } + expect(foundCitationMetadata).toBe(true); + }); + }); + + describe('aggregateResponses', () => { + it('handles no candidates, and promptFeedback', () => { + const responsesToAggregate: GenerateContentResponse[] = [ + { + promptFeedback: { + blockReason: BlockReason.SAFETY, + safetyRatings: [ + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + probability: HarmProbability.LOW, + } as SafetyRating, + ], + }, + }, + ]; + const response = aggregateResponses(responsesToAggregate); + expect(response.candidates).toBeUndefined(); + expect(response.promptFeedback?.blockReason).toBe(BlockReason.SAFETY); + }); + + describe('multiple responses, has candidates', () => { + let response: GenerateContentResponse; + beforeAll(() => { + const responsesToAggregate: GenerateContentResponse[] = [ + { + candidates: [ + { + index: 0, + content: { + role: 'user', + parts: [{ text: 'hello.' }], + }, + finishReason: FinishReason.STOP, + finishMessage: 'something', + safetyRatings: [ + { + category: HarmCategory.HARM_CATEGORY_HARASSMENT, + probability: HarmProbability.NEGLIGIBLE, + } as SafetyRating, + ], + }, + ], + promptFeedback: { + blockReason: BlockReason.SAFETY, + safetyRatings: [ + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + probability: HarmProbability.LOW, + } as SafetyRating, + ], + }, + }, + { + candidates: [ + { + index: 0, + content: { + role: 'user', + parts: [{ text: 'angry stuff' }], + }, + finishReason: FinishReason.STOP, + finishMessage: 'something', + safetyRatings: [ + { + category: HarmCategory.HARM_CATEGORY_HARASSMENT, + probability: HarmProbability.NEGLIGIBLE, + } as SafetyRating, + ], + citationMetadata: { + citations: [ + { + startIndex: 0, + endIndex: 20, + uri: 'sourceurl', + license: '', + }, + ], + }, + }, + ], + promptFeedback: { + blockReason: BlockReason.OTHER, + safetyRatings: [ + { + category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + probability: HarmProbability.HIGH, + } as SafetyRating, + ], + }, + }, + { + candidates: [ + { + index: 0, + content: { + role: 'user', + parts: [{ text: '...more stuff' }], + }, + finishReason: FinishReason.MAX_TOKENS, + finishMessage: 'too many tokens', + safetyRatings: [ + { + category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + probability: HarmProbability.MEDIUM, + } as SafetyRating, + ], + citationMetadata: { + citations: [ + { + startIndex: 0, + endIndex: 20, + uri: 'sourceurl', + license: '', + }, + { + startIndex: 150, + endIndex: 155, + uri: 'sourceurl', + license: '', + }, + ], + }, + }, + ], + promptFeedback: { + blockReason: BlockReason.OTHER, + safetyRatings: [ + { + category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, + probability: HarmProbability.HIGH, + } as SafetyRating, + ], + }, + }, + ]; + response = aggregateResponses(responsesToAggregate); + }); + + it('aggregates text across responses', () => { + expect(response.candidates?.length).toBe(1); + expect(response.candidates?.[0]!.content.parts.map(({ text }) => text)).toEqual([ + 'hello.', + 'angry stuff', + '...more stuff', + ]); + }); + + it("takes the last response's promptFeedback", () => { + expect(response.promptFeedback?.blockReason).toBe(BlockReason.OTHER); + }); + + it("takes the last response's finishReason", () => { + expect(response.candidates?.[0]!.finishReason).toBe(FinishReason.MAX_TOKENS); + }); + + it("takes the last response's finishMessage", () => { + expect(response.candidates?.[0]!.finishMessage).toBe('too many tokens'); + }); + + it("takes the last response's candidate safetyRatings", () => { + expect(response.candidates?.[0]!.safetyRatings?.[0]!.category).toBe( + HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, + ); + expect(response.candidates?.[0]!.safetyRatings?.[0]!.probability).toBe( + HarmProbability.MEDIUM, + ); + }); + + it('collects all citations into one array', () => { + expect(response.candidates?.[0]!.citationMetadata?.citations.length).toBe(2); + expect(response.candidates?.[0]!.citationMetadata?.citations[0]!.startIndex).toBe(0); + expect(response.candidates?.[0]!.citationMetadata?.citations[1]!.startIndex).toBe(150); + }); + }); + }); +}); From 3f9b29ecea7efa9afee105f9ba5b06bae6f693de Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 20 Jan 2025 12:55:41 +0000 Subject: [PATCH 045/115] port over api tests --- packages/vertexai/__tests__/api.test.ts | 87 +++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 packages/vertexai/__tests__/api.test.ts diff --git a/packages/vertexai/__tests__/api.test.ts b/packages/vertexai/__tests__/api.test.ts new file mode 100644 index 0000000000..3171bae1a7 --- /dev/null +++ b/packages/vertexai/__tests__/api.test.ts @@ -0,0 +1,87 @@ +/** + * @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 { ModelParams, VertexAIErrorCode } from '../lib/types'; +import { VertexAIError } from '../lib/errors'; +import { getGenerativeModel } from '../lib/index'; +import { expect } from 'chai'; +import { VertexAI } from './public-types'; +import { GenerativeModel } from './models/generative-model'; + +const fakeVertexAI: VertexAI = { + app: { + name: 'DEFAULT', + automaticDataCollectionEnabled: true, + options: { + apiKey: 'key', + projectId: 'my-project' + } + }, + location: 'us-central1' +}; + +describe('Top level API', () => { + it('getGenerativeModel throws if no model is provided', () => { + try { + getGenerativeModel(fakeVertexAI, {} as ModelParams); + } catch (e) { + expect((e as VertexAIError).code).includes(VertexAIErrorCode.NO_MODEL); + expect((e as VertexAIError).message).includes( + `VertexAI: Must provide a model name. Example: ` + + `getGenerativeModel({ model: 'my-model-name' }) (vertexAI/${VertexAIErrorCode.NO_MODEL})` + ); + } + }); + it('getGenerativeModel throws if no apiKey is provided', () => { + const fakeVertexNoApiKey = { + ...fakeVertexAI, + app: { options: { projectId: 'my-project' } } + } as VertexAI; + try { + getGenerativeModel(fakeVertexNoApiKey, { model: 'my-model' }); + } catch (e) { + expect((e as VertexAIError).code).includes(VertexAIErrorCode.NO_API_KEY); + expect((e as VertexAIError).message).equals( + `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})` + ); + } + }); + it('getGenerativeModel throws if no projectId is provided', () => { + const fakeVertexNoProject = { + ...fakeVertexAI, + app: { options: { apiKey: 'my-key' } } + } as VertexAI; + try { + getGenerativeModel(fakeVertexNoProject, { model: 'my-model' }); + } catch (e) { + expect((e as VertexAIError).code).includes( + VertexAIErrorCode.NO_PROJECT_ID + ); + expect((e as VertexAIError).message).equals( + `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})` + ); + } + }); + it('getGenerativeModel gets a GenerativeModel', () => { + const genModel = getGenerativeModel(fakeVertexAI, { model: 'my-model' }); + expect(genModel).to.be.an.instanceOf(GenerativeModel); + expect(genModel.model).to.equal('publishers/google/models/my-model'); + }); +}); From 89c34fd462c6a911ba0ab16875e4e35cf1eb6c2d Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 20 Jan 2025 13:16:28 +0000 Subject: [PATCH 046/115] test: update chat session helper --- .../__tests__/chat-session-helpers.test.ts | 241 +++++++++--------- 1 file changed, 121 insertions(+), 120 deletions(-) diff --git a/packages/vertexai/__tests__/chat-session-helpers.test.ts b/packages/vertexai/__tests__/chat-session-helpers.test.ts index f96fa95d33..8bc81f4eab 100644 --- a/packages/vertexai/__tests__/chat-session-helpers.test.ts +++ b/packages/vertexai/__tests__/chat-session-helpers.test.ts @@ -21,126 +21,127 @@ import { FirebaseError } from '@firebase/util'; describe('chat-session-helpers', () => { describe('validateChatHistory', () => { - const TCS: Array<{ history: Content[]; isValid: boolean }> = [ - { - history: [{ role: 'user', parts: [{ text: 'hi' }] }], - isValid: true, - }, - { - history: [ - { - role: 'user', - parts: [{ text: 'hi' }, { inlineData: { mimeType: 'image/jpeg', data: 'base64==' } }], - }, - ], - isValid: true, - }, - { - history: [ - { role: 'user', parts: [{ text: 'hi' }] }, - { role: 'model', parts: [{ text: 'hi' }, { text: 'hi' }] }, - ], - isValid: true, - }, - { - history: [ - { role: 'user', parts: [{ text: 'hi' }] }, - { - role: 'model', - parts: [{ functionCall: { name: 'greet', args: { name: 'user' } } }], - }, - ], - isValid: true, - }, - { - history: [ - { role: 'user', parts: [{ text: 'hi' }] }, - { - role: 'model', - parts: [{ functionCall: { name: 'greet', args: { name: 'user' } } }], - }, - { - role: 'function', - parts: [ - { - functionResponse: { name: 'greet', response: { name: 'user' } }, - }, - ], - }, - ], - isValid: true, - }, - { - history: [ - { role: 'user', parts: [{ text: 'hi' }] }, - { - role: 'model', - parts: [{ functionCall: { name: 'greet', args: { name: 'user' } } }], - }, - { - role: 'function', - parts: [ - { - functionResponse: { name: 'greet', response: { name: 'user' } }, - }, - ], - }, - { - role: 'model', - parts: [{ text: 'hi name' }], - }, - ], - isValid: true, - }, - { - //@ts-expect-error - history: [{ role: 'user', parts: '' }], - isValid: false, - }, - { - //@ts-expect-error - history: [{ role: 'user' }], - isValid: false, - }, - { - history: [{ role: 'user', parts: [] }], - isValid: false, - }, - { - history: [{ role: 'model', parts: [{ text: 'hi' }] }], - isValid: false, - }, - { - history: [ - { - role: 'function', - parts: [ - { - functionResponse: { name: 'greet', response: { name: 'user' } }, - }, - ], - }, - ], - isValid: false, - }, - { - history: [ - { role: 'user', parts: [{ text: 'hi' }] }, - { role: 'user', parts: [{ text: 'hi' }] }, - ], - isValid: false, - }, - { - history: [ - { role: 'user', parts: [{ text: 'hi' }] }, - { role: 'model', parts: [{ text: 'hi' }] }, - { role: 'model', parts: [{ text: 'hi' }] }, - ], - isValid: false, - }, - ]; - TCS.forEach((tc, index) => { - it(`case ${index}`, () => { + it('check chat history', () => { + const TCS: Array<{ history: Content[]; isValid: boolean }> = [ + { + history: [{ role: 'user', parts: [{ text: 'hi' }] }], + isValid: true, + }, + { + history: [ + { + role: 'user', + parts: [{ text: 'hi' }, { inlineData: { mimeType: 'image/jpeg', data: 'base64==' } }], + }, + ], + isValid: true, + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { role: 'model', parts: [{ text: 'hi' }, { text: 'hi' }] }, + ], + isValid: true, + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { + role: 'model', + parts: [{ functionCall: { name: 'greet', args: { name: 'user' } } }], + }, + ], + isValid: true, + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { + role: 'model', + parts: [{ functionCall: { name: 'greet', args: { name: 'user' } } }], + }, + { + role: 'function', + parts: [ + { + functionResponse: { name: 'greet', response: { name: 'user' } }, + }, + ], + }, + ], + isValid: true, + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { + role: 'model', + parts: [{ functionCall: { name: 'greet', args: { name: 'user' } } }], + }, + { + role: 'function', + parts: [ + { + functionResponse: { name: 'greet', response: { name: 'user' } }, + }, + ], + }, + { + role: 'model', + parts: [{ text: 'hi name' }], + }, + ], + isValid: true, + }, + { + //@ts-expect-error + history: [{ role: 'user', parts: '' }], + isValid: false, + }, + { + //@ts-expect-error + history: [{ role: 'user' }], + isValid: false, + }, + { + history: [{ role: 'user', parts: [] }], + isValid: false, + }, + { + history: [{ role: 'model', parts: [{ text: 'hi' }] }], + isValid: false, + }, + { + history: [ + { + role: 'function', + parts: [ + { + functionResponse: { name: 'greet', response: { name: 'user' } }, + }, + ], + }, + ], + isValid: false, + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { role: 'user', parts: [{ text: 'hi' }] }, + ], + isValid: false, + }, + { + history: [ + { role: 'user', parts: [{ text: 'hi' }] }, + { role: 'model', parts: [{ text: 'hi' }] }, + { role: 'model', parts: [{ text: 'hi' }] }, + ], + isValid: false, + }, + ]; + + TCS.forEach(tc => { const fn = (): void => validateChatHistory(tc.history); if (tc.isValid) { expect(fn).not.toThrow(); From 6ce27e1e49f20d9ded9cb35adc70c4bdc6b8020c Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 20 Jan 2025 13:16:37 +0000 Subject: [PATCH 047/115] rm dead code --- packages/vertexai/__tests__/generate-content.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/vertexai/__tests__/generate-content.test.ts b/packages/vertexai/__tests__/generate-content.test.ts index c9ae29b9c2..2b5f4dd732 100644 --- a/packages/vertexai/__tests__/generate-content.test.ts +++ b/packages/vertexai/__tests__/generate-content.test.ts @@ -34,10 +34,6 @@ const fakeApiSettings: ApiSettings = { location: 'us-central1', }; -// const requestOptions: RequestOptions = { -// timeout: 1000, -// }; - const fakeRequestParams: GenerateContentRequest = { contents: [{ parts: [{ text: 'hello' }], role: 'user' }], generationConfig: { From 0c49ca192378269e5e78aac6b34362ac208b9001 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 20 Jan 2025 13:16:51 +0000 Subject: [PATCH 048/115] test: api tests --- packages/vertexai/__tests__/api.test.ts | 52 +++++++++++++------------ 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/packages/vertexai/__tests__/api.test.ts b/packages/vertexai/__tests__/api.test.ts index 3171bae1a7..f9a5ad5281 100644 --- a/packages/vertexai/__tests__/api.test.ts +++ b/packages/vertexai/__tests__/api.test.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,13 +12,15 @@ * 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 { describe, expect, it } from '@jest/globals'; import { ModelParams, VertexAIErrorCode } from '../lib/types'; import { VertexAIError } from '../lib/errors'; import { getGenerativeModel } from '../lib/index'; -import { expect } from 'chai'; -import { VertexAI } from './public-types'; -import { GenerativeModel } from './models/generative-model'; + +import { VertexAI } from '../lib/public-types'; +import { GenerativeModel } from '../lib/models/generative-model'; const fakeVertexAI: VertexAI = { app: { @@ -27,10 +28,10 @@ const fakeVertexAI: VertexAI = { automaticDataCollectionEnabled: true, options: { apiKey: 'key', - projectId: 'my-project' - } + projectId: 'my-project', + }, }, - location: 'us-central1' + location: 'us-central1', }; describe('Top level API', () => { @@ -38,50 +39,51 @@ describe('Top level API', () => { try { getGenerativeModel(fakeVertexAI, {} as ModelParams); } catch (e) { - expect((e as VertexAIError).code).includes(VertexAIErrorCode.NO_MODEL); - expect((e as VertexAIError).message).includes( + expect((e as VertexAIError).code).toContain(VertexAIErrorCode.NO_MODEL); + expect((e as VertexAIError).message).toContain( `VertexAI: Must provide a model name. Example: ` + - `getGenerativeModel({ model: 'my-model-name' }) (vertexAI/${VertexAIErrorCode.NO_MODEL})` + `getGenerativeModel({ model: 'my-model-name' }) (vertexAI/${VertexAIErrorCode.NO_MODEL})`, ); } }); + it('getGenerativeModel throws if no apiKey is provided', () => { const fakeVertexNoApiKey = { ...fakeVertexAI, - app: { options: { projectId: 'my-project' } } + app: { options: { projectId: 'my-project' } }, } as VertexAI; try { getGenerativeModel(fakeVertexNoApiKey, { model: 'my-model' }); } catch (e) { - expect((e as VertexAIError).code).includes(VertexAIErrorCode.NO_API_KEY); - expect((e as VertexAIError).message).equals( + expect((e as VertexAIError).code).toContain(VertexAIErrorCode.NO_API_KEY); + expect((e as VertexAIError).message).toBe( `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})` + ` contain a valid API key. (vertexAI/${VertexAIErrorCode.NO_API_KEY})`, ); } }); + it('getGenerativeModel throws if no projectId is provided', () => { const fakeVertexNoProject = { ...fakeVertexAI, - app: { options: { apiKey: 'my-key' } } + app: { options: { apiKey: 'my-key' } }, } as VertexAI; try { getGenerativeModel(fakeVertexNoProject, { model: 'my-model' }); } catch (e) { - expect((e as VertexAIError).code).includes( - VertexAIErrorCode.NO_PROJECT_ID - ); - expect((e as VertexAIError).message).equals( + expect((e as VertexAIError).code).toContain(VertexAIErrorCode.NO_PROJECT_ID); + expect((e as VertexAIError).message).toBe( `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})` + `to contain a valid project ID. (vertexAI/${VertexAIErrorCode.NO_PROJECT_ID})`, ); } }); + it('getGenerativeModel gets a GenerativeModel', () => { const genModel = getGenerativeModel(fakeVertexAI, { model: 'my-model' }); - expect(genModel).to.be.an.instanceOf(GenerativeModel); - expect(genModel.model).to.equal('publishers/google/models/my-model'); + expect(genModel).toBeInstanceOf(GenerativeModel); + expect(genModel.model).toBe('publishers/google/models/my-model'); }); }); From 70bf9c22de3bbd43a460ce542bd9cb597dbb0f93 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 20 Jan 2025 13:19:12 +0000 Subject: [PATCH 049/115] test: service test --- packages/vertexai/__tests__/service.test.ts | 45 +++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 packages/vertexai/__tests__/service.test.ts diff --git a/packages/vertexai/__tests__/service.test.ts b/packages/vertexai/__tests__/service.test.ts new file mode 100644 index 0000000000..eef84521f7 --- /dev/null +++ b/packages/vertexai/__tests__/service.test.ts @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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 { describe, expect, it } from '@jest/globals'; +import { DEFAULT_LOCATION } from '../lib/constants'; +import { VertexAIService } from '../lib/service'; + +const fakeApp = { + name: 'DEFAULT', + automaticDataCollectionEnabled: true, + options: { + apiKey: 'key', + projectId: 'my-project', + }, +}; + +describe('VertexAIService', () => { + it('uses default location if not specified', () => { + const vertexAI = new VertexAIService(fakeApp); + expect(vertexAI.location).toBe(DEFAULT_LOCATION); + }); + + it('uses custom location if specified', () => { + const vertexAI = new VertexAIService( + fakeApp, + /* authProvider */ undefined, + /* appCheckProvider */ undefined, + { location: 'somewhere' }, + ); + expect(vertexAI.location).toBe('somewhere'); + }); +}); From 223e40a421fc933d62a45497da38d683f5b25e53 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 20 Jan 2025 14:10:08 +0000 Subject: [PATCH 050/115] chore: move vertex mock script to scripts/ --- package.json | 2 +- vertex_mock_responses.sh => scripts/vertex_mock_responses.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename vertex_mock_responses.sh => scripts/vertex_mock_responses.sh (94%) diff --git a/package.json b/package.json index 466ff52bd9..ecb500542b 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "lint:spellcheck": "spellchecker --quiet --files=\"docs/**/*.md\" --dictionaries=\"./.spellcheck.dict.txt\" --reports=\"spelling.json\" --plugins spell indefinite-article repeated-words syntax-mentions syntax-urls frontmatter", "tsc:compile": "tsc --project .", "lint:all": "yarn lint && yarn lint:markdown && yarn lint:spellcheck && yarn tsc:compile", - "tests:vertex:mocks": "./vertex_mock_responses.sh && yarn ts-node ./packages/vertexai/__tests__/test-utils/convert-mocks.ts", + "tests:vertex:mocks": "./scripts/vertex_mock_responses.sh && yarn ts-node ./packages/vertexai/__tests__/test-utils/convert-mocks.ts", "tests:jest": "yarn tests:vertex:mocks && jest", "tests:jest-watch": "tests:vertex:mocks && jest --watch", "tests:jest-coverage": "tests:vertex:mocks && jest --coverage", diff --git a/vertex_mock_responses.sh b/scripts/vertex_mock_responses.sh similarity index 94% rename from vertex_mock_responses.sh rename to scripts/vertex_mock_responses.sh index 0d58789e4f..92c516befe 100755 --- a/vertex_mock_responses.sh +++ b/scripts/vertex_mock_responses.sh @@ -21,7 +21,7 @@ RESPONSES_VERSION='v5.*' # The major version of mock responses to use REPO_NAME="vertexai-sdk-test-data" REPO_LINK="https://github.com/FirebaseExtended/$REPO_NAME.git" -cd "$(dirname "$0")/packages/vertexai/__tests__/test-utils" || exit +cd "$(dirname "$0")/../packages/vertexai/__tests__/test-utils" || exit rm -rf "$REPO_NAME" git clone "$REPO_LINK" --quiet || exit cd "$REPO_NAME" || exit From af911c977fac662e83a6138648b2099b390a1c47 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 20 Jan 2025 14:32:24 +0000 Subject: [PATCH 051/115] chore: update license headers --- .../__tests__/chat-session-helpers.test.ts | 8 ++++---- .../vertexai/__tests__/chat-session.test.ts | 8 ++++---- .../vertexai/__tests__/count-tokens.test.ts | 8 ++++---- .../vertexai/__tests__/test-utils/base64cat.ts | 8 ++++---- .../__tests__/test-utils/convert-mocks.ts | 8 ++++---- .../__tests__/test-utils/mock-response.ts | 8 ++++---- packages/vertexai/lib/constants.ts | 8 ++++---- packages/vertexai/lib/errors.ts | 8 ++++---- .../lib/methods/chat-session-helpers.ts | 8 ++++---- packages/vertexai/lib/methods/chat-session.ts | 8 ++++---- packages/vertexai/lib/methods/count-tokens.ts | 8 ++++---- .../vertexai/lib/methods/generate-content.ts | 8 ++++---- .../vertexai/lib/models/generative-model.ts | 8 ++++---- packages/vertexai/lib/polyfills.ts | 17 +++++++++++++++++ packages/vertexai/lib/public-types.ts | 8 ++++---- .../vertexai/lib/requests/request-helpers.ts | 8 ++++---- packages/vertexai/lib/requests/request.ts | 8 ++++---- .../vertexai/lib/requests/response-helpers.ts | 8 ++++---- .../vertexai/lib/requests/schema-builder.ts | 8 ++++---- packages/vertexai/lib/requests/stream-reader.ts | 8 ++++---- packages/vertexai/lib/service.ts | 8 ++++---- 21 files changed, 97 insertions(+), 80 deletions(-) diff --git a/packages/vertexai/__tests__/chat-session-helpers.test.ts b/packages/vertexai/__tests__/chat-session-helpers.test.ts index 8bc81f4eab..be8b6a5a39 100644 --- a/packages/vertexai/__tests__/chat-session-helpers.test.ts +++ b/packages/vertexai/__tests__/chat-session-helpers.test.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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 { describe, expect, it } from '@jest/globals'; import { validateChatHistory } from '../lib/methods/chat-session-helpers'; diff --git a/packages/vertexai/__tests__/chat-session.test.ts b/packages/vertexai/__tests__/chat-session.test.ts index cd96aa32e6..603695f405 100644 --- a/packages/vertexai/__tests__/chat-session.test.ts +++ b/packages/vertexai/__tests__/chat-session.test.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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 { describe, expect, it, afterEach, jest } from '@jest/globals'; diff --git a/packages/vertexai/__tests__/count-tokens.test.ts b/packages/vertexai/__tests__/count-tokens.test.ts index 3cd7b78970..75ba7818e3 100644 --- a/packages/vertexai/__tests__/count-tokens.test.ts +++ b/packages/vertexai/__tests__/count-tokens.test.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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 { describe, expect, it, afterEach, jest } from '@jest/globals'; import { getMockResponse } from './test-utils/mock-response'; diff --git a/packages/vertexai/__tests__/test-utils/base64cat.ts b/packages/vertexai/__tests__/test-utils/base64cat.ts index 45325a1bf5..dae911726b 100644 --- a/packages/vertexai/__tests__/test-utils/base64cat.ts +++ b/packages/vertexai/__tests__/test-utils/base64cat.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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. + * */ export const base64Cat = diff --git a/packages/vertexai/__tests__/test-utils/convert-mocks.ts b/packages/vertexai/__tests__/test-utils/convert-mocks.ts index bbca9aa4cf..25b0ab0588 100644 --- a/packages/vertexai/__tests__/test-utils/convert-mocks.ts +++ b/packages/vertexai/__tests__/test-utils/convert-mocks.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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. + * */ /** diff --git a/packages/vertexai/__tests__/test-utils/mock-response.ts b/packages/vertexai/__tests__/test-utils/mock-response.ts index bc95f3e359..88b5642054 100644 --- a/packages/vertexai/__tests__/test-utils/mock-response.ts +++ b/packages/vertexai/__tests__/test-utils/mock-response.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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 { mocksLookup } from './mocks-lookup'; diff --git a/packages/vertexai/lib/constants.ts b/packages/vertexai/lib/constants.ts index a9af794707..50acc705a8 100644 --- a/packages/vertexai/lib/constants.ts +++ b/packages/vertexai/lib/constants.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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 { version } from './version'; diff --git a/packages/vertexai/lib/errors.ts b/packages/vertexai/lib/errors.ts index 0603b0350d..3a370159f5 100644 --- a/packages/vertexai/lib/errors.ts +++ b/packages/vertexai/lib/errors.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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 { FirebaseError } from '@firebase/util'; diff --git a/packages/vertexai/lib/methods/chat-session-helpers.ts b/packages/vertexai/lib/methods/chat-session-helpers.ts index 4b9bb56db0..91fb290f70 100644 --- a/packages/vertexai/lib/methods/chat-session-helpers.ts +++ b/packages/vertexai/lib/methods/chat-session-helpers.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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 { Content, POSSIBLE_ROLES, Part, Role, VertexAIErrorCode } from '../types'; diff --git a/packages/vertexai/lib/methods/chat-session.ts b/packages/vertexai/lib/methods/chat-session.ts index 75b15948b9..47a14ff72f 100644 --- a/packages/vertexai/lib/methods/chat-session.ts +++ b/packages/vertexai/lib/methods/chat-session.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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 { diff --git a/packages/vertexai/lib/methods/count-tokens.ts b/packages/vertexai/lib/methods/count-tokens.ts index 10d41cffa8..f33021035a 100644 --- a/packages/vertexai/lib/methods/count-tokens.ts +++ b/packages/vertexai/lib/methods/count-tokens.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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 { CountTokensRequest, CountTokensResponse, RequestOptions } from '../types'; diff --git a/packages/vertexai/lib/methods/generate-content.ts b/packages/vertexai/lib/methods/generate-content.ts index 6d1a6ecb27..9d627832c8 100644 --- a/packages/vertexai/lib/methods/generate-content.ts +++ b/packages/vertexai/lib/methods/generate-content.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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 { diff --git a/packages/vertexai/lib/models/generative-model.ts b/packages/vertexai/lib/models/generative-model.ts index 111cefa427..ac59e4bae6 100644 --- a/packages/vertexai/lib/models/generative-model.ts +++ b/packages/vertexai/lib/models/generative-model.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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 { generateContent, generateContentStream } from '../methods/generate-content'; diff --git a/packages/vertexai/lib/polyfills.ts b/packages/vertexai/lib/polyfills.ts index db76c9911a..01fae01f7c 100644 --- a/packages/vertexai/lib/polyfills.ts +++ b/packages/vertexai/lib/polyfills.ts @@ -1,3 +1,20 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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. + * + */ + // @ts-ignore import { polyfill } from 'react-native-polyfill-globals/src/fetch'; polyfill(); diff --git a/packages/vertexai/lib/public-types.ts b/packages/vertexai/lib/public-types.ts index 280fee9d1c..0cf689d2ef 100644 --- a/packages/vertexai/lib/public-types.ts +++ b/packages/vertexai/lib/public-types.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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 { FirebaseApp } from '@firebase/app'; diff --git a/packages/vertexai/lib/requests/request-helpers.ts b/packages/vertexai/lib/requests/request-helpers.ts index 77809f1edd..5aa7799b83 100644 --- a/packages/vertexai/lib/requests/request-helpers.ts +++ b/packages/vertexai/lib/requests/request-helpers.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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 { Content, GenerateContentRequest, Part, VertexAIErrorCode } from '../types'; diff --git a/packages/vertexai/lib/requests/request.ts b/packages/vertexai/lib/requests/request.ts index 82669a5b0f..cd17c8222d 100644 --- a/packages/vertexai/lib/requests/request.ts +++ b/packages/vertexai/lib/requests/request.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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 { ErrorDetails, RequestOptions, VertexAIErrorCode } from '../types'; diff --git a/packages/vertexai/lib/requests/response-helpers.ts b/packages/vertexai/lib/requests/response-helpers.ts index c7abc9d923..0ca212e98b 100644 --- a/packages/vertexai/lib/requests/response-helpers.ts +++ b/packages/vertexai/lib/requests/response-helpers.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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 { diff --git a/packages/vertexai/lib/requests/schema-builder.ts b/packages/vertexai/lib/requests/schema-builder.ts index c7ce1aff66..f35e12858c 100644 --- a/packages/vertexai/lib/requests/schema-builder.ts +++ b/packages/vertexai/lib/requests/schema-builder.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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'; diff --git a/packages/vertexai/lib/requests/stream-reader.ts b/packages/vertexai/lib/requests/stream-reader.ts index 2662cb473b..bb06311d76 100644 --- a/packages/vertexai/lib/requests/stream-reader.ts +++ b/packages/vertexai/lib/requests/stream-reader.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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 { ReadableStream } from 'web-streams-polyfill'; diff --git a/packages/vertexai/lib/service.ts b/packages/vertexai/lib/service.ts index df4295c0b6..4b8b418883 100644 --- a/packages/vertexai/lib/service.ts +++ b/packages/vertexai/lib/service.ts @@ -1,9 +1,8 @@ -/** - * @license - * Copyright 2024 Google LLC +/* + * Copyright (c) 2016-present Invertase Limited & Contributors * * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * you may not use this library except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 @@ -13,6 +12,7 @@ * 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 { FirebaseApp } from '@firebase/app'; From 59c6aaa290725488a98106e67a5b849fcdd058e1 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 20 Jan 2025 14:42:16 +0000 Subject: [PATCH 052/115] chore: make note on API versions --- packages/vertexai/lib/constants.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/vertexai/lib/constants.ts b/packages/vertexai/lib/constants.ts index 50acc705a8..f333ada893 100644 --- a/packages/vertexai/lib/constants.ts +++ b/packages/vertexai/lib/constants.ts @@ -23,6 +23,8 @@ export const DEFAULT_LOCATION = 'us-central1'; export const DEFAULT_BASE_URL = 'https://firebasevertexai.googleapis.com'; +// This is the default API version for the VertexAI API. At some point, should be able to change when the feature becomes available. +// `v1beta` & `stable` available: https://cloud.google.com/vertex-ai/docs/reference#versions export const DEFAULT_API_VERSION = 'v1beta'; export const PACKAGE_VERSION = version; From 6137aba9b3ad2b402d864060f2aa83236fbc15f5 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 20 Jan 2025 16:57:41 +0000 Subject: [PATCH 053/115] fix: pin `web-streams-polyfill` dependency --- packages/vertexai/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vertexai/package.json b/packages/vertexai/package.json index f8c5260bf5..c1f69b1f7b 100644 --- a/packages/vertexai/package.json +++ b/packages/vertexai/package.json @@ -87,6 +87,6 @@ "react-native-fetch-api": "^3.0.0", "react-native-polyfill-globals": "^3.1.0", "text-encoding": "^0.7.0", - "web-streams-polyfill": "^4.1.0" + "web-streams-polyfill": "^3.3.2" } } From 18023996fd18fc6304950e09ce00be43f4fc462b Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 20 Jan 2025 17:04:45 +0000 Subject: [PATCH 054/115] chore: remove version.ts from git --- .gitignore | 1 + packages/vertexai/lib/version.ts | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 packages/vertexai/lib/version.ts diff --git a/.gitignore b/.gitignore index 755403b091..c24cbc1f88 100644 --- a/.gitignore +++ b/.gitignore @@ -535,6 +535,7 @@ Pods/* **/Pods/** **/dist/ packages/**/version.js +packages/**/version.ts typedoc.raw.json tests/ios/Firebase tests/ios/resetXcode.sh diff --git a/packages/vertexai/lib/version.ts b/packages/vertexai/lib/version.ts deleted file mode 100644 index 997ce3b865..0000000000 --- a/packages/vertexai/lib/version.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Generated by genversion. -export const version = '0.0.1'; From ffa11411712370774f5c1538f94847a1d5b76976 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 20 Jan 2025 17:10:09 +0000 Subject: [PATCH 055/115] fix: mock-response types --- packages/vertexai/__tests__/test-utils/mock-response.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/vertexai/__tests__/test-utils/mock-response.ts b/packages/vertexai/__tests__/test-utils/mock-response.ts index 88b5642054..190a22afb9 100644 --- a/packages/vertexai/__tests__/test-utils/mock-response.ts +++ b/packages/vertexai/__tests__/test-utils/mock-response.ts @@ -14,7 +14,7 @@ * limitations under the License. * */ - +import { ReadableStream } from 'web-streams-polyfill'; import { mocksLookup } from './mocks-lookup'; /** @@ -46,7 +46,7 @@ export function getMockResponseStreaming( const fullText = mocksLookup[filename]; return { - body: getChunkedStream(fullText, chunkLength), + body: getChunkedStream(fullText!, chunkLength), }; } @@ -54,6 +54,6 @@ export function getMockResponse(filename: string): Partial { const fullText = mocksLookup[filename]; return { ok: true, - json: () => Promise.resolve(JSON.parse(fullText)), + json: () => Promise.resolve(JSON.parse(fullText!)), }; } From b95c2abd7faa27b840d7d28942ecb376c1e514d0 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 20 Jan 2025 18:22:21 +0000 Subject: [PATCH 056/115] chore: update genversion script --- packages/vertexai/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vertexai/package.json b/packages/vertexai/package.json index c1f69b1f7b..e384dcfc2a 100644 --- a/packages/vertexai/package.json +++ b/packages/vertexai/package.json @@ -6,7 +6,7 @@ "main": "./dist/module/index.js", "types": "./dist/typescript/module/lib/index.d.ts", "scripts": { - "build": "genversion --semi lib/version.js && genversion --esm --semi lib/version.ts", + "build": "genversion --esm --semi lib/version.ts", "build:clean": "rimraf android/build && rimraf ios/build", "prepare": "yarn run build && bob build" }, From 185ac48d63a7b95a20c3473f88c0bfba0d54c734 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 21 Jan 2025 15:04:43 +0000 Subject: [PATCH 057/115] chore: move logger to app --- packages/app/lib/index.d.ts | 4 + packages/app/lib/internal/index.js | 1 + packages/app/lib/internal/logger.d.ts | 85 ++++++++++ packages/app/lib/internal/logger.js | 211 ++++++++++++++++++++++++ packages/vertexai/lib/logger.ts | 229 +------------------------- 5 files changed, 302 insertions(+), 228 deletions(-) create mode 100644 packages/app/lib/internal/logger.d.ts create mode 100644 packages/app/lib/internal/logger.js diff --git a/packages/app/lib/index.d.ts b/packages/app/lib/index.d.ts index 87a512c347..152c6853ce 100644 --- a/packages/app/lib/index.d.ts +++ b/packages/app/lib/index.d.ts @@ -611,6 +611,10 @@ export namespace Utils { */ export const utils: ReactNativeFirebase.FirebaseModuleWithStatics; +declare module '@react-native-firebase/app/lib/internal'; +export * from './internal/logger'; +declare const Logger: Logger; + export * from './modular'; declare const module: ReactNativeFirebase.Module; diff --git a/packages/app/lib/internal/index.js b/packages/app/lib/internal/index.js index be91f457f6..e125f82d29 100644 --- a/packages/app/lib/internal/index.js +++ b/packages/app/lib/internal/index.js @@ -23,3 +23,4 @@ export * from './registry/app'; export * from './registry/namespace'; export * from './registry/nativeModule'; export { default as SharedEventEmitter } from './SharedEventEmitter'; +export * from './logger'; diff --git a/packages/app/lib/internal/logger.d.ts b/packages/app/lib/internal/logger.d.ts new file mode 100644 index 0000000000..e48f506274 --- /dev/null +++ b/packages/app/lib/internal/logger.d.ts @@ -0,0 +1,85 @@ +export type LogLevelString = 'debug' | 'verbose' | 'info' | 'warn' | 'error' | 'silent'; + +export interface LogOptions { + level: LogLevelString; +} + +export type LogCallback = (callbackParams: LogCallbackParams) => void; + +export interface LogCallbackParams { + level: LogLevelString; + message: string; + args: unknown[]; + type: string; +} + +/** + * A container for all of the Logger instances + */ +export const instances: Logger[] = []; + +/** + * The JS SDK supports 5 log levels and also allows a user the ability to + * silence the logs altogether. + * + * The order is a follows: + * DEBUG < VERBOSE < INFO < WARN < ERROR + * + * All of the log types above the current log level will be captured (i.e. if + * you set the log level to `INFO`, errors will still be logged, but `DEBUG` and + * `VERBOSE` logs will not) + */ +export enum LogLevel { + DEBUG, + VERBOSE, + INFO, + WARN, + ERROR, + SILENT, +} + +type LevelStringToEnum = { + debug: LogLevel.DEBUG; + verbose: LogLevel.VERBOSE; + info: LogLevel.INFO; + warn: LogLevel.WARN; + error: LogLevel.ERROR; + silent: LogLevel.SILENT; +}; + +/** + * The default log level + */ +type DefaultLogLevel = LogLevel.INFO; + +/** + * We allow users the ability to pass their own log handler. We will pass the + * type of log, the current log level, and any other arguments passed (i.e. the + * messages that the user wants to log) to this function. + */ +export type LogHandler = (loggerInstance: Logger, logType: LogLevel, ...args: unknown[]) => void; + +export class Logger { + constructor(name: string); + + get logLevel(): LogLevel; + set logLevel(val: LogLevel); + + setLogLevel(val: LogLevel | LogLevelString): void; + + get logHandler(): LogHandler; + set logHandler(val: LogHandler); + + get userLogHandler(): LogHandler | null; + set userLogHandler(val: LogHandler | null); + + debug(...args: unknown[]): void; + log(...args: unknown[]): void; + info(...args: unknown[]): void; + warn(...args: unknown[]): void; + error(...args: unknown[]): void; +} + +export const setLogLevel: (level: LogLevelString | LogLevel) => void; + +export const setUserLogHandler: (logCallback: LogCallback | null, options?: LogOptions) => void; diff --git a/packages/app/lib/internal/logger.js b/packages/app/lib/internal/logger.js new file mode 100644 index 0000000000..23af27ea0f --- /dev/null +++ b/packages/app/lib/internal/logger.js @@ -0,0 +1,211 @@ +/* + * Copyright (c) 2016-present Invertase Limited & Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this library 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. + * + */ + +/** + * @typedef {import('./logger').Logger} Logger + * @typedef {import('./logger').setLogLevel} setLogLevel + * @typedef {import('./logger').setUserLogHandler} setUserLogHandler + * @typedef {import('./logger').LogHandler} LogHandler + * @typedef {import('./logger').LogLevel} LogLevel + * @typedef {import('./logger').LevelStringToEnum} LevelStringToEnum + * @typedef {import('./logger').DefaultLogLevel} DefaultLogLevel + */ + +/** + * By default, `console.debug` is not displayed in the developer console (in + * chrome). To avoid forcing users to have to opt-in to these logs twice + * (i.e. once for firebase, and once in the console), we are sending `DEBUG` + * logs to the `console.log` function. + */ +const ConsoleMethod = { + [LogLevel.DEBUG]: 'log', + [LogLevel.VERBOSE]: 'log', + [LogLevel.INFO]: 'info', + [LogLevel.WARN]: 'warn', + [LogLevel.ERROR]: 'error', +}; + +/** + * The default log handler will forward DEBUG, VERBOSE, INFO, WARN, and ERROR + * messages on to their corresponding console counterparts (if the log method + * is supported by the current log level) + * @type {LogHandler} + */ +const defaultLogHandler = (instance, logType, ...args) => { + if (logType < instance.logLevel) { + return; + } + const now = new Date().toISOString(); + const method = ConsoleMethod[logType]; + if (method) { + // 'log' | 'info' | 'warn' | 'error' + console[method](`[${now}] ${instance.name}:`, ...args); + } else { + throw new Error(`Attempted to log a message with an invalid logType (value: ${logType})`); + } +}; + +const defaultLogLevel = LogLevel.INFO; + +/** + * @type {Logger} + */ + +export class Logger { + /** + * Gives you an instance of a Logger to capture messages according to + * Firebase's logging scheme. + * + * @param name The name that the logs will be associated with + */ + constructor(name) { + /** + * Capture the current instance for later use + */ + this.name = name; + instances.push(this); + } + + /** + * The log level of the given Logger instance. + */ + _logLevel = defaultLogLevel; + + get logLevel() { + return this._logLevel; + } + + set logLevel(val) { + if (!(val in LogLevel)) { + throw new TypeError(`Invalid value "${val}" assigned to \`logLevel\``); + } + this._logLevel = val; + } + + // Workaround for setter/getter having to be the same type. + setLogLevel(val) { + this._logLevel = typeof val === 'string' ? levelStringToEnum[val] : val; + } + + /** + * The main (internal) log handler for the Logger instance. + * Can be set to a new function in internal package code but not by user. + */ + _logHandler = defaultLogHandler; + get logHandler() { + return this._logHandler; + } + set logHandler(val) { + if (typeof val !== 'function') { + throw new TypeError('Value assigned to `logHandler` must be a function'); + } + this._logHandler = val; + } + + /** + * The optional, additional, user-defined log handler for the Logger instance. + */ + _userLogHandler = null; + get userLogHandler() { + return this._userLogHandler; + } + set userLogHandler(val) { + this._userLogHandler = val; + } + + /** + * The functions below are all based on the `console` interface + */ + + debug(...args) { + this._userLogHandler && this._userLogHandler(this, LogLevel.DEBUG, ...args); + this._logHandler(this, LogLevel.DEBUG, ...args); + } + log(...args) { + this._userLogHandler && this._userLogHandler(this, LogLevel.VERBOSE, ...args); + this._logHandler(this, LogLevel.VERBOSE, ...args); + } + info(...args) { + this._userLogHandler && this._userLogHandler(this, LogLevel.INFO, ...args); + this._logHandler(this, LogLevel.INFO, ...args); + } + warn(...args) { + this._userLogHandler && this._userLogHandler(this, LogLevel.WARN, ...args); + this._logHandler(this, LogLevel.WARN, ...args); + } + error(...args) { + this._userLogHandler && this._userLogHandler(this, LogLevel.ERROR, ...args); + this._logHandler(this, LogLevel.ERROR, ...args); + } +} + +/** + * @type {setLogLevel} + */ +export function setLogLevel(level) { + instances.forEach(inst => { + inst.setLogLevel(level); + }); +} + +/** + * @type {setUserLogHandler} + */ +export function setUserLogHandler(logCallback, options) { + for (const instance of instances) { + let customLogLevel = null; + if (options && options.level) { + customLogLevel = levelStringToEnum[options.level]; + } + if (logCallback === null) { + instance.userLogHandler = null; + } else { + instance.userLogHandler = (instance, level, ...args) => { + const message = args + .map(arg => { + if (arg == null) { + return null; + } else if (typeof arg === 'string') { + return arg; + } else if (typeof arg === 'number' || typeof arg === 'boolean') { + return arg.toString(); + } else if (arg instanceof Error) { + return arg.message; + } else { + try { + return JSON.stringify(arg); + } catch (ignored) { + return null; + } + } + }) + .filter(arg => arg) + .join(' '); + if (level >= (customLogLevel ?? instance.logLevel)) { + logCallback({ + level: LogLevel[level].toLowerCase(), + message, + args, + type: instance.name, + }); + } + }; + } + } +} + +export const Logger = Logger; diff --git a/packages/vertexai/lib/logger.ts b/packages/vertexai/lib/logger.ts index 681e9f8ac0..57f02cf391 100644 --- a/packages/vertexai/lib/logger.ts +++ b/packages/vertexai/lib/logger.ts @@ -15,233 +15,6 @@ * */ -export type LogLevelString = 'debug' | 'verbose' | 'info' | 'warn' | 'error' | 'silent'; - -export interface LogOptions { - level: LogLevelString; -} - -export type LogCallback = (callbackParams: LogCallbackParams) => void; - -export interface LogCallbackParams { - level: LogLevelString; - message: string; - args: unknown[]; - type: string; -} - -/** - * A container for all of the Logger instances - */ -export const instances: Logger[] = []; - -/** - * The JS SDK supports 5 log levels and also allows a user the ability to - * silence the logs altogether. - * - * The order is a follows: - * DEBUG < VERBOSE < INFO < WARN < ERROR - * - * All of the log types above the current log level will be captured (i.e. if - * you set the log level to `INFO`, errors will still be logged, but `DEBUG` and - * `VERBOSE` logs will not) - */ -export enum LogLevel { - DEBUG, - VERBOSE, - INFO, - WARN, - ERROR, - SILENT, -} - -const levelStringToEnum: { [key in LogLevelString]: LogLevel } = { - debug: LogLevel.DEBUG, - verbose: LogLevel.VERBOSE, - info: LogLevel.INFO, - warn: LogLevel.WARN, - error: LogLevel.ERROR, - silent: LogLevel.SILENT, -}; - -/** - * The default log level - */ -const defaultLogLevel: LogLevel = LogLevel.INFO; - -/** - * We allow users the ability to pass their own log handler. We will pass the - * type of log, the current log level, and any other arguments passed (i.e. the - * messages that the user wants to log) to this function. - */ -export type LogHandler = (loggerInstance: Logger, logType: LogLevel, ...args: unknown[]) => void; - -/** - * By default, `console.debug` is not displayed in the developer console (in - * chrome). To avoid forcing users to have to opt-in to these logs twice - * (i.e. once for firebase, and once in the console), we are sending `DEBUG` - * logs to the `console.log` function. - */ -const ConsoleMethod = { - [LogLevel.DEBUG]: 'log', - [LogLevel.VERBOSE]: 'log', - [LogLevel.INFO]: 'info', - [LogLevel.WARN]: 'warn', - [LogLevel.ERROR]: 'error', -}; - -/** - * The default log handler will forward DEBUG, VERBOSE, INFO, WARN, and ERROR - * messages on to their corresponding console counterparts (if the log method - * is supported by the current log level) - */ -const defaultLogHandler: LogHandler = (instance, logType, ...args): void => { - if (logType < instance.logLevel) { - return; - } - const now = new Date().toISOString(); - const method = ConsoleMethod[logType as keyof typeof ConsoleMethod]; - if (method) { - console[method as 'log' | 'info' | 'warn' | 'error'](`[${now}] ${instance.name}:`, ...args); - } else { - throw new Error(`Attempted to log a message with an invalid logType (value: ${logType})`); - } -}; - -export class Logger { - /** - * Gives you an instance of a Logger to capture messages according to - * Firebase's logging scheme. - * - * @param name The name that the logs will be associated with - */ - constructor(public name: string) { - /** - * Capture the current instance for later use - */ - instances.push(this); - } - - /** - * The log level of the given Logger instance. - */ - private _logLevel = defaultLogLevel; - - get logLevel(): LogLevel { - return this._logLevel; - } - - set logLevel(val: LogLevel) { - if (!(val in LogLevel)) { - throw new TypeError(`Invalid value "${val}" assigned to \`logLevel\``); - } - this._logLevel = val; - } - - // Workaround for setter/getter having to be the same type. - setLogLevel(val: LogLevel | LogLevelString): void { - this._logLevel = typeof val === 'string' ? levelStringToEnum[val] : val; - } - - /** - * The main (internal) log handler for the Logger instance. - * Can be set to a new function in internal package code but not by user. - */ - private _logHandler: LogHandler = defaultLogHandler; - get logHandler(): LogHandler { - return this._logHandler; - } - set logHandler(val: LogHandler) { - if (typeof val !== 'function') { - throw new TypeError('Value assigned to `logHandler` must be a function'); - } - this._logHandler = val; - } - - /** - * The optional, additional, user-defined log handler for the Logger instance. - */ - private _userLogHandler: LogHandler | null = null; - get userLogHandler(): LogHandler | null { - return this._userLogHandler; - } - set userLogHandler(val: LogHandler | null) { - this._userLogHandler = val; - } - - /** - * The functions below are all based on the `console` interface - */ - - debug(...args: unknown[]): void { - this._userLogHandler && this._userLogHandler(this, LogLevel.DEBUG, ...args); - this._logHandler(this, LogLevel.DEBUG, ...args); - } - log(...args: unknown[]): void { - this._userLogHandler && this._userLogHandler(this, LogLevel.VERBOSE, ...args); - this._logHandler(this, LogLevel.VERBOSE, ...args); - } - info(...args: unknown[]): void { - this._userLogHandler && this._userLogHandler(this, LogLevel.INFO, ...args); - this._logHandler(this, LogLevel.INFO, ...args); - } - warn(...args: unknown[]): void { - this._userLogHandler && this._userLogHandler(this, LogLevel.WARN, ...args); - this._logHandler(this, LogLevel.WARN, ...args); - } - error(...args: unknown[]): void { - this._userLogHandler && this._userLogHandler(this, LogLevel.ERROR, ...args); - this._logHandler(this, LogLevel.ERROR, ...args); - } -} - -export function setLogLevel(level: LogLevelString | LogLevel): void { - instances.forEach(inst => { - inst.setLogLevel(level); - }); -} - -export function setUserLogHandler(logCallback: LogCallback | null, options?: LogOptions): void { - for (const instance of instances) { - let customLogLevel: LogLevel | null = null; - if (options && options.level) { - customLogLevel = levelStringToEnum[options.level]; - } - if (logCallback === null) { - instance.userLogHandler = null; - } else { - instance.userLogHandler = (instance: Logger, level: LogLevel, ...args: unknown[]) => { - const message = args - .map(arg => { - if (arg == null) { - return null; - } else if (typeof arg === 'string') { - return arg; - } else if (typeof arg === 'number' || typeof arg === 'boolean') { - return arg.toString(); - } else if (arg instanceof Error) { - return arg.message; - } else { - try { - return JSON.stringify(arg); - } catch (ignored) { - return null; - } - } - }) - .filter(arg => arg) - .join(' '); - if (level >= (customLogLevel ?? instance.logLevel)) { - logCallback({ - level: LogLevel[level].toLowerCase() as LogLevelString, - message, - args, - type: instance.name, - }); - } - }; - } - } -} +import { Logger } from '@react-native-firebase/app'; export const logger = new Logger('@firebase/vertexai'); From f699ea9bdb4ca7522f92986642167c4674ba56e0 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 21 Jan 2025 15:25:59 +0000 Subject: [PATCH 058/115] fix: wire up `onLog()` & `setLogLevel()` for VertexAI --- packages/app/lib/internal/registry/app.js | 3 +++ packages/app/lib/modular/index.js | 13 ++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/app/lib/internal/registry/app.js b/packages/app/lib/internal/registry/app.js index b5da4fbbbd..a6a8332a41 100644 --- a/packages/app/lib/internal/registry/app.js +++ b/packages/app/lib/internal/registry/app.js @@ -28,6 +28,7 @@ import FirebaseApp from '../../FirebaseApp'; import { DEFAULT_APP_NAME } from '../constants'; import { setReactNativeAsyncStorageInternal } from '../asyncStorage'; import { getAppModule } from './nativeModule'; +import { setLogLevel as setLogLevelInternal } from '../logger'; const APP_REGISTRY = {}; let onAppCreateFn = null; @@ -203,6 +204,8 @@ export function setLogLevel(logLevel) { if (!['error', 'warn', 'info', 'debug', 'verbose'].includes(logLevel)) { throw new Error('LogLevel must be one of "error", "warn", "info", "debug", "verbose"'); } + // This is setting LogLevel for VertexAI which does not wrap around native SDK + setLogLevelInternal(logLevel); if (isIOS || isOther) { getAppModule().setLogLevel(logLevel); diff --git a/packages/app/lib/modular/index.js b/packages/app/lib/modular/index.js index dd72166309..d48a538650 100644 --- a/packages/app/lib/modular/index.js +++ b/packages/app/lib/modular/index.js @@ -6,11 +6,14 @@ import { initializeApp as initializeAppCompat, setLogLevel as setLogLevelCompat, } from '../internal'; +import { setUserLogHandler } from '../internal/logger'; /** * @typedef {import('..').ReactNativeFirebase.FirebaseApp} FirebaseApp * @typedef {import('..').ReactNativeFirebase.FirebaseAppOptions} FirebaseAppOptions * @typedef {import('..').ReactNativeFirebase.LogLevelString} LogLevelString + * @typedef {import('../internal/logger').LogCallback} LogCallback + * @typedef {import('../internal/logger').LogOptions} LogOptions */ /** @@ -34,13 +37,13 @@ export function registerVersion(libraryKeyOrName, version, variant) { } /** - * Sets log handler for all Firebase SDKs. - * @param {Function} logCallback - The callback function to handle logs. - * @param {Object} [options] - Optional settings for log handling. - * @returns {Promise} + * Sets log handler for VertexAI only currently. + * @param {LogCallback | null} logCallback - The callback function to handle logs. + * @param {LogOptions} [options] - Optional settings for log handling. + * @returns {void} */ export function onLog(logCallback, options) { - throw new Error('onLog is only supported on Web'); + setUserLogHandler(logCallback, options); } /** From 5bbcd8f12f5f083437a74e9c86b62db302821f3e Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 21 Jan 2025 15:28:17 +0000 Subject: [PATCH 059/115] chore: rm unneeded TS declaration --- packages/app/lib/index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app/lib/index.d.ts b/packages/app/lib/index.d.ts index 152c6853ce..96e765ab90 100644 --- a/packages/app/lib/index.d.ts +++ b/packages/app/lib/index.d.ts @@ -611,7 +611,6 @@ export namespace Utils { */ export const utils: ReactNativeFirebase.FirebaseModuleWithStatics; -declare module '@react-native-firebase/app/lib/internal'; export * from './internal/logger'; declare const Logger: Logger; From c60e043e57395b84c44f81f603872bb21b173790 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 21 Jan 2025 16:01:04 +0000 Subject: [PATCH 060/115] chore: remove FirebaseService class --- packages/vertexai/lib/service.ts | 8 ++------ packages/vertexai/lib/types/internal.ts | 14 -------------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/packages/vertexai/lib/service.ts b/packages/vertexai/lib/service.ts index 4b8b418883..00d93746dc 100644 --- a/packages/vertexai/lib/service.ts +++ b/packages/vertexai/lib/service.ts @@ -18,9 +18,9 @@ import { FirebaseApp } from '@firebase/app'; import { VertexAI, VertexAIOptions } from './public-types'; import { DEFAULT_LOCATION } from './constants'; -import { _FirebaseService, InternalAppCheck, InternalAuth } from './types/internal'; +import { InternalAppCheck, InternalAuth } from './types/internal'; -export class VertexAIService implements VertexAI, _FirebaseService { +export class VertexAIService implements VertexAI { auth: InternalAuth | null; appCheck: InternalAppCheck | null; location: string; @@ -35,8 +35,4 @@ export class VertexAIService implements VertexAI, _FirebaseService { this.appCheck = appCheck || null; this.location = this.options?.location || DEFAULT_LOCATION; } - - _delete(): Promise { - return Promise.resolve(); - } } diff --git a/packages/vertexai/lib/types/internal.ts b/packages/vertexai/lib/types/internal.ts index 68dbc068ac..5b48331d19 100644 --- a/packages/vertexai/lib/types/internal.ts +++ b/packages/vertexai/lib/types/internal.ts @@ -14,7 +14,6 @@ * limitations under the License. * */ -import { FirebaseApp } from '@firebase/app'; export interface ApiSettings { apiKey: string; @@ -24,19 +23,6 @@ export interface ApiSettings { getAppCheckToken?: () => Promise; } -/** - * @internal - */ -// eslint-disable-next-line @typescript-eslint/naming-convention -export interface _FirebaseService { - app: FirebaseApp; - /** - * Delete the service and free it's resources - called from - * {@link @firebase/app#deleteApp | deleteApp()} - */ - _delete(): Promise; -} - export interface InternalAppCheck { /** * Requests Firebase App Check token. From c5d34fdf7357490ed0aa9c6544e893371d0c40b9 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Tue, 21 Jan 2025 16:03:29 +0000 Subject: [PATCH 061/115] chore: update yarn.lock --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 33d33a3356..67a3b2b593 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7588,7 +7588,7 @@ __metadata: languageName: unknown linkType: soft -"@react-native-firebase/vertexai@npm:0.0.1, @react-native-firebase/vertexai@workspace:packages/vertexai": +"@react-native-firebase/vertexai@workspace:packages/vertexai": version: 0.0.0-use.local resolution: "@react-native-firebase/vertexai@workspace:packages/vertexai" dependencies: @@ -7598,7 +7598,7 @@ __metadata: react-native-polyfill-globals: "npm:^3.1.0" text-encoding: "npm:^0.7.0" typescript: "npm:^5.7.2" - web-streams-polyfill: "npm:^4.1.0" + web-streams-polyfill: "npm:^3.3.2" peerDependencies: "@react-native-firebase/app": 21.6.2 languageName: unknown @@ -25817,10 +25817,10 @@ __metadata: languageName: node linkType: hard -"web-streams-polyfill@npm:^4.1.0": - version: 4.1.0 - resolution: "web-streams-polyfill@npm:4.1.0" - checksum: 10/c6e802e6b83a8dc72acee8ec14b3a4af46d5ea4a15d0260303d7e4cab3c600716ff998b70457bc3ac0998073c0e73344c89400bfcc5f49a9bc0b10bb263c2f5f +"web-streams-polyfill@npm:^3.3.2": + version: 3.3.3 + resolution: "web-streams-polyfill@npm:3.3.3" + checksum: 10/8e7e13501b3834094a50abe7c0b6456155a55d7571312b89570012ef47ec2a46d766934768c50aabad10a9c30dd764a407623e8bfcc74fcb58495c29130edea9 languageName: node linkType: hard From 3e07cd80f830edcc59a95698824ab10eb0525509 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 22 Jan 2025 12:52:30 +0000 Subject: [PATCH 062/115] test: make vertex response scripts only checkout repo if not existing --- .../__tests__/test-utils/convert-mocks.ts | 25 +++++++++--- scripts/vertex_mock_responses.sh | 39 +++++++++++++------ 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/packages/vertexai/__tests__/test-utils/convert-mocks.ts b/packages/vertexai/__tests__/test-utils/convert-mocks.ts index 25b0ab0588..8d41e9e0f6 100644 --- a/packages/vertexai/__tests__/test-utils/convert-mocks.ts +++ b/packages/vertexai/__tests__/test-utils/convert-mocks.ts @@ -15,19 +15,32 @@ * */ -/** - * Converts mock text files into a js file that karma can read without - * using fs. - */ - // eslint-disable-next-line @typescript-eslint/no-require-imports const fs = require('fs'); // eslint-disable-next-line @typescript-eslint/no-require-imports const { join } = require('path'); -const mockResponseDir = join(__dirname, 'vertexai-sdk-test-data/mock-responses'); +function findMockResponseDir(): string { + const directories = fs + .readdirSync(__dirname, { withFileTypes: true }) + .filter( + (dirent: any) => dirent.isDirectory() && dirent.name.startsWith('vertexai-sdk-test-data'), + ) + .map((dirent: any) => dirent.name); + + if (directories.length === 0) { + throw new Error('No directory starting with "vertexai-sdk-test-data*" found.'); + } + + if (directories.length > 1) { + throw new Error('Multiple directories starting with "vertexai-sdk-test-data*" found'); + } + + return join(__dirname, directories[0], 'mock-responses'); +} async function main(): Promise { + const mockResponseDir = findMockResponseDir(); const list = fs.readdirSync(mockResponseDir); const lookup: Record = {}; // eslint-disable-next-line guard-for-in diff --git a/scripts/vertex_mock_responses.sh b/scripts/vertex_mock_responses.sh index 92c516befe..ab265dc6e9 100755 --- a/scripts/vertex_mock_responses.sh +++ b/scripts/vertex_mock_responses.sh @@ -17,19 +17,36 @@ # This script replaces mock response files for Vertex AI unit tests with a fresh # clone of the shared repository of Vertex AI test data. -RESPONSES_VERSION='v5.*' # The major version of mock responses to use REPO_NAME="vertexai-sdk-test-data" REPO_LINK="https://github.com/FirebaseExtended/$REPO_NAME.git" +# Get the latest tag from the remote repository +LATEST_TAG=$(git ls-remote --tags "$REPO_LINK" | \ +awk -F'/' '{print $3}' | \ +sort -V | \ +tail -n1) + + +# Define the directory name using REPO_NAME and LATEST_TAG. +CLONE_DIR="${REPO_NAME}_${LATEST_TAG//./_}" cd "$(dirname "$0")/../packages/vertexai/__tests__/test-utils" || exit -rm -rf "$REPO_NAME" -git clone "$REPO_LINK" --quiet || exit -cd "$REPO_NAME" || exit - -# Find and checkout latest tag matching major version -TAG=$(git tag -l "$RESPONSES_VERSION" --sort=v:refname | tail -n1) -if [ -z "$TAG" ]; then - echo "Error: No tag matching '$RESPONSES_VERSION' found in $REPO_NAME" - exit + +# Remove any directories that start with REPO_NAME but are not CLONE_DIR +for dir in "${REPO_NAME}"*; do + if [ "$dir" != "$CLONE_DIR" ] && [ -d "$dir" ]; then + echo "Removing old directory: $dir" + rm -rf "$dir" + fi +done + +# Check if CLONE_DIR exists, exit if it does +if [ -d "$CLONE_DIR" ]; then + echo "Exiting vertex_mock_responses script as repo exists locally already." + exit 0 fi -git checkout "$TAG" --quiet + + +git clone "$REPO_LINK" "$CLONE_DIR" --quiet || exit +cd "$CLONE_DIR" || exit + +git checkout "$LATEST_TAG" --quiet From ca51da3f0d0c4cc3f29d71efecc44aef141dd8a1 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 22 Jan 2025 13:07:00 +0000 Subject: [PATCH 063/115] chore: move script call into vertex prepare --- package.json | 6 +++--- packages/vertexai/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index ecb500542b..44f4db9fb3 100644 --- a/package.json +++ b/package.json @@ -19,9 +19,9 @@ "tsc:compile": "tsc --project .", "lint:all": "yarn lint && yarn lint:markdown && yarn lint:spellcheck && yarn tsc:compile", "tests:vertex:mocks": "./scripts/vertex_mock_responses.sh && yarn ts-node ./packages/vertexai/__tests__/test-utils/convert-mocks.ts", - "tests:jest": "yarn tests:vertex:mocks && jest", - "tests:jest-watch": "tests:vertex:mocks && jest --watch", - "tests:jest-coverage": "tests:vertex:mocks && jest --coverage", + "tests:jest": "jest", + "tests:jest-watch": "jest --watch", + "tests:jest-coverage": "jest --coverage", "tests:packager:chrome": "cd tests && yarn react-native start --reset-cache", "tests:packager:jet": "cd tests && cross-env REACT_DEBUGGER=\"echo nope\" yarn react-native start", "tests:packager:jet-ci": "cd tests && (mkdir $HOME/.metro || true) && cross-env TMPDIR=$HOME/.metro REACT_DEBUGGER=\"echo nope\" yarn react-native start", diff --git a/packages/vertexai/package.json b/packages/vertexai/package.json index e384dcfc2a..cb7aefda87 100644 --- a/packages/vertexai/package.json +++ b/packages/vertexai/package.json @@ -8,7 +8,7 @@ "scripts": { "build": "genversion --esm --semi lib/version.ts", "build:clean": "rimraf android/build && rimraf ios/build", - "prepare": "yarn run build && bob build" + "prepare": "yarn run build && bob build && yarn tests:vertex:mocks" }, "repository": { "type": "git", From 673de9c2b508b390810df628c6d3aacd04a942b3 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 22 Jan 2025 13:09:18 +0000 Subject: [PATCH 064/115] chore: move to start --- packages/vertexai/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vertexai/package.json b/packages/vertexai/package.json index cb7aefda87..49bc80eeed 100644 --- a/packages/vertexai/package.json +++ b/packages/vertexai/package.json @@ -8,7 +8,7 @@ "scripts": { "build": "genversion --esm --semi lib/version.ts", "build:clean": "rimraf android/build && rimraf ios/build", - "prepare": "yarn run build && bob build && yarn tests:vertex:mocks" + "prepare": "yarn tests:vertex:mocks && yarn run build && bob build" }, "repository": { "type": "git", From 6ce04ad57e03cd8268add71e73ec6c4cb4ea849d Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 22 Jan 2025 14:54:13 +0000 Subject: [PATCH 065/115] fix: Logger internals, make it private --- packages/app/lib/index.d.ts | 2 -- packages/app/lib/internal/index.js | 2 +- packages/app/lib/internal/logger.js | 15 ++++++++++++--- packages/app/lib/internal/registry/app.js | 2 +- packages/vertexai/lib/logger.ts | 2 +- 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/packages/app/lib/index.d.ts b/packages/app/lib/index.d.ts index 96e765ab90..03105bbc71 100644 --- a/packages/app/lib/index.d.ts +++ b/packages/app/lib/index.d.ts @@ -611,8 +611,6 @@ export namespace Utils { */ export const utils: ReactNativeFirebase.FirebaseModuleWithStatics; -export * from './internal/logger'; -declare const Logger: Logger; export * from './modular'; diff --git a/packages/app/lib/internal/index.js b/packages/app/lib/internal/index.js index e125f82d29..120bea13b6 100644 --- a/packages/app/lib/internal/index.js +++ b/packages/app/lib/internal/index.js @@ -23,4 +23,4 @@ export * from './registry/app'; export * from './registry/namespace'; export * from './registry/nativeModule'; export { default as SharedEventEmitter } from './SharedEventEmitter'; -export * from './logger'; +export { Logger } from './logger'; diff --git a/packages/app/lib/internal/logger.js b/packages/app/lib/internal/logger.js index 23af27ea0f..d2e7bc392e 100644 --- a/packages/app/lib/internal/logger.js +++ b/packages/app/lib/internal/logger.js @@ -25,6 +25,15 @@ * @typedef {import('./logger').DefaultLogLevel} DefaultLogLevel */ +const LogLevel = { + DEBUG: 0, + VERBOSE: 1, + INFO: 2, + WARN: 3, + ERROR: 4, + SILENT: 5, +}; + /** * By default, `console.debug` is not displayed in the developer console (in * chrome). To avoid forcing users to have to opt-in to these logs twice @@ -61,6 +70,8 @@ const defaultLogHandler = (instance, logType, ...args) => { const defaultLogLevel = LogLevel.INFO; +export const instances = []; + /** * @type {Logger} */ @@ -156,7 +167,7 @@ export class Logger { /** * @type {setLogLevel} */ -export function setLogLevel(level) { +export function setLogLevelInternal(level) { instances.forEach(inst => { inst.setLogLevel(level); }); @@ -207,5 +218,3 @@ export function setUserLogHandler(logCallback, options) { } } } - -export const Logger = Logger; diff --git a/packages/app/lib/internal/registry/app.js b/packages/app/lib/internal/registry/app.js index a6a8332a41..592a5ef388 100644 --- a/packages/app/lib/internal/registry/app.js +++ b/packages/app/lib/internal/registry/app.js @@ -28,7 +28,7 @@ import FirebaseApp from '../../FirebaseApp'; import { DEFAULT_APP_NAME } from '../constants'; import { setReactNativeAsyncStorageInternal } from '../asyncStorage'; import { getAppModule } from './nativeModule'; -import { setLogLevel as setLogLevelInternal } from '../logger'; +import { setLogLevelInternal } from '../logger'; const APP_REGISTRY = {}; let onAppCreateFn = null; diff --git a/packages/vertexai/lib/logger.ts b/packages/vertexai/lib/logger.ts index 57f02cf391..68cc8ed104 100644 --- a/packages/vertexai/lib/logger.ts +++ b/packages/vertexai/lib/logger.ts @@ -15,6 +15,6 @@ * */ -import { Logger } from '@react-native-firebase/app'; +import { Logger } from '@react-native-firebase/app/lib/internal/logger'; export const logger = new Logger('@firebase/vertexai'); From cf9bfa3286a7237accb2a179ab532b42614e95eb Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 22 Jan 2025 15:09:32 +0000 Subject: [PATCH 066/115] fix: try ignoring TS build issue --- packages/vertexai/lib/logger.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/vertexai/lib/logger.ts b/packages/vertexai/lib/logger.ts index 68cc8ed104..bb517dfa49 100644 --- a/packages/vertexai/lib/logger.ts +++ b/packages/vertexai/lib/logger.ts @@ -14,7 +14,7 @@ * limitations under the License. * */ - +// @ts-ignore import { Logger } from '@react-native-firebase/app/lib/internal/logger'; export const logger = new Logger('@firebase/vertexai'); From 4d049d4df3dd71b97e0b333a871499a8f7f9cc59 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 22 Jan 2025 16:15:30 +0000 Subject: [PATCH 067/115] test: remove e2e test for onLog and add unit test --- packages/app/__tests__/app.test.ts | 20 +++++++++++++++++++- packages/app/lib/internal/logger.js | 13 ++++++++++++- packages/app/lib/modular/index.d.ts | 18 ++++++++++++++---- 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/packages/app/__tests__/app.test.ts b/packages/app/__tests__/app.test.ts index 96aa57dc16..fba9faccbd 100644 --- a/packages/app/__tests__/app.test.ts +++ b/packages/app/__tests__/app.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from '@jest/globals'; +import { jest, describe, expect, it } from '@jest/globals'; import { deleteApp, @@ -9,6 +9,7 @@ import { getApp, setLogLevel, } from '../lib'; +import { Logger } from '../lib/internal/logger'; describe('App', function () { describe('modular', function () { @@ -39,5 +40,22 @@ describe('App', function () { it('`setLogLevel` function is properly exposed to end user', function () { expect(setLogLevel).toBeDefined(); }); + + it('`onLog()` is called when using Logger (currently only VertexAI uses `onLog()`)', function () { + const logger = new Logger('@firebase/vertexai'); + const spy2 = jest.fn(); + + onLog(spy2); + logger.info('test'); + + expect(spy2).toHaveBeenCalledWith( + expect.objectContaining({ + args: ['test'], + level: 'info', + message: 'test', + type: '@firebase/vertexai', + }), + ); + }); }); }); diff --git a/packages/app/lib/internal/logger.js b/packages/app/lib/internal/logger.js index d2e7bc392e..8f5cc814a1 100644 --- a/packages/app/lib/internal/logger.js +++ b/packages/app/lib/internal/logger.js @@ -34,6 +34,17 @@ const LogLevel = { SILENT: 5, }; +// mimic the LogLevel in firebase-js-sdk TS +const reverseLogLevel = obj => { + const reversed = {}; + for (const [key, value] of Object.entries(obj)) { + reversed[value] = key; + } + return reversed; +}; + +const LogLevelReversed = reverseLogLevel(LogLevel); + /** * By default, `console.debug` is not displayed in the developer console (in * chrome). To avoid forcing users to have to opt-in to these logs twice @@ -208,7 +219,7 @@ export function setUserLogHandler(logCallback, options) { .join(' '); if (level >= (customLogLevel ?? instance.logLevel)) { logCallback({ - level: LogLevel[level].toLowerCase(), + level: LogLevelReversed[level].toLowerCase(), message, args, type: instance.name, diff --git a/packages/app/lib/modular/index.d.ts b/packages/app/lib/modular/index.d.ts index 4173586c0c..04cb090dd1 100644 --- a/packages/app/lib/modular/index.d.ts +++ b/packages/app/lib/modular/index.d.ts @@ -25,13 +25,23 @@ export function registerVersion( ): Promise; /** - * Sets log handler for all Firebase SDKs. + * Sets log handler for all Firebase SDKs. Currently only supported on VertexAI. * @param logCallback - The callback function to handle logs. * @param options - Optional settings for log handling. - * @returns Promise - * @throws Error - onLog is only supported on Web + * @returns */ -export function onLog(logCallback: (logData: any) => void, options?: any): Promise; + +interface LogCallbackParams { + level: LogLevelString; + message: string; + args: unknown[]; + type: string; +} + +export function onLog( + logCallback: (callbackParams: LogCallbackParams) => void, + options?: any, +): void; /** * Gets the list of all initialized apps. From 45d9bcb42baaa6c672a20986d9482a3140861a1e Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 22 Jan 2025 17:18:35 +0000 Subject: [PATCH 068/115] chore: remove onLog e2e test --- packages/app/e2e/app.e2e.js | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/app/e2e/app.e2e.js b/packages/app/e2e/app.e2e.js index dc891bbf15..80bfe6d097 100644 --- a/packages/app/e2e/app.e2e.js +++ b/packages/app/e2e/app.e2e.js @@ -312,16 +312,5 @@ describe('modular', function () { e.message.should.equal('registerVersion is only supported on Web'); } }); - - it('onLog is not supported on react-native', async function () { - const { onLog } = modular; - - try { - await onLog(() => {}, {}); - throw new Error('Should have rejected incorrect onLog'); - } catch (e) { - e.message.should.equal('onLog is only supported on Web'); - } - }); }); }); From d4891163f5054a9ef4c2f6d7b3ff076f49f0fc29 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 24 Jan 2025 14:50:05 +0000 Subject: [PATCH 069/115] chore: eslint update --- .eslintignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.eslintignore b/.eslintignore index f906059ad2..0b184cad30 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,4 +8,6 @@ docs packages/template/project/** app.playground.js type-test.ts -packages/**/modular/dist/** \ No newline at end of file +packages/**/modular/dist/** +packages/vertexai/__tests__/test-utils +packages/vertexai/dist From a8988940abdfb34a86b82cdd4e62b49d1a611fc4 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Fri, 24 Jan 2025 14:51:22 +0000 Subject: [PATCH 070/115] chore: fix lint issues --- packages/app/lib/index.d.ts | 1 - packages/app/lib/internal/logger.js | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/lib/index.d.ts b/packages/app/lib/index.d.ts index 03105bbc71..87a512c347 100644 --- a/packages/app/lib/index.d.ts +++ b/packages/app/lib/index.d.ts @@ -611,7 +611,6 @@ export namespace Utils { */ export const utils: ReactNativeFirebase.FirebaseModuleWithStatics; - export * from './modular'; declare const module: ReactNativeFirebase.Module; diff --git a/packages/app/lib/internal/logger.js b/packages/app/lib/internal/logger.js index 8f5cc814a1..b15604f9de 100644 --- a/packages/app/lib/internal/logger.js +++ b/packages/app/lib/internal/logger.js @@ -73,6 +73,7 @@ const defaultLogHandler = (instance, logType, ...args) => { const method = ConsoleMethod[logType]; if (method) { // 'log' | 'info' | 'warn' | 'error' + // eslint-disable-next-line no-console console[method](`[${now}] ${instance.name}:`, ...args); } else { throw new Error(`Attempted to log a message with an invalid logType (value: ${logType})`); From bb66d56c5f8b483fde7036e35b0466db4a66a9cc Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 27 Jan 2025 12:26:08 +0000 Subject: [PATCH 071/115] fix: fetch API issue with global polyfill. not needed --- packages/vertexai/lib/polyfills.ts | 5 ----- packages/vertexai/package.json | 4 ++-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/vertexai/lib/polyfills.ts b/packages/vertexai/lib/polyfills.ts index 01fae01f7c..8278065d4e 100644 --- a/packages/vertexai/lib/polyfills.ts +++ b/packages/vertexai/lib/polyfills.ts @@ -14,11 +14,6 @@ * limitations under the License. * */ - -// @ts-ignore -import { polyfill } from 'react-native-polyfill-globals/src/fetch'; -polyfill(); - // @ts-ignore import { ReadableStream as ReadableStreamPolyfill } from 'web-streams-polyfill/dist/ponyfill'; // @ts-ignore diff --git a/packages/vertexai/package.json b/packages/vertexai/package.json index 49bc80eeed..e24139b120 100644 --- a/packages/vertexai/package.json +++ b/packages/vertexai/package.json @@ -8,7 +8,8 @@ "scripts": { "build": "genversion --esm --semi lib/version.ts", "build:clean": "rimraf android/build && rimraf ios/build", - "prepare": "yarn tests:vertex:mocks && yarn run build && bob build" + "compile": "bob build", + "prepare": "yarn tests:vertex:mocks && yarn run build && yarn compile" }, "repository": { "type": "git", @@ -85,7 +86,6 @@ ], "dependencies": { "react-native-fetch-api": "^3.0.0", - "react-native-polyfill-globals": "^3.1.0", "text-encoding": "^0.7.0", "web-streams-polyfill": "^3.3.2" } From 354d28f2ea38e3eb70a11796825d461f38475a3c Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 27 Jan 2025 12:38:34 +0000 Subject: [PATCH 072/115] fix: stream now global polyfill has been removed --- packages/vertexai/lib/polyfills.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/vertexai/lib/polyfills.ts b/packages/vertexai/lib/polyfills.ts index 8278065d4e..c2ed23c4fe 100644 --- a/packages/vertexai/lib/polyfills.ts +++ b/packages/vertexai/lib/polyfills.ts @@ -15,8 +15,21 @@ * */ // @ts-ignore -import { ReadableStream as ReadableStreamPolyfill } from 'web-streams-polyfill/dist/ponyfill'; +import { polyfillGlobal } from 'react-native/Libraries/Utilities/PolyfillFunctions'; // @ts-ignore -globalThis.ReadableStream = ReadableStreamPolyfill; +import { ReadableStream } from 'web-streams-polyfill/dist/ponyfill'; +// @ts-ignore +import { fetch, Headers, Request, Response } from 'react-native-fetch-api'; + +polyfillGlobal( + 'fetch', + () => + (...args: any[]) => + fetch(args[0], { ...args[1], reactNative: { textStreaming: true } }), +); +polyfillGlobal('Headers', () => Headers); +polyfillGlobal('Request', () => Request); +polyfillGlobal('Response', () => Response); +polyfillGlobal('ReadableStream', () => ReadableStream); import 'text-encoding'; From 256697c6031aef811c96090855871ebd5412ea44 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 27 Jan 2025 17:51:56 +0000 Subject: [PATCH 073/115] chore: update yarn.lock --- yarn.lock | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/yarn.lock b/yarn.lock index 67a3b2b593..f671a73b3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7595,7 +7595,6 @@ __metadata: "@types/text-encoding": "npm:^0" react-native-builder-bob: "npm:^0.35.2" react-native-fetch-api: "npm:^3.0.0" - react-native-polyfill-globals: "npm:^3.1.0" text-encoding: "npm:^0.7.0" typescript: "npm:^5.7.2" web-streams-polyfill: "npm:^3.3.2" @@ -21920,20 +21919,6 @@ __metadata: languageName: node linkType: hard -"react-native-polyfill-globals@npm:^3.1.0": - version: 3.1.0 - resolution: "react-native-polyfill-globals@npm:3.1.0" - peerDependencies: - base-64: "*" - react-native-fetch-api: "*" - react-native-get-random-values: "*" - react-native-url-polyfill: "*" - text-encoding: "*" - web-streams-polyfill: "*" - checksum: 10/1385de6de67bf65842a3c4549f22a91ba39de576f8ccdd3e3ad79d67defc9953f8d6a2a53aedfbcd04344b8c8359b1c92242de4624066d680f40ca0310f2f7d6 - languageName: node - linkType: hard - "react-native@npm:*": version: 0.73.4 resolution: "react-native@npm:0.73.4" From 411ba71c7c7fb4165f010178745e4791fd0bbd75 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Mon, 27 Jan 2025 17:53:36 +0000 Subject: [PATCH 074/115] docs: vertexAI documentation --- docs/sidebar.yaml | 4 + docs/vertexai/usage/index.md | 369 +++++++++++++++++++++++++++++++++++ 2 files changed, 373 insertions(+) create mode 100644 docs/vertexai/usage/index.md diff --git a/docs/sidebar.yaml b/docs/sidebar.yaml index 8bf19b5c34..e6d4944982 100644 --- a/docs/sidebar.yaml +++ b/docs/sidebar.yaml @@ -127,6 +127,10 @@ - - KY Integration - '/perf/ky-integration' - '//static.invertase.io/assets/firebase/performance-monitoring.svg' +- - VertexAi + - - - Usage + - '/vertex-ai/usage' + - '//static.invertase.io/assets/social/firebase-logo.png' - - Legacy docs - - - Migrating to v6 - '/migrating-to-v6' diff --git a/docs/vertexai/usage/index.md b/docs/vertexai/usage/index.md new file mode 100644 index 0000000000..948de677e7 --- /dev/null +++ b/docs/vertexai/usage/index.md @@ -0,0 +1,369 @@ +--- +title: VertexAI +description: Installation and getting started with VertexAI. +icon: //static.invertase.io/assets/social/firebase-logo.png +next: /analytics/usage +previous: /remote-config/usage +--- + +# Installation + +This module requires that the `@react-native-firebase/app` module is already setup and installed. To install the "app" module, view the +[Getting Started](/) documentation. + +```bash +# Install & setup the app module +yarn add @react-native-firebase/app + +# Install the vertexai module +yarn add @react-native-firebase/vertexai +``` + +# What does it do + +The Vertex AI Gemini API gives you access to the latest generative AI models from Google. If you need to call the Vertex AI Gemini API directly from your mobile or web app – rather than server-side — you can use the Vertex AI in Firebase SDKs. See the [VertexAI documentation on the firebase website](https://firebase.google.com/docs/vertex-ai) for further information. + +# Usage + +## Generate text from text-only input + +You can call the Gemini API with input that includes only text. For these calls, you need to use a model that supports text-only prompts (like Gemini 1.5 Pro). + +Use `generateContent()` which waits for the entire response before returning: + +```js +import React from 'react'; +import { AppRegistry, Button, Text, View } from 'react-native'; +import { getApp } from '@react-native-firebase/app'; +import { getVertexAI, getGenerativeModel } from '@react-native-firebase/vertexai'; + +function App() { + return ( + +