diff --git a/docs/extras/ecosystem/integrations/makersuite.mdx b/docs/extras/ecosystem/integrations/makersuite.mdx new file mode 100644 index 000000000000..de10951c2958 --- /dev/null +++ b/docs/extras/ecosystem/integrations/makersuite.mdx @@ -0,0 +1,124 @@ +# Google MakerSuite + +Google's [MakerSuite](https://makersuite.google.com/) is a web-based playground +for creating and saving chat, text, and "data" prompts that work with the +Google PaLM API and model. These prompts include the text, which may include +"test input" that act as template parameters, and parameters for the model +including the model name, temperature, etc. + +LangChain.js provides the `MakerSuiteHub` class which lets you pull this prompt +from Google Drive, where they are saved. Once pulled, you can convert the prompt +into a LangChain Template, an LLM Model, or a chain that combines the two. This +hub class has a simple time-based in-memory cache of prompts, so it is not always +accessing the prompt saved in Google Drive. + +Using MakerSuite in this way allows you to treat it as a simple Content Management +System (CMS) of sorts or allows separation of tasks between prompt authors and +other developers. + +## Setup + +You do not need any additional packages beyond those that are required for +either the Google PaLM [text](/docs/modules/model_io/models/llms/integrations/google_palm) +or [chat](/docs/modules/model_io/models/chat/integrations/google_palm) model: + +```bash npm2yarn +npm install google-auth-library @google-ai/generativelanguage +``` + +## Credentials and Authorization + +You will need two sets of credentials: + +- An API Key to access the PaLM API. + + Create this at [Google MakerSuite](https://makersuite.google.com/app/apikey). + Then set the key as `GOOGLE_PALM_API_KEY` environment variable. + +- Credentials for a service account that has been permitted access to the + Google Drive APIs. + + These credentials may be used in one of three ways: + + - You are logged into an account (using `gcloud auth application-default login`) + permitted to that project. + - You are running on a machine using a service account that is permitted + to the project. + - You have downloaded the credentials for a service account that is permitted + to the project and set the `GOOGLE_APPLICATION_CREDENTIALS` environment + variable to the path of this file. + +This service account should also be permitted to the MakerSuite folder in Google +Drive or to the specific prompt file itself. Even if the prompt file is permitted +for anyone to read - you will still need a service account that is permitted to +access Google Drive. + +## The Prompt File ID + +The easiest way to get the ID of the prompt file is to open it in MakerSuite +and examine the URL. The URL should look something like: + +``` +https://makersuite.google.com/app/prompts/1gxLasQIeQdwR4wxtV_nb93b_g9f0GaMm +``` + +The final portion of this, `1gxLasQIeQdwR4wxtV_nb93b_g9f0GaMm` is the ID. + +We will be using this in our examples below. This prompt contains a Template +that is equivalent to: + +``` +What would be a good name for a company that makes {product} +``` + +With model parameters set that include: + +- Model name: Text Bison +- Temperature: 0.7 +- Max outputs: 1 +- Standard safety settings + +## Use + +The most typical way to use the hub consists of two parts: + +1. Creating the `MakerSuiteHub` class once. +2. Pulling the prompt, getting the chain, and providing values for the template + to get the result. + +```typescript +// Create the hub class +import { MakerSuiteHub } from "langchain/experimental/hubs/makersuite/googlemakersuitehub"; +const hub = new MakerSuiteHub(); + +// Pull the prompt, get the chain, and invoke it with the template values +const prompt = await hub.pull("1gxLasQIeQdwR4wxtV_nb93b_g9f0GaMm"); +const result = await prompt.toChain().invoke({ product: "socks" }); +console.log("text chain result", result); +``` + +### Configuring the hub + +Since the hub implements a basic in-memory time-based cache, you can configure +how long until a prompt that is saved in the cache will be reloaded. + +This value defaults to 0, indicating it will always be loaded from Google Drive, +or you can set it to the number of milliseconds it will be valid in the cache: + +```typescript +const hub = new MakerSuiteHub({ + cacheTimeout: 3600000, // One hour +}); +``` + +### Getting the Template or Model + +In some cases, you may need to get just the template or just the model +that is represented by the prompt. + +```typescript +const template = prompt.toTemplate(); +const textModel = prompt.toModel() as GooglePaLM; +const chatModel = prompt.toModel() as ChatGooglePaLM; +``` + diff --git a/langchain/scripts/check-tree-shaking.js b/langchain/scripts/check-tree-shaking.js index b4ef008ba42f..c6a13196bea0 100644 --- a/langchain/scripts/check-tree-shaking.js +++ b/langchain/scripts/check-tree-shaking.js @@ -36,6 +36,7 @@ export function listExternals() { "firebase-admin/app", "firebase-admin/firestore", "web-auth-library/google", + "@google-ai/generativelanguage/build/protos/protos.js", ]; } diff --git a/langchain/scripts/create-entrypoints.js b/langchain/scripts/create-entrypoints.js index 392716705941..6e17fda20d6d 100644 --- a/langchain/scripts/create-entrypoints.js +++ b/langchain/scripts/create-entrypoints.js @@ -255,6 +255,8 @@ const entrypoints = { "experimental/chat_models/bittensor", "experimental/llms/bittensor": "experimental/llms/bittensor", + "experimental/hubs/makersuite/googlemakersuitehub": + "experimental/hubs/makersuite/googlemakersuitehub", // evaluation evaluation: "evaluation/index", }; @@ -412,6 +414,7 @@ const requiresOptionalDependency = [ "experimental/multimodal_embeddings/googlevertexai", "experimental/chat_models/anthropic_functions", "experimental/llms/bittensor", + "experimental/hubs/makersuite/googlemakersuitehub", ]; // List of test-exports-* packages which we use to test that the exports field diff --git a/langchain/src/experimental/hubs/makersuite/googlemakersuitehub.ts b/langchain/src/experimental/hubs/makersuite/googlemakersuitehub.ts new file mode 100644 index 000000000000..d6acb6122b3e --- /dev/null +++ b/langchain/src/experimental/hubs/makersuite/googlemakersuitehub.ts @@ -0,0 +1,424 @@ +import type { protos } from "@google-ai/generativelanguage"; +import type { google } from "@google-ai/generativelanguage/build/protos/protos.js"; +import { GoogleAuth, GoogleAuthOptions } from "google-auth-library"; + +import { GooglePaLM } from "../../../llms/googlepalm.js"; +import { ChatGooglePaLM } from "../../../chat_models/googlepalm.js"; +import { PromptTemplate } from "../../../prompts/index.js"; +import { BaseLanguageModel } from "../../../base_language/index.js"; +import { Runnable } from "../../../schema/runnable/index.js"; +import { + AsyncCaller, + AsyncCallerCallOptions, +} from "../../../util/async_caller.js"; +import { + GoogleResponse, + GoogleVertexAIConnectionParams, +} from "../../../types/googlevertexai-types.js"; +import { GoogleConnection } from "../../../util/googlevertexai-connection.js"; + +/** + * Configuration that allows us to load or pull a prompt that has been created + * by the Google MakerSuite site and saved in Google Drive. + * + * There is a simple in-memory cache of these prompts that is refreshed + * after the cacheTimeout specified in the configuration. + */ +export interface MakerSuiteHubConfig { + /** + * How long, in milliseconds, before a prompt is assumed stale and should + * be refreshed from the copy in Google Drive. + */ + cacheTimeout: number; + + caller?: AsyncCaller; +} + +type MakerSuitePromptType = "text" | "data" | "chat"; + +export interface MakerSuitePromptVariable { + variableId: string; + displayName: string; +} + +export interface MakerSuiteRunSettings { + temperature?: number; + model: string; + candidateCount?: number; + topP?: number; + topK?: number; + maxOutputTokens: number; + safetySettings?: protos.google.ai.generativelanguage.v1beta2.ISafetySetting[]; +} + +export interface MakerSuiteTextPromptData { + textPrompt: { + value?: string; + variables?: MakerSuitePromptVariable[]; + }; + runSettings?: MakerSuiteRunSettings; + testExamples?: unknown; +} + +export interface MakerSuiteDataPromptColumn { + columnId: string; + displayName: string; + isInput?: boolean; +} + +export interface MakerSuiteDataPromptRow { + rowId: string; + columnBindings: Record; +} + +export interface MakerSuiteDataPromptData { + dataPrompt: { + preamble: string; + columns: MakerSuiteDataPromptColumn[]; + rows: MakerSuiteDataPromptRow[]; + rowsUsed: string[]; + }; + runSettings?: MakerSuiteRunSettings; + testExamples?: unknown; +} + +export interface MakerSuiteChatExchange { + request?: string; + response?: string; + source: string; + id: string; +} + +export interface MakerSuiteChatPromptData { + multiturnPrompt: { + preamble: string; + primingExchanges: MakerSuiteChatExchange[]; + sessions: { + sessionExchanges: MakerSuiteChatExchange[]; + }[]; + }; + runSettings?: MakerSuiteRunSettings; +} + +/** + * These are the possible formats that the JSON generated by MakerSuite + * and saved in Google Drive can be. + */ +export type MakerSuitePromptData = + | MakerSuiteTextPromptData + | MakerSuiteDataPromptData + | MakerSuiteChatPromptData; + +/** + * A class that represents the Prompt that has been created by MakerSuite + * and loaded from Google Drive. It exposes methods to turn this prompt + * into a Template, a Model, and into a chain consisting of these two elements. + * In general, this class should be created by the MakerSuiteHub class and + * not instantiated manually. + */ +export class MakerSuitePrompt { + promptType: MakerSuitePromptType; + + promptData: MakerSuitePromptData; + + constructor(promptData: MakerSuitePromptData) { + this.promptData = promptData; + this._determinePromptType(); + } + + _determinePromptType() { + if (Object.hasOwn(this.promptData, "textPrompt")) { + this.promptType = "text"; + } else if (Object.hasOwn(this.promptData, "dataPrompt")) { + this.promptType = "data"; + } else if (Object.hasOwn(this.promptData, "multiturnPrompt")) { + this.promptType = "chat"; + } else { + const error = new Error("Unable to identify prompt type."); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (error as any).promptData = this.promptData; + throw error; + } + } + + _promptValueText(): string { + return ( + (this.promptData as MakerSuiteTextPromptData)?.textPrompt?.value ?? "" + ); + } + + _promptValueData(): string { + const promptData: MakerSuiteDataPromptData = this + .promptData as MakerSuiteDataPromptData; + const dataPrompt = promptData?.dataPrompt; + let prompt = `${dataPrompt?.preamble}\n` || ""; + + dataPrompt?.rows.forEach((row) => { + // Add the data for each row, as long as it is listed as used + if (dataPrompt?.rowsUsed.includes(row.rowId)) { + // Add each input column + dataPrompt?.columns.forEach((column) => { + if (column.isInput) { + prompt += `${column.displayName} ${ + row.columnBindings[column.columnId] + }\n`; + } + }); + + // Add each output column + dataPrompt?.columns.forEach((column) => { + if (!column.isInput) { + prompt += `${column.displayName} ${ + row.columnBindings[column.columnId] + }\n`; + } + }); + } + }); + + // Add the input column prompts + dataPrompt?.columns.forEach((column) => { + if (column.isInput) { + prompt += `${column.displayName} {${column.displayName.replace( + ":", + "" + )}}\n`; + } + }); + + // Add just the first output column + const firstOutput = dataPrompt?.columns.find((column) => !column.isInput); + prompt += `${firstOutput?.displayName} `; + + return prompt; + } + + _promptValueChat(): string { + return ( + (this.promptData as MakerSuiteChatPromptData)?.multiturnPrompt + ?.preamble ?? "" + ); + } + + _promptValue(): string { + switch (this.promptType) { + case "text": + return this._promptValueText(); + case "data": + return this._promptValueData(); + case "chat": + return this._promptValueChat(); + default: + throw new Error(`Invalid promptType: ${this.promptType}`); + } + } + + /** + * Create a template from the prompt, including any "test input" specified + * in MakerSuite as a template parameter. + */ + toTemplate(): PromptTemplate { + const value = this._promptValue(); + const promptString = value.replaceAll(/{{.*:(.+):.*}}/g, "{$1}"); + return PromptTemplate.fromTemplate(promptString); + } + + _modelName(): string { + let ret = this.promptData?.runSettings?.model; + if (!ret) { + ret = + this.promptType === "chat" + ? "models/chat-bison-001" + : "models/text-bison-001"; + } + return ret; + } + + _examples(): google.ai.generativelanguage.v1beta2.IExample[] { + const promptData: MakerSuiteChatPromptData = this + .promptData as MakerSuiteChatPromptData; + const ret: google.ai.generativelanguage.v1beta2.IExample[] = + promptData?.multiturnPrompt?.primingExchanges + .map((exchange) => { + const example: google.ai.generativelanguage.v1beta2.IExample = {}; + if (exchange?.request) { + example.input = { + content: exchange.request, + }; + } + if (exchange?.response) { + example.output = { + content: exchange.response, + }; + } + return example; + }) + .filter((value) => Object.keys(value).length); + return ret; + } + + /** + * Create a model from the prompt with all the parameters (model name, + * temperature, etc) set as they were in MakerSuite. + */ + toModel(): BaseLanguageModel { + const modelName = this._modelName(); + const modelSettings = { + modelName, + ...this.promptData?.runSettings, + }; + if (this.promptType === "chat") { + const examples = this._examples(); + return new ChatGooglePaLM({ + examples, + ...modelSettings, + }); + } else { + return new GooglePaLM(modelSettings); + } + } + + /** + * Create a RunnableSequence based on the template and model that can + * be created from this prompt. The template will have parameters available + * based on the "test input" that was set in MakerSuite, and the model + * will have the parameters (model name, temperature, etc) from those in + * MakerSuite. + */ + toChain() { + return this.toTemplate().pipe(this.toModel() as Runnable); + } +} + +interface DriveFileReadParams + extends GoogleVertexAIConnectionParams { + fileId: string; +} + +interface DriveCallOptions extends AsyncCallerCallOptions { + // Empty, I think +} + +interface DriveFileMakerSuiteResponse extends GoogleResponse { + data: MakerSuitePromptData; +} + +export class DriveFileReadConnection + extends GoogleConnection + implements DriveFileReadParams +{ + endpoint: string; + + apiVersion: string; + + fileId: string; + + constructor(fields: DriveFileReadParams, caller: AsyncCaller) { + super( + caller, + new GoogleAuth({ + scopes: "https://www.googleapis.com/auth/drive.readonly", + ...fields.authOptions, + }) + ); + + this.endpoint = fields.endpoint ?? "www.googleapis.com"; + this.apiVersion = fields.apiVersion ?? "v3"; + + this.fileId = fields.fileId; + } + + async buildUrl(): Promise { + return `https://${this.endpoint}/drive/${this.apiVersion}/files/${this.fileId}?alt=media`; + } + + buildMethod(): string { + return "GET"; + } + + async request( + options?: DriveCallOptions + ): Promise { + return this._request(undefined, options ?? {}); + } +} + +export interface CacheEntry { + updated: number; + prompt: MakerSuitePrompt; +} + +/** + * A class allowing access to MakerSuite prompts that have been saved in + * Google Drive. + * MakerSuite prompts are pulled based on their Google Drive ID (which is available + * from Google Drive or as part of the URL when the prompt is open in MakerSuite). + * There is a basic cache that will store the prompt in memory for a time specified + * in milliseconds. This defaults to 0, indicating the prompt should always be + * pulled from Google Drive. + */ +export class MakerSuiteHub { + cache: Record = {}; + + cacheTimeout: number; + + caller: AsyncCaller; + + constructor(config?: MakerSuiteHubConfig) { + this.cacheTimeout = config?.cacheTimeout ?? 0; + this.caller = config?.caller ?? new AsyncCaller({}); + } + + /** + * Is the current cache entry valid, or does it need to be refreshed. + * It will need to be refreshed under any of the following conditions: + * - It does not currently exist in the cache + * - The cacheTimeout is 0 + * - The cache was last updated longer ago than the cacheTimeout allows + * @param entry - The cache entry, including when this prompt was last refreshed + */ + isValid(entry: CacheEntry): boolean { + // If we don't have a record, it can't be valid + // And if the cache timeout is 0, we will always refresh, so the cache is invalid + if (!entry || this.cacheTimeout === 0) { + return false; + } + + const now = Date.now(); + const expiration = entry.updated + this.cacheTimeout; + return expiration > now; + } + + /** + * Get a MakerSuitePrompt that is specified by the Google Drive ID. + * This will always be loaded from Google Drive. + * @param id + * @return A MakerSuitePrompt which can be used to create a template, model, or chain. + */ + async forcePull(id: string): Promise { + const fields: DriveFileReadParams = { + fileId: id, + }; + const connection = new DriveFileReadConnection(fields, this.caller); + const result = await connection.request(); + const ret = new MakerSuitePrompt(result.data); + this.cache[id] = { + prompt: ret, + updated: Date.now(), + }; + return ret; + } + + /** + * Get a MakerSuitePrompt that is specified by the Google Drive ID. This may be + * loaded from Google Drive or, if there is a valid copy in the cache, the cached + * copy will be returned. + * @param id + * @return A MakerSuitePrompt which can be used to create a template, model, or chain. + */ + async pull(id: string): Promise { + const entry = this.cache[id]; + const ret = this.isValid(entry) ? entry.prompt : await this.forcePull(id); + return ret; + } +} diff --git a/langchain/src/experimental/hubs/makersuite/tests/googlemakersuite-files/chatPrompt.json b/langchain/src/experimental/hubs/makersuite/tests/googlemakersuite-files/chatPrompt.json new file mode 100644 index 000000000000..ab221bcbe65b --- /dev/null +++ b/langchain/src/experimental/hubs/makersuite/tests/googlemakersuite-files/chatPrompt.json @@ -0,0 +1,43 @@ +{ + "runSettings": { + "temperature": 0.25, + "model": "models/chat-bison-001", + "candidateCount": 1, + "topP": 0.95, + "topK": 40, + "maxOutputTokens": 1024 + }, + "multiturnPrompt": { + "preamble": "You are responsible for returning date and time information based on the user\u0027s input. All your date and time responses should be as an ISO8601 formatted response that include the year, month, day, hour, minute, second, and time zone offset. You should only return this time information and nothing else.\n", + "primingExchanges": [ + { + "request": "What time is it?", + "response": "2023-09-16T02:03:04-0500", + "source": "MODEL_EDITED", + "id": "4B26EE6A-6171-4626-80B6-207491D106BF" + }, + { + "source": "USER", + "id": "36D72C06-423E-4E00-BA27-DEC44CBA5A13" + } + ], + "sessions": [ + { + "sessionExchanges": [ + { + "request": "What time is it?", + "response": "The time is 2023-09-16T02:04:05-0500.", + "source": "MODEL", + "id": "9E4D6C9A-81D2-4EE9-BE8B-96C84F1FDA64" + }, + { + "request": "What time was it one week ago?", + "response": "The time one week ago was 2023-09-09T02:04:05-0500.", + "source": "MODEL", + "id": "EA76CED1-F626-46F3-8CCA-4C9C821B2003" + } + ] + } + ] + } +} diff --git a/langchain/src/experimental/hubs/makersuite/tests/googlemakersuite-files/dataPrompt.json b/langchain/src/experimental/hubs/makersuite/tests/googlemakersuite-files/dataPrompt.json new file mode 100644 index 000000000000..6a5d6878f99e --- /dev/null +++ b/langchain/src/experimental/hubs/makersuite/tests/googlemakersuite-files/dataPrompt.json @@ -0,0 +1,108 @@ +{ + "dataPrompt": { + "preamble": "Given a product description, you should return a name for that product that includes something about rainbows.", + "columns": [ + { + "columnId": "CFA438B7-313B-4A50-AF74-3ACBC9216A4B", + "displayName": "description:", + "isInput": true + }, + { + "columnId": "7D38B937-6636-4AAC-9DFD-B4A60D98C011", + "displayName": "product:" + } + ], + "rows": [ + { + "columnBindings": { + "CFA438B7-313B-4A50-AF74-3ACBC9216A4B": "socks", + "7D38B937-6636-4AAC-9DFD-B4A60D98C011": "spectrum socks" + }, + "rowId": "D412F0BE-5C81-401D-917C-758A6F14EEC4" + }, + { + "columnBindings": { + "CFA438B7-313B-4A50-AF74-3ACBC9216A4B": "hair ties", + "7D38B937-6636-4AAC-9DFD-B4A60D98C011": "rainbows^2" + }, + "rowId": "C77A45B4-74B4-41F3-AD8E-06F0C1D9D820" + } + ], + "rowsUsed": [ + "D412F0BE-5C81-401D-917C-758A6F14EEC4", + "C77A45B4-74B4-41F3-AD8E-06F0C1D9D820" + ] + }, + "runSettings": { + "temperature": 0.7, + "model": "models/text-bison-001", + "candidateCount": 1, + "topP": 0.95, + "topK": 40, + "maxOutputTokens": 1024, + "safetySettings": [ + { + "category": "HARM_CATEGORY_DEROGATORY", + "threshold": "BLOCK_LOW_AND_ABOVE" + }, + { + "category": "HARM_CATEGORY_TOXICITY", + "threshold": "BLOCK_LOW_AND_ABOVE" + }, + { + "category": "HARM_CATEGORY_VIOLENCE", + "threshold": "BLOCK_MEDIUM_AND_ABOVE" + }, + { + "category": "HARM_CATEGORY_SEXUAL", + "threshold": "BLOCK_MEDIUM_AND_ABOVE" + }, + { + "category": "HARM_CATEGORY_MEDICAL", + "threshold": "BLOCK_MEDIUM_AND_ABOVE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS", + "threshold": "BLOCK_MEDIUM_AND_ABOVE" + } + ] + }, + "testExamples": [ + { + "inputBindings": { + "CFA438B7-313B-4A50-AF74-3ACBC9216A4B": "condoms" + }, + "modelResponses": [ + { + "response": "rainbow rhapsody", + "safetyRatings": [ + { + "category": "HARM_CATEGORY_DEROGATORY", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_TOXICITY", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_VIOLENCE", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_SEXUAL", + "probability": "LOW" + }, + { + "category": "HARM_CATEGORY_MEDICAL", + "probability": "NEGLIGIBLE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS", + "probability": "NEGLIGIBLE" + } + ] + } + ] + } + ] +} diff --git a/langchain/src/experimental/hubs/makersuite/tests/googlemakersuite-files/textPrompt.json b/langchain/src/experimental/hubs/makersuite/tests/googlemakersuite-files/textPrompt.json new file mode 100644 index 000000000000..15297be307d5 --- /dev/null +++ b/langchain/src/experimental/hubs/makersuite/tests/googlemakersuite-files/textPrompt.json @@ -0,0 +1,53 @@ +{ + "textPrompt": { + "value": "What would be a good name for a company that makes {{30E275F8-0B60-4E71-843D-9865F4D4EFD4:product:​}}?", + "variables": [ + { + "variableId": "30E275F8-0B60-4E71-843D-9865F4D4EFD4", + "displayName": "product" + } + ] + }, + "runSettings": { + "temperature": 0.7, + "model": "models/text-bison-001", + "candidateCount": 1, + "topP": 0.95, + "topK": 40, + "maxOutputTokens": 1024, + "safetySettings": [ + { + "category": "HARM_CATEGORY_DEROGATORY", + "threshold": "BLOCK_LOW_AND_ABOVE" + }, + { + "category": "HARM_CATEGORY_TOXICITY", + "threshold": "BLOCK_LOW_AND_ABOVE" + }, + { + "category": "HARM_CATEGORY_VIOLENCE", + "threshold": "BLOCK_MEDIUM_AND_ABOVE" + }, + { + "category": "HARM_CATEGORY_SEXUAL", + "threshold": "BLOCK_MEDIUM_AND_ABOVE" + }, + { + "category": "HARM_CATEGORY_MEDICAL", + "threshold": "BLOCK_MEDIUM_AND_ABOVE" + }, + { + "category": "HARM_CATEGORY_DANGEROUS", + "threshold": "BLOCK_MEDIUM_AND_ABOVE" + } + ] + }, + "testExamples": [ + { + "inputBindings": { + "30E275F8-0B60-4E71-843D-9865F4D4EFD4": "" + }, + "modelResponses": [{}] + } + ] +} diff --git a/langchain/src/experimental/hubs/makersuite/tests/googlemakersuitehub.int.test.ts b/langchain/src/experimental/hubs/makersuite/tests/googlemakersuitehub.int.test.ts new file mode 100644 index 000000000000..e4ea2436c8ac --- /dev/null +++ b/langchain/src/experimental/hubs/makersuite/tests/googlemakersuitehub.int.test.ts @@ -0,0 +1,103 @@ +// noinspection DuplicatedCode + +import fs from "fs"; +import { fileURLToPath } from "node:url"; +import * as path from "path"; + +import { describe, test } from "@jest/globals"; +import { + DriveFileReadConnection, + MakerSuiteHub, + MakerSuitePrompt, +} from "../googlemakersuitehub.js"; +import { AsyncCaller } from "../../../../util/async_caller.js"; +import { HumanMessage } from "../../../../schema/index.js"; +import { ChatGooglePaLM } from "../../../../chat_models/googlepalm.js"; +import { GooglePaLM } from "../../../../llms/googlepalm.js"; + +describe.skip("Google Maker Suite Hub Integration", () => { + describe("Prompt", () => { + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + + const chatFile = JSON.parse( + fs.readFileSync( + `${__dirname}/googlemakersuite-files/chatPrompt.json`, + "utf8" + ) + ); + const dataFile = JSON.parse( + fs.readFileSync( + `${__dirname}/googlemakersuite-files/dataPrompt.json`, + "utf8" + ) + ); + const textFile = JSON.parse( + fs.readFileSync( + `${__dirname}/googlemakersuite-files/textPrompt.json`, + "utf8" + ) + ); + + test("text chain", async () => { + const prompt = new MakerSuitePrompt(textFile); + const chain = prompt.toChain(); + const result = await chain.invoke({ + product: "shoes", + }); + console.log("text chain result", result); + expect(result).toBeTruthy(); + }); + + test("data chain", async () => { + const prompt = new MakerSuitePrompt(dataFile); + const chain = prompt.toChain(); + const result = await chain.invoke({ + description: "shoes", + }); + console.log("data chain result", result); + expect(result).toBeTruthy(); + }); + + test("chat model", async () => { + const prompt = new MakerSuitePrompt(chatFile); + const model = prompt.toModel() as ChatGooglePaLM; + const message = new HumanMessage("Hello!"); + const result = await model.call([message]); + expect(result).toBeTruthy(); + console.log({ result }); + }); + }); + + describe("Drive", () => { + test("file get media", async () => { + const fileId = "1IAWobj3BYvbj5X3JOAKaoXTcNJlZLdpK"; + const caller = new AsyncCaller({}); + const connection = new DriveFileReadConnection({ fileId }, caller); + console.log("connection client", connection?.client); + const result = await connection.request(); + console.log(result); + }); + }); + + describe("Hub", () => { + const hub = new MakerSuiteHub(); + + test("text model", async () => { + const prompt = await hub.pull("1gxLasQIeQdwR4wxtV_nb93b_g9f0GaMm"); + const model = prompt.toModel() as GooglePaLM; + const result = await model.call( + "What would be a good name for a company that makes socks" + ); + console.log("text chain result", result); + expect(result).toBeTruthy(); + }); + + test("text chain", async () => { + const prompt = await hub.pull("1gxLasQIeQdwR4wxtV_nb93b_g9f0GaMm"); + const result = await prompt.toChain().invoke({ product: "socks" }); + console.log("text chain result", result); + expect(result).toBeTruthy(); + }); + }); +}); diff --git a/langchain/src/experimental/hubs/makersuite/tests/googlemakersuitehub.test.ts b/langchain/src/experimental/hubs/makersuite/tests/googlemakersuitehub.test.ts new file mode 100644 index 000000000000..cd12a3de87c3 --- /dev/null +++ b/langchain/src/experimental/hubs/makersuite/tests/googlemakersuitehub.test.ts @@ -0,0 +1,163 @@ +import fs from "fs"; +import { fileURLToPath } from "node:url"; +import * as path from "path"; + +import { describe, expect, test } from "@jest/globals"; +import { MakerSuiteHub, MakerSuitePrompt } from "../googlemakersuitehub.js"; +import { ChatGooglePaLM } from "../../../../chat_models/googlepalm.js"; + +describe("Google Maker Suite Hub", () => { + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + + const chatFile = JSON.parse( + fs.readFileSync( + `${__dirname}/googlemakersuite-files/chatPrompt.json`, + "utf8" + ) + ); + const dataFile = JSON.parse( + fs.readFileSync( + `${__dirname}/googlemakersuite-files/dataPrompt.json`, + "utf8" + ) + ); + const textFile = JSON.parse( + fs.readFileSync( + `${__dirname}/googlemakersuite-files/textPrompt.json`, + "utf8" + ) + ); + + // We don't need a real key + // eslint-disable-next-line no-process-env + process.env.GOOGLE_PALM_API_KEY = "test"; + + describe("Prompt", () => { + test("text type", () => { + const prompt = new MakerSuitePrompt(textFile); + expect(prompt.promptType).toEqual("text"); + }); + + test("text template", () => { + const prompt = new MakerSuitePrompt(textFile); + const template = prompt.toTemplate(); + expect(template.template).toEqual( + "What would be a good name for a company that makes {product}?" + ); + }); + + test("text model", () => { + const prompt = new MakerSuitePrompt(textFile); + const model = prompt.toModel(); + // console.log(model.lc_namespace); + expect(model.lc_namespace).toEqual(["langchain", "llms", "googlepalm"]); + }); + + test("data type", () => { + const prompt = new MakerSuitePrompt(dataFile); + expect(prompt.promptType).toEqual("data"); + }); + + test("data template", () => { + const prompt = new MakerSuitePrompt(dataFile); + const template = prompt.toTemplate(); + // console.log("data template", template.template); + expect(template.template).toEqual( + "Given a product description, you should return a name for that product that includes something about rainbows.\n" + + "description: socks\n" + + "product: spectrum socks\n" + + "description: hair ties\n" + + "product: rainbows^2\n" + + "description: {description}\n" + + "product: " + ); + }); + + test("data model", () => { + const prompt = new MakerSuitePrompt(dataFile); + const model = prompt.toModel(); + expect(model.lc_namespace).toEqual(["langchain", "llms", "googlepalm"]); + }); + + test("chat type", () => { + const prompt = new MakerSuitePrompt(chatFile); + expect(prompt.promptType).toEqual("chat"); + }); + + test("chat model", () => { + const prompt = new MakerSuitePrompt(chatFile); + const model = prompt.toModel(); + expect(model.lc_namespace).toEqual([ + "langchain", + "chat_models", + "googlepalm", + ]); + expect((model as ChatGooglePaLM).examples).toEqual([ + { + input: { content: "What time is it?" }, + output: { content: "2023-09-16T02:03:04-0500" }, + }, + ]); + }); + }); + + describe("MakerSuiteHub", () => { + test("isValid no entry", () => { + const nonexistentId = "nonexistent"; + const hub = new MakerSuiteHub({ cacheTimeout: 1000 }); + const entry = hub.cache[nonexistentId]; + const isValid = hub.isValid(entry); + expect(isValid).toEqual(false); + }); + + test("isValid timeout 0", () => { + // This should never be valid because the cache timeout will be 0 + const fakeId = "fake"; + const hub = new MakerSuiteHub({ cacheTimeout: 0 }); + const entry = { + updated: Date.now(), + prompt: new MakerSuitePrompt({ + textPrompt: { + value: "test", + }, + }), + }; + hub.cache[fakeId] = entry; + const isValid = hub.isValid(entry); + expect(isValid).toEqual(false); + }); + + test("isValid valid", () => { + const fakeId = "fake"; + const hub = new MakerSuiteHub({ cacheTimeout: 60000 }); + const entry = { + updated: Date.now(), + prompt: new MakerSuitePrompt({ + textPrompt: { + value: "test", + }, + }), + }; + hub.cache[fakeId] = entry; + const isValid = hub.isValid(entry); + expect(isValid).toEqual(true); + }); + + test("isValid timeout", () => { + const fakeId = "fake"; + const hub = new MakerSuiteHub({ cacheTimeout: 60000 }); + const entry = { + updated: Date.now() - 100000, + prompt: new MakerSuitePrompt({ + textPrompt: { + value: "test", + }, + }), + }; + hub.cache[fakeId] = entry; + const isValid = hub.isValid(entry); + expect(isValid).toEqual(false); + }); + }); +}); diff --git a/langchain/src/types/googlevertexai-types.ts b/langchain/src/types/googlevertexai-types.ts index f6b957055939..422aee5ae01b 100644 --- a/langchain/src/types/googlevertexai-types.ts +++ b/langchain/src/types/googlevertexai-types.ts @@ -1,11 +1,14 @@ import { BaseLLMParams } from "../llms/base.js"; -export interface GoogleVertexAIConnectionParams { +export interface GoogleConnectionParams { + authOptions?: AuthOptions; +} + +export interface GoogleVertexAIConnectionParams + extends GoogleConnectionParams { /** Hostname for the API call */ endpoint?: string; - authOptions?: AuthOptions; - /** Region where the LLM is stored */ location?: string; @@ -53,12 +56,12 @@ export interface GoogleVertexAIBaseLLMInput GoogleVertexAIConnectionParams, GoogleVertexAIModelParams {} -export interface GoogleVertexAIResponse { +export interface GoogleResponse { // eslint-disable-next-line @typescript-eslint/no-explicit-any data: any; } -export interface GoogleVertexAIBasePrediction extends GoogleVertexAIResponse { +export interface GoogleVertexAIBasePrediction extends GoogleResponse { // eslint-disable-next-line @typescript-eslint/no-explicit-any safetyAttributes?: any; } @@ -71,7 +74,7 @@ export interface GoogleVertexAILLMResponse< }; } -export interface GoogleVertexAIAbstractedClient { +export interface GoogleAbstractedClient { request: (opts: { url?: string; method?: "GET" | "POST"; diff --git a/langchain/src/util/googlevertexai-connection.ts b/langchain/src/util/googlevertexai-connection.ts index de89847025d3..d4aafa969fcf 100644 --- a/langchain/src/util/googlevertexai-connection.ts +++ b/langchain/src/util/googlevertexai-connection.ts @@ -6,44 +6,26 @@ import type { GoogleVertexAIConnectionParams, GoogleVertexAILLMResponse, GoogleVertexAIModelParams, - GoogleVertexAIResponse, - GoogleVertexAIAbstractedClient, + GoogleResponse, + GoogleAbstractedClient, } from "../types/googlevertexai-types.js"; -export abstract class GoogleVertexAIConnection< +export abstract class GoogleConnection< CallOptions extends AsyncCallerCallOptions, - ResponseType extends GoogleVertexAIResponse, - AuthOptions -> implements GoogleVertexAIConnectionParams -{ + ResponseType extends GoogleResponse +> { caller: AsyncCaller; - endpoint = "us-central1-aiplatform.googleapis.com"; - - location = "us-central1"; - - apiVersion = "v1"; - - client: GoogleVertexAIAbstractedClient; + client: GoogleAbstractedClient; - constructor( - fields: GoogleVertexAIConnectionParams | undefined, - caller: AsyncCaller, - client: GoogleVertexAIAbstractedClient - ) { + constructor(caller: AsyncCaller, client: GoogleAbstractedClient) { this.caller = caller; - - this.endpoint = fields?.endpoint ?? this.endpoint; - this.location = fields?.location ?? this.location; - this.apiVersion = fields?.apiVersion ?? this.apiVersion; this.client = client; } abstract buildUrl(): Promise; - buildMethod(): string { - return "POST"; - } + abstract buildMethod(): string; async _request( data: unknown | undefined, @@ -75,6 +57,39 @@ export abstract class GoogleVertexAIConnection< } } +export abstract class GoogleVertexAIConnection< + CallOptions extends AsyncCallerCallOptions, + ResponseType extends GoogleResponse, + AuthOptions + > + extends GoogleConnection + implements GoogleVertexAIConnectionParams +{ + endpoint = "us-central1-aiplatform.googleapis.com"; + + location = "us-central1"; + + apiVersion = "v1"; + + constructor( + fields: GoogleVertexAIConnectionParams | undefined, + caller: AsyncCaller, + client: GoogleAbstractedClient + ) { + super(caller, client); + this.caller = caller; + + this.endpoint = fields?.endpoint ?? this.endpoint; + this.location = fields?.location ?? this.location; + this.apiVersion = fields?.apiVersion ?? this.apiVersion; + this.client = client; + } + + buildMethod(): string { + return "POST"; + } +} + export class GoogleVertexAILLMConnection< CallOptions extends BaseLanguageModelCallOptions, InstanceType, @@ -86,12 +101,12 @@ export class GoogleVertexAILLMConnection< { model: string; - client: GoogleVertexAIAbstractedClient; + client: GoogleAbstractedClient; constructor( fields: GoogleVertexAIBaseLLMInput | undefined, caller: AsyncCaller, - client: GoogleVertexAIAbstractedClient + client: GoogleAbstractedClient ) { super(fields, caller, client); this.client = client; diff --git a/langchain/src/util/googlevertexai-webauth.ts b/langchain/src/util/googlevertexai-webauth.ts index cd36da83219f..b01b4ef7b9f8 100644 --- a/langchain/src/util/googlevertexai-webauth.ts +++ b/langchain/src/util/googlevertexai-webauth.ts @@ -4,7 +4,7 @@ import { Credentials, } from "web-auth-library/google"; import { getEnvironmentVariable } from "./env.js"; -import type { GoogleVertexAIAbstractedClient } from "../types/googlevertexai-types.js"; +import type { GoogleAbstractedClient } from "../types/googlevertexai-types.js"; export type WebGoogleAuthOptions = { credentials: string | Credentials; @@ -12,7 +12,7 @@ export type WebGoogleAuthOptions = { accessToken?: string; }; -export class WebGoogleAuth implements GoogleVertexAIAbstractedClient { +export class WebGoogleAuth implements GoogleAbstractedClient { options: WebGoogleAuthOptions; constructor(options?: WebGoogleAuthOptions) { diff --git a/langchain/src/vectorstores/googlevertexai.ts b/langchain/src/vectorstores/googlevertexai.ts index 73f8378227f7..a3630f9dfabe 100644 --- a/langchain/src/vectorstores/googlevertexai.ts +++ b/langchain/src/vectorstores/googlevertexai.ts @@ -12,7 +12,7 @@ import { } from "../util/async_caller.js"; import { GoogleVertexAIConnectionParams, - GoogleVertexAIResponse, + GoogleResponse, } from "../types/googlevertexai-types.js"; import { Docstore } from "../schema/index.js"; @@ -46,7 +46,7 @@ interface DeployedIndex { // There are other attributes, but we don't care about them right now } -interface IndexEndpointResponse extends GoogleVertexAIResponse { +interface IndexEndpointResponse extends GoogleResponse { data: { deployedIndexes: DeployedIndex[]; publicEndpointDomainName: string; @@ -101,7 +101,7 @@ interface RemoveDatapointRequest { datapointIds: string[]; } -interface RemoveDatapointResponse extends GoogleVertexAIResponse { +interface RemoveDatapointResponse extends GoogleResponse { // Should be empty } @@ -165,7 +165,7 @@ interface UpsertDatapointRequest { datapoints: IndexDatapoint[]; } -interface UpsertDatapointResponse extends GoogleVertexAIResponse { +interface UpsertDatapointResponse extends GoogleResponse { // Should be empty } @@ -239,7 +239,7 @@ interface FindNeighborsResponseNearestNeighbor { neighbors: FindNeighborsResponseNeighbor[]; } -interface FindNeighborsResponse extends GoogleVertexAIResponse { +interface FindNeighborsResponse extends GoogleResponse { data: { nearestNeighbors: FindNeighborsResponseNearestNeighbor[]; };