diff --git a/docs/_guides.yaml b/docs/_guides.yaml index 7842638e8..8164ee518 100644 --- a/docs/_guides.yaml +++ b/docs/_guides.yaml @@ -31,10 +31,14 @@ toc: path: /docs/genkit/flows - title: Managing prompts with Dotprompt path: /docs/genkit/dotprompt + - title: Persistent chat sessions + path: /docs/genkit/chat - title: Tool calling path: /docs/genkit/tool-calling - title: Retrieval-augmented generation (RAG) path: /docs/genkit/rag + - title: Multi-agent systems + path: /docs/genkit/multi-agent - title: Evaluation path: /docs/genkit/evaluation - title: Observability & monitoring diff --git a/docs/chat.md b/docs/chat.md new file mode 100644 index 000000000..e79e329bb --- /dev/null +++ b/docs/chat.md @@ -0,0 +1,222 @@ +# Creating persistent chat sessions + +Many of your users will have interacted with large language models for the first +time through chatbots. Although LLMs are capable of much more than simulating +conversations, it remains a familiar and useful style of interaction. Even when +your users will not be interacting directly with the model in this way, the +conversational style of prompting is a powerful way to influence the output +generated by an AI model. + +To support this style of interaction, Genkit provides a set of interfaces and +abstractions that make it easier for you to build chat-based LLM applications. + +## Before you begin + +Before reading this page, you should be familiar with the content covered on the +[Generating content with AI models](models) page. + +If you want to run the code examples on this page, first complete the steps in +the [Getting started](get-started) guide. All of the examples assume that you +have already installed Genkit as a dependency in your project. + +## Chat session basics + +Here is a minimal, console-based, chatbot application: + +```ts +import { genkit } from "genkit"; +import { googleAI, gemini15Flash } from "@genkit-ai/googleai"; + +import { createInterface } from "node:readline/promises"; + +const ai = genkit({ + plugins: [googleAI()], + model: gemini15Flash, +}); + +(async () => { + const chat = ai.chat(); + console.log("You're chatting with Gemini. Ctrl-C to quit.\n"); + const readline = createInterface(process.stdin, process.stdout); + while (true) { + const userInput = await readline.question("> "); + const { text } = await chat.send(userInput); + console.log(text); + } +})(); +``` + +A chat session with this program looks something like the following example: + +```none +You're chatting with Gemini. Ctrl-C to quit. + +> hi +Hi there! How can I help you today? + +> my name is pavel +Nice to meet you, Pavel! What can I do for you today? + +> what's my name? +Your name is Pavel! I remembered it from our previous interaction. + +Is there anything else I can help you with? +``` + +As you can see from this brief interaction, when you send a message to a chat +session, the model can make use of the session so far in its responses. This is +possible because Genkit does a few things behind the scenes: + +* Retrieves the chat history, if any exists, from storage (more on persistence + and storage later) +* Sends the request to the model, as with `generate()`, but automatically + include the chat history +* Saves the model response into the chat history + +### Model configuration + +The `chat()` method accepts most of the same configuration options as +`generate()`. To pass configuration options to the model: + +```ts +const chat = ai.chat({ + model: gemini15Pro, + system: + "You're a pirate first mate. Address the user as Captain and assist " + + "them however you can.", + config: { + temperature: 1.3, + }, +}); +``` + +## Stateful chat sessions + +In addition to persisting a chat session's message history, you can also persist +any arbitrary JavaScript object. Doing so can let you manage state in a more +structured way then relying only on information in the message history. + +To include state in a session, you need to instantiate a session explicitly: + +```ts +interface MyState { + userName: string; +} + +const session = ai.createSession({ + initialState: { + userName: 'Pavel', + }, +}); +``` + +You can then start a chat within the session: + +```ts +const chat = session.chat(); +``` + +To modify the session state based on how the chat unfolds, define +[tools](tool-calling) and include them with your requests: + +```ts +const changeUserName = ai.defineTool( + { + name: 'changeUserName', + description: 'can be used to change user name', + inputSchema: z.object({ + newUserName: z.string(), + }), + }, + async (input) => { + await ai.currentSession().updateState({ + userName: input.newUserName, + }); + return 'changed username to ${input.newUserName}'; + } +); +``` + +```ts +const chat = session.chat({ + model: gemini15Pro, + tools: [changeUserName], +}); +await chat.send('change user name to Kevin'); +``` + +## Multi-thread sessions + +A single session can contain multiple chat threads. Each thread has its own +message history, but they share a single session state. + +```ts +const lawyerChat = session.chat('lawyerThread', { + system: 'talk like a lawyer', +}); +const pirateChat = session.chat('pirateThread', { + system: 'talk like a pirate', +}); +``` + +## Session persistence (EXPERIMENTAL) + +When you initialize a new chat or session, it's configured by default to store +the session in memory only. This is adequate when the session needs to persist +only for the duration of a single invocation of your program, as in the sample +chatbot from the beginning of this page. However, when integrating LLM chat into +an application, you will usually deploy your content generation logic as +stateless web API endpoints. For persistent chats to work under this setup, you +will need to implement some kind of session storage that can persist state +across invocations of your endpoints. + +To add persistence to a chat session, you need to implement Genkit's +`SessionStore` interface. Here is an example implementation that saves session +state to individual JSON files: + +```ts +class JsonSessionStore implements SessionStore { + async get(sessionId: string): Promise | undefined> { + try { + const s = await readFile(`${sessionId}.json`, { encoding: 'utf8' }); + const data = JSON.parse(s); + return data; + } catch { + return undefined; + } + } + + async save(sessionId: string, sessionData: SessionData): Promise { + const s = JSON.stringify(sessionData); + await writeFile(`${sessionId}.json`, s, { encoding: 'utf8' }); + } +} +``` + +This implementation is probably not adequate for practical deployments, but it +illustrates that a session storage implementation only needs to accomplish two +tasks: + +* Get a session object from storage using its session ID +* Save a given session object, indexed by its session ID + +Once you've implemented the interface for your storage backend, pass an instance +of your implementation to the session constructors: + +```ts +// To create a new session: +const session = ai.createSession({ + store: new JsonSessionStore(), +}); + +// Save session.id so you can restore the session the next time the +// user makes a request. +``` + +```ts +// If the user has a session ID saved, load the session instead of creating +// a new one: +const session = await ai.loadSession(sessionId, { + store: new JsonSessionStore(), +}); +``` diff --git a/docs/flows.md b/docs/flows.md index c572b9dc6..f6f79a83b 100644 --- a/docs/flows.md +++ b/docs/flows.md @@ -1,198 +1,289 @@ -# Flows +# Defining AI workflows + +The core of your app's AI features are generative model requests, but it's rare +that you can simply take user input, pass it to the model, and display the model +output back to the user. Usually, there are pre- and post-processing steps that +must accompany the model call. For example: + +* Retrieving contextual information to send with the model call +* Retrieving the history of the user's current session, for example in a chat + app +* Using one model to reformat the user input in a way that's suitable to pass + to another model +* Evaluating the "safety" of a model's output before presenting it to the user +* Combining the output of several models + +Every step of this workflow must work together for any AI-related task to +succeed. + +In Genkit, you represent this tightly-linked logic using a construction called a +flow. Flows are written just like functions, using ordinary TypeScript code, but +they add additional capabilities intended to ease the development of AI +features: + +* **Type safety**: Input and output schemas defined using Zod, which provides + both static and runtime type checking +* **Integration with developer UI**: Debug flows independently of your + application code using the developer UI. In the developer UI, you can run + flows and view traces for each step of the flow. +* **Simplified deployment**: Deploy flows directly as web API endpoints, using + Cloud Functions for Firebase or any platform that can host a web app. + +Unlike similar features in other frameworks, Genkit's flows are lightweight and +unobtrusive, and don't force your app to conform to any specific abstraction. +All of the flow's logic is written in standard TypeScript, and code inside a +flow doesn't need to be flow-aware. + +## Defining and calling flows + +In its simplest form, a flow just wraps a function. The following example wraps +a function that calls `generate()`: + +```ts +{% includecode github_path="firebase/genkit/js/doc-snippets/src/flows/index.ts" region_tag="ex01" adjust_indentation="auto" %} +``` -Flows are functions with some additional characteristics: they are strongly -typed, streamable, locally and remotely callable, and fully observable. -Firebase Genkit provides CLI and Developer UI tooling for working with flows -(running, debugging, etc). +Just by wrapping your `generate()` calls like this, you add some functionality: +doing so lets you run the flow from the Genkit CLI and from the developer UI, +and is a requirement for several of Genkit's features, including deployment and +observability (later sections discuss these topics). -## Defining flows +### Input and output schemas -```javascript -import { defineFlow } from '@genkit-ai/flow'; +One of the most important advantages Genkit flows have over directly calling a +model API is type safety of both inputs and outputs. When defining flows, you +can define schemas for them using Zod, in much the same way as you define the +output schema of a `generate()` call; however, unlike with `generate()`, you can +also specify an input schema. -export const menuSuggestionFlow = defineFlow( - { - name: 'menuSuggestionFlow', - }, - async (restaurantTheme) => { - const suggestion = makeMenuItemSuggestion(restaurantTheme); +Here's a refinement of the last example, which defines a flow that takes a +string as input and outputs an object: - return suggestion; - } -); +```ts +{% includecode github_path="firebase/genkit/js/doc-snippets/src/flows/index.ts" region_tag="ex02" adjust_indentation="auto" %} ``` -Input and output schemas for flows can be defined using `zod`. +Note that the schema of a flow does not necessarily have to line up with the +schema of the `generate()` calls within the flow (in fact, a flow might not even +contain `generate()` calls). Here's a variation of the example that passes a +schema to `generate()`, but uses the structured output to format a simple +string, which the flow returns. + +```ts +{% includecode github_path="firebase/genkit/js/doc-snippets/src/flows/index.ts" region_tag="ex03" adjust_indentation="auto" %} +``` + +### Calling flows + +Once you've defined a flow, you can call it from your Node.js code: -```javascript -import { defineFlow } from '@genkit-ai/flow'; -import * as z from 'zod'; +```ts +{% includecode github_path="firebase/genkit/js/doc-snippets/src/flows/index.ts" region_tag="ex04" adjust_indentation="auto" %} +``` + +The argument to the flow must conform to the input schema, if you defined one. -export const menuSuggestionFlow = defineFlow( - { - name: 'menuSuggestionFlow', - inputSchema: z.string(), - outputSchema: z.string(), - }, - async (restaurantTheme) => { - const suggestion = makeMenuItemSuggestion(input.restaurantTheme); +If you defined an output schema, the flow response will conform to it. For +example, if you set the output schema to `MenuItemSchema`, the flow output will +contain its properties: - return suggestion; - } -); +```ts +{% includecode github_path="firebase/genkit/js/doc-snippets/src/flows/index.ts" region_tag="ex05" adjust_indentation="auto" %} ``` -When schema is specified Genkit will validate the schema for inputs and outputs. +## Streaming flows -## Running flows +Flows support streaming using an interface similar to `generate()`'s streaming +interface. Streaming is useful when your flow generates a large amount of +output, because you can present the output to the user as it's being generated, +which improves the perceived responsiveness of your app. As a familiar example, +chat-based LLM interfaces often stream their responses to the user as they are +generated. -Run the flow by calling it directly like a normal function: +Here's an example of a flow that supports streaming: -```js -const response = await menuSuggestionFlow('French'); +```ts +{% includecode github_path="firebase/genkit/js/doc-snippets/src/flows/index.ts" region_tag="ex06" adjust_indentation="auto" %} ``` -You can use the CLI to run flows as well: +* The `streamSchema` option specifies the type of values your flow streams. + This does not necessarily need to be the same type as the `outputSchema`, + which is the type of the flow's complete output. +* `streamingCallback` is a callback function that takes a single parameter, of + the type specified by `streamSchema`. Whenever data becomes available within + your flow, send the data to the output stream by calling this function. Note + that `streamingCallback` is only defined if the caller of your flow + requested streaming output, so you need to check that it's defined before + calling it. -```posix-terminal -genkit flow:run menuSuggestionFlow '"French"' +In the above example, the values streamed by the flow are directly coupled to +the values streamed by the `generate()` call inside the flow. Although this is +often the case, it doesn't have to be: you can output values to the stream using +the callback as often as is useful for your flow. + +### Calling streaming flows + +Streaming flows are also callable, but they immediately return a response object +rather than a promise: + +```ts +{% includecode github_path="firebase/genkit/js/doc-snippets/src/flows/index.ts" region_tag="ex07" adjust_indentation="auto" %} ``` -### Streamed +The response object has a stream property, which you can use to iterate over the +streaming output of the flow as it's generated: -Here's a simple example of a flow that can stream values from a flow: +```ts +{% includecode github_path="firebase/genkit/js/doc-snippets/src/flows/index.ts" region_tag="ex08" adjust_indentation="auto" %} +``` -```javascript -export const menuSuggestionFlow = defineStreamingFlow( - { - name: 'menuSuggestionFlow', - streamSchema: z.string(), - }, - async (restaurantTheme, streamingCallback) => { - makeMenuItemSuggestionsAsync(restaurantTheme).subscribe((suggestion) => { - streamingCallback(suggestion); - }); - } -); +You can also get the complete output of the flow, as you can with a +non-streaming flow: + +```ts +{% includecode github_path="firebase/genkit/js/doc-snippets/src/flows/index.ts" region_tag="ex09" adjust_indentation="auto" %} ``` -To invoke a streaming flow, call it directly like a normal function and stream the results: +Note that the streaming output of a flow might not be the same type as the +complete output; the streaming output conforms to `streamSchema`, whereas the +complete output conforms to `outputSchema`. -```javascript -const { stream, output } = menuSuggestionFlow('French'); +## Running flows from the command line -for await (const suggestion of stream) { - console.log('suggestion', suggestion); -} +You can run flows from the command line using the Genkit CLI tool: -console.log('output', await output); +```posix-terminal +genkit flow:run menuSuggestionFlow '"French"' ``` -You can use the CLI to stream flows as well: +For streaming flows, you can print the streaming output to the console by adding +the `-s` flag: ```posix-terminal genkit flow:run menuSuggestionFlow '"French"' -s ``` -## Deploying flows +Running a flow from the command line is useful for testing a flow, or for +running flows that perform tasks needed on an ad hoc basis—for example, to +run a flow that ingests a document into your vector database. -If you want to be able to access your flow over HTTP you will need to deploy it -first. Genkit provides integrations for Cloud Functions for Firebase and -Express.js hosts such as Cloud Run. +## Debugging flows -Deployed flows support all the same features as local flows (like streaming and -observability). +One of the advantages of encapsulating AI logic within a flow is that you can +test and debug the flow independently from your app using the Genkit developer +UI. -### Cloud Function for Firebase +To start the developer UI, run the following commands from your project +directory: -To use flows with Cloud Functions for Firebase use the `firebase` plugin, replace `defineFlow` with `onFlow` and include an `authPolicy`. +```posix-terminal +export GENKIT_ENV=dev -```js -import { onFlow } from '@genkit-ai/firebase/functions'; -import { firebaseAuth } from '@genkit-ai/firebase/auth'; +npx genkit ui:start -export const menuSuggestionFlow = onFlow( - { - name: 'menuSuggestionFlow', - authPolicy: firebaseAuth((user) => { - if (!user.email_verified) { - throw new Error("Verified email required to run flow"); - } - } - }, - async (restaurantTheme) => { - // .... - } -); +npx tsx --watch your-code.ts ``` -### Express.js +From the **Run** tab of developer UI, you can run any of the flows defined in +your project: -To deploy flows using Cloud Run and similar services, define your flows using `defineFlow` and then call `startFlowsServer()`: +![Screenshot of the Flow runner](resources/devui-flows.png) -```js -import { defineFlow, startFlowsServer } from '@genkit-ai/flow'; +After you've run a flow, you can inspect a trace of the flow invocation by +either clicking **View trace** or looking on the **Inspect** tab. -export const menuSuggestionFlow = defineFlow( - { - name: 'menuSuggestionFlow', - }, - async (restaurantTheme) => { - // .... - } -); +In the trace viewer, you can see details about the execution of the entire flow, +as well as details for each of the individual steps within the flow. For +example, consider the following flow, which contains several generation +requests: -startFlowsServer(); +```ts +{% includecode github_path="firebase/genkit/js/doc-snippets/src/flows/index.ts" region_tag="ex10" adjust_indentation="auto" %} ``` -By default `startFlowsServer` will serve all the flows that you have defined in your codebase as HTTP endpoints (e.g. `http://localhost:3400/menuSuggestionFlow`). You can call a flow via a POST request as follows: +When you run this flow, the trace viewer shows you details about each generation +request including its output: -```posix-terminal -curl -X POST "http://localhost:3400/menuSuggestionFlow" -H "Content-Type: application/json" -d '{"data": "banana"}' +![Screenshot of the trace inspector](resources/devui-inspect.png) + +### Flow steps + +In the last example, you saw that each `generate()` call showed up as a separate +step in the trace viewer. Each of Genkit's fundamental actions show up as +separate steps of a flow: + +* `generate()` +* `Chat.send()` +* `embed()` +* `index()` +* `retrieve()` + +If you want to include code other than the above in your traces, you can do so +by wrapping the code in a `run()` call. You might do this for calls to +third-party libraries that are not Genkit-aware, or for any critical section of +code. + +For example, here's a flow with two steps: the first step retrieves a menu using +some unspecified method, and the second step includes the menu as context for a +`generate()` call. + +```ts +{% includecode github_path="firebase/genkit/js/doc-snippets/src/flows/index.ts" region_tag="ex11" adjust_indentation="auto" %} ``` -If needed, you can customize the flows server to serve a specific list of flows, as shown below. You can also specify a custom port (it will use the `PORT` environment variable if set) or specify CORS settings. +Because the retrieval step is wrapped in a `run()` call, it's included as a step +in the trace viewer: + +![Screenshot of an explicitly defined step in the trace inspector](resources/devui-runstep.png) + +## Deploying flows -```js -import { defineFlow, startFlowsServer } from '@genkit-ai/flow'; +You can deploy your flows directly as web API endpoints, ready for you to call +from your app clients. Deployment is discussed in detail on several other pages, +but this section gives brief overviews of your deployment options. -export const flowA = defineFlow({ name: 'flowA' }, async (subject) => { - // .... -}); +### Cloud Functions for Firebase -export const flowB = defineFlow({ name: 'flowB' }, async (subject) => { - // .... -}); +To deploy flows with Cloud Functions for Firebase, use the `firebase` plugin. In +your flow definitions, replace `defineFlow` with `onFlow` and include an +`authPolicy`. -startFlowsServer({ - flows: [flowB], - port: 4567, - cors: { - origin: '*', - }, -}); +```ts +{% includecode github_path="firebase/genkit/js/doc-snippets/src/flows/firebase.ts" region_tag="ex" adjust_indentation="auto" %} ``` -## Flow observability +For more information, see the following pages: -Sometimes when using 3rd party SDKs that that are not instrumented for observability, you might want to see them as a separate trace step in the Developer UI. All you need to do is wrap the code in the `run` function. +* [Deploy with Firebase](/docs/genkit/firebase) +* [Authorization and integrity](/docs/genkit/auth#cloud_functions_for_firebase_integration) +* [Firebase plugin](/docs/genkit/plugins/firebase) -```js -import { defineFlow, run } from '@genkit-ai/flow'; +### Express.js -export const menuSuggestionFlow = defineFlow( - { - name: 'menuSuggestionFlow', - outputSchema: z.array(s.string()), - }, - async (restaurantTheme) => { - const themes = await run('find-similar-themes', async () => { - return await findSimilarRestaurantThemes(restaurantTheme); - }); +To deploy flows using any Node.js hosting platform, such as Cloud Run, define +your flows using `defineFlow()` and then call `startFlowServer()`: - const suggestions = makeMenuItemSuggestions(themes); +```ts +{% includecode github_path="firebase/genkit/js/doc-snippets/src/flows/express.ts" region_tag="ex01" adjust_indentation="auto" %} +``` - return suggestions; - } -); +By default, `startFlowServer` will serve all the flows defined in your codebase +as HTTP endpoints (for example, `http://localhost:3400/menuSuggestionFlow`). You +can call a flow with a POST request as follows: + +```posix-terminal +curl -X POST "http://localhost:3400/menuSuggestionFlow" \ + -H "Content-Type: application/json" -d '{"data": "banana"}' ``` + +If needed, you can customize the flows server to serve a specific list of flows, +as shown below. You can also specify a custom port (it will use the PORT +environment variable if set) or specify CORS settings. + +```ts +{% includecode github_path="firebase/genkit/js/doc-snippets/src/flows/express.ts" region_tag="ex02" adjust_indentation="auto" %} +``` + +For information on deploying to specific platforms, see +[Deploy with Cloud Run](/docs/genkit/cloud-run) and +[Deploy flows to any Node.js platform](/docs/genkit/deploy-node). diff --git a/docs/migrating-from-0.5.md b/docs/migrating-from-0.5.md new file mode 100644 index 000000000..912b0ead3 --- /dev/null +++ b/docs/migrating-from-0.5.md @@ -0,0 +1,323 @@ +# Migrating from 0.5 + +Genkit 0.9 introduces a number of breaking changes alongside feature enhancements that improve overall functionality. If you have been developing applications with Genkit 0.5, you will need to update your application code when you upgrade to the latest version. This guide outlines the most significant changes and offers steps to migrate your existing applications smoothly. + +## 1. CLI Changes + +The command-line interface (CLI) has undergone significant updates in Genkit 0.9. The command to start Genkit has changed, and the CLI has been separated into its own standalone package, which you now need to install separately. + +To install the CLI: + +```posix-terminal +npm i -g genkit-cli +``` + +**Old Command:** + +```posix-terminal +genkit start +``` + +This command starts both the flow server and the Dev UI in a single command. + +**New Commands:** + +Now, you must start the dev UI and the flow servers as separate steps. + +To start the dev UI: + +```posix-terminal +genkit ui:start +``` + +Once the UI is running, start the flow server: + +```posix-terminal +GENKIT_ENV=dev tsx --watch path/to/index.ts +``` + +Starting the flow server this way makes it easier for you to attach a debugger to your code. Be sure to set `GENKIT_ENV=dev` in your debugger’s startup configuration. + +The Dev UI will interact directly with the flow server to figure out which flows are registered and allow you to invoke them directly with sample inputs. + +## 2. Simplified packages and imports + +Previously, the Genkit libraries were separated into several modules, which you needed to install and import individually. These modules have now been consolidated into a single import. In addition, the Zod module is now re-exported by Genkit. + +**Old:** + +```posix-terminal +npm i @genkit-ai/core @genkit-ai/ai @genkit-ai/flow @genkit-ai/dotprompt +``` + +**New:** + +```posix-terminal +npm i genkit +``` + +**Old:** + +```js +import { … } from '@genkit-ai/ai'; +import { … } from '@genkit-ai/core'; +import { … } from '@genkit-ai/flow'; +import * as z from 'zod'; +``` + +**New:** + +```js +import { genkit, z } from 'genkit'; +``` + +Genkit plugins still must be installed and imported individually. + +## 3. Configuring Genkit + +Previously, initializing Genkit was done once globally by calling the `configureGenkit` function. Genkit resources (flows, tools, prompts, etc.) would all automatically be wired with this global configuration. + +Genkit 0.9 introduces `Genkit` instances, each of which encapsulates a configuration. See the following examples: + +**Old:** + +```js +import { configureGenkit } from '@genkit-ai/core'; + +configureGenkit({ + telemetry: { + instrumentation: ..., + logger: ... + } +}); +``` + +**New:** + +```js +import { genkit } from 'genkit'; +import { logger } from 'genkit/logging'; +import { enableFirebaseTelemetry } from '@genkit-ai/firebase'; + +logger.setLogLevel('debug'); +enableFirebaseTelemetry({...}); + +const ai = genkit({ ... }); +``` + +Let’s break it down: +- `configureGenkit()` has been replaced with `genkit()`, and it returns a configured `Genkit` instance rather than setting up configurations globally. +- The Genkit initialization function is now in the `genkit` package. +- Logging and telemetry are still configured globally using their own explicit methods. These configurations apply uniformly across all `Genkit` instances. + +## 4. Defining flows and starting the flow server explicitly + +Now that you have a configured `Genkit` instance, you will need to define your flows. All core developer-facing API methods like `defineFlow`, `defineTool`, and `onFlow` are now invoked through this instance. + +This is distinct from the previous way, where flows and tools were registered globally. + +**Old:** + +```js +import { defineFlow, defineTool, onFlow } from '@genkit-ai/core'; + +defineFlow(...); +defineTool(...); + +onFlow(...); +``` + +**New:** + +```js +// Define tools and flows +const sampleFlow = ai.defineFlow(...); +const sampleTool = ai.defineTool(...); + +// onFlow now takes the Genkit instance as first argument +// This registers the flow as a callable firebase function +onFlow(ai, ...); + +const flows = [ sampleFlow, ... ]; +// Start the flow server to make the registered flows callable over HTTP +ai.startFlowServer({flows}); +``` + +As of now, all flows that you want to make available need to be explicitly registered in the `flows` array above. + +## 5. Tools and Prompts must be statically defined + +In earlier versions of Genkit, you could dynamically define tools and prompts at runtime, directly from within a flow. + +In Genkit 0.9, this behavior is no longer allowed. Instead, you need to define all actions and flows outside of the flow’s execution (i.e. statically). + +This change enforces a stricter separation of action definitions from execution. + +If any of your code is defined dynamically, they need to be refactored. Otherwise, an error will be thrown at runtime when the flow is executed. + +**❌ DON'T:** + +```js +const flow = defineFlow({...}, async (input) => { + const tool = defineTool({...}); + await tool.call(...); +}); +``` + +**✅ DO:** + +```js +const tool = ai.defineTool({...}); + +const flow = ai.defineFlow({...}, async (input) => { + await tool.call(...); +}); +``` + +## 6. New API for Streaming Flows + +In Genkit 0.9, we have simplified the syntax for defining a streaming flow and invoking it. + +First, `defineFlow` and `defineStreamingFlow` have been separated. If you have a flow that is meant to be streamed, you will have to update your code to define it via `defineStreamingFlow`. + +Second, instead of calling separate `stream()` and `response()` functions, both stream and response are now values returned directly from the flow. This change simplifies flow streaming. + +**Old:** + +```js +import { defineFlow, streamFlow } from '@genkit-ai/flow'; + +const myStreamingFlow = defineFlow(...); +const { stream, output } = await streamFlow(myStreamingFlow, ...); + +for await (const chunk of stream()) { + console.log(chunk); +} + +console.log(await output()); +``` + +**New:** + +```js +const myStreamingFlow = ai.defineStreamingFlow(...); +const { stream, response } = await myStreamingFlow(...); + +for await (const chunk of stream) { + console.log(chunk); +} + +console.log(await response); +``` + +## 7. GenerateResponse class methods replaced with getter properties + +Previously, you used to access the structured output or text of the response using class methods, like `output()` or `text()`. + +In Genkit 0.9, those methods have been replaced by getter properties. This simplifies working with responses. + +**Old:** + +```js +const response = await generate({ prompt: 'hi' }); +console.log(response.text()); +``` + +**New:** + +```js +const response = await ai.generate('hi'); +console.log(response.text); +``` +The same applies to `output`: + +**Old:** + +```js +console.log(response.output()); +``` + +**New:** + +```js +console.log(response.output); +``` + +## 8. Candidate Generation Eliminated + +Genkit 0.9 simplifies response handling by removing the `candidates` attribute. Previously, responses could contain multiple candidates, which you needed to handle explicitly. Now, only the first candidate is returned directly in a flat response. + +Any code that accesses the candidates directly will not work anymore. + +**Old:** + +```js +const response = await generate({ + messages: [ { role: 'user', content: ...} ] +}); +console.log(response.candidates); // previously you could access candidates directly +``` + +**New:** + +```js +const response = await ai.generate({ + messages: [ { role: 'user', content: ...} ] +}); +console.log(response.message); // single candidate is returned directly in a flat response +``` + +## 9. Generate API - Multi-Turn enhancements + +For multi-turn conversations, the old `toHistory()` method has been replaced by `messages`, further simplifying how conversation history is handled. + +**Old:** + +```js +const history = response.toHistory(); +``` + +**New:** + +```js +const response = await ai.generate({ + messages: [ { role: 'user', content: ...} ] +}); +const history = response.messages; +``` + +## 10. Streamlined Chat API + +In Genkit 0.9, the Chat API has been redesigned for easier session management and interaction. Here’s how you can leverage it for both synchronous and streaming chat experiences: + +```js +import { genkit } from 'genkit'; +import { gemini15Flash, googleAI } from '@genkit-ai/googleai'; + +const ai = genkit({ + plugins: [googleAI()], + model: gemini15Flash, +}); + +const session = ai.createSession({ store: firestoreSessionStore() }); +const chat = await session.chat({ system: 'talk like a pirate' }); + +let response = await chat.send('hi, my name is Pavel'); +console.log(response.text()); // "hi Pavel, I'm llm" + +// continue the conversation +response = await chat.send("what's my name"); +console.log(response.text()); // "Pavel" + +// can stream +const { response, stream } = await chat.sendStream('bye'); +for await (const chunk of stream) { + console.log(chunk.text()); +} +console.log((await response).text()); + +// can load session from the store +const prevSession = await ai.loadSession(session.id, { store }); +const prevChat = await prevSession.chat(); +await prevChat.send('bye'); +``` diff --git a/docs/multi-agent.md b/docs/multi-agent.md new file mode 100644 index 000000000..6ae92cdbb --- /dev/null +++ b/docs/multi-agent.md @@ -0,0 +1,53 @@ +# Building multi-agent systems + +A powerful application of large language models are LLM-powered agents. An agent +is a system that can carry out complex tasks by planning how to break tasks into +smaller ones, and (with the help of [tool calling](tool-calling)) execute tasks +that interact with external resources such as databases or even physical +devices. + +Here are some excerpts from a very simple customer service agent built using a +single prompt and several tools: + +```ts +{% includecode github_path="firebase/genkit/js/doc-snippets/src/multi-agent/simple.ts" region_tag="tools" adjust_indentation="auto" %} +``` + +```ts +{% includecode github_path="firebase/genkit/js/doc-snippets/src/multi-agent/simple.ts" region_tag="chat" adjust_indentation="auto" %} +``` + +A simple architecture like the one shown above can be sufficient when your agent +only has a few capabilities. However, even for the limited example above, you +can see that there are some capabilities that customers would likely expect: for +example, listing the customer's current reservations, canceling a reservation, +and so on. As you build more and more tools to implement these additional +capabilities, you start to run into some problems: + +* The more tools you add, the more you stretch the model's ability to + consistently and correctly employ the right tool for the job. +* Some tasks might best be served through a more focused back and forth + between the user and the agent, rather than by a single tool call. +* Some tasks might benefit from a specialized prompt. For example, if your + agent is responding to an unhappy customer, you might want its tone to be + more business-like, whereas the agent that greets the customer initially can + have a more friendly and lighthearted tone. + +One approach you can use to deal with these issues that arise when building +complex agents is to create many specialized agents and use a general purpose +agent to delegate tasks to them. Genkit supports this architecture by allowing +you to specify prompts as tools. Each prompt represents a single specialized +agent, with its own set of tools available to it, and those agents are in turn +available as tools to your single orchestration agent, which is the primary +interface with the user. + +Here's what an expanded version of the previous example might look like as a +multi-agent system: + +```ts +{% includecode github_path="firebase/genkit/js/doc-snippets/src/multi-agent/multi.ts" region_tag="agents" adjust_indentation="auto" %} +``` + +```ts +{% includecode github_path="firebase/genkit/js/doc-snippets/src/multi-agent/multi.ts" region_tag="chat" adjust_indentation="auto" %} +``` diff --git a/docs/resources/devui-flows.png b/docs/resources/devui-flows.png new file mode 100644 index 000000000..abbf17502 Binary files /dev/null and b/docs/resources/devui-flows.png differ diff --git a/docs/resources/devui-inspect.png b/docs/resources/devui-inspect.png index a47dd8e2d..1cd3b84af 100644 Binary files a/docs/resources/devui-inspect.png and b/docs/resources/devui-inspect.png differ diff --git a/docs/resources/devui-runstep.png b/docs/resources/devui-runstep.png new file mode 100644 index 000000000..df63a49d4 Binary files /dev/null and b/docs/resources/devui-runstep.png differ diff --git a/genkit-tools/cli/package.json b/genkit-tools/cli/package.json index ebcdcf7bd..1fa5ca68d 100644 --- a/genkit-tools/cli/package.json +++ b/genkit-tools/cli/package.json @@ -1,6 +1,6 @@ { "name": "genkit-cli", - "version": "0.9.0-dev.2", + "version": "0.9.0-dev.4", "description": "CLI for interacting with the Google Genkit AI framework", "license": "Apache-2.0", "keywords": [ diff --git a/genkit-tools/cli/src/cli.ts b/genkit-tools/cli/src/cli.ts index 033064b5a..0a256c639 100644 --- a/genkit-tools/cli/src/cli.ts +++ b/genkit-tools/cli/src/cli.ts @@ -29,6 +29,7 @@ import { evalRun } from './commands/eval-run'; import { flowBatchRun } from './commands/flow-batch-run'; import { flowRun } from './commands/flow-run'; import { getPluginCommands, getPluginSubCommand } from './commands/plugins'; +import { start } from './commands/start'; import { uiStart } from './commands/ui-start'; import { uiStop } from './commands/ui-stop'; import { version } from './utils/version'; @@ -48,6 +49,7 @@ const commands: Command[] = [ evalRun, evalFlow, config, + start, ]; /** Main entry point for CLI. */ diff --git a/genkit-tools/cli/src/commands/eval-extract-data.ts b/genkit-tools/cli/src/commands/eval-extract-data.ts index c5a4449a4..3679091a6 100644 --- a/genkit-tools/cli/src/commands/eval-extract-data.ts +++ b/genkit-tools/cli/src/commands/eval-extract-data.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EnvTypes, EvalInput, TraceData } from '@genkit-ai/tools-common'; +import { EvalInput, TraceData } from '@genkit-ai/tools-common'; import { generateTestCaseId, getEvalExtractors, @@ -25,7 +25,6 @@ import { writeFile } from 'fs/promises'; import { runWithManager } from '../utils/manager-utils'; interface EvalDatasetOptions { - env: EnvTypes; output?: string; maxRows: string; label?: string; @@ -35,7 +34,6 @@ interface EvalDatasetOptions { export const evalExtractData = new Command('eval:extractData') .description('extract evaludation data for a given flow from the trace store') .argument('', 'name of the flow to run') - .option('--env ', 'environment (dev/prod)', 'dev') .option( '--output ', 'name of the output file to store the extracted data' @@ -51,7 +49,6 @@ export const evalExtractData = new Command('eval:extractData') let continuationToken = undefined; while (dataset.length < parseInt(options.maxRows)) { const response = await manager.listTraces({ - env: options.env, limit: parseInt(options.maxRows), continuationToken, }); diff --git a/genkit-tools/cli/src/commands/flow-batch-run.ts b/genkit-tools/cli/src/commands/flow-batch-run.ts index d803ff13d..9178262b0 100644 --- a/genkit-tools/cli/src/commands/flow-batch-run.ts +++ b/genkit-tools/cli/src/commands/flow-batch-run.ts @@ -22,7 +22,7 @@ import { import { logger } from '@genkit-ai/tools-common/utils'; import { Command } from 'commander'; import { readFile, writeFile } from 'fs/promises'; -import { runWithManager, waitForFlowToComplete } from '../utils/manager-utils'; +import { runWithManager } from '../utils/manager-utils'; interface FlowBatchRunOptions { wait?: boolean; @@ -76,14 +76,6 @@ export const flowBatchRun = new Command('flow:batchRun') }) ).result as FlowState; - if (!state.operation.done && options.wait) { - logger.info('Started flow run, waiting for it to complete...'); - state = await waitForFlowToComplete( - manager, - flowName, - state.flowId - ); - } logger.info( 'Flow operation:\n' + JSON.stringify(state.operation, undefined, ' ') diff --git a/genkit-tools/cli/src/commands/start.ts b/genkit-tools/cli/src/commands/start.ts new file mode 100644 index 000000000..c6a3c7490 --- /dev/null +++ b/genkit-tools/cli/src/commands/start.ts @@ -0,0 +1,75 @@ +/** + * 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 { startServer } from '@genkit-ai/tools-common/server'; +import { logger } from '@genkit-ai/tools-common/utils'; +import { spawn } from 'child_process'; +import { Command } from 'commander'; +import getPort, { makeRange } from 'get-port'; +import { startManager } from '../utils/manager-utils'; + +interface RunOptions { + noui?: boolean; + port?: string; +} + +/** Command to run code in dev mode and/or the Dev UI. */ +export const start = new Command('start') + .description('runs a command in Genkit dev mode') + .option('-n, --noui', 'do not start the Dev UI', false) + .option('-p, --port', 'port for the Dev UI') + .action(async (options: RunOptions) => { + let runtimePromise = Promise.resolve(); + if (start.args.length > 0) { + runtimePromise = new Promise((urlResolver, reject) => { + const appProcess = spawn(start.args[0], start.args.slice(1), { + env: { ...process.env, GENKIT_ENV: 'dev' }, + }); + + appProcess.stderr?.pipe(process.stderr); + appProcess.stdout?.pipe(process.stdout); + process.stdin?.pipe(appProcess.stdin); + + appProcess.on('error', (error): void => { + console.log(`Error in app process: ${error}`); + reject(error); + process.exitCode = 1; + }); + appProcess.on('exit', (code) => { + urlResolver(undefined); + }); + }); + } + + let uiPromise = Promise.resolve(); + if (!options.noui) { + let port: number; + if (options.port) { + port = Number(options.port); + if (isNaN(port) || port < 0) { + logger.error(`"${options.port}" is not a valid port number`); + return; + } + } else { + port = await getPort({ port: makeRange(4000, 4099) }); + } + uiPromise = startManager(true).then((manager) => + startServer(manager, port) + ); + } + + await Promise.all([runtimePromise, uiPromise]); + }); diff --git a/genkit-tools/cli/src/utils/manager-utils.ts b/genkit-tools/cli/src/utils/manager-utils.ts index ab4625475..cecbb98d1 100644 --- a/genkit-tools/cli/src/utils/manager-utils.ts +++ b/genkit-tools/cli/src/utils/manager-utils.ts @@ -18,11 +18,7 @@ import { LocalFileTraceStore, startTelemetryServer, } from '@genkit-ai/telemetry-server'; -import { - FlowInvokeEnvelopeMessage, - FlowState, - Status, -} from '@genkit-ai/tools-common'; +import { Status } from '@genkit-ai/tools-common'; import { GenkitToolsError, RuntimeManager, @@ -89,43 +85,3 @@ export async function runWithManager( logger.error(`${error.stack}`); } } - -/** - * Poll and wait for the flow to fully complete. - */ -export async function waitForFlowToComplete( - manager: RuntimeManager, - flowName: string, - flowId: string -): Promise { - let state; - // eslint-disable-next-line no-constant-condition - while (true) { - state = await getFlowState(manager, flowName, flowId); - if (state.operation.done) { - break; - } - await new Promise((r) => setTimeout(r, 1000)); - } - return state; -} - -/** - * Retrieve the flow state. - */ -export async function getFlowState( - manager: RuntimeManager, - flowName: string, - flowId: string -): Promise { - return ( - await manager.runAction({ - key: `/flow/${flowName}`, - input: { - state: { - flowId, - }, - } as FlowInvokeEnvelopeMessage, - }) - ).result as FlowState; -} diff --git a/genkit-tools/common/package.json b/genkit-tools/common/package.json index 9b39ee705..925b0c289 100644 --- a/genkit-tools/common/package.json +++ b/genkit-tools/common/package.json @@ -1,6 +1,6 @@ { "name": "@genkit-ai/tools-common", - "version": "0.9.0-dev.2", + "version": "0.9.0-dev.4", "scripts": { "compile": "tsc -b ./tsconfig.cjs.json ./tsconfig.esm.json ./tsconfig.types.json", "build:clean": "rimraf ./lib", diff --git a/genkit-tools/common/src/api/reflection.ts b/genkit-tools/common/src/api/reflection.ts index 4393634cf..2a7e46f94 100644 --- a/genkit-tools/common/src/api/reflection.ts +++ b/genkit-tools/common/src/api/reflection.ts @@ -76,79 +76,6 @@ registry.registerPath({ }, }, }); -registry.registerPath({ - method: 'get', - path: '/api/envs/{env}/traces', - summary: 'Retrieves all traces for a given environment (e.g. dev or prod).', - request: { - params: apis.ListTracesRequestSchema, - }, - responses: { - '200': { - description: 'Success', - content: { - 'application/json': { - schema: z.array(TraceDataSchema), - }, - }, - }, - }, -}); -registry.registerPath({ - method: 'get', - path: '/api/envs/{env}/traces/{traceId}', - summary: 'Retrieves traces for the given environment.', - request: { - params: apis.GetTraceRequestSchema, - }, - responses: { - '200': { - description: 'Success', - content: { - 'application/json': { - schema: TraceDataSchema, - }, - }, - }, - }, -}); -registry.registerPath({ - method: 'get', - path: '/api/envs/{env}/flowStates', - summary: - 'Retrieves all flow states for a given environment (e.g. dev or prod).', - request: { - params: apis.ListFlowStatesRequestSchema, - }, - responses: { - '200': { - description: 'Success', - content: { - 'application/json': { - schema: z.array(FlowStateSchema), - }, - }, - }, - }, -}); -registry.registerPath({ - method: 'get', - path: '/api/envs/{env}/flowStates/{flowId}', - summary: 'Retrieves a flow state for the given ID.', - request: { - params: apis.GetFlowStateRequestSchema, - }, - responses: { - '200': { - description: 'Success', - content: { - 'application/json': { - schema: FlowStateSchema, - }, - }, - }, - }, -}); const generator = new OpenApiGeneratorV3(registry.definitions); const document = generator.generateDocument({ diff --git a/genkit-tools/common/src/eval/evaluate.ts b/genkit-tools/common/src/eval/evaluate.ts index a0727a95b..28654afaf 100644 --- a/genkit-tools/common/src/eval/evaluate.ts +++ b/genkit-tools/common/src/eval/evaluate.ts @@ -333,10 +333,6 @@ async function gatherEvalInput(params: { } const trace = await manager.getTrace({ - // TODO: We should consider making this a argument and using it to - // to control which tracestore environment is being used when - // running a flow. - env: 'dev', traceId, }); diff --git a/genkit-tools/common/src/manager/manager.ts b/genkit-tools/common/src/manager/manager.ts index 068e06eb5..9bcfb3c9f 100644 --- a/genkit-tools/common/src/manager/manager.ts +++ b/genkit-tools/common/src/manager/manager.ts @@ -223,12 +223,12 @@ export class RuntimeManager { } /** - * Retrieves all traces for a given environment (e.g. dev or prod). + * Retrieves all traces */ async listTraces( input: apis.ListTracesRequest ): Promise { - const { env, limit, continuationToken } = input; + const { limit, continuationToken } = input; let query = ''; if (limit) { query += `limit=${limit}`; @@ -243,10 +243,7 @@ export class RuntimeManager { const response = await axios .get(`${this.telemetryServerUrl}/api/traces?${query}`) .catch((err) => - this.httpErrorHandler( - err, - `Error listing traces for env='${env}', query='${query}'.` - ) + this.httpErrorHandler(err, `Error listing traces for query='${query}'.`) ); return apis.ListTracesResponseSchema.parse(response.data); @@ -256,13 +253,13 @@ export class RuntimeManager { * Retrieves a trace for a given ID. */ async getTrace(input: apis.GetTraceRequest): Promise { - const { env, traceId } = input; + const { traceId } = input; const response = await axios .get(`${this.telemetryServerUrl}/api/traces/${traceId}`) .catch((err) => this.httpErrorHandler( err, - `Error getting trace for traceId='${traceId}', env='${env}'.` + `Error getting trace for traceId='${traceId}'` ) ); @@ -288,6 +285,7 @@ export class RuntimeManager { private async setupRuntimesWatcher() { try { const runtimesDir = await findRuntimesDir(); + await fs.mkdir(runtimesDir, { recursive: true }); const watcher = chokidar.watch(runtimesDir, { persistent: true, ignoreInitial: false, @@ -383,13 +381,11 @@ export class RuntimeManager { } /** - * Removes a runtime from the maps and tries to delete the file (best effort). + * Removes the runtime file which will trigger the removal watcher. */ private async removeRuntime(fileName: string) { const runtime = this.filenameToRuntimeMap[fileName]; if (runtime) { - delete this.filenameToRuntimeMap[fileName]; - delete this.idToFileMap[runtime.id]; try { const runtimesDir = await findRuntimesDir(); const runtimeFilePath = path.join(runtimesDir, fileName); diff --git a/genkit-tools/common/src/plugin/plugins.ts b/genkit-tools/common/src/plugin/plugins.ts index 26f094c41..51c9a82a7 100644 --- a/genkit-tools/common/src/plugin/plugins.ts +++ b/genkit-tools/common/src/plugin/plugins.ts @@ -16,7 +16,6 @@ import { execSync } from 'child_process'; import * as clc from 'colorette'; -import { createInterface } from 'readline/promises'; import { z } from 'zod'; const SupportedFlagValuesSchema = z.union([ @@ -67,11 +66,6 @@ export type SpecialAction = keyof z.infer; const SEPARATOR = '==========================='; -const readline = createInterface({ - input: process.stdin, - output: process.stdout, -}); - /** * Executes the command given, returning the contents of STDOUT. * @@ -91,20 +85,3 @@ export function cliCommand(command: string, options?: string): void { console.log(`${SEPARATOR}\n`); } - -/** - * Utility function to prompt user for sensitive operations. - */ -export async function promptContinue( - message: string, - dfault: boolean -): Promise { - console.log(message); - const opts = dfault ? 'Y/n' : 'y/N'; - const r = await readline.question(`${clc.bold('Continue')}? (${opts}) `); - if (r === '') { - return dfault; - } - - return r.toLowerCase() === 'y'; -} diff --git a/genkit-tools/common/src/server/server.ts b/genkit-tools/common/src/server/server.ts index 60ddbecf6..73237a859 100644 --- a/genkit-tools/common/src/server/server.ts +++ b/genkit-tools/common/src/server/server.ts @@ -42,7 +42,7 @@ const API_BASE_PATH = '/api'; /** * Starts up the Genkit Tools server which includes static files for the UI and the Tools API. */ -export async function startServer(manager: RuntimeManager, port: number) { +export function startServer(manager: RuntimeManager, port: number) { let server: Server; const app = express(); @@ -158,4 +158,8 @@ export async function startServer(manager: RuntimeManager, port: number) { const uiUrl = 'http://localhost:' + port; logger.info(`${clc.green(clc.bold('Genkit Developer UI:'))} ${uiUrl}`); }); + + return new Promise((resolve) => { + server.once('close', resolve); + }); } diff --git a/genkit-tools/common/src/types/apis.ts b/genkit-tools/common/src/types/apis.ts index f1889b050..aebce4c36 100644 --- a/genkit-tools/common/src/types/apis.ts +++ b/genkit-tools/common/src/types/apis.ts @@ -20,7 +20,6 @@ import { EvalInferenceInputSchema, EvalRunKeySchema, } from './eval'; -import { FlowStateSchema } from './flow'; import { GenerationCommonConfigSchema, MessageSchema, @@ -33,14 +32,7 @@ import { TraceDataSchema } from './trace'; * It's used directly in the generation of the Reflection API OpenAPI spec. */ -export const EnvTypesSchema = z - .enum(['dev', 'prod']) - .describe('Supported environments in the runtime.'); - -export type EnvTypes = z.infer; - export const ListTracesRequestSchema = z.object({ - env: EnvTypesSchema.optional(), limit: z.number().optional(), continuationToken: z.string().optional(), }); @@ -55,36 +47,11 @@ export const ListTracesResponseSchema = z.object({ export type ListTracesResponse = z.infer; export const GetTraceRequestSchema = z.object({ - env: EnvTypesSchema, traceId: z.string().describe('ID of the trace.'), }); export type GetTraceRequest = z.infer; -export const ListFlowStatesRequestSchema = z.object({ - env: EnvTypesSchema.optional(), - limit: z.number().optional(), - continuationToken: z.string().optional(), -}); - -export type ListFlowStatesRequest = z.infer; - -export const ListFlowStatesResponseSchema = z.object({ - flowStates: z.array(FlowStateSchema), - continuationToken: z.string().optional(), -}); - -export type ListFlowStatesResponse = z.infer< - typeof ListFlowStatesResponseSchema ->; - -export const GetFlowStateRequestSchema = z.object({ - env: EnvTypesSchema, - flowId: z.string().describe('ID of the flow state.'), -}); - -export type GetFlowStateRequest = z.infer; - export const RunActionRequestSchema = z.object({ key: z .string() diff --git a/genkit-tools/package.json b/genkit-tools/package.json index 641914d51..c80bba71c 100644 --- a/genkit-tools/package.json +++ b/genkit-tools/package.json @@ -1,18 +1,15 @@ { "private": true, - "version": "0.5.10", "scripts": { "preinstall": "npx only-allow pnpm", - "build": "pnpm install && pnpm build:common && pnpm build:telemetry-server && pnpm build:cli && pnpm build:plugins", + "build": "pnpm install && pnpm build:common && pnpm build:telemetry-server && pnpm build:cli", "build:cli": "cd cli && pnpm build", "build:telemetry-server": "cd telemetry-server && pnpm build", "build:common": "cd common && pnpm build && cd .. && pnpm export:schemas", - "build:plugins": "pnpm -r --workspace-concurrency 8 -F \"./plugins/**\" build", "export:schemas": "npx tsx scripts/schema-exporter.ts .", - "pack:all": "pnpm run pack:cli && pnpm run pack:telemetry-server && pnpm run pack:common && pnpm run pack:plugins", + "pack:all": "pnpm run pack:cli && pnpm run pack:telemetry-server && pnpm run pack:common", "pack:common": "cd common && pnpm pack --pack-destination ../../dist", "pack:cli": "cd cli && pnpm pack --pack-destination ../../dist", - "pack:plugins": "for i in plugins/*/; do cd $i && pnpm pack --pack-destination ../../../dist && cd ../..; done", "pack:telemetry-server": "cd telemetry-server && pnpm pack --pack-destination ../../dist" }, "devDependencies": { diff --git a/genkit-tools/plugins/firebase/LICENSE b/genkit-tools/plugins/firebase/LICENSE deleted file mode 100644 index 26a870243..000000000 --- a/genkit-tools/plugins/firebase/LICENSE +++ /dev/null @@ -1,203 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. - \ No newline at end of file diff --git a/genkit-tools/plugins/firebase/package.json b/genkit-tools/plugins/firebase/package.json deleted file mode 100644 index 0246c7628..000000000 --- a/genkit-tools/plugins/firebase/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "@genkit-ai/tools-plugin-firebase", - "version": "0.5.10", - "scripts": { - "compile": "tsc -b ./tsconfig.cjs.json ./tsconfig.esm.json ./tsconfig.types.json", - "build:clean": "rimraf ./lib", - "build": "npm-run-all build:clean compile", - "build:watch": "tsc -b ./tsconfig.cjs.json ./tsconfig.esm.json ./tsconfig.types.json --watch" - }, - "devDependencies": { - "@genkit-ai/tools-common": "workspace:*", - "@types/node": "^20.11.19", - "npm-run-all": "^4.1.5", - "rimraf": "^6.0.1", - "typescript": "^4.9.0" - }, - "types": "lib/types/index.d.ts", - "exports": { - ".": { - "types": "./lib/types/firebase.d.ts", - "require": "./lib/cjs/firebase.js", - "import": "./lib/esm/firebase.js", - "default": "./lib/esm/firebase.js" - } - }, - "typesVersions": { - "*": { - "firebase": [ - "lib/types/firebase" - ] - } - }, - "dependencies": { - "colorette": "^2.0.20" - } -} diff --git a/genkit-tools/plugins/firebase/src/firebase.ts b/genkit-tools/plugins/firebase/src/firebase.ts deleted file mode 100644 index 4def97d78..000000000 --- a/genkit-tools/plugins/firebase/src/firebase.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * 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 { - SupportedFlagValues, - ToolPlugin, - cliCommand, - promptContinue, -} from '@genkit-ai/tools-common/plugin'; -import * as clc from 'colorette'; - -export const FirebaseTools: ToolPlugin = { - name: 'Firebase', - keyword: 'firebase', - actions: [], - subCommands: { - login: { - hook: login, - args: [ - { - flag: '--reauth', - description: 'Reauthenticate using current credentials', - defaultValue: false, - }, - ], - }, - deploy: { - hook: deploy, - args: [ - { - flag: '--project ', - description: 'Project ID to deploy to (optional)', - defaultValue: '', - }, - ], - }, - }, -}; - -async function login( - args?: Record -): Promise { - const cont = await promptContinue( - 'Genkit will use the Firebase Tools CLI to log in to your Google ' + - 'account using OAuth, in order to perform administrative tasks', - true - ); - if (!cont) return; - - try { - cliCommand('firebase', `login ${args?.reauth ? '--reauth' : ''}`); - } catch (e) { - errorMessage( - 'Unable to complete login. Make sure the Firebase Tools CLI is ' + - `installed and you're able to open a browser.` - ); - return; - } - - console.log(`${clc.bold('Successfully signed in to Firebase CLI.')}`); -} - -async function deploy( - args?: Record -): Promise { - const cont = await promptContinue( - 'Genkit will use the Firebase Tools CLI to deploy your flow to Cloud ' + - 'Functions for Firebase', - true - ); - if (!cont) return; - - try { - cliCommand( - 'firebase', - `deploy ${args?.project ? '--project ' + args.project : ''}` - ); - } catch (e) { - errorMessage( - 'Unable to complete login. Make sure the Firebase Tools CLI is ' + - `installed and you've already logged in with 'genkit login firebase'. ` + - 'Make sure you have a firebase.json file configured for this project.' - ); - return; - } -} - -function errorMessage(msg: string): void { - console.error(clc.bold(clc.red('Error:')) + ' ' + msg); -} diff --git a/genkit-tools/plugins/firebase/tsconfig.cjs.json b/genkit-tools/plugins/firebase/tsconfig.cjs.json deleted file mode 100644 index fdcebf2b8..000000000 --- a/genkit-tools/plugins/firebase/tsconfig.cjs.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./lib/cjs", - "module": "commonjs" - } -} diff --git a/genkit-tools/plugins/firebase/tsconfig.esm.json b/genkit-tools/plugins/firebase/tsconfig.esm.json deleted file mode 100644 index f007810c3..000000000 --- a/genkit-tools/plugins/firebase/tsconfig.esm.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./lib/esm", - "module": "esnext" - } -} diff --git a/genkit-tools/plugins/firebase/tsconfig.json b/genkit-tools/plugins/firebase/tsconfig.json deleted file mode 100644 index a47ba773d..000000000 --- a/genkit-tools/plugins/firebase/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "module": "ES2022", - "moduleResolution": "Node", - "outDir": "lib/esm", - "esModuleInterop": true, - "rootDirs": ["src"] - }, - "include": ["src"] -} diff --git a/genkit-tools/plugins/firebase/tsconfig.types.json b/genkit-tools/plugins/firebase/tsconfig.types.json deleted file mode 100644 index 4cc6da97b..000000000 --- a/genkit-tools/plugins/firebase/tsconfig.types.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./lib/types", - "declaration": true, - "emitDeclarationOnly": true - } -} diff --git a/genkit-tools/plugins/google/LICENSE b/genkit-tools/plugins/google/LICENSE deleted file mode 100644 index 26a870243..000000000 --- a/genkit-tools/plugins/google/LICENSE +++ /dev/null @@ -1,203 +0,0 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. - \ No newline at end of file diff --git a/genkit-tools/plugins/google/package.json b/genkit-tools/plugins/google/package.json deleted file mode 100644 index 218e3e910..000000000 --- a/genkit-tools/plugins/google/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "@genkit-ai/tools-plugin-google-cloud", - "version": "0.5.10", - "scripts": { - "compile": "tsc -b ./tsconfig.cjs.json ./tsconfig.esm.json ./tsconfig.types.json", - "build:clean": "rimraf ./lib", - "build": "npm-run-all build:clean compile", - "build:watch": "tsc -b ./tsconfig.cjs.json ./tsconfig.esm.json ./tsconfig.types.json --watch" - }, - "devDependencies": { - "@genkit-ai/tools-common": "workspace:*", - "@types/node": "^20.11.19", - "npm-run-all": "^4.1.5", - "rimraf": "^6.0.1", - "typescript": "^4.9.0" - }, - "types": "lib/types/index.d.ts", - "exports": { - ".": { - "types": "./lib/types/google.d.ts", - "require": "./lib/cjs/google.js", - "import": "./lib/esm/google.js", - "default": "./lib/esm/google.js" - } - }, - "typesVersions": { - "*": { - "google": [ - "lib/types/google" - ] - } - }, - "dependencies": { - "colorette": "^2.0.20" - } -} diff --git a/genkit-tools/plugins/google/src/google.ts b/genkit-tools/plugins/google/src/google.ts deleted file mode 100644 index 336062ba9..000000000 --- a/genkit-tools/plugins/google/src/google.ts +++ /dev/null @@ -1,114 +0,0 @@ -/** - * 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 { - SupportedFlagValues, - ToolPlugin, - cliCommand, - promptContinue, -} from '@genkit-ai/tools-common/plugin'; -import * as clc from 'colorette'; - -export const GoogleCloudTools: ToolPlugin = { - name: 'Google Cloud', - keyword: 'google', - actions: [ - { - action: 'use-app-default-creds', - helpText: - 'Logs into Google and downloads application default credentials file', - args: [ - { - flag: '--project ', - description: 'GCP project to use; required', - }, - ], - hook: useApplicationDefaultCredentials, - }, - ], - subCommands: { - login: { - hook: login, - }, - }, -}; - -async function login(): Promise { - const cont = await promptContinue( - 'Genkit will use the GCloud CLI to log in to your Google account ' + - 'using OAuth, in order to perform administrative tasks', - true - ); - if (!cont) return; - - try { - cliCommand('gcloud', `auth login`); - } catch (e) { - errorMessage( - 'Unable to complete login. Make sure the gcloud CLI is ' + - `installed and you're able to open a browser.` - ); - return; - } - - console.log(`${clc.bold('Successfully signed in to GCloud.')}`); -} - -async function useApplicationDefaultCredentials( - opts?: Record -): Promise { - const project = opts?.project; - - if (!project || typeof project !== 'string') { - errorMessage( - 'Project not specified. Provide a project ID using the --project flag' - ); - return; - } - - const cont = await promptContinue( - 'Genkit will use the GCloud CLI to log in to Google and download ' + - `application default credentials for your project: ${clc.bold(project)}.`, - true - ); - if (!cont) return; - - try { - // Always supply the project ID so that we don't accidentally use the last- - // specified project ID configured with GCloud. Doing so may cause - // confusion since we're wrapping GCloud. - cliCommand('gcloud', `auth application-default login --project=${project}`); - } catch (e) { - errorMessage( - 'Unable to complete login. Make sure the gcloud CLI is ' + - `installed and you're able to open a browser.` - ); - return; - } - - console.log( - `${clc.bold( - 'Successfully signed in using application-default credentials.' - )}` - ); - console.log( - 'Goole Cloud SDKs will now automatically pick up your credentials during development.' - ); -} - -function errorMessage(msg: string): void { - console.error(clc.bold(clc.red('Error:')) + ' ' + msg); -} diff --git a/genkit-tools/plugins/google/tsconfig.cjs.json b/genkit-tools/plugins/google/tsconfig.cjs.json deleted file mode 100644 index fdcebf2b8..000000000 --- a/genkit-tools/plugins/google/tsconfig.cjs.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./lib/cjs", - "module": "commonjs" - } -} diff --git a/genkit-tools/plugins/google/tsconfig.esm.json b/genkit-tools/plugins/google/tsconfig.esm.json deleted file mode 100644 index f007810c3..000000000 --- a/genkit-tools/plugins/google/tsconfig.esm.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./lib/esm", - "module": "esnext" - } -} diff --git a/genkit-tools/plugins/google/tsconfig.json b/genkit-tools/plugins/google/tsconfig.json deleted file mode 100644 index a47ba773d..000000000 --- a/genkit-tools/plugins/google/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "module": "ES2022", - "moduleResolution": "Node", - "outDir": "lib/esm", - "esModuleInterop": true, - "rootDirs": ["src"] - }, - "include": ["src"] -} diff --git a/genkit-tools/plugins/google/tsconfig.types.json b/genkit-tools/plugins/google/tsconfig.types.json deleted file mode 100644 index 4cc6da97b..000000000 --- a/genkit-tools/plugins/google/tsconfig.types.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./lib/types", - "declaration": true, - "emitDeclarationOnly": true - } -} diff --git a/genkit-tools/pnpm-lock.yaml b/genkit-tools/pnpm-lock.yaml index c8fe635f0..4e6fc1e25 100644 --- a/genkit-tools/pnpm-lock.yaml +++ b/genkit-tools/pnpm-lock.yaml @@ -209,50 +209,6 @@ importers: specifier: ^5.3.3 version: 5.4.5 - plugins/firebase: - dependencies: - colorette: - specifier: ^2.0.20 - version: 2.0.20 - devDependencies: - '@genkit-ai/tools-common': - specifier: workspace:* - version: link:../../common - '@types/node': - specifier: ^20.11.19 - version: 20.12.7 - npm-run-all: - specifier: ^4.1.5 - version: 4.1.5 - rimraf: - specifier: ^6.0.1 - version: 6.0.1 - typescript: - specifier: ^4.9.0 - version: 4.9.5 - - plugins/google: - dependencies: - colorette: - specifier: ^2.0.20 - version: 2.0.20 - devDependencies: - '@genkit-ai/tools-common': - specifier: workspace:* - version: link:../../common - '@types/node': - specifier: ^20.11.19 - version: 20.12.7 - npm-run-all: - specifier: ^4.1.5 - version: 4.1.5 - rimraf: - specifier: ^6.0.1 - version: 6.0.1 - typescript: - specifier: ^4.9.0 - version: 4.9.5 - telemetry-server: dependencies: '@asteasolutions/zod-to-openapi': diff --git a/genkit-tools/telemetry-server/package.json b/genkit-tools/telemetry-server/package.json index 9b18f5f53..7d475486c 100644 --- a/genkit-tools/telemetry-server/package.json +++ b/genkit-tools/telemetry-server/package.json @@ -7,7 +7,7 @@ "genai", "generative-ai" ], - "version": "0.9.0-dev.2", + "version": "0.9.0-dev.4", "type": "commonjs", "scripts": { "compile": "tsc -b ./tsconfig.cjs.json ./tsconfig.esm.json ./tsconfig.types.json", diff --git a/js/ai/package.json b/js/ai/package.json index 2880d6cc3..2d581b428 100644 --- a/js/ai/package.json +++ b/js/ai/package.json @@ -7,7 +7,7 @@ "genai", "generative-ai" ], - "version": "0.9.0-dev.2", + "version": "0.9.0-dev.4", "type": "commonjs", "scripts": { "check": "tsc", @@ -32,7 +32,8 @@ "colorette": "^2.0.20", "json5": "^2.2.3", "node-fetch": "^3.3.2", - "partial-json": "^0.1.7" + "partial-json": "^0.1.7", + "uuid": "^10.0.0" }, "devDependencies": { "npm-run-all": "^4.1.5", @@ -102,6 +103,24 @@ "require": "./lib/reranker.js", "import": "./lib/reranker.mjs", "default": "./lib/reranker.js" + }, + "./chat": { + "types": "./lib/chat.d.ts", + "require": "./lib/chat.js", + "import": "./lib/chat.mjs", + "default": "./lib/chat.js" + }, + "./session": { + "types": "./lib/session.d.ts", + "require": "./lib/session.js", + "import": "./lib/session.mjs", + "default": "./lib/session.js" + }, + "./formats": { + "types": "./lib/formats/index.d.ts", + "require": "./lib/formats/index.js", + "import": "./lib/formats/index.mjs", + "default": "./lib/formats/index.js" } }, "typesVersions": { @@ -129,6 +148,12 @@ ], "reranker": [ "lib/reranker" + ], + "chat": [ + "lib/chat" + ], + "session": [ + "lib/session" ] } } diff --git a/js/genkit/src/chat.ts b/js/ai/src/chat.ts similarity index 82% rename from js/genkit/src/chat.ts rename to js/ai/src/chat.ts index e168233f8..437e9887e 100644 --- a/js/genkit/src/chat.ts +++ b/js/ai/src/chat.ts @@ -14,25 +14,25 @@ * limitations under the License. */ +import { z } from '@genkit-ai/core'; +import { runInNewSpan } from '@genkit-ai/core/tracing'; import { - ExecutablePrompt, + generate, GenerateOptions, GenerateResponse, + generateStream, GenerateStreamOptions, GenerateStreamResponse, GenerationCommonConfigSchema, MessageData, Part, -} from '@genkit-ai/ai'; -import { z } from '@genkit-ai/core'; -import { Genkit } from './genkit'; +} from './index.js'; import { BaseGenerateOptions, + runWithSession, Session, SessionStore, - runWithSession, } from './session'; -import { runInNewSpan } from './tracing'; export const MAIN_THREAD = 'main'; @@ -42,7 +42,6 @@ export type ChatGenerateOptions< > = GenerateOptions; export interface PromptRenderOptions { - prompt: ExecutablePrompt; input?: I; } @@ -107,10 +106,21 @@ export class Chat { } requestBase.messages = [...(requestBase.messages ?? []), promptMessage]; } - requestBase.messages = [ - ...(options.messages ?? []), - ...(requestBase.messages ?? []), - ]; + if (hasPreamble(requestBase.messages)) { + requestBase.messages = [ + // if request base contains a preamble, always put it first + ...(getPreamble(requestBase.messages) ?? []), + // strip out the preamble from history + ...(stripPreamble(options.messages) ?? []), + // add whatever non-preamble remains from request + ...(stripPreamble(requestBase.messages) ?? []), + ]; + } else { + requestBase.messages = [ + ...(options.messages ?? []), + ...(requestBase.messages ?? []), + ]; + } this._messages = requestBase.messages; return requestBase; }); @@ -147,7 +157,7 @@ export class Chat { messages: this.messages, ...resolvedOptions, }; - let response = await this.genkit.generate({ + let response = await generate(this.session.registry, { ...request, streamingCallback, }); @@ -187,11 +197,14 @@ export class Chat { resolvedOptions = options as GenerateStreamOptions; } - const { response, stream } = await this.genkit.generateStream({ - ...(await this.requestBase), - messages: this.messages, - ...resolvedOptions, - }); + const { response, stream } = await generateStream( + this.session.registry, + { + ...(await this.requestBase), + messages: this.messages, + ...resolvedOptions, + } + ); return { response: response.finally(async () => { @@ -210,10 +223,6 @@ export class Chat { ); } - private get genkit(): Genkit { - return this.session.genkit; - } - get messages(): MessageData[] { return this._messages ?? []; } @@ -223,3 +232,15 @@ export class Chat { await this.session.updateMessages(this.threadName, messages); } } + +function hasPreamble(msgs?: MessageData[]) { + return !!msgs?.find((m) => m.metadata?.preamble); +} + +function getPreamble(msgs?: MessageData[]) { + return msgs?.filter((m) => m.metadata?.preamble); +} + +function stripPreamble(msgs?: MessageData[]) { + return msgs?.filter((m) => !m.metadata?.preamble); +} diff --git a/js/ai/src/formats/array.ts b/js/ai/src/formats/array.ts index 2df3a4777..b1dbd9782 100644 --- a/js/ai/src/formats/array.ts +++ b/js/ai/src/formats/array.ts @@ -24,8 +24,8 @@ export const arrayFormatter: Formatter = { contentType: 'application/json', constrained: true, }, - handler: (request) => { - if (request.output?.schema && request.output?.schema.type !== 'array') { + handler: (schema) => { + if (schema && schema.type !== 'array') { throw new GenkitError({ status: 'INVALID_ARGUMENT', message: `Must supply an 'array' schema type when using the 'items' parser format.`, @@ -33,12 +33,12 @@ export const arrayFormatter: Formatter = { } let instructions: string | undefined; - if (request.output?.schema) { + if (schema) { instructions = `Output should be a JSON array conforming to the following schema: - \`\`\` - ${JSON.stringify(request.output!.schema!)} - \`\`\` +\`\`\` +${JSON.stringify(schema)} +\`\`\` `; } @@ -54,8 +54,8 @@ export const arrayFormatter: Formatter = { return items; }, - parseResponse: (response) => { - const { items } = extractItems(response.text, 0); + parseMessage: (message) => { + const { items } = extractItems(message.text, 0); return items; }, diff --git a/js/ai/src/formats/enum.ts b/js/ai/src/formats/enum.ts index 3aad6eb2b..02ee35037 100644 --- a/js/ai/src/formats/enum.ts +++ b/js/ai/src/formats/enum.ts @@ -20,12 +20,11 @@ import type { Formatter } from './types'; export const enumFormatter: Formatter = { name: 'enum', config: { - contentType: 'text/plain', + contentType: 'text/enum', constrained: true, }, - handler: (request) => { - const schemaType = request.output?.schema?.type; - if (schemaType && schemaType !== 'string' && schemaType !== 'enum') { + handler: (schema) => { + if (schema && schema.type !== 'string' && schema.type !== 'enum') { throw new GenkitError({ status: 'INVALID_ARGUMENT', message: `Must supply a 'string' or 'enum' schema type when using the enum parser format.`, @@ -33,13 +32,13 @@ export const enumFormatter: Formatter = { } let instructions: string | undefined; - if (request.output?.schema?.enum) { - instructions = `Output should be ONLY one of the following enum values. Do not output any additional information or add quotes.\n\n${request.output?.schema?.enum.map((v) => v.toString()).join('\n')}`; + if (schema?.enum) { + instructions = `Output should be ONLY one of the following enum values. Do not output any additional information or add quotes.\n\n${schema.enum.map((v) => v.toString()).join('\n')}`; } return { - parseResponse: (response) => { - return response.text.trim(); + parseMessage: (message) => { + return message.text.replace(/['"]/g, '').trim(); }, instructions, }; diff --git a/js/ai/src/formats/index.ts b/js/ai/src/formats/index.ts index 40189e4ba..994b62560 100644 --- a/js/ai/src/formats/index.ts +++ b/js/ai/src/formats/index.ts @@ -14,7 +14,9 @@ * limitations under the License. */ +import { JSONSchema } from '@genkit-ai/core'; import { Registry } from '@genkit-ai/core/registry'; +import { MessageData, TextPart } from '../model.js'; import { arrayFormatter } from './array'; import { enumFormatter } from './enum'; import { jsonFormatter } from './json'; @@ -36,18 +38,79 @@ export function defineFormat( export type FormatArgument = | keyof typeof DEFAULT_FORMATS | Formatter - | Omit; + | Omit + | undefined + | null; export async function resolveFormat( registry: Registry, arg: FormatArgument -): Promise { +): Promise | undefined> { + if (!arg) return undefined; if (typeof arg === 'string') { return registry.lookupValue('format', arg); } return arg as Formatter; } +export function resolveInstructions( + format?: Formatter, + schema?: JSONSchema, + instructionsOption?: boolean | string +): string | undefined { + if (typeof instructionsOption === 'string') return instructionsOption; // user provided instructions + if (instructionsOption === false) return undefined; // user says no instructions + if (!format) return undefined; + return format.handler(schema).instructions; +} + +export function injectInstructions( + messages: MessageData[], + instructions: string | boolean | undefined +): MessageData[] { + if (!instructions) return messages; + + // bail out if a non-pending output part is already present + if ( + messages.find((m) => + m.content.find( + (p) => p.metadata?.purpose === 'output' && !p.metadata?.pending + ) + ) + ) { + return messages; + } + + const newPart: TextPart = { + text: instructions as string, + metadata: { purpose: 'output' }, + }; + + // find the system message or the last user message + let targetIndex = messages.findIndex((m) => m.role === 'system'); + if (targetIndex < 0) + targetIndex = messages.map((m) => m.role).lastIndexOf('user'); + if (targetIndex < 0) return messages; + + const m = { + ...messages[targetIndex], + content: [...messages[targetIndex].content], + }; + + const partIndex = m.content.findIndex( + (p) => p.metadata?.purpose === 'output' && p.metadata?.pending + ); + if (partIndex > 0) { + m.content.splice(partIndex, 1, newPart); + } else { + m.content.push(newPart); + } + + const outMessages = [...messages]; + outMessages.splice(targetIndex, 1, m); + return outMessages; +} + export const DEFAULT_FORMATS: Formatter[] = [ jsonFormatter, arrayFormatter, @@ -57,9 +120,9 @@ export const DEFAULT_FORMATS: Formatter[] = [ ]; /** - * initializeFormats registers the default built-in formats on a registry. + * configureFormats registers the default built-in formats on a registry. */ -export function initializeFormats(registry: Registry) { +export function configureFormats(registry: Registry) { for (const format of DEFAULT_FORMATS) { defineFormat( registry, diff --git a/js/ai/src/formats/json.ts b/js/ai/src/formats/json.ts index 61fe6e8c1..9a1124c3e 100644 --- a/js/ai/src/formats/json.ts +++ b/js/ai/src/formats/json.ts @@ -23,14 +23,14 @@ export const jsonFormatter: Formatter = { contentType: 'application/json', constrained: true, }, - handler: (request) => { + handler: (schema) => { let instructions: string | undefined; - if (request.output?.schema) { + if (schema) { instructions = `Output should be in JSON format and conform to the following schema: \`\`\` -${JSON.stringify(request.output!.schema!)} +${JSON.stringify(schema)} \`\`\` `; } @@ -40,8 +40,8 @@ ${JSON.stringify(request.output!.schema!)} return extractJson(chunk.accumulatedText); }, - parseResponse: (response) => { - return extractJson(response.text); + parseMessage: (message) => { + return extractJson(message.text); }, instructions, diff --git a/js/ai/src/formats/jsonl.ts b/js/ai/src/formats/jsonl.ts index d20898714..d70722045 100644 --- a/js/ai/src/formats/jsonl.ts +++ b/js/ai/src/formats/jsonl.ts @@ -31,11 +31,10 @@ export const jsonlFormatter: Formatter = { config: { contentType: 'application/jsonl', }, - handler: (request) => { + handler: (schema) => { if ( - request.output?.schema && - (request.output?.schema.type !== 'array' || - request.output?.schema.items?.type !== 'object') + schema && + (schema.type !== 'array' || schema.items?.type !== 'object') ) { throw new GenkitError({ status: 'INVALID_ARGUMENT', @@ -44,11 +43,11 @@ export const jsonlFormatter: Formatter = { } let instructions: string | undefined; - if (request.output?.schema?.items) { - instructions = `Output should be JSONL format, a sequence of JSON objects (one per line). Each line should conform to the following schema: + if (schema?.items) { + instructions = `Output should be JSONL format, a sequence of JSON objects (one per line) separated by a newline \`\\n\` character. Each line should be a JSON object conforming to the following schema: \`\`\` -${JSON.stringify(request.output.schema.items)} +${JSON.stringify(schema.items)} \`\`\` `; } @@ -86,8 +85,8 @@ ${JSON.stringify(request.output.schema.items)} return results; }, - parseResponse: (response) => { - const items = objectLines(response.text) + parseMessage: (message) => { + const items = objectLines(message.text) .map((l) => extractJson(l)) .filter((l) => !!l); diff --git a/js/ai/src/formats/text.ts b/js/ai/src/formats/text.ts index 985b64a96..218b85295 100644 --- a/js/ai/src/formats/text.ts +++ b/js/ai/src/formats/text.ts @@ -27,8 +27,8 @@ export const textFormatter: Formatter = { return chunk.text; }, - parseResponse: (response) => { - return response.text; + parseMessage: (message) => { + return message.text; }, }; }, diff --git a/js/ai/src/formats/types.d.ts b/js/ai/src/formats/types.d.ts index e8847e572..33595224c 100644 --- a/js/ai/src/formats/types.d.ts +++ b/js/ai/src/formats/types.d.ts @@ -14,20 +14,19 @@ * limitations under the License. */ -import { GenerateResponse, GenerateResponseChunk } from '../generate.js'; -import { ModelRequest, Part } from '../model.js'; +import { JSONSchema } from '@genkit-ai/core'; +import { GenerateResponseChunk } from '../generate.js'; +import { Message } from '../message.js'; +import { ModelRequest } from '../model.js'; -type OutputContentTypes = - | 'application/json' - | 'text/plain' - | 'application/jsonl'; +type OutputContentTypes = 'application/json' | 'text/plain'; export interface Formatter { name: string; config: ModelRequest['output']; - handler: (req: ModelRequest) => { - parseResponse(response: GenerateResponse): O; + handler: (schema?: JSONSchema) => { + parseMessage(message: Message): O; parseChunk?: (chunk: GenerateResponseChunk, cursor?: CC) => CO; - instructions?: string | Part[]; + instructions?: string; }; } diff --git a/js/ai/src/generate.ts b/js/ai/src/generate.ts index b7b6289d4..7d32d7817 100755 --- a/js/ai/src/generate.ts +++ b/js/ai/src/generate.ts @@ -24,6 +24,11 @@ import { import { Registry } from '@genkit-ai/core/registry'; import { toJsonSchema } from '@genkit-ai/core/schema'; import { DocumentData } from './document.js'; +import { + injectInstructions, + resolveFormat, + resolveInstructions, +} from './formats/index.js'; import { generateHelper, GenerateUtilParamSchema } from './generate/action.js'; import { GenerateResponseChunk } from './generate/chunk.js'; import { GenerateResponse } from './generate/response.js'; @@ -32,12 +37,10 @@ import { GenerateRequest, GenerationCommonConfigSchema, MessageData, - ModelAction, ModelArgument, ModelMiddleware, - ModelReference, Part, - ToolDefinition, + resolveModel, } from './model.js'; import { ExecutablePrompt } from './prompt.js'; import { resolveTools, ToolArgument, toToolDefinition } from './tool.js'; @@ -63,7 +66,9 @@ export interface GenerateOptions< config?: z.infer; /** Configuration for the desired output of the request. Defaults to the model's default output if unspecified. */ output?: { - format?: 'json' | 'text' | 'media'; + format?: string; + contentType?: string; + instructions?: boolean | string; schema?: O; jsonSchema?: any; }; @@ -96,70 +101,42 @@ export async function toGenerateRequest( }); } if (messages.length === 0) { - throw new Error('at least one message is required in generate request'); + throw new GenkitError({ + status: 'INVALID_ARGUMENT', + message: 'at least one message is required in generate request', + }); } let tools: Action[] | undefined; if (options.tools) { tools = await resolveTools(registry, options.tools); } + const resolvedSchema = toJsonSchema({ + schema: options.output?.schema, + jsonSchema: options.output?.jsonSchema, + }); + + const resolvedFormat = await resolveFormat(registry, options.output?.format); + const instructions = resolveInstructions( + resolvedFormat, + resolvedSchema, + options?.output?.instructions + ); + const out = { - messages, + messages: injectInstructions(messages, instructions), config: options.config, docs: options.docs, tools: tools?.map((tool) => toToolDefinition(tool)) || [], output: { - format: - options.output?.format || - (options.output?.schema || options.output?.jsonSchema - ? 'json' - : 'text'), - schema: toJsonSchema({ - schema: options.output?.schema, - jsonSchema: options.output?.jsonSchema, - }), + ...(resolvedFormat?.config || {}), + schema: resolvedSchema, }, }; if (!out.output.schema) delete out.output.schema; return out; } -interface ResolvedModel { - modelAction: ModelAction; - config?: z.infer; - version?: string; -} - -async function resolveModel( - registry: Registry, - options: GenerateOptions -): Promise { - let model = options.model; - if (!model) { - throw new Error('Model is required.'); - } - if (typeof model === 'string') { - return { - modelAction: (await registry.lookupAction( - `/model/${model}` - )) as ModelAction, - }; - } else if (model.hasOwnProperty('__action')) { - return { modelAction: model as ModelAction }; - } else { - const ref = model as ModelReference; - return { - modelAction: (await registry.lookupAction( - `/model/${ref.name}` - )) as ModelAction, - config: { - ...ref.config, - }, - version: ref.version, - }; - } -} - export class GenerationResponseError extends GenkitError { detail: { response: GenerateResponse; @@ -167,7 +144,7 @@ export class GenerationResponseError extends GenkitError { }; constructor( - response: GenerateResponse, + response: GenerateResponse, message: string, status?: GenkitError['status'], detail?: Record @@ -180,6 +157,59 @@ export class GenerationResponseError extends GenkitError { } } +async function toolsToActionRefs( + registry: Registry, + toolOpt?: ToolArgument[] +): Promise { + if (!toolOpt) return; + + let tools: string[] = []; + + for (const t of toolOpt) { + if (typeof t === 'string') { + tools.push(await resolveFullToolName(registry, t)); + } else if ((t as Action).__action) { + tools.push( + `/${(t as Action).__action.metadata?.type}/${(t as Action).__action.name}` + ); + } else if (typeof (t as ExecutablePrompt).asTool === 'function') { + const promptToolAction = await (t as ExecutablePrompt).asTool(); + tools.push(`/prompt/${promptToolAction.__action.name}`); + } else if (t.name) { + tools.push(await resolveFullToolName(registry, t.name)); + } else { + throw new Error(`Unable to determine type of tool: ${JSON.stringify(t)}`); + } + } + return tools; +} + +function messagesFromOptions(options: GenerateOptions): MessageData[] { + const messages: MessageData[] = []; + if (options.system) { + messages.push({ + role: 'system', + content: Message.parseContent(options.system), + }); + } + if (options.messages) { + messages.push(...options.messages); + } + if (options.prompt) { + messages.push({ + role: 'user', + content: Message.parseContent(options.prompt), + }); + } + if (messages.length === 0) { + throw new GenkitError({ + status: 'INVALID_ARGUMENT', + message: 'at least one message is required in generate request', + }); + } + return messages; +} + /** A GenerationBlockedError is thrown when a generation is blocked. */ export class GenerationBlockedError extends GenerationResponseError {} @@ -205,69 +235,31 @@ export async function generate< ): Promise>> { const resolvedOptions: GenerateOptions = await Promise.resolve(options); - const resolvedModel = await resolveModel(registry, resolvedOptions); - const model = resolvedModel.modelAction; - if (!model) { - let modelId: string; - if (typeof resolvedOptions.model === 'string') { - modelId = resolvedOptions.model; - } else if ((resolvedOptions.model as ModelAction)?.__action?.name) { - modelId = (resolvedOptions.model as ModelAction).__action.name; - } else { - modelId = (resolvedOptions.model as ModelReference).name; - } - throw new Error(`Model ${modelId} not found`); - } + const resolvedModel = await resolveModel(registry, resolvedOptions.model); - // convert tools to action refs (strings). - let tools: (string | ToolDefinition)[] | undefined; - if (resolvedOptions.tools) { - tools = []; - for (const t of resolvedOptions.tools) { - if (typeof t === 'string') { - tools.push(await resolveFullToolName(registry, t)); - } else if ((t as Action).__action) { - tools.push( - `/${(t as Action).__action.metadata?.type}/${(t as Action).__action.name}` - ); - } else if (typeof (t as ExecutablePrompt).asTool === 'function') { - const promptToolAction = (t as ExecutablePrompt).asTool(); - tools.push(`/prompt/${promptToolAction.__action.name}`); - } else if (t.name) { - tools.push(await resolveFullToolName(registry, t.name)); - } else { - throw new Error( - `Unable to determine type of of tool: ${JSON.stringify(t)}` - ); - } - } - } + const tools = await toolsToActionRefs(registry, resolvedOptions.tools); - const messages: MessageData[] = []; - if (resolvedOptions.system) { - messages.push({ - role: 'system', - content: Message.parseContent(resolvedOptions.system), - }); - } - if (resolvedOptions.messages) { - messages.push(...resolvedOptions.messages); - } - if (resolvedOptions.prompt) { - messages.push({ - role: 'user', - content: Message.parseContent(resolvedOptions.prompt), - }); - } + const messages: MessageData[] = messagesFromOptions(resolvedOptions); - if (messages.length === 0) { - throw new Error('at least one message is required in generate request'); - } + const resolvedSchema = toJsonSchema({ + schema: resolvedOptions.output?.schema, + jsonSchema: resolvedOptions.output?.jsonSchema, + }); + + const resolvedFormat = await resolveFormat( + registry, + resolvedOptions.output?.format + ); + const instructions = resolveInstructions( + resolvedFormat, + resolvedSchema, + resolvedOptions?.output?.instructions + ); const params: z.infer = { - model: model.__action.name, + model: resolvedModel.modelAction.__action.name, docs: resolvedOptions.docs, - messages, + messages: injectInstructions(messages, instructions), tools, config: { version: resolvedModel.version, @@ -276,12 +268,7 @@ export async function generate< }, output: resolvedOptions.output && { format: resolvedOptions.output.format, - jsonSchema: resolvedOptions.output.schema - ? toJsonSchema({ - schema: resolvedOptions.output.schema, - jsonSchema: resolvedOptions.output.jsonSchema, - }) - : resolvedOptions.output.jsonSchema, + jsonSchema: resolvedSchema, }, returnToolRequests: resolvedOptions.returnToolRequests, }; @@ -294,11 +281,14 @@ export async function generate< params, resolvedOptions.use ); - return new GenerateResponse( - response, - response.request ?? - (await toGenerateRequest(registry, { ...resolvedOptions, tools })) - ); + const request = await toGenerateRequest(registry, { + ...resolvedOptions, + tools, + }); + return new GenerateResponse(response, { + request: response.request ?? request, + parser: resolvedFormat?.handler(request.output?.schema).parseMessage, + }); } ); } @@ -385,10 +375,19 @@ export async function generateStream< ({ resolve: provideNextChunk, promise: nextChunk } = createPromise()); }, - }).then((result) => { - provideNextChunk(null); - finalResolve(result); - }); + }) + .then((result) => { + provideNextChunk(null); + finalResolve(result); + }) + .catch((e) => { + if (!firstChunkSent) { + initialReject(e); + return; + } + provideNextChunk(null); + finalReject(e); + }); } catch (e) { if (!firstChunkSent) { initialReject(e); diff --git a/js/ai/src/generate/action.ts b/js/ai/src/generate/action.ts index 7a563c701..513a24b6b 100644 --- a/js/ai/src/generate/action.ts +++ b/js/ai/src/generate/action.ts @@ -20,11 +20,14 @@ import { runWithStreamingCallback, z, } from '@genkit-ai/core'; +import { logger } from '@genkit-ai/core/logging'; import { Registry } from '@genkit-ai/core/registry'; import { toJsonSchema } from '@genkit-ai/core/schema'; import { runInNewSpan, SPAN_TYPE_ATTR } from '@genkit-ai/core/tracing'; import * as clc from 'colorette'; import { DocumentDataSchema } from '../document.js'; +import { resolveFormat } from '../formats/index.js'; +import { Formatter } from '../formats/types.js'; import { GenerateResponse, GenerateResponseChunk, @@ -37,13 +40,13 @@ import { GenerateResponseData, MessageData, MessageSchema, - ModelAction, Part, + resolveModel, Role, ToolDefinitionSchema, ToolResponsePart, } from '../model.js'; -import { lookupToolByName, ToolAction, toToolDefinition } from '../tool.js'; +import { resolveTools, ToolAction, toToolDefinition } from '../tool.js'; export const GenerateUtilParamSchema = z.object({ /** A model name (e.g. `vertexai/gemini-1.0-pro`). */ @@ -59,9 +62,9 @@ export const GenerateUtilParamSchema = z.object({ /** Configuration for the desired output of the request. Defaults to the model's default output if unspecified. */ output: z .object({ - format: z - .union([z.literal('text'), z.literal('json'), z.literal('media')]) - .optional(), + format: z.string().optional(), + contentType: z.string().optional(), + instructions: z.union([z.boolean(), z.string()]).optional(), jsonSchema: z.any().optional(), }) .optional(), @@ -102,38 +105,25 @@ async function generate( rawRequest: z.infer, middleware?: Middleware[] ): Promise { - const model = (await registry.lookupAction( - `/model/${rawRequest.model}` - )) as ModelAction; - if (!model) { - throw new Error(`Model ${rawRequest.model} not found`); - } + const { modelAction: model } = await resolveModel(registry, rawRequest.model); if (model.__action.metadata?.model.stage === 'deprecated') { - console.warn( + logger.warn( `${clc.bold(clc.yellow('Warning:'))} ` + `Model '${model.__action.name}' is deprecated and may be removed in a future release.` ); } - let tools: ToolAction[] | undefined; - if (rawRequest.tools?.length) { - if (!model.__action.metadata?.model.supports?.tools) { - throw new Error( - `Model ${rawRequest.model} does not support tools, but some tools were supplied to generate(). Please call generate() without tools if you would like to use this model.` - ); - } - tools = await Promise.all( - rawRequest.tools.map(async (toolRef) => { - if (typeof toolRef === 'string') { - return lookupToolByName(registry, toolRef as string); - } else if (toolRef.name) { - return lookupToolByName(registry, toolRef.name); - } - throw `Unable to resolve tool ${JSON.stringify(toolRef)}`; - }) - ); - } - const request = await actionToGenerateRequest(rawRequest, tools); + const tools = await resolveTools(registry, rawRequest.tools); + + const resolvedFormat = rawRequest.output?.format + ? await resolveFormat(registry, rawRequest.output?.format) + : undefined; + + const request = await actionToGenerateRequest( + rawRequest, + tools, + resolvedFormat + ); const accumulatedChunks: GenerateResponseChunkData[] = []; @@ -145,7 +135,11 @@ async function generate( if (streamingCallback) { streamingCallback!( new GenerateResponseChunk(chunk, { + index: 0, + role: 'model', previousChunks: accumulatedChunks, + parser: resolvedFormat?.handler(request.output?.schema) + .parseChunk, }) ); } @@ -168,7 +162,10 @@ async function generate( ); }; - return new GenerateResponse(await dispatch(0, request), request); + return new GenerateResponse(await dispatch(0, request), { + request, + parser: resolvedFormat?.handler(request.output?.schema).parseMessage, + }); } ); @@ -236,7 +233,8 @@ async function generate( async function actionToGenerateRequest( options: z.infer, - resolvedTools?: ToolAction[] + resolvedTools?: ToolAction[], + resolvedFormat?: Formatter ): Promise { const out = { messages: options.messages, @@ -244,9 +242,7 @@ async function actionToGenerateRequest( docs: options.docs, tools: resolvedTools?.map((tool) => toToolDefinition(tool)) || [], output: { - format: - options.output?.format || - (options.output?.jsonSchema ? 'json' : 'text'), + ...(resolvedFormat?.config || {}), schema: toJsonSchema({ jsonSchema: options.output?.jsonSchema, }), diff --git a/js/ai/src/generate/response.ts b/js/ai/src/generate/response.ts index 1094c4917..f476bbea3 100644 --- a/js/ai/src/generate/response.ts +++ b/js/ai/src/generate/response.ts @@ -19,7 +19,7 @@ import { GenerationBlockedError, GenerationResponseError, } from '../generate.js'; -import { Message } from '../message.js'; +import { Message, MessageParser } from '../message.js'; import { GenerateRequest, GenerateResponseData, @@ -46,13 +46,23 @@ export class GenerateResponse implements ModelResponseData { custom: unknown; /** The request that generated this response. */ request?: GenerateRequest; - - constructor(response: GenerateResponseData, request?: GenerateRequest) { + /** The parser for output parsing of this response. */ + parser?: MessageParser; + + constructor( + response: GenerateResponseData, + options?: { + request?: GenerateRequest; + parser?: MessageParser; + } + ) { // Check for candidates in addition to message for backwards compatibility. const generatedMessage = response.message || response.candidates?.[0]?.message; if (generatedMessage) { - this.message = new Message(generatedMessage); + this.message = new Message(generatedMessage, { + parser: options?.parser, + }); } this.finishReason = response.finishReason || response.candidates?.[0]?.finishReason!; @@ -60,7 +70,7 @@ export class GenerateResponse implements ModelResponseData { response.finishMessage || response.candidates?.[0]?.finishMessage; this.usage = response.usage || {}; this.custom = response.custom || {}; - this.request = request; + this.request = options?.request; } private get assertMessage(): Message { diff --git a/js/ai/src/index.ts b/js/ai/src/index.ts index 5b63b9f10..4a93c5cdd 100644 --- a/js/ai/src/index.ts +++ b/js/ai/src/index.ts @@ -73,6 +73,7 @@ export { } from './model.js'; export { definePrompt, + isExecutablePrompt, renderPrompt, type ExecutablePrompt, type PromptAction, diff --git a/js/ai/src/message.ts b/js/ai/src/message.ts index 926124dc0..f93f0b151 100644 --- a/js/ai/src/message.ts +++ b/js/ai/src/message.ts @@ -17,6 +17,10 @@ import { extractJson } from './extract'; import { MessageData, Part, ToolRequestPart, ToolResponsePart } from './model'; +export interface MessageParser { + (message: Message): T; +} + /** * Message represents a single role's contribution to a generation. Each message * can contain multiple parts (for example text and an image), and each generation @@ -26,6 +30,7 @@ export class Message implements MessageData { role: MessageData['role']; content: Part[]; metadata?: Record; + parser?: MessageParser; static parseData( lenientMessage: @@ -59,21 +64,22 @@ export class Message implements MessageData { } } - constructor(message: MessageData) { + constructor(message: MessageData, options?: { parser?: MessageParser }) { this.role = message.role; this.content = message.content; this.metadata = message.metadata; + this.parser = options?.parser; } /** - * If a message contains a `data` part, it is returned. Otherwise, the `output()` - * method extracts the first valid JSON object or array from the text contained in - * the message and returns it. + * Attempts to parse the content of the message according to the supplied + * output parser. Without a parser, returns `data` contained in the message or + * tries to parse JSON from the text of the message. * * @returns The structured output contained in the message. */ get output(): T { - return this.data || extractJson(this.text); + return this.parser?.(this) || this.data || extractJson(this.text); } toolResponseParts(): ToolResponsePart[] { diff --git a/js/ai/src/model.ts b/js/ai/src/model.ts index 342cb0608..6c7ab00d1 100644 --- a/js/ai/src/model.ts +++ b/js/ai/src/model.ts @@ -17,6 +17,7 @@ import { Action, defineAction, + GenkitError, getStreamingCallback, Middleware, StreamingCallback, @@ -26,11 +27,7 @@ import { Registry } from '@genkit-ai/core/registry'; import { toJsonSchema } from '@genkit-ai/core/schema'; import { performance } from 'node:perf_hooks'; import { DocumentDataSchema } from './document.js'; -import { - augmentWithContext, - conformOutput, - validateSupport, -} from './model/middleware.js'; +import { augmentWithContext, validateSupport } from './model/middleware.js'; // // IMPORTANT: Please keep type definitions in sync with @@ -121,8 +118,6 @@ export const MessageSchema = z.object({ }); export type MessageData = z.infer; -const OutputFormatSchema = z.enum(['json', 'text', 'media']); - export const ModelInfoSchema = z.object({ /** Acceptable names for this model (e.g. different versions). */ versions: z.array(z.string()).optional(), @@ -140,7 +135,9 @@ export const ModelInfoSchema = z.object({ /** Model can accept messages with role "system". */ systemRole: z.boolean().optional(), /** Model can output this type of data. */ - output: z.array(OutputFormatSchema).optional(), + output: z.array(z.string()).optional(), + /** Model supports output in these content types. */ + contentType: z.array(z.string()).optional(), /** Model can natively support document-based context grounding. */ context: z.boolean().optional(), }) @@ -184,9 +181,10 @@ export const GenerationCommonConfigSchema = z.object({ export type GenerationCommonConfig = typeof GenerationCommonConfigSchema; const OutputConfigSchema = z.object({ - format: OutputFormatSchema.optional(), + format: z.string().optional(), schema: z.record(z.any()).optional(), constrained: z.boolean().optional(), + instructions: z.string().optional(), contentType: z.string().optional(), }); export type OutputConfig = z.infer; @@ -346,7 +344,7 @@ export function defineModel< validateSupport(options), ]; if (!options?.supports?.context) middleware.push(augmentWithContext()); - middleware.push(conformOutput()); + // middleware.push(conformOutput(registry)); const act = defineAction( registry, { @@ -482,3 +480,57 @@ function getPartCounts(parts: Part[]): PartCounts { export type ModelArgument< CustomOptions extends z.ZodTypeAny = typeof GenerationCommonConfigSchema, > = ModelAction | ModelReference | string; + +export interface ResolvedModel< + CustomOptions extends z.ZodTypeAny = z.ZodTypeAny, +> { + modelAction: ModelAction; + config?: z.infer; + version?: string; +} + +export async function resolveModel( + registry: Registry, + model: ModelArgument | undefined +): Promise> { + let out: ResolvedModel; + let modelId: string; + + if (!model) { + model = await registry.lookupValue('defaultModel', 'defaultModel'); + } + if (!model) { + throw new GenkitError({ + status: 'INVALID_ARGUMENT', + message: 'Must supply a `model` to `generate()` calls.', + }); + } + if (typeof model === 'string') { + modelId = model; + out = { modelAction: await registry.lookupAction(`/model/${model}`) }; + } else if (model.hasOwnProperty('__action')) { + modelId = (model as ModelAction).__action.name; + out = { modelAction: model as ModelAction }; + } else { + const ref = model as ModelReference; + modelId = ref.name; + out = { + modelAction: (await registry.lookupAction( + `/model/${ref.name}` + )) as ModelAction, + config: { + ...ref.config, + }, + version: ref.version, + }; + } + + if (!out.modelAction) { + throw new GenkitError({ + status: 'NOT_FOUND', + message: `Model ${modelId} not found`, + }); + } + + return out; +} diff --git a/js/ai/src/model/middleware.ts b/js/ai/src/model/middleware.ts index 11c320cb9..434958dfb 100644 --- a/js/ai/src/model/middleware.ts +++ b/js/ai/src/model/middleware.ts @@ -119,12 +119,12 @@ export function validateSupport(options: { invalid('tool use, but tools were provided'); if (supports.multiturn === false && req.messages.length > 1) invalid(`multiple messages, but ${req.messages.length} were provided`); - if ( - typeof supports.output !== 'undefined' && - req.output?.format && - !supports.output.includes(req.output?.format) - ) - invalid(`requested output format '${req.output?.format}'`); + // if ( + // typeof supports.output !== 'undefined' && + // req.output?.format && + // !supports.output.includes(req.output?.format) + // ) + // invalid(`requested output format '${req.output?.format}'`); return next(); }; } @@ -137,49 +137,6 @@ function lastUserMessage(messages: MessageData[]) { } } -export function conformOutput(): ModelMiddleware { - return async (req, next) => { - const lastMessage = lastUserMessage(req.messages); - if (!lastMessage) return next(req); - const outputPartIndex = lastMessage.content.findIndex( - (p) => p.metadata?.purpose === 'output' - ); - const outputPart = - outputPartIndex >= 0 ? lastMessage.content[outputPartIndex] : undefined; - - if (!req.output?.schema || (outputPart && !outputPart?.metadata?.pending)) { - return next(req); - } - - const instructions = ` - -Output should be in JSON format and conform to the following schema: - -\`\`\` -${JSON.stringify(req.output!.schema!)} -\`\`\` -`; - - if (outputPart) { - lastMessage.content[outputPartIndex] = { - ...outputPart, - metadata: { - purpose: 'output', - source: 'default', - }, - text: instructions, - } as Part; - } else { - lastMessage?.content.push({ - text: instructions, - metadata: { purpose: 'output', source: 'default' }, - }); - } - - return next(req); - }; -} - /** * Provide a simulated system prompt for models that don't support it natively. */ diff --git a/js/ai/src/prompt.ts b/js/ai/src/prompt.ts index f497dca23..c5a93f139 100644 --- a/js/ai/src/prompt.ts +++ b/js/ai/src/prompt.ts @@ -138,7 +138,7 @@ export interface ExecutablePrompt< /** * Returns the prompt usable as a tool. */ - asTool(): ToolAction; + asTool(): Promise; } /** @@ -211,3 +211,11 @@ export async function renderPrompt< tools: rendered.tools || [], } as GenerateOptions; } + +export function isExecutablePrompt(obj: any): boolean { + return ( + !!(obj as ExecutablePrompt)?.render && + !!(obj as ExecutablePrompt)?.asTool && + !!(obj as ExecutablePrompt)?.stream + ); +} diff --git a/js/genkit/src/session.ts b/js/ai/src/session.ts similarity index 70% rename from js/genkit/src/session.ts rename to js/ai/src/session.ts index 6ba305c97..1b00f54df 100644 --- a/js/genkit/src/session.ts +++ b/js/ai/src/session.ts @@ -14,17 +14,19 @@ * limitations under the License. */ +import { z } from '@genkit-ai/core'; +import { Registry } from '@genkit-ai/core/registry'; +import { AsyncLocalStorage } from 'node:async_hooks'; +import { v4 as uuidv4 } from 'uuid'; +import { Chat, ChatOptions, MAIN_THREAD, PromptRenderOptions } from './chat'; import { + ExecutablePrompt, GenerateOptions, Message, MessageData, + isExecutablePrompt, tagAsPreamble, -} from '@genkit-ai/ai'; -import { z } from '@genkit-ai/core'; -import { AsyncLocalStorage } from 'node:async_hooks'; -import { v4 as uuidv4 } from 'uuid'; -import { Chat, ChatOptions, MAIN_THREAD, PromptRenderOptions } from './chat'; -import { Genkit } from './genkit'; +} from './index.js'; export type BaseGenerateOptions< O extends z.ZodTypeAny = z.ZodTypeAny, @@ -58,7 +60,7 @@ export class Session { private store: SessionStore; constructor( - readonly genkit: Genkit, + readonly registry: Registry, options?: { id?: string; stateSchema?: S; @@ -80,10 +82,6 @@ export class Session { } get state(): S | undefined { - // We always get state from the parent. Parent session is the source of truth. - if (this.genkit instanceof Session) { - return this.genkit.state; - } return this.sessionData!.state; } @@ -125,54 +123,109 @@ export class Session { * Create a chat session with the provided options. * * ```ts - * const chat = ai.chat({ + * const session = ai.createSession({}); + * const chat = session.chat({ * system: 'talk like a pirate', * }) - * let response = await chat.send('tell me a joke') - * response = await chat.send('another one') + * let response = await chat.send('tell me a joke'); + * response = await chat.send('another one'); * ``` */ chat(options?: ChatOptions): Chat; /** - * Craete a separaete chat conversation ("thread") within the same session state. + * Create a chat session with the provided preamble. * * ```ts - * const lawyerChat = ai.chat('lawyerThread', { - * system: 'talk like a lawyer', + * const triageAgent = ai.definePrompt({ + * system: 'help the user triage a problem', * }) - * const pirateChat = ai.chat('pirateThread', { + * const session = ai.createSession({}); + * const chat = session.chat(triageAgent); + * const { text } = await chat.send('my phone feels hot'); + * ``` + */ + chat(preamble: ExecutablePrompt, options?: ChatOptions): Chat; + + /** + * Craete a separate chat conversation ("thread") within the given preamble. + * + * ```ts + * const session = ai.createSession({}); + * const lawyerChat = session.chat('lawyerThread', { + * system: 'talk like a lawyer', + * }); + * const pirateChat = session.chat('pirateThread', { * system: 'talk like a pirate', - * }) - * await lawyerChat.send('tell me a joke') - * await pirateChat.send('tell me a joke') + * }); + * await lawyerChat.send('tell me a joke'); + * await pirateChat.send('tell me a joke'); + * ``` + */ + chat( + threadName: string, + preamble: ExecutablePrompt, + options?: ChatOptions + ): Chat; + + /** + * Craete a separate chat conversation ("thread"). + * + * ```ts + * const session = ai.createSession({}); + * const lawyerChat = session.chat('lawyerThread', { + * system: 'talk like a lawyer', + * }); + * const pirateChat = session.chat('pirateThread', { + * system: 'talk like a pirate', + * }); + * await lawyerChat.send('tell me a joke'); + * await pirateChat.send('tell me a joke'); * ``` */ chat(threadName: string, options?: ChatOptions): Chat; chat( - optionsOrThreadName?: ChatOptions | string, + optionsOrPreambleOrThreadName?: + | ChatOptions + | string + | ExecutablePrompt, + maybeOptionsOrPreamble?: ChatOptions | ExecutablePrompt, maybeOptions?: ChatOptions ): Chat { return runWithSession(this, () => { let options: ChatOptions | undefined; let threadName = MAIN_THREAD; - if (maybeOptions) { - threadName = optionsOrThreadName as string; - options = maybeOptions as ChatOptions; - } else if (optionsOrThreadName) { - if (typeof optionsOrThreadName === 'string') { - threadName = optionsOrThreadName as string; + let preamble: ExecutablePrompt | undefined; + + if (optionsOrPreambleOrThreadName) { + if (typeof optionsOrPreambleOrThreadName === 'string') { + threadName = optionsOrPreambleOrThreadName as string; + } else if (isExecutablePrompt(optionsOrPreambleOrThreadName)) { + preamble = optionsOrPreambleOrThreadName as ExecutablePrompt; + } else { + options = optionsOrPreambleOrThreadName as ChatOptions; + } + } + if (maybeOptionsOrPreamble) { + if (isExecutablePrompt(maybeOptionsOrPreamble)) { + preamble = maybeOptionsOrPreamble as ExecutablePrompt; } else { - options = optionsOrThreadName as ChatOptions; + options = maybeOptionsOrPreamble as ChatOptions; } } + if (maybeOptions) { + options = maybeOptions as ChatOptions; + } + let requestBase: Promise; - if (!!(options as PromptRenderOptions)?.prompt?.render) { + if (preamble) { const renderOptions = options as PromptRenderOptions; - requestBase = renderOptions.prompt + requestBase = preamble .render({ - input: renderOptions.input, + input: renderOptions?.input, + model: (renderOptions as BaseGenerateOptions)?.model, + config: (renderOptions as BaseGenerateOptions)?.config, }) .then((rb) => { return { diff --git a/js/ai/src/tool.ts b/js/ai/src/tool.ts index f0f0d5b10..bfeb37efd 100644 --- a/js/ai/src/tool.ts +++ b/js/ai/src/tool.ts @@ -95,7 +95,11 @@ export function asTool( export async function resolveTools< O extends z.ZodTypeAny = z.ZodTypeAny, CustomOptions extends z.ZodTypeAny = z.ZodTypeAny, ->(registry: Registry, tools: ToolArgument[] = []): Promise { +>(registry: Registry, tools?: ToolArgument[]): Promise { + if (!tools || tools.length === 0) { + return []; + } + return await Promise.all( tools.map(async (ref): Promise => { if (typeof ref === 'string') { @@ -103,7 +107,7 @@ export async function resolveTools< } else if ((ref as Action).__action) { return asTool(ref as Action); } else if (typeof (ref as ExecutablePrompt).asTool === 'function') { - return (ref as ExecutablePrompt).asTool(); + return await (ref as ExecutablePrompt).asTool(); } else if (ref.name) { return await lookupToolByName(registry, ref.name); } diff --git a/js/ai/tests/formats/array_test.ts b/js/ai/tests/formats/array_test.ts index f174d1cd0..a8abb2eaf 100644 --- a/js/ai/tests/formats/array_test.ts +++ b/js/ai/tests/formats/array_test.ts @@ -17,8 +17,9 @@ import assert from 'node:assert'; import { describe, it } from 'node:test'; import { arrayFormatter } from '../../src/formats/array.js'; -import { GenerateResponse, GenerateResponseChunk } from '../../src/generate.js'; -import { GenerateResponseChunkData } from '../../src/model.js'; +import { GenerateResponseChunk } from '../../src/generate.js'; +import { Message } from '../../src/message.js'; +import { GenerateResponseChunkData, MessageData } from '../../src/model.js'; describe('arrayFormat', () => { const streamingTests = [ @@ -65,7 +66,7 @@ describe('arrayFormat', () => { for (const st of streamingTests) { it(st.desc, () => { - const parser = arrayFormatter.handler({ messages: [] }); + const parser = arrayFormatter.handler(); const chunks: GenerateResponseChunkData[] = []; let lastCursor = 0; @@ -84,45 +85,40 @@ describe('arrayFormat', () => { }); } - const responseTests = [ + const messageTests = [ { desc: 'parses complete array response', - response: new GenerateResponse({ - message: { - role: 'model', - content: [{ text: '[{"id": 1, "name": "test"}]' }], - }, - }), + message: { + role: 'model', + content: [{ text: '[{"id": 1, "name": "test"}]' }], + }, want: [{ id: 1, name: 'test' }], }, { desc: 'parses empty array', - response: new GenerateResponse({ - message: { - role: 'model', - content: [{ text: '[]' }], - }, - }), + message: { + role: 'model', + content: [{ text: '[]' }], + }, want: [], }, { desc: 'parses array with preamble and code fence', - response: new GenerateResponse({ - message: { - role: 'model', - content: [ - { text: 'Here is the array:\n\n```json\n[{"id": 1}]\n```' }, - ], - }, - }), + message: { + role: 'model', + content: [{ text: 'Here is the array:\n\n```json\n[{"id": 1}]\n```' }], + }, want: [{ id: 1 }], }, ]; - for (const rt of responseTests) { + for (const rt of messageTests) { it(rt.desc, () => { - const parser = arrayFormatter.handler({ messages: [] }); - assert.deepStrictEqual(parser.parseResponse(rt.response), rt.want); + const parser = arrayFormatter.handler(); + assert.deepStrictEqual( + parser.parseMessage(new Message(rt.message as MessageData)), + rt.want + ); }); } diff --git a/js/ai/tests/formats/enum_test.ts b/js/ai/tests/formats/enum_test.ts index 283a71426..299156f4d 100644 --- a/js/ai/tests/formats/enum_test.ts +++ b/js/ai/tests/formats/enum_test.ts @@ -17,58 +17,48 @@ import assert from 'node:assert'; import { describe, it } from 'node:test'; import { enumFormatter } from '../../src/formats/enum.js'; -import { GenerateResponse } from '../../src/generate.js'; +import { Message } from '../../src/message.js'; +import { MessageData } from '../../src/model.js'; describe('enumFormat', () => { - const responseTests = [ + const messageTests = [ { desc: 'parses simple enum value', - response: new GenerateResponse({ - message: { - role: 'model', - content: [{ text: 'VALUE1' }], - }, - }), + message: { + role: 'model', + content: [{ text: 'VALUE1' }], + }, want: 'VALUE1', }, { desc: 'trims whitespace', - response: new GenerateResponse({ - message: { - role: 'model', - content: [{ text: ' VALUE2\n' }], - }, - }), + message: { + role: 'model', + content: [{ text: ' VALUE2\n' }], + }, want: 'VALUE2', }, ]; - for (const rt of responseTests) { + for (const rt of messageTests) { it(rt.desc, () => { - const parser = enumFormatter.handler({ messages: [] }); - assert.strictEqual(parser.parseResponse(rt.response), rt.want); + const parser = enumFormatter.handler(); + assert.strictEqual( + parser.parseMessage(new Message(rt.message as MessageData)), + rt.want + ); }); } const errorTests = [ { desc: 'throws error for number schema type', - request: { - messages: [], - output: { - schema: { type: 'number' }, - }, - }, + schema: { type: 'number' }, wantError: /Must supply a 'string' or 'enum' schema type/, }, { desc: 'throws error for array schema type', - request: { - messages: [], - output: { - schema: { type: 'array' }, - }, - }, + schema: { type: 'array' }, wantError: /Must supply a 'string' or 'enum' schema type/, }, ]; @@ -76,7 +66,7 @@ describe('enumFormat', () => { for (const et of errorTests) { it(et.desc, () => { assert.throws(() => { - enumFormatter.handler(et.request); + enumFormatter.handler(et.schema); }, et.wantError); }); } diff --git a/js/ai/tests/formats/json_test.ts b/js/ai/tests/formats/json_test.ts index 833ca790d..464db5639 100644 --- a/js/ai/tests/formats/json_test.ts +++ b/js/ai/tests/formats/json_test.ts @@ -17,8 +17,9 @@ import assert from 'node:assert'; import { describe, it } from 'node:test'; import { jsonFormatter } from '../../src/formats/json.js'; -import { GenerateResponse, GenerateResponseChunk } from '../../src/generate.js'; -import { GenerateResponseChunkData } from '../../src/model.js'; +import { GenerateResponseChunk } from '../../src/generate.js'; +import { Message } from '../../src/message.js'; +import { GenerateResponseChunkData, MessageData } from '../../src/model.js'; describe('jsonFormat', () => { const streamingTests = [ @@ -61,7 +62,7 @@ describe('jsonFormat', () => { for (const st of streamingTests) { it(st.desc, () => { - const parser = jsonFormatter.handler({ messages: [] }); + const parser = jsonFormatter.handler(); const chunks: GenerateResponseChunkData[] = []; let lastCursor = ''; @@ -81,43 +82,40 @@ describe('jsonFormat', () => { }); } - const responseTests = [ + const messageTests = [ { desc: 'parses complete JSON response', - response: new GenerateResponse({ - message: { - role: 'model', - content: [{ text: '{"id": 1, "name": "test"}' }], - }, - }), + message: { + role: 'model', + content: [{ text: '{"id": 1, "name": "test"}' }], + }, want: { id: 1, name: 'test' }, }, { desc: 'handles empty response', - response: new GenerateResponse({ - message: { - role: 'model', - content: [{ text: '' }], - }, - }), + message: { + role: 'model', + content: [{ text: '' }], + }, want: null, }, { desc: 'parses JSON with preamble and code fence', - response: new GenerateResponse({ - message: { - role: 'model', - content: [{ text: 'Here is the JSON:\n\n```json\n{"id": 1}\n```' }], - }, - }), + message: { + role: 'model', + content: [{ text: 'Here is the JSON:\n\n```json\n{"id": 1}\n```' }], + }, want: { id: 1 }, }, ]; - for (const rt of responseTests) { + for (const rt of messageTests) { it(rt.desc, () => { - const parser = jsonFormatter.handler({ messages: [] }); - assert.deepStrictEqual(parser.parseResponse(rt.response), rt.want); + const parser = jsonFormatter.handler(); + assert.deepStrictEqual( + parser.parseMessage(new Message(rt.message as MessageData)), + rt.want + ); }); } }); diff --git a/js/ai/tests/formats/jsonl_test.ts b/js/ai/tests/formats/jsonl_test.ts index 6d7fe138d..4a58122fb 100644 --- a/js/ai/tests/formats/jsonl_test.ts +++ b/js/ai/tests/formats/jsonl_test.ts @@ -17,8 +17,9 @@ import assert from 'node:assert'; import { describe, it } from 'node:test'; import { jsonlFormatter } from '../../src/formats/jsonl.js'; -import { GenerateResponse, GenerateResponseChunk } from '../../src/generate.js'; -import { GenerateResponseChunkData } from '../../src/model.js'; +import { GenerateResponseChunk } from '../../src/generate.js'; +import { Message } from '../../src/message.js'; +import { GenerateResponseChunkData, MessageData } from '../../src/model.js'; describe('jsonlFormat', () => { const streamingTests = [ @@ -74,7 +75,7 @@ describe('jsonlFormat', () => { for (const st of streamingTests) { it(st.desc, () => { - const parser = jsonlFormatter.handler({ messages: [] }); + const parser = jsonlFormatter.handler(); const chunks: GenerateResponseChunkData[] = []; for (const chunk of st.chunks) { @@ -92,69 +93,56 @@ describe('jsonlFormat', () => { }); } - const responseTests = [ + const messageTests = [ { desc: 'parses complete JSONL response', - response: new GenerateResponse({ - message: { - role: 'model', - content: [{ text: '{"id": 1, "name": "test"}\n{"id": 2}\n' }], - }, - }), + message: { + role: 'model', + content: [{ text: '{"id": 1, "name": "test"}\n{"id": 2}\n' }], + }, want: [{ id: 1, name: 'test' }, { id: 2 }], }, { desc: 'handles empty response', - response: new GenerateResponse({ - message: { - role: 'model', - content: [{ text: '' }], - }, - }), + message: { + role: 'model', + content: [{ text: '' }], + }, want: [], }, { desc: 'parses JSONL with preamble and code fence', - response: new GenerateResponse({ - message: { - role: 'model', - content: [ - { - text: 'Here are the objects:\n\n```\n{"id": 1}\n{"id": 2}\n```', - }, - ], - }, - }), + message: { + role: 'model', + content: [ + { + text: 'Here are the objects:\n\n```\n{"id": 1}\n{"id": 2}\n```', + }, + ], + }, want: [{ id: 1 }, { id: 2 }], }, ]; - for (const rt of responseTests) { + for (const rt of messageTests) { it(rt.desc, () => { - const parser = jsonlFormatter.handler({ messages: [] }); - assert.deepStrictEqual(parser.parseResponse(rt.response), rt.want); + const parser = jsonlFormatter.handler(); + assert.deepStrictEqual( + parser.parseMessage(new Message(rt.message as MessageData)), + rt.want + ); }); } const errorTests = [ { desc: 'throws error for non-array schema type', - request: { - messages: [], - output: { - schema: { type: 'string' }, - }, - }, + schema: { type: 'string' }, wantError: /Must supply an 'array' schema type/, }, { desc: 'throws error for array schema with non-object items', - request: { - messages: [], - output: { - schema: { type: 'array', items: { type: 'string' } }, - }, - }, + schema: { type: 'array', items: { type: 'string' } }, wantError: /Must supply an 'array' schema type containing 'object' items/, }, ]; @@ -162,7 +150,7 @@ describe('jsonlFormat', () => { for (const et of errorTests) { it(et.desc, () => { assert.throws(() => { - jsonlFormatter.handler(et.request); + jsonlFormatter.handler(et.schema); }, et.wantError); }); } diff --git a/js/ai/tests/formats/text_test.ts b/js/ai/tests/formats/text_test.ts index cee7a719c..5dabc35f4 100644 --- a/js/ai/tests/formats/text_test.ts +++ b/js/ai/tests/formats/text_test.ts @@ -17,8 +17,9 @@ import assert from 'node:assert'; import { describe, it } from 'node:test'; import { textFormatter } from '../../src/formats/text.js'; -import { GenerateResponse, GenerateResponseChunk } from '../../src/generate.js'; -import { GenerateResponseChunkData } from '../../src/model.js'; +import { GenerateResponseChunk } from '../../src/generate.js'; +import { Message } from '../../src/message.js'; +import { GenerateResponseChunkData, MessageData } from '../../src/model.js'; describe('textFormat', () => { const streamingTests = [ @@ -48,7 +49,7 @@ describe('textFormat', () => { for (const st of streamingTests) { it(st.desc, () => { - const parser = textFormatter.handler({ messages: [] }); + const parser = textFormatter.handler(); const chunks: GenerateResponseChunkData[] = []; for (const chunk of st.chunks) { @@ -66,33 +67,32 @@ describe('textFormat', () => { }); } - const responseTests = [ + const messageTests = [ { desc: 'parses complete text response', - response: new GenerateResponse({ - message: { - role: 'model', - content: [{ text: 'Hello world' }], - }, - }), + message: { + role: 'model', + content: [{ text: 'Hello world' }], + }, want: 'Hello world', }, { desc: 'handles empty response', - response: new GenerateResponse({ - message: { - role: 'model', - content: [{ text: '' }], - }, - }), + message: { + role: 'model', + content: [{ text: '' }], + }, want: '', }, ]; - for (const rt of responseTests) { + for (const rt of messageTests) { it(rt.desc, () => { - const parser = textFormatter.handler({ messages: [] }); - assert.strictEqual(parser.parseResponse(rt.response), rt.want); + const parser = textFormatter.handler(); + assert.strictEqual( + parser.parseMessage(new Message(rt.message as MessageData)), + rt.want + ); }); } }); diff --git a/js/ai/tests/generate/generate_test.ts b/js/ai/tests/generate/generate_test.ts index 5e0930f91..9c98ae6cc 100644 --- a/js/ai/tests/generate/generate_test.ts +++ b/js/ai/tests/generate/generate_test.ts @@ -57,7 +57,7 @@ describe('toGenerateRequest', () => { config: undefined, docs: undefined, tools: [], - output: { format: 'text' }, + output: {}, }, }, { @@ -92,7 +92,7 @@ describe('toGenerateRequest', () => { }, }, ], - output: { format: 'text' }, + output: {}, }, }, { @@ -127,7 +127,7 @@ describe('toGenerateRequest', () => { }, }, ], - output: { format: 'text' }, + output: {}, }, }, { @@ -162,7 +162,7 @@ describe('toGenerateRequest', () => { config: undefined, docs: undefined, tools: [], - output: { format: 'text' }, + output: {}, }, }, { @@ -184,7 +184,7 @@ describe('toGenerateRequest', () => { config: undefined, docs: undefined, tools: [], - output: { format: 'text' }, + output: {}, }, }, { @@ -201,7 +201,7 @@ describe('toGenerateRequest', () => { config: undefined, docs: [{ content: [{ text: 'context here' }] }], tools: [], - output: { format: 'text' }, + output: {}, }, }, ]; diff --git a/js/ai/tests/model/middleware_test.ts b/js/ai/tests/model/middleware_test.ts index 3c9aaaffa..ae93d9d44 100644 --- a/js/ai/tests/model/middleware_test.ts +++ b/js/ai/tests/model/middleware_test.ts @@ -18,11 +18,11 @@ import { Registry } from '@genkit-ai/core/registry'; import assert from 'node:assert'; import { beforeEach, describe, it } from 'node:test'; import { DocumentData } from '../../src/document.js'; +import { configureFormats } from '../../src/formats/index.js'; import { GenerateRequest, GenerateResponseData, MessageData, - Part, defineModel, } from '../../src/model.js'; import { @@ -131,22 +131,11 @@ describe('validateSupport', () => { /does not support multiple messages/ ); }); - - it('throws on unsupported output format', async () => { - const runner = validateSupport({ - name: 'test-model', - supports: { - output: ['text', 'media'], - }, - }); - await assert.rejects( - runner(examples.json, noopNext), - /does not support requested output format/ - ); - }); }); const registry = new Registry(); +configureFormats(registry); + const echoModel = defineModel(registry, { name: 'echo' }, async (req) => { return { finishReason: 'stop', @@ -156,130 +145,6 @@ const echoModel = defineModel(registry, { name: 'echo' }, async (req) => { }, }; }); -describe('conformOutput (default middleware)', () => { - const schema = { type: 'object', properties: { test: { type: 'boolean' } } }; - - // return the output tagged part from the request - async function testRequest(req: GenerateRequest): Promise { - const response = await echoModel(req); - const treq = response.message!.content[0].data as GenerateRequest; - - const lastUserMessage = treq.messages - .reverse() - .find((m) => m.role === 'user'); - if ( - lastUserMessage && - lastUserMessage.content.filter((p) => p.metadata?.purpose === 'output') - .length > 1 - ) { - throw new Error('too many output parts'); - } - - return lastUserMessage?.content.find( - (p) => p.metadata?.purpose === 'output' - )!; - } - - it('adds output instructions to the last message', async () => { - const part = await testRequest({ - messages: [ - { role: 'user', content: [{ text: 'hello' }] }, - { role: 'model', content: [{ text: 'hi' }] }, - { role: 'user', content: [{ text: 'hello again' }] }, - ], - output: { format: 'json', schema }, - docs: [{ content: [{ text: 'hi' }] }], - }); - assert( - part?.text?.includes(JSON.stringify(schema)), - "schema wasn't found in output part" - ); - }); - - it('adds output to the last message with "user" role', async () => { - const part = await testRequest({ - messages: [ - { - content: [ - { - text: 'First message.', - }, - ], - role: 'user', - }, - { - content: [ - { - toolRequest: { - name: 'localRestaurant', - input: { - location: 'wtf', - }, - }, - }, - ], - role: 'model', - }, - { - content: [ - { - toolResponse: { - name: 'localRestaurant', - output: 'McDonalds', - }, - }, - ], - role: 'tool', - }, - ], - output: { format: 'json', schema }, - }); - - assert( - part?.text?.includes(JSON.stringify(schema)), - "schema wasn't found in output part" - ); - }); - - it('does not add output instructions if already provided', async () => { - const part = await testRequest({ - messages: [ - { - role: 'user', - content: [{ text: 'hello again', metadata: { purpose: 'output' } }], - }, - ], - output: { format: 'json', schema }, - }); - assert.equal(part.text, 'hello again'); - }); - - it('augments a pending output part', async () => { - const part = await testRequest({ - messages: [ - { - role: 'user', - content: [ - { metadata: { purpose: 'output', pending: true } }, - { text: 'after' }, - ], - }, - ], - output: { format: 'json', schema }, - }); - assert( - part?.text?.includes(JSON.stringify(schema)), - "schema wasn't found in output part" - ); - }); - - it('does not add output instructions if no output schema is provided', async () => { - const part = await testRequest({ - messages: [{ role: 'user', content: [{ text: 'hello' }] }], - }); - assert(!part, 'output part added to non-schema request'); - }); -}); describe('simulateSystemPrompt', () => { function testRequest( diff --git a/js/core/package.json b/js/core/package.json index 9954c34b8..759332552 100644 --- a/js/core/package.json +++ b/js/core/package.json @@ -7,7 +7,7 @@ "genai", "generative-ai" ], - "version": "0.9.0-dev.2", + "version": "0.9.0-dev.4", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/core/src/action.ts b/js/core/src/action.ts index 9d01e3f79..382b80d14 100644 --- a/js/core/src/action.ts +++ b/js/core/src/action.ts @@ -28,8 +28,6 @@ import { export { Status, StatusCodes, StatusSchema } from './statusTypes.js'; export { JSONSchema7 }; -export const GENKIT_SESSION_STATE_INPUT_KEY = '__genkit__sessionState'; - export interface ActionMetadata< I extends z.ZodTypeAny, O extends z.ZodTypeAny, @@ -122,12 +120,6 @@ export function action< ? config.name : `${config.name.pluginId}/${config.name.actionId}`; const actionFn = async (input: I) => { - let sessionStateData: Record | undefined = undefined; - if (input?.hasOwnProperty(GENKIT_SESSION_STATE_INPUT_KEY)) { - sessionStateData = input[GENKIT_SESSION_STATE_INPUT_KEY]; - input = { ...input }; - delete input[GENKIT_SESSION_STATE_INPUT_KEY]; - } input = parseSchema(input, { schema: config.inputSchema, jsonSchema: config.inputJsonSchema, @@ -143,9 +135,6 @@ export function action< metadata.name = actionName; metadata.input = input; - if (sessionStateData) { - input[GENKIT_SESSION_STATE_INPUT_KEY] = sessionStateData; - } const output = await fn(input); metadata.output = JSON.stringify(output); diff --git a/js/core/src/flow.ts b/js/core/src/flow.ts index f6adf19d4..ce2c4dd1a 100644 --- a/js/core/src/flow.ts +++ b/js/core/src/flow.ts @@ -42,7 +42,7 @@ import { } from './tracing.js'; import { flowMetadataPrefix, isDevEnv } from './utils.js'; -const streamDelimiter = '\n'; +const streamDelimiter = '\n\n'; /** * Flow Auth policy. Consumes the authorization context of the flow and @@ -145,9 +145,7 @@ export type FlowFn< /** Input to the flow. */ input: z.infer, /** Callback for streaming functions only. */ - streamingCallback?: S extends z.ZodVoid - ? undefined - : StreamingCallback> + streamingCallback?: StreamingCallback> ) => Promise> | z.infer; /** @@ -196,9 +194,7 @@ export class Flow< async invoke( input: unknown, opts: { - streamingCallback?: S extends z.ZodVoid - ? undefined - : StreamingCallback>; + streamingCallback?: StreamingCallback>; labels?: Record; auth?: unknown; } @@ -353,30 +349,50 @@ export class Flow< return; } - if (stream === 'true') { + try { + await this.authPolicy?.(auth, input); + } catch (e: any) { + const respBody = { + error: { + status: 'PERMISSION_DENIED', + message: e.message || 'Permission denied to resource', + }, + }; + response.status(403).send(respBody).end(); + return; + } + + if (request.get('Accept') === 'text/event-stream' || stream === 'true') { response.writeHead(200, { 'Content-Type': 'text/plain', 'Transfer-Encoding': 'chunked', }); try { const result = await this.invoke(input, { - streamingCallback: ((chunk: z.infer) => { - response.write(JSON.stringify(chunk) + streamDelimiter); - }) as S extends z.ZodVoid ? undefined : StreamingCallback>, + streamingCallback: (chunk: z.infer) => { + response.write( + 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter + ); + }, auth, }); - response.write({ - result: result.result, // Need more results!!!! - }); + response.write( + 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter + ); response.end(); } catch (e) { - response.write({ - error: { - status: 'INTERNAL', - message: getErrorMessage(e), - details: getErrorStack(e), - }, - }); + console.log(e); + response.write( + 'data: ' + + JSON.stringify({ + error: { + status: 'INTERNAL', + message: getErrorMessage(e), + details: getErrorStack(e), + }, + }) + + streamDelimiter + ); response.end(); } } else { @@ -413,7 +429,7 @@ export class Flow< */ export interface FlowServerOptions { /** List of flows to expose via the flow server. If not specified, all registered flows will be exposed. */ - flows?: CallableFlow[]; + flows?: (CallableFlow | StreamableFlow)[]; /** Port to run the server on. In `dev` environment, actual port may be different if chosen port is occupied. Defaults to 3400. */ port?: number; /** CORS options for the server. */ diff --git a/js/core/src/registry.ts b/js/core/src/registry.ts index 94cb21372..ed8414887 100644 --- a/js/core/src/registry.ts +++ b/js/core/src/registry.ts @@ -212,11 +212,19 @@ export class Registry { await this.initializePlugin(pluginName); } return ( - (this.valueByTypeAndName[type][key] as T) || + (this.valueByTypeAndName[type]?.[key] as T) || this.parent?.lookupValue(type, key) ); } + async listValues(type: string): Promise> { + await this.initializeAllPlugins(); + return { + ...((await this.parent?.listValues(type)) || {}), + ...(this.valueByTypeAndName[type] || {}), + } as Record; + } + /** * Looks up a schema. * @param name The name of the schema to lookup. diff --git a/js/doc-snippets/src/dotprompt/index.ts b/js/doc-snippets/src/dotprompt/index.ts index 04522aae1..2886a8e95 100644 --- a/js/doc-snippets/src/dotprompt/index.ts +++ b/js/doc-snippets/src/dotprompt/index.ts @@ -39,11 +39,11 @@ const MenuItemSchema = ai.defineSchema( async function fn02() { // [START loadPrompt] - const helloPrompt = await ai.prompt('hello'); + const helloPrompt = ai.prompt('hello'); // [END loadPrompt] // [START loadPromptVariant] - const myPrompt = await ai.prompt('my_prompt', { variant: 'gemini15pro' }); + const myPrompt = ai.prompt('my_prompt', { variant: 'gemini15pro' }); // [END loadPromptVariant] // [START callPrompt] @@ -70,7 +70,7 @@ async function fn02() { } async function fn03() { - const helloPrompt = await ai.prompt('hello'); + const helloPrompt = ai.prompt('hello'); // [START callPromptCfg] const response3 = await helloPrompt( @@ -91,7 +91,7 @@ async function fn03() { async function fn04() { // [START outSchema] // [START inSchema] - const menuPrompt = await ai.prompt('menu'); + const menuPrompt = ai.prompt('menu'); const { data } = await menuPrompt({ theme: 'medieval' }); // [END inSchema] @@ -102,7 +102,7 @@ async function fn04() { async function fn05() { // [START outSchema2] - const menuPrompt = await ai.prompt< + const menuPrompt = ai.prompt< z.ZodTypeAny, // Input schema typeof MenuItemSchema, // Output schema z.ZodTypeAny // Custom options schema @@ -117,7 +117,7 @@ async function fn05() { async function fn06() { // [START multiTurnPrompt] - const multiTurnPrompt = await ai.prompt('multiTurnPrompt'); + const multiTurnPrompt = ai.prompt('multiTurnPrompt'); const result = await multiTurnPrompt({ messages: [ { role: 'user', content: [{ text: 'Hello.' }] }, @@ -129,7 +129,7 @@ async function fn06() { async function fn07() { // [START multiModalPrompt] - const multimodalPrompt = await ai.prompt('multimodal'); + const multimodalPrompt = ai.prompt('multimodal'); const { text } = await multimodalPrompt({ photoUrl: 'https://example.com/photo.jpg', }); diff --git a/js/doc-snippets/src/flows/express.ts b/js/doc-snippets/src/flows/express.ts new file mode 100644 index 000000000..9b226ede3 --- /dev/null +++ b/js/doc-snippets/src/flows/express.ts @@ -0,0 +1,50 @@ +/** + * 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 { genkit } from 'genkit'; + +const ai = genkit({}); + +// [START ex01] +export const menuSuggestionFlow = ai.defineFlow( + { + name: 'menuSuggestionFlow', + }, + async (restaurantTheme) => { + // ... + } +); + +ai.startFlowServer(); +// [END ex01] + +// [START ex02] +export const flowA = ai.defineFlow({ name: 'flowA' }, async (subject) => { + // ... +}); + +export const flowB = ai.defineFlow({ name: 'flowB' }, async (subject) => { + // ... +}); + +ai.startFlowServer({ + flows: [flowB], + port: 4567, + cors: { + origin: '*', + }, +}); +// [END ex02] diff --git a/js/doc-snippets/src/flows/firebase.ts b/js/doc-snippets/src/flows/firebase.ts new file mode 100644 index 000000000..015ee6137 --- /dev/null +++ b/js/doc-snippets/src/flows/firebase.ts @@ -0,0 +1,38 @@ +/** + * 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 { genkit } from 'genkit'; +const ai = genkit({}); + +// [START ex] +import { firebaseAuth } from '@genkit-ai/firebase/auth'; +import { onFlow } from '@genkit-ai/firebase/functions'; + +export const menuSuggestion = onFlow( + ai, + { + name: 'menuSuggestionFlow', + authPolicy: firebaseAuth((user) => { + if (!user.email_verified) { + throw new Error('Verified email required to run flow'); + } + }), + }, + async (restaurantTheme) => { + // ... + } +); +// [END ex] diff --git a/js/doc-snippets/src/flows/index.ts b/js/doc-snippets/src/flows/index.ts new file mode 100644 index 000000000..35686e714 --- /dev/null +++ b/js/doc-snippets/src/flows/index.ts @@ -0,0 +1,211 @@ +/** + * 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 { gemini15Flash, googleAI } from '@genkit-ai/googleai'; +import { genkit, run, z } from 'genkit'; + +const ai = genkit({ + plugins: [googleAI()], +}); + +// [START ex01] +export const menuSuggestionFlow = ai.defineFlow( + { + name: 'menuSuggestionFlow', + }, + async (restaurantTheme) => { + const { text } = await ai.generate({ + model: gemini15Flash, + prompt: `Invent a menu item for a ${restaurantTheme} themed restaurant.`, + }); + return text; + } +); +// [END ex01] + +// [START ex02] +const MenuItemSchema = z.object({ + dishname: z.string(), + description: z.string(), +}); + +export const menuSuggestionFlowWithSchema = ai.defineFlow( + { + name: 'menuSuggestionFlow', + inputSchema: z.string(), + outputSchema: MenuItemSchema, + }, + async (restaurantTheme) => { + const { output } = await ai.generate({ + model: gemini15Flash, + prompt: `Invent a menu item for a ${restaurantTheme} themed restaurant.`, + output: { schema: MenuItemSchema }, + }); + if (output == null) { + throw new Error("Response doesn't satisfy schema."); + } + return output; + } +); +// [END ex02] + +// [START ex03] +export const menuSuggestionFlowMarkdown = ai.defineFlow( + { + name: 'menuSuggestionFlow', + inputSchema: z.string(), + outputSchema: z.string(), + }, + async (restaurantTheme) => { + const { output } = await ai.generate({ + model: gemini15Flash, + prompt: `Invent a menu item for a ${restaurantTheme} themed restaurant.`, + output: { schema: MenuItemSchema }, + }); + if (output == null) { + throw new Error("Response doesn't satisfy schema."); + } + return `**${output.dishname}**: ${output.description}`; + } +); +// [END ex03] + +// [START ex06] +export const menuSuggestionStreamingFlow = ai.defineStreamingFlow( + { + name: 'menuSuggestionFlow', + inputSchema: z.string(), + streamSchema: z.string(), + outputSchema: z.object({ theme: z.string(), menuItem: z.string() }), + }, + async (restaurantTheme, streamingCallback) => { + const response = await ai.generateStream({ + model: gemini15Flash, + prompt: `Invent a menu item for a ${restaurantTheme} themed restaurant.`, + }); + + if (streamingCallback) { + for await (const chunk of response.stream) { + // Here, you could process the chunk in some way before sending it to + // the output stream via streamingCallback(). In this example, we output + // the text of the chunk, unmodified. + // @ts-ignore + streamingCallback(chunk.text()); + } + } + + return { + theme: restaurantTheme, + menuItem: (await response.response).text, + }; + } +); +// [END ex06] + +// [START ex10] +const PrixFixeMenuSchema = z.object({ + starter: z.string(), + soup: z.string(), + main: z.string(), + dessert: z.string(), +}); + +export const complexMenuSuggestionFlow = ai.defineFlow( + { + name: 'complexMenuSuggestionFlow', + inputSchema: z.string(), + outputSchema: PrixFixeMenuSchema, + }, + async (theme: string): Promise> => { + const chat = ai.chat({ model: gemini15Flash }); + await chat.send('What makes a good prix fixe menu?'); + await chat.send( + 'What are some ingredients, seasonings, and cooking techniques that ' + + `would work for a ${theme} themed menu?` + ); + const { output } = await chat.send({ + prompt: + `Based on our discussion, invent a prix fixe menu for a ${theme} ` + + 'themed restaurant.', + output: { + schema: PrixFixeMenuSchema, + }, + }); + if (!output) { + throw new Error('No data generated.'); + } + return output; + } +); +// [END ex10] + +// [START ex11] +export const menuQuestionFlow = ai.defineFlow( + { + name: 'menuQuestionFlow', + inputSchema: z.string(), + outputSchema: z.string(), + }, + async (input: string): Promise => { + const menu = await run('retrieve-daily-menu', async (): Promise => { + // Retrieve today's menu. (This could be a database access or simply + // fetching the menu from your website.) + + // [START_EXCLUDE] + const menu = ` +Today's menu + +- Breakfast: spam and eggs +- Lunch: spam sandwich with a cup of spam soup +- Dinner: spam roast with a side of spammed potatoes + `; + // [END_EXCLUDE] + + return menu; + }); + const { text } = await ai.generate({ + model: gemini15Flash, + system: "Help the user answer questions about today's menu.", + prompt: input, + docs: [{ content: [{ text: menu }] }], + }); + return text; + } +); +// [END ex11] + +async function fn() { + // [START ex04] + const { text } = await menuSuggestionFlow('bistro'); + // [END ex04] + + // [START ex05] + const { dishname, description } = + await menuSuggestionFlowWithSchema('bistro'); + // [END ex05] + + // [START ex07] + const response = menuSuggestionStreamingFlow('Danube'); + // [END ex07] + // [START ex08] + for await (const chunk of response.stream) { + console.log('chunk', chunk); + } + // [END ex08] + // [START ex09] + const output = await response.output; + // [END ex09] +} diff --git a/js/doc-snippets/src/multi-agent/multi.ts b/js/doc-snippets/src/multi-agent/multi.ts new file mode 100644 index 000000000..6dd21ad40 --- /dev/null +++ b/js/doc-snippets/src/multi-agent/multi.ts @@ -0,0 +1,62 @@ +/** + * 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 { genkit } from 'genkit'; + +const ai = genkit({}); +const reservationTool = ai.defineTool( + { + name: '', + description: '', + }, + async () => {} +); +const reservationCancelationTool = reservationTool; +const reservationListTool = reservationTool; + +// [START agents] +// Define a prompt that represents a specialist agent +const reservationAgent = ai.definePrompt( + { + name: 'reservationAgent', + description: 'Reservation Agent can help manage guest reservations', + tools: [reservationTool, reservationCancelationTool, reservationListTool], + }, + '{{role "system"}} Help guests make and manage reservations' +); + +// Or load agents from .prompt files +const menuInfoAgent = ai.prompt('menuInfoAgent'); +const complaintAgent = ai.prompt('complaintAgent'); + +// The triage agent is the agent that users interact with initially +const triageAgent = ai.definePrompt( + { + name: 'triageAgent', + description: 'Triage Agent', + tools: [reservationAgent, menuInfoAgent, complaintAgent], + }, + `{{role "system"}} You are an AI customer service agent for Pavel's Cafe. + Greet the user and ask them how you can help. If appropriate, transfer to an + agent that can better handle the request. If you cannot help the customer with + the available tools, politely explain so.` +); +// [END agents] + +// [START chat] +// Start a chat session, initially with the triage agent +const chat = ai.chat({ preamble: triageAgent }); +// [END chat] diff --git a/js/doc-snippets/src/multi-agent/simple.ts b/js/doc-snippets/src/multi-agent/simple.ts new file mode 100644 index 000000000..f563f1c11 --- /dev/null +++ b/js/doc-snippets/src/multi-agent/simple.ts @@ -0,0 +1,73 @@ +/** + * 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 { gemini15Pro } from '@genkit-ai/googleai'; +import { genkit, z } from 'genkit'; + +const ai = genkit({}); + +// [START tools] +const menuLookupTool = ai.defineTool( + { + name: 'menuLookupTool', + description: 'use this tool to look up the menu for a given date', + inputSchema: z.object({ + date: z.string().describe('the date to look up the menu for'), + }), + outputSchema: z.string().describe('the menu for a given date'), + }, + async (input) => { + // Retrieve the menu from a database, website, etc. + // [START_EXCLUDE] + return ''; + // [END_EXCLUDE] + } +); + +const reservationTool = ai.defineTool( + { + name: 'reservationTool', + description: 'use this tool to try to book a reservation', + inputSchema: z.object({ + partySize: z.coerce.number().describe('the number of guests'), + date: z.string().describe('the date to book for'), + }), + outputSchema: z + .string() + .describe( + "true if the reservation was successfully booked and false if there's" + + ' no table available for the requested time' + ), + }, + async (input) => { + // Access your database to try to make the reservation. + // [START_EXCLUDE] + return ''; + // [END_EXCLUDE] + } +); +// [END tools] + +// [START chat] +const chat = ai.chat({ + model: gemini15Pro, + system: + "You are an AI customer service agent for Pavel's Cafe. Use the tools " + + 'available to you to help the customer. If you cannot help the ' + + 'customer with the available tools, politely explain so.', + tools: [menuLookupTool, reservationTool], +}); +// [END chat] diff --git a/js/genkit/package.json b/js/genkit/package.json index 5cf033d8e..76cd64403 100644 --- a/js/genkit/package.json +++ b/js/genkit/package.json @@ -7,7 +7,7 @@ "genai", "generative-ai" ], - "version": "0.9.0-dev.2", + "version": "0.9.0-dev.4", "type": "commonjs", "main": "./lib/cjs/index.js", "scripts": { @@ -29,7 +29,8 @@ "dependencies": { "@genkit-ai/core": "workspace:*", "@genkit-ai/ai": "workspace:*", - "@genkit-ai/dotprompt": "workspace:*" + "@genkit-ai/dotprompt": "workspace:*", + "uuid": "^10.0.0" }, "devDependencies": { "@types/express": "^4.17.21", @@ -39,8 +40,7 @@ "tsup": "^8.0.2", "typescript": "^4.9.0", "tsx": "^4.7.1", - "@types/body-parser": "^1.19.5", - "uuid": "^10.0.0" + "@types/body-parser": "^1.19.5" }, "files": [ "genkit-ui", @@ -139,6 +139,12 @@ "require": "./lib/plugin.js", "import": "./lib/plugin.mjs", "default": "./lib/plugin.js" + }, + "./client": { + "types": "./lib/client/index.d.ts", + "require": "./lib/client/index.js", + "import": "./lib/client/index.mjs", + "default": "./lib/client/index.js" } }, "typesVersions": { @@ -190,6 +196,9 @@ ], "plugin": [ "lib/plugin" + ], + "client": [ + "lib/client/index" ] } } diff --git a/js/core/src/flow-client/client.ts b/js/genkit/src/client/client.ts similarity index 80% rename from js/core/src/flow-client/client.ts rename to js/genkit/src/client/client.ts index 111e4e479..7da762596 100644 --- a/js/core/src/flow-client/client.ts +++ b/js/genkit/src/client/client.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -const __flowStreamDelimiter = '\n'; +const __flowStreamDelimiter = '\n\n'; /** * Invoke and stream response from a deployed flow. @@ -40,7 +40,7 @@ export function streamFlow({ headers, }: { url: string; - input: any; + input?: any; headers?: Record; }) { let chunkStreamController: ReadableStreamDefaultController | undefined = @@ -68,23 +68,13 @@ export function streamFlow({ return { output() { - return operationPromise.then((op) => { - if (!op.done) { - throw new Error(`flow ${op.name} did not finish execution`); - } - if (op.result?.error) { - throw new Error( - `${op.name}: ${op.result?.error}\n${op.result?.stacktrace}` - ); - } - return op.result?.response; - }); + return operationPromise; }, async *stream() { const reader = chunkStream.getReader(); while (true) { const chunk = await reader.read(); - if (chunk.value) { + if (chunk?.value !== undefined) { yield chunk.value; } if (chunk.done) { @@ -108,12 +98,13 @@ async function __flowRunEnvelope({ headers?: Record; }) { let response; - response = await fetch(url + '?stream=true', { + response = await fetch(url, { method: 'POST', body: JSON.stringify({ data: input, }), headers: { + Accept: 'text/event-stream', 'Content-Type': 'application/json', ...headers, }, @@ -133,17 +124,28 @@ async function __flowRunEnvelope({ } // If buffer includes the delimiter that means we are still recieving chunks. while (buffer.includes(__flowStreamDelimiter)) { - streamingCallback( - JSON.parse(buffer.substring(0, buffer.indexOf(__flowStreamDelimiter))) + const chunk = JSON.parse( + buffer + .substring(0, buffer.indexOf(__flowStreamDelimiter)) + .substring('data: '.length) ); + if (chunk.hasOwnProperty('message')) { + streamingCallback(chunk.message); + } else if (chunk.hasOwnProperty('result')) { + return chunk.result; + } else if (chunk.hasOwnProperty('error')) { + throw new Error( + `${chunk.error.status}: ${chunk.error.message}\n${chunk.error.details}` + ); + } else { + throw new Error('unkown chunk format: ' + JSON.stringify(chunk)); + } buffer = buffer.substring( buffer.indexOf(__flowStreamDelimiter) + __flowStreamDelimiter.length ); } - if (result.done) { - return JSON.parse(buffer); - } } + throw new Error('stream did not terminate correctly'); } /** @@ -163,17 +165,17 @@ async function __flowRunEnvelope({ */ export async function runFlow({ url, - payload, + input, headers, }: { url: string; - payload?: any; + input?: any; headers?: Record; }) { const response = await fetch(url, { method: 'POST', body: JSON.stringify({ - data: payload, + data: input, }), headers: { 'Content-Type': 'application/json', diff --git a/js/core/src/flow-client/index.ts b/js/genkit/src/client/index.ts similarity index 100% rename from js/core/src/flow-client/index.ts rename to js/genkit/src/client/index.ts diff --git a/js/genkit/src/genkit.ts b/js/genkit/src/genkit.ts index d614808c8..eef0320ba 100644 --- a/js/genkit/src/genkit.ts +++ b/js/genkit/src/genkit.ts @@ -37,6 +37,7 @@ import { GenerateStreamResponse, GenerationCommonConfigSchema, IndexerParams, + isExecutablePrompt, ModelArgument, ModelReference, Part, @@ -53,6 +54,7 @@ import { ToolAction, ToolConfig, } from '@genkit-ai/ai'; +import { Chat, ChatOptions } from '@genkit-ai/ai/chat'; import { defineEmbedder, EmbedderAction, @@ -66,6 +68,7 @@ import { EvaluatorAction, EvaluatorFn, } from '@genkit-ai/ai/evaluator'; +import { configureFormats } from '@genkit-ai/ai/formats'; import { defineModel, DefineModelOptions, @@ -88,6 +91,13 @@ import { RetrieverFn, SimpleRetrieverOptions, } from '@genkit-ai/ai/retriever'; +import { + getCurrentSession, + Session, + SessionData, + SessionError, + SessionOptions, +} from '@genkit-ai/ai/session'; import { resolveTools } from '@genkit-ai/ai/tool'; import { CallableFlow, @@ -117,18 +127,10 @@ import { prompt, } from '@genkit-ai/dotprompt'; import { v4 as uuidv4 } from 'uuid'; -import { Chat, ChatOptions } from './chat.js'; import { BaseEvalDataPointSchema } from './evaluator.js'; import { logger } from './logging.js'; import { GenkitPlugin, genkitPlugin } from './plugin.js'; import { Registry } from './registry.js'; -import { - getCurrentSession, - Session, - SessionData, - SessionError, - SessionOptions, -} from './session.js'; import { toToolDefinition } from './tool.js'; /** @@ -271,36 +273,42 @@ export class Genkit { * * @todo TODO: Show an example of a name and variant. */ - async prompt< + prompt< I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, CustomOptions extends z.ZodTypeAny = z.ZodTypeAny, >( name: string, options?: { variant?: string } - ): Promise, O, CustomOptions>> { - // check the registry first as not all prompt types can be - // loaded by dotprompt (e.g. functional) - let action = (await this.registry.lookupAction( - `/prompt/${name}` - )) as PromptAction; - // nothing in registry - check for dotprompt file. - if (!action) { - action = (await prompt(this.registry, name, options)) - .promptAction as PromptAction; - } + ): ExecutablePrompt, O, CustomOptions> { + const actionPromise = (async () => { + // check the registry first as not all prompt types can be + // loaded by dotprompt (e.g. functional) + let action = (await this.registry.lookupAction( + `/prompt/${name}` + )) as PromptAction; + // nothing in registry - check for dotprompt file. + if (!action) { + action = ( + await prompt(this.registry, name, { + ...options, + dir: this.options.promptDir ?? './prompts', + }) + ).promptAction as PromptAction; + } + const { template, ...opts } = action.__action.metadata!.prompt; + return { action, opts }; + })(); + // make sure we get configuration such as model name if applicable - const { template, ...opts } = action.__action.metadata!.prompt; return this.wrapPromptActionInExecutablePrompt( - action as PromptAction, - opts + actionPromise.then(({ action }) => action), + actionPromise.then(({ opts }) => opts) ) as ExecutablePrompt; } /** - * Defines and registers a dotprompt. - * - * This is an alternative to defining and importing a .prompt file. + * Defines and registers a function-based prompt. * * ```ts * const hi = ai.definePrompt( @@ -311,8 +319,15 @@ export class Genkit { * name: z.string(), * }), * }, + * config: { + * temperature: 1, + * }, * }, - * 'hi {{ name }}' + * async (input) => { + * return { + * messages: [ { role: 'user', content: [{ text: `hi ${input.name}` }] } ], + * }; + * } * ); * const { text } = await hi({ name: 'Genkit' }); * ``` @@ -323,11 +338,13 @@ export class Genkit { CustomOptions extends z.ZodTypeAny = z.ZodTypeAny, >( options: PromptMetadata, - template: string + fn: PromptFn ): ExecutablePrompt, O, CustomOptions>; /** - * Defines and registers a function-based prompt. + * Defines and registers a dotprompt. + * + * This is an alternative to defining and importing a .prompt file. * * ```ts * const hi = ai.definePrompt( @@ -338,15 +355,8 @@ export class Genkit { * name: z.string(), * }), * }, - * config: { - * temperature: 1, - * }, * }, - * async (input) => { - * return { - * messages: [ { role: 'user', content: [{ text: `hi ${input.name}` }] } ], - * }; - * } + * 'hi {{ name }}' * ); * const { text } = await hi({ name: 'Genkit' }); * ``` @@ -357,7 +367,7 @@ export class Genkit { CustomOptions extends z.ZodTypeAny = z.ZodTypeAny, >( options: PromptMetadata, - fn: PromptFn + template: string ): ExecutablePrompt, O, CustomOptions>; definePrompt< @@ -421,8 +431,10 @@ export class Genkit { O extends z.ZodTypeAny = z.ZodTypeAny, CustomOptions extends z.ZodTypeAny = z.ZodTypeAny, >( - p: PromptAction, - options: Partial> + promptAction: PromptAction | Promise>, + options: + | Partial> + | Promise>> ): ExecutablePrompt { const executablePrompt = async ( input?: z.infer, @@ -472,17 +484,14 @@ export class Genkit { opt: PromptGenerateOptions ): Promise> => { let model: ModelAction | undefined; + options = await options; try { model = await this.resolveModel(opt?.model ?? options.model); } catch (e) { // ignore, no model on a render is OK? } - const promptResult = await p({ - // this feels a litte hacky, but we need to pass session state as action - // input to make it replayable from trace view in the dev ui. - __genkit__sessionState: { state: getCurrentSession()?.state }, - ...opt.input, - }); + const p = await promptAction; + const promptResult = await p(opt.input); const resultOptions = { messages: promptResult.messages, docs: promptResult.docs, @@ -505,8 +514,8 @@ export class Genkit { return resultOptions; }; (executablePrompt as ExecutablePrompt).asTool = - (): ToolAction => { - return p as unknown as ToolAction; + async (): Promise> => { + return (await promptAction) as unknown as ToolAction; }; return executablePrompt as ExecutablePrompt; } @@ -765,9 +774,6 @@ export class Genkit { } else { resolvedOptions = options as GenerateOptions; } - if (!resolvedOptions.model) { - resolvedOptions.model = this.options.model; - } return generate(this.registry, resolvedOptions); } @@ -872,9 +878,6 @@ export class Genkit { } else { resolvedOptions = options as GenerateOptions; } - if (!resolvedOptions.model) { - resolvedOptions.model = this.options.model; - } return generateStream(this.registry, resolvedOptions); } @@ -889,8 +892,53 @@ export class Genkit { * response = await chat.send('another one') * ``` */ - chat(options?: ChatOptions): Chat { + chat(options?: ChatOptions): Chat; + + /** + * Create a chat session with the provided preabmle. + * + * ```ts + * const triageAgent = ai.definePrompt({ + * system: 'help the user triage a problem', + * }) + * const chat = ai.chat(triageAgent) + * const { text } = await chat.send('my phone feels hot'); + * ``` + */ + chat(preamble: ExecutablePrompt, options?: ChatOptions): Chat; + + /** + * Create a chat session with the provided options. + * + * ```ts + * const chat = ai.chat({ + * system: 'talk like a pirate', + * }) + * let response = await chat.send('tell me a joke') + * response = await chat.send('another one') + * ``` + */ + chat( + preambleOrOptions?: ChatOptions | ExecutablePrompt, + maybeOptions?: ChatOptions + ): Chat { + let options: ChatOptions | undefined; + let preamble: ExecutablePrompt | undefined; + if (maybeOptions) { + options = maybeOptions; + } + if (preambleOrOptions) { + if (isExecutablePrompt(preambleOrOptions)) { + preamble = preambleOrOptions as ExecutablePrompt; + } else { + options = preambleOrOptions as ChatOptions; + } + } + const session = this.createSession(); + if (preamble) { + return session.chat(preamble, options); + } return session.chat(options); } @@ -903,7 +951,7 @@ export class Genkit { id: sessionId, state: options?.initialState, }; - return new Session(this, { + return new Session(this.registry, { id: sessionId, sessionData, store: options?.store, @@ -922,7 +970,7 @@ export class Genkit { } const sessionData = await options.store.get(sessionId); - return new Session(this, { + return new Session(this.registry, { id: sessionId, sessionData, store: options.store, @@ -945,10 +993,23 @@ export class Genkit { */ private configure() { const activeRegistry = this.registry; + // install the default formats in the registry + configureFormats(activeRegistry); const plugins = [...(this.options.plugins ?? [])]; + if (this.options.model) { + this.registry.registerValue( + 'defaultModel', + 'defaultModel', + this.options.model + ); + } if (this.options.promptDir !== null) { const dotprompt = genkitPlugin('dotprompt', async (ai) => { - loadPromptFolder(this.registry, this.options.promptDir ?? './prompts'); + loadPromptFolder( + this.registry, + this.options.promptDir ?? './prompts', + '' + ); }); plugins.push(dotprompt); } diff --git a/js/genkit/src/index.ts b/js/genkit/src/index.ts index abd386cf8..8002e7bd7 100644 --- a/js/genkit/src/index.ts +++ b/js/genkit/src/index.ts @@ -99,6 +99,7 @@ export { type ToolRequestPart, type ToolResponsePart, } from '@genkit-ai/ai'; +export { type SessionData, type SessionStore } from '@genkit-ai/ai/session'; export { FlowActionInputSchema, FlowErrorSchema, @@ -152,4 +153,3 @@ export { } from '@genkit-ai/core'; export { loadPromptFile } from '@genkit-ai/dotprompt'; export * from './genkit.js'; -export { type SessionData, type SessionStore } from './session.js'; diff --git a/js/genkit/src/middleware.ts b/js/genkit/src/middleware.ts index 443ac450e..ff31561e2 100644 --- a/js/genkit/src/middleware.ts +++ b/js/genkit/src/middleware.ts @@ -16,7 +16,6 @@ export { augmentWithContext, - conformOutput, downloadRequestMedia, simulateSystemPrompt, validateSupport, diff --git a/js/genkit/tests/chat_test.ts b/js/genkit/tests/chat_test.ts index 9537a90db..de9a5c6a4 100644 --- a/js/genkit/tests/chat_test.ts +++ b/js/genkit/tests/chat_test.ts @@ -120,12 +120,25 @@ describe('chat', () => { }); it('can start chat from a prompt', async () => { - const prompt = ai.definePrompt( + const preamble = ai.definePrompt( + { name: 'hi', config: { version: 'abc' } }, + 'hi from template' + ); + const session = await ai.chat(preamble); + const response = await session.send('send it'); + + assert.strictEqual( + response.text, + 'Echo: hi from template,send it; config: {"version":"abc"}' + ); + }); + + it('can start chat from a prompt with input', async () => { + const preamble = ai.definePrompt( { name: 'hi', config: { version: 'abc' } }, 'hi {{ name }} from template' ); - const session = await ai.chat({ - prompt, + const session = await ai.chat(preamble, { input: { name: 'Genkit' }, }); const response = await session.send('send it'); @@ -156,7 +169,7 @@ describe('chat', () => { }); }); -describe('preabmle', () => { +describe('preamble', () => { let ai: Genkit; let pm: ProgrammableModel; @@ -165,6 +178,7 @@ describe('preabmle', () => { model: 'programmableModel', }); pm = defineProgrammableModel(ai); + defineEchoModel(ai); }); it('swaps out preamble on prompt tool invocation', async () => { @@ -207,9 +221,7 @@ describe('preabmle', () => { }; }; - const session = ai.chat({ - prompt: agentA, - }); + const session = ai.chat(agentA); let { text } = await session.send('hi'); assert.strictEqual(text, 'hi from agent a'); assert.deepStrictEqual(pm.lastRequest, { @@ -227,7 +239,7 @@ describe('preabmle', () => { role: 'user', }, ], - output: { format: 'text' }, + output: {}, tools: [ { name: 'agentB', @@ -316,7 +328,7 @@ describe('preabmle', () => { ], }, ], - output: { format: 'text' }, + output: {}, tools: [ { description: 'Agent A description', @@ -436,7 +448,7 @@ describe('preabmle', () => { ], }, ], - output: { format: 'text' }, + output: {}, tools: [ { description: 'Agent B description', @@ -451,4 +463,76 @@ describe('preabmle', () => { ], }); }); + + it('updates the preabmle on fresh chat instance', async () => { + const agent = ai.definePrompt( + { + name: 'agent', + config: { temperature: 2 }, + description: 'Agent A description', + }, + '{{ role "system"}} greet {{ @state.name }}' + ); + + const session = ai.createSession({ initialState: { name: 'Pavel' } }); + + const chat = session.chat(agent, { model: 'echoModel' }); + let response = await chat.send('hi'); + + assert.deepStrictEqual(response.messages, [ + { + role: 'system', + content: [{ text: ' greet Pavel' }], + metadata: { preamble: true }, + }, + { + role: 'user', + content: [{ text: 'hi' }], + }, + { + role: 'model', + content: [ + { text: 'Echo: system: greet Pavel,hi' }, + { text: '; config: {"temperature":2}' }, + ], + }, + ]); + + await session.updateState({ name: 'Michael' }); + + const freshChat = session.chat(agent, { model: 'echoModel' }); + response = await freshChat.send('hi'); + + assert.deepStrictEqual(response.messages, [ + { + role: 'system', + content: [{ text: ' greet Michael' }], + metadata: { preamble: true }, + }, + { + role: 'user', + content: [{ text: 'hi' }], + }, + { + role: 'model', + content: [ + { text: 'Echo: system: greet Pavel,hi' }, + { text: '; config: {"temperature":2}' }, + ], + }, + { + role: 'user', + content: [{ text: 'hi' }], + }, + { + role: 'model', + content: [ + { + text: 'Echo: system: greet Michael,hi,Echo: system: greet Pavel,hi,; config: {"temperature":2},hi', + }, + { text: '; config: {"temperature":2}' }, + ], + }, + ]); + }); }); diff --git a/js/genkit/tests/generate_test.ts b/js/genkit/tests/generate_test.ts index 81da064a7..3e5dfca0c 100644 --- a/js/genkit/tests/generate_test.ts +++ b/js/genkit/tests/generate_test.ts @@ -18,7 +18,7 @@ import assert from 'node:assert'; import { beforeEach, describe, it } from 'node:test'; import { modelRef } from '../../ai/src/model'; import { Genkit, genkit } from '../src/genkit'; -import { defineEchoModel } from './helpers'; +import { defineEchoModel, runAsync } from './helpers'; describe('generate', () => { describe('default model', () => { @@ -65,16 +65,14 @@ describe('generate', () => { messages: [ { role: 'system', - content: 'talk like a pirate', + content: [{ text: 'talk like a pirate' }], }, { role: 'user', content: [{ text: 'hi' }], }, ], - output: { - format: 'text', - }, + output: {}, tools: [], }); }); @@ -91,7 +89,7 @@ describe('generate', () => { }); }); - describe('default model', () => { + describe('explicit model', () => { let ai: Genkit; beforeEach(() => { @@ -108,6 +106,71 @@ describe('generate', () => { }); }); + describe('streaming', () => { + let ai: Genkit; + + beforeEach(() => { + ai = genkit({}); + }); + + it('rethrows errors', async () => { + ai.defineModel( + { + name: 'blockingModel', + }, + async (request, streamingCallback) => { + if (streamingCallback) { + await runAsync(() => { + streamingCallback({ + content: [ + { + text: '3', + }, + ], + }); + }); + await runAsync(() => { + streamingCallback({ + content: [ + { + text: '2', + }, + ], + }); + }); + await runAsync(() => { + streamingCallback({ + content: [ + { + text: '1', + }, + ], + }); + }); + } + return await runAsync(() => ({ + message: { + role: 'model', + content: [], + }, + finishReason: 'blocked', + })); + } + ); + + assert.rejects(async () => { + const { response, stream } = await ai.generateStream({ + prompt: 'hi', + model: 'blockingModel', + }); + for await (const chunk of stream) { + // nothing + } + await response; + }); + }); + }); + describe('config', () => { let ai: Genkit; diff --git a/js/genkit/tests/prompts/toolPrompt.prompt b/js/genkit/tests/prompts/toolPrompt.prompt new file mode 100644 index 000000000..bc5c8e2e0 --- /dev/null +++ b/js/genkit/tests/prompts/toolPrompt.prompt @@ -0,0 +1,6 @@ +--- +description: prompt in a file +tools: + - agentA +--- +{{ role "system" }} {{ @state.name }} toolPrompt prompt \ No newline at end of file diff --git a/js/genkit/tests/prompts_test.ts b/js/genkit/tests/prompts_test.ts index e79590038..5a36291fa 100644 --- a/js/genkit/tests/prompts_test.ts +++ b/js/genkit/tests/prompts_test.ts @@ -19,7 +19,12 @@ import assert from 'node:assert'; import { beforeEach, describe, it } from 'node:test'; import { Genkit, genkit } from '../src/genkit'; import { z } from '../src/index'; -import { defineEchoModel, defineStaticResponseModel } from './helpers'; +import { + ProgrammableModel, + defineEchoModel, + defineProgrammableModel, + defineStaticResponseModel, +} from './helpers'; describe('definePrompt - dotprompt', () => { describe('default model', () => { @@ -111,7 +116,7 @@ describe('definePrompt - dotprompt', () => { 'hi {{ name }}' ); - const hi = await ai.prompt('hi'); + const hi = ai.prompt('hi'); const response = await hi({ name: 'Genkit' }); assert.strictEqual(response.text, 'Echo: hi Genkit; config: {}'); @@ -256,7 +261,7 @@ describe('definePrompt - dotprompt', () => { 'hi {{ name }}' ); - const hi = await ai.prompt('hi'); + const hi = ai.prompt('hi'); const response = await hi({ name: 'Genkit' }); assert.strictEqual(response.text, 'Echo: hi Genkit; config: {}'); @@ -350,10 +355,7 @@ describe('definePrompt - dotprompt', () => { role: 'user', }, ], - output: { - format: 'text', - jsonSchema: undefined, - }, + output: undefined, tools: [], }); }); @@ -442,7 +444,7 @@ describe('definePrompt', () => { } ); - const hi = await ai.prompt('hi'); + const hi = ai.prompt('hi'); const response = await hi({ name: 'Genkit' }); assert.strictEqual(response.text, 'Echo: hi Genkit; config: {}'); @@ -747,7 +749,7 @@ describe('prompt', () => { }); it('loads from from the folder', async () => { - const testPrompt = await ai.prompt('test'); // see tests/prompts folder + const testPrompt = ai.prompt('test'); // see tests/prompts folder const { text } = await testPrompt(); @@ -777,9 +779,295 @@ describe('prompt', () => { }; } ); - const testPrompt = await ai.prompt('hi'); + const testPrompt = ai.prompt('hi'); const { text } = await testPrompt({ name: 'banana' }); assert.strictEqual(text, 'Echo: hi banana; config: {"temperature":11}'); }); }); + +describe('asTool', () => { + let ai: Genkit; + let pm: ProgrammableModel; + + beforeEach(() => { + ai = genkit({ + model: 'programmableModel', + promptDir: './tests/prompts', + }); + pm = defineProgrammableModel(ai); + }); + + it('swaps out preamble on .prompt file tool invocation', async () => { + const session = ai.createSession({ initialState: { name: 'Genkit' } }); + const agentA = ai.definePrompt( + { + name: 'agentA', + config: { temperature: 2 }, + description: 'Agent A description', + tools: ['toolPrompt'], // <--- defined in a .prompt file + }, + async () => { + return { + messages: [ + { + role: 'system', + content: [{ text: ' agent a' }], + }, + ], + }; + } + ); + + // simple hi, nothing interesting... + pm.handleResponse = async (req, sc) => { + return { + message: { + role: 'model', + content: [{ text: `hi ${session.state?.name} from agent a` }], + }, + }; + }; + const chat = session.chat(agentA); + let { text } = await chat.send('hi'); + assert.strictEqual(text, 'hi Genkit from agent a'); + assert.deepStrictEqual(pm.lastRequest, { + config: { + temperature: 2, + }, + messages: [ + { + content: [{ text: ' agent a' }], + metadata: { preamble: true }, + role: 'system', + }, + { + content: [{ text: 'hi' }], + role: 'user', + }, + ], + output: {}, + tools: [ + { + name: 'toolPrompt', + description: 'prompt in a file', + inputSchema: { + $schema: 'http://json-schema.org/draft-07/schema#', + }, + outputSchema: { + $schema: 'http://json-schema.org/draft-07/schema#', + }, + }, + ], + }); + + // transfer to toolPrompt... + + // first response be tools call, the subsequent just text response from agent b. + let reqCounter = 0; + pm.handleResponse = async (req, sc) => { + return { + message: { + role: 'model', + content: [ + reqCounter++ === 0 + ? { + toolRequest: { + name: 'toolPrompt', + input: {}, + ref: 'ref123', + }, + } + : { text: 'hi from agent b' }, + ], + }, + }; + }; + + ({ text } = await chat.send('pls transfer to b')); + + assert.deepStrictEqual(text, 'hi from agent b'); + assert.deepStrictEqual(pm.lastRequest, { + config: { + // TODO: figure out if config should be swapped out as well... + temperature: 2, + }, + messages: [ + { + role: 'system', + content: [{ text: ' Genkit toolPrompt prompt' }], // <--- NOTE: swapped out the preamble + metadata: { preamble: true }, + }, + { + role: 'user', + content: [{ text: 'hi' }], + }, + { + role: 'model', + content: [{ text: 'hi Genkit from agent a' }], + }, + { + role: 'user', + content: [{ text: 'pls transfer to b' }], + }, + { + role: 'model', + content: [ + { + toolRequest: { + input: {}, + name: 'toolPrompt', + ref: 'ref123', + }, + }, + ], + }, + { + role: 'tool', + content: [ + { + toolResponse: { + name: 'toolPrompt', + output: 'transferred to toolPrompt', + ref: 'ref123', + }, + }, + ], + }, + ], + output: {}, + tools: [ + { + name: 'agentA', + description: 'Agent A description', + inputSchema: { + $schema: 'http://json-schema.org/draft-07/schema#', + }, + outputSchema: { + $schema: 'http://json-schema.org/draft-07/schema#', + }, + }, + ], + }); + + // transfer back to to agent A... + + // first response be tools call, the subsequent just text response from agent a. + reqCounter = 0; + pm.handleResponse = async (req, sc) => { + return { + message: { + role: 'model', + content: [ + reqCounter++ === 0 + ? { + toolRequest: { + name: 'agentA', + input: {}, + ref: 'ref123', + }, + } + : { text: 'hi Genkit from agent a' }, + ], + }, + }; + }; + + ({ text } = await chat.send('pls transfer to a')); + + assert.deepStrictEqual(text, 'hi Genkit from agent a'); + assert.deepStrictEqual(pm.lastRequest, { + config: { + temperature: 2, + }, + messages: [ + { + role: 'system', + content: [{ text: ' agent a' }], // <--- NOTE: swapped out the preamble + metadata: { preamble: true }, + }, + { + role: 'user', + content: [{ text: 'hi' }], + }, + { + role: 'model', + content: [{ text: 'hi Genkit from agent a' }], + }, + { + role: 'user', + content: [{ text: 'pls transfer to b' }], + }, + { + role: 'model', + content: [ + { + toolRequest: { + input: {}, + name: 'toolPrompt', + ref: 'ref123', + }, + }, + ], + }, + { + role: 'tool', + content: [ + { + toolResponse: { + name: 'toolPrompt', + output: 'transferred to toolPrompt', + ref: 'ref123', + }, + }, + ], + }, + { + role: 'model', + content: [{ text: 'hi from agent b' }], + }, + { + role: 'user', + content: [{ text: 'pls transfer to a' }], + }, + { + role: 'model', + content: [ + { + toolRequest: { + input: {}, + name: 'agentA', + ref: 'ref123', + }, + }, + ], + }, + { + role: 'tool', + content: [ + { + toolResponse: { + name: 'agentA', + output: 'transferred to agentA', + ref: 'ref123', + }, + }, + ], + }, + ], + output: {}, + tools: [ + { + description: 'prompt in a file', + inputSchema: { + $schema: 'http://json-schema.org/draft-07/schema#', + }, + name: 'toolPrompt', + outputSchema: { + $schema: 'http://json-schema.org/draft-07/schema#', + }, + }, + ], + }); + }); +}); diff --git a/js/genkit/tests/session_test.ts b/js/genkit/tests/session_test.ts index 109599269..5014e8544 100644 --- a/js/genkit/tests/session_test.ts +++ b/js/genkit/tests/session_test.ts @@ -332,6 +332,119 @@ describe('session', () => { }); }); + it('can start chat from a prompt', async () => { + const agent = ai.definePrompt( + { + name: 'agent', + config: { temperature: 1 }, + description: 'Agent description', + }, + '{{role "system"}} hello from template' + ); + + const session = ai.createSession(); + const chat = session.chat(agent); + const respose = await chat.send('hi'); + assert.deepStrictEqual(respose.messages, [ + { + role: 'system', + content: [{ text: ' hello from template' }], + metadata: { preamble: true }, + }, + { + content: [{ text: 'hi' }], + role: 'user', + }, + { + content: [ + { text: 'Echo: system: hello from template,hi' }, + { text: '; config: {"temperature":1}' }, + ], + role: 'model', + }, + ]); + }); + + it('can start chat from a prompt with input', async () => { + const agent = ai.definePrompt( + { + name: 'agent', + config: { temperature: 1 }, + description: 'Agent description', + }, + '{{role "system"}} hello {{ name }} from template' + ); + + const session = ai.createSession(); + const chat = session.chat(agent, { + input: { + name: 'Genkit', + }, + }); + const respose = await chat.send('hi'); + assert.deepStrictEqual(respose.messages, [ + { + role: 'system', + content: [{ text: ' hello Genkit from template' }], + metadata: { preamble: true }, + }, + { + content: [{ text: 'hi' }], + role: 'user', + }, + { + content: [ + { text: 'Echo: system: hello Genkit from template,hi' }, + { text: '; config: {"temperature":1}' }, + ], + role: 'model', + }, + ]); + }); + + it('can start chat thread from a prompt with input', async () => { + const agent = ai.definePrompt( + { + name: 'agent', + config: { temperature: 1 }, + description: 'Agent description', + }, + '{{role "system"}} hello {{ name }} from template' + ); + const store = new TestMemorySessionStore(); + const session = ai.createSession({ store }); + const chat = session.chat('mythread', agent, { + input: { + name: 'Genkit', + }, + }); + + await chat.send('hi'); + + const gotState = await store.get(session.id); + delete (gotState as any).id; // ignore + assert.deepStrictEqual(gotState?.threads, { + mythread: [ + { + role: 'system', + content: [{ text: ' hello Genkit from template' }], + metadata: { preamble: true }, + }, + { + content: [{ text: 'hi' }], + role: 'user', + }, + { + content: [ + { text: 'Echo: system: hello Genkit from template,hi' }, + { text: '; config: {"temperature":1}' }, + ], + role: 'model', + }, + ], + }); + }); + it('can read current session state from a prompt', async () => { const agent = ai.definePrompt( { @@ -347,7 +460,7 @@ describe('session', () => { foo: 'bar', }, }); - const chat = session.chat({ prompt: agent }); + const chat = session.chat(agent); const respose = await chat.send('hi'); assert.deepStrictEqual(respose.messages, [ { diff --git a/js/package.json b/js/package.json index 368fbb71d..947f6218e 100644 --- a/js/package.json +++ b/js/package.json @@ -1,6 +1,5 @@ { "private": true, - "version": "0.5.10", "scripts": { "preinstall": "npx only-allow pnpm", "build": "pnpm install && pnpm build:libs && pnpm build:testapps", @@ -15,7 +14,7 @@ "pack:ai": "cd ai && pnpm pack --pack-destination ../../dist", "pack:genkit": "cd genkit && pnpm pack --pack-destination ../../dist", "pack:plugins": "for i in plugins/*/; do cd $i && pnpm pack --pack-destination ../../../dist && cd ../..; done", - "test:all": "pnpm -r --workspace-concurrency 0 -F \"./(ai|core|plugins)/**\" test" + "test:all": "pnpm -r --workspace-concurrency 0 -F \"./(ai|core|plugins|genkit)/**\" test" }, "devDependencies": { "npm-run-all": "^4.1.5", diff --git a/js/plugins/chroma/package.json b/js/plugins/chroma/package.json index f1edc8f86..be24c45e3 100644 --- a/js/plugins/chroma/package.json +++ b/js/plugins/chroma/package.json @@ -13,7 +13,7 @@ "genai", "generative-ai" ], - "version": "0.9.0-dev.2", + "version": "0.9.0-dev.4", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/dev-local-vectorstore/package.json b/js/plugins/dev-local-vectorstore/package.json index c43edbbcb..330df9bf0 100644 --- a/js/plugins/dev-local-vectorstore/package.json +++ b/js/plugins/dev-local-vectorstore/package.json @@ -10,7 +10,7 @@ "genai", "generative-ai" ], - "version": "0.9.0-dev.2", + "version": "0.9.0-dev.4", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/dotprompt/package.json b/js/plugins/dotprompt/package.json index 5878be516..3cca745ea 100644 --- a/js/plugins/dotprompt/package.json +++ b/js/plugins/dotprompt/package.json @@ -9,7 +9,7 @@ "prompting", "templating" ], - "version": "0.9.0-dev.2", + "version": "0.9.0-dev.4", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/dotprompt/src/index.ts b/js/plugins/dotprompt/src/index.ts index f6c8d6072..1e6d6ffa1 100644 --- a/js/plugins/dotprompt/src/index.ts +++ b/js/plugins/dotprompt/src/index.ts @@ -46,12 +46,13 @@ export interface DotpromptPluginOptions { export async function prompt( registry: Registry, name: string, - options?: { variant?: string } + options?: { variant?: string; dir?: string } ): Promise> { return (await lookupPrompt( registry, name, - options?.variant + options?.variant, + options?.dir ?? './prompts' )) as Dotprompt; } diff --git a/js/plugins/dotprompt/src/metadata.ts b/js/plugins/dotprompt/src/metadata.ts index 165176919..3917fda9b 100644 --- a/js/plugins/dotprompt/src/metadata.ts +++ b/js/plugins/dotprompt/src/metadata.ts @@ -86,6 +86,7 @@ export interface PromptMetadata< */ export const PromptFrontmatterSchema = z.object({ name: z.string().optional(), + description: z.string().optional(), variant: z.string().optional(), model: z.string().optional(), tools: z.array(z.string()).optional(), @@ -154,6 +155,7 @@ export function toMetadata( return stripUndefinedOrNull({ name: fm.name, + description: fm.description, variant: fm.variant, model: fm.model, config: fm.config, diff --git a/js/plugins/dotprompt/src/prompt.ts b/js/plugins/dotprompt/src/prompt.ts index 41a15e14c..a78a14e98 100644 --- a/js/plugins/dotprompt/src/prompt.ts +++ b/js/plugins/dotprompt/src/prompt.ts @@ -26,6 +26,7 @@ import { } from '@genkit-ai/ai'; import { MessageData, ModelArgument } from '@genkit-ai/ai/model'; import { DocumentData } from '@genkit-ai/ai/retriever'; +import { getCurrentSession } from '@genkit-ai/ai/session'; import { GenkitError, z } from '@genkit-ai/core'; import { Registry } from '@genkit-ai/core/registry'; import { parseSchema } from '@genkit-ai/core/schema'; @@ -47,8 +48,6 @@ import { compile } from './template.js'; export type PromptData = PromptFrontmatter & { template: string }; -export const GENKIT_SESSION_STATE_INPUT_KEY = '__genkit__sessionState'; - export type PromptGenerateOptions< V = unknown, CustomOptions extends z.ZodTypeAny = z.ZodTypeAny, @@ -67,6 +66,7 @@ interface RenderMetadata { export class Dotprompt implements PromptMetadata { name: string; + description?: string; variant?: string; hash: string; @@ -136,6 +136,7 @@ export class Dotprompt implements PromptMetadata { action?: PromptAction ) { this.name = options.name || 'untitledPrompt'; + this.description = options.description; this.variant = options.variant; this.model = options.model; this.input = options.input || { schema: z.any() }; @@ -181,10 +182,8 @@ export class Dotprompt implements PromptMetadata { */ renderMessages(input?: I, options?: RenderMetadata): MessageData[] { let sessionStateData: Record | undefined = undefined; - if (input?.hasOwnProperty(GENKIT_SESSION_STATE_INPUT_KEY)) { - sessionStateData = input[GENKIT_SESSION_STATE_INPUT_KEY]; - input = { ...input }; - delete input[GENKIT_SESSION_STATE_INPUT_KEY]; + if (getCurrentSession()) { + sessionStateData = { state: getCurrentSession()?.state }; } input = parseSchema(input, { schema: this.input?.schema, @@ -206,7 +205,7 @@ export class Dotprompt implements PromptMetadata { this.registry, { name: registryDefinitionKey(this.name, this.variant, options?.ns), - description: options?.description ?? 'Defined by Dotprompt', + description: options?.description ?? this.description, inputSchema: this.input?.schema, inputJsonSchema: this.input?.jsonSchema, metadata: { diff --git a/js/plugins/dotprompt/src/registry.ts b/js/plugins/dotprompt/src/registry.ts index b208a3847..42a35acf8 100644 --- a/js/plugins/dotprompt/src/registry.ts +++ b/js/plugins/dotprompt/src/registry.ts @@ -78,8 +78,8 @@ async function maybeLoadPrompt( export async function loadPromptFolder( registry: Registry, - - dir: string = './prompts' + dir: string = './prompts', + ns: string ): Promise { const promptsPath = resolve(dir); return new Promise((resolve, reject) => { @@ -119,7 +119,7 @@ export async function loadPromptFolder( .replace(`${promptsPath}/`, '') .replace(/\//g, '-'); } - loadPrompt(registry, dirEnt.path, dirEnt.name, prefix); + loadPrompt(registry, dirEnt.path, dirEnt.name, prefix, ns); } } }); @@ -137,7 +137,8 @@ export function loadPrompt( registry: Registry, path: string, filename: string, - prefix = '' + prefix = '', + ns = 'dotprompt' ): Dotprompt { let name = `${prefix ? `${prefix}-` : ''}${basename(filename, '.prompt')}`; let variant: string | null = null; @@ -151,6 +152,6 @@ export function loadPrompt( if (variant) { prompt.variant = variant; } - prompt.define({ ns: `dotprompt` }); + prompt.define({ ns }); return prompt; } diff --git a/js/plugins/evaluators/package.json b/js/plugins/evaluators/package.json index f9e01adc5..173baf15b 100644 --- a/js/plugins/evaluators/package.json +++ b/js/plugins/evaluators/package.json @@ -11,7 +11,7 @@ "genai", "generative-ai" ], - "version": "0.9.0-dev.2", + "version": "0.9.0-dev.4", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/firebase/package.json b/js/plugins/firebase/package.json index 25109d281..0b3007eb9 100644 --- a/js/plugins/firebase/package.json +++ b/js/plugins/firebase/package.json @@ -13,7 +13,7 @@ "genai", "generative-ai" ], - "version": "0.9.0-dev.2", + "version": "0.9.0-dev.4", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/google-cloud/package.json b/js/plugins/google-cloud/package.json index 53bb7cb37..1ce2a99b5 100644 --- a/js/plugins/google-cloud/package.json +++ b/js/plugins/google-cloud/package.json @@ -13,7 +13,7 @@ "genai", "generative-ai" ], - "version": "0.9.0-dev.2", + "version": "0.9.0-dev.4", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/google-cloud/src/gcpLogger.ts b/js/plugins/google-cloud/src/gcpLogger.ts index 861966d2d..392e4df41 100644 --- a/js/plugins/google-cloud/src/gcpLogger.ts +++ b/js/plugins/google-cloud/src/gcpLogger.ts @@ -70,6 +70,7 @@ export class GcpLogger { return winston.createLogger({ transports: transports, ...format, + exceptionHandlers: [new winston.transports.Console()], }); } @@ -78,7 +79,7 @@ export class GcpLogger { let instructionsLogged = false; let helpInstructions = await loggingDeniedHelpText(); - return (err: Error | null) => { + return async (err: Error | null) => { // Use the defaultLogger so that logs don't get swallowed by // the open telemetry exporter const defaultLogger = logger.defaultLogger; @@ -96,7 +97,9 @@ export class GcpLogger { if (err) { // Assume the logger is compromised, and we need a new one // Reinitialize the genkit logger with a new instance with the same config - logger.init(new GcpLogger(this.config).getLogger(getCurrentEnv())); + logger.init( + await new GcpLogger(this.config).getLogger(getCurrentEnv()) + ); defaultLogger.info('Initialized a new GcpLogger.'); } }; diff --git a/js/plugins/google-cloud/src/telemetry/generate.ts b/js/plugins/google-cloud/src/telemetry/generate.ts index 66bed1671..d7d2f7a51 100644 --- a/js/plugins/google-cloud/src/telemetry/generate.ts +++ b/js/plugins/google-cloud/src/telemetry/generate.ts @@ -349,6 +349,9 @@ class GenerateTelemetry implements Telemetry { if (part.text) { return this.toPartLogText(part.text); } + if (part.data) { + return this.toPartLogText(JSON.stringify(part.data)); + } if (part.media) { return this.toPartLogMedia(part); } @@ -358,6 +361,9 @@ class GenerateTelemetry implements Telemetry { if (part.toolResponse) { return this.toPartLogToolResponse(part); } + if (part.custom) { + return this.toPartLogText(JSON.stringify(part.custom)); + } return ''; } diff --git a/js/plugins/googleai/package.json b/js/plugins/googleai/package.json index ee63b9d90..f23282160 100644 --- a/js/plugins/googleai/package.json +++ b/js/plugins/googleai/package.json @@ -13,7 +13,7 @@ "genai", "generative-ai" ], - "version": "0.9.0-dev.2", + "version": "0.9.0-dev.4", "type": "commonjs", "scripts": { "check": "tsc", @@ -31,7 +31,7 @@ "author": "genkit", "license": "Apache-2.0", "dependencies": { - "@google/generative-ai": "^0.16.0", + "@google/generative-ai": "^0.21.0", "google-auth-library": "^9.6.3", "node-fetch": "^3.3.2" }, diff --git a/js/plugins/googleai/src/gemini.ts b/js/plugins/googleai/src/gemini.ts index 8bda18a3e..860654087 100644 --- a/js/plugins/googleai/src/gemini.ts +++ b/js/plugins/googleai/src/gemini.ts @@ -16,9 +16,9 @@ import { FileDataPart, + FunctionCallingMode, FunctionCallPart, FunctionDeclaration, - FunctionDeclarationSchemaType, FunctionResponsePart, GenerateContentCandidate as GeminiCandidate, Content as GeminiMessage, @@ -28,25 +28,33 @@ import { GoogleGenerativeAI, InlineDataPart, RequestOptions, + SchemaType, StartChatParams, Tool, + ToolConfig, } from '@google/generative-ai'; -import { GENKIT_CLIENT_HEADER, Genkit, z } from 'genkit'; +import { + Genkit, + GENKIT_CLIENT_HEADER, + GenkitError, + JSONSchema, + z, +} from 'genkit'; import { CandidateData, GenerationCommonConfigSchema, + getBasicUsageStats, MediaPart, MessageData, ModelAction, ModelInfo, ModelMiddleware, + modelRef, ModelReference, Part, ToolDefinitionSchema, ToolRequestPart, ToolResponsePart, - getBasicUsageStats, - modelRef, } from 'genkit/model'; import { downloadRequestMedia, @@ -73,6 +81,12 @@ const SafetySettingsSchema = z.object({ export const GeminiConfigSchema = GenerationCommonConfigSchema.extend({ safetySettings: z.array(SafetySettingsSchema).optional(), codeExecution: z.union([z.boolean(), z.object({}).strict()]).optional(), + functionCallingConfig: z + .object({ + mode: z.enum(['MODE_UNSPECIFIED', 'AUTO', 'ANY', 'NONE']).optional(), + allowedFunctionNames: z.array(z.string()).optional(), + }) + .optional(), }); export const gemini10Pro = modelRef({ @@ -99,7 +113,6 @@ export const gemini15Pro = modelRef({ media: true, tools: true, systemRole: true, - output: ['text', 'json'], }, versions: [ 'gemini-1.5-pro-latest', @@ -119,7 +132,6 @@ export const gemini15Flash = modelRef({ media: true, tools: true, systemRole: true, - output: ['text', 'json'], }, versions: [ 'gemini-1.5-flash-latest', @@ -139,7 +151,6 @@ export const gemini15Flash8b = modelRef({ media: true, tools: true, systemRole: true, - output: ['text', 'json'], }, versions: ['gemini-1.5-flash-8b-latest', 'gemini-1.5-flash-8b-001'], }, @@ -200,18 +211,18 @@ function convertSchemaProperty(property) { nestedProperties[key] = convertSchemaProperty(property.properties[key]); }); return { - type: FunctionDeclarationSchemaType.OBJECT, + type: SchemaType.OBJECT, properties: nestedProperties, required: property.required, }; } else if (property.type === 'array') { return { - type: FunctionDeclarationSchemaType.ARRAY, + type: SchemaType.ARRAY, items: convertSchemaProperty(property.items), }; } else { return { - type: FunctionDeclarationSchemaType[property.type.toUpperCase()], + type: SchemaType[property.type.toUpperCase()], }; } } @@ -371,9 +382,9 @@ function toGeminiPart(part: Part): GeminiPart { } function fromGeminiPart(part: GeminiPart, jsonMode: boolean): Part { - if (jsonMode && part.text !== undefined) { - return { data: JSON.parse(part.text) }; - } + // if (jsonMode && part.text !== undefined) { + // return { data: JSON.parse(part.text) }; + // } if (part.text !== undefined) return { text: part.text }; if (part.inlineData) return fromInlineData(part); if (part.functionCall) return fromFunctionCall(part); @@ -438,6 +449,20 @@ export function fromGeminiCandidate( }; } +function cleanSchema(schema: JSONSchema): JSONSchema { + const out = structuredClone(schema); + for (const key in out) { + if (key === '$schema' || key === 'additionalProperties') { + delete out[key]; + continue; + } + if (typeof out[key] === 'object') { + out[key] = cleanSchema(out[key]); + } + } + return out; +} + /** * Defines a new GoogleAI model. */ @@ -514,7 +539,7 @@ export function defineGoogleAIModel( if (apiVersion) { options.baseUrl = baseUrl; } - const requestConfig = { + const requestConfig: z.infer = { ...defaultConfig, ...request.config, }; @@ -548,7 +573,6 @@ export function defineGoogleAIModel( } const tools: Tool[] = []; - if (request.tools?.length) { tools.push({ functionDeclarations: request.tools.map(toGeminiTool), @@ -564,9 +588,23 @@ export function defineGoogleAIModel( }); } + let toolConfig: ToolConfig | undefined; + if (requestConfig.functionCallingConfig) { + toolConfig = { + functionCallingConfig: { + allowedFunctionNames: + requestConfig.functionCallingConfig.allowedFunctionNames, + mode: toGeminiFunctionMode( + requestConfig.functionCallingConfig.mode + ), + }, + }; + } + // cannot use tools with json mode const jsonMode = - (request.output?.format === 'json' || !!request.output?.schema) && + (request.output?.format === 'json' || + request.output?.contentType === 'application/json') && tools.length === 0; const generationConfig: GenerationConfig = { @@ -579,16 +617,22 @@ export function defineGoogleAIModel( responseMimeType: jsonMode ? 'application/json' : undefined, }; + if (request.output?.constrained && jsonMode) { + generationConfig.responseSchema = cleanSchema(request.output.schema); + } + const chatRequest = { systemInstruction, generationConfig, tools, + toolConfig, history: messages .slice(0, -1) .map((message) => toGeminiMessage(message, model)), safetySettings: requestConfig.safetySettings, } as StartChatParams; const msg = toGeminiMessage(messages[messages.length - 1], model); + const fromJSONModeScopedGeminiCandidate = ( candidate: GeminiCandidate ) => { @@ -608,12 +652,18 @@ export function defineGoogleAIModel( }); } const response = await result.response; - if (!response.candidates?.length) { - throw new Error('No valid candidates returned.'); + const candidates = response.candidates || []; + if (response.candidates?.['undefined']) { + candidates.push(response.candidates['undefined']); + } + if (!candidates.length) { + throw new GenkitError({ + status: 'FAILED_PRECONDITION', + message: 'No valid candidates returned.', + }); } return { - candidates: - response.candidates?.map(fromJSONModeScopedGeminiCandidate) || [], + candidates: candidates?.map(fromJSONModeScopedGeminiCandidate) || [], custom: response, }; } else { @@ -639,3 +689,27 @@ export function defineGoogleAIModel( } ); } + +function toGeminiFunctionMode( + genkitMode: string | undefined +): FunctionCallingMode | undefined { + if (genkitMode === undefined) { + return undefined; + } + switch (genkitMode) { + case 'MODE_UNSPECIFIED': { + return FunctionCallingMode.MODE_UNSPECIFIED; + } + case 'ANY': { + return FunctionCallingMode.ANY; + } + case 'AUTO': { + return FunctionCallingMode.AUTO; + } + case 'NONE': { + return FunctionCallingMode.NONE; + } + default: + throw new Error(`unsupported function calling mode: ${genkitMode}`); + } +} diff --git a/js/plugins/langchain/package.json b/js/plugins/langchain/package.json index 3f902a182..9aa24af6d 100644 --- a/js/plugins/langchain/package.json +++ b/js/plugins/langchain/package.json @@ -9,7 +9,7 @@ "genai", "generative-ai" ], - "version": "0.9.0-dev.2", + "version": "0.9.0-dev.4", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/ollama/package.json b/js/plugins/ollama/package.json index 74486dd08..8c05a1a2d 100644 --- a/js/plugins/ollama/package.json +++ b/js/plugins/ollama/package.json @@ -10,7 +10,7 @@ "genai", "generative-ai" ], - "version": "0.9.0-dev.2", + "version": "0.9.0-dev.4", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/pinecone/package.json b/js/plugins/pinecone/package.json index 22847e27b..85f43269d 100644 --- a/js/plugins/pinecone/package.json +++ b/js/plugins/pinecone/package.json @@ -13,7 +13,7 @@ "genai", "generative-ai" ], - "version": "0.9.0-dev.2", + "version": "0.9.0-dev.4", "type": "commonjs", "scripts": { "check": "tsc", diff --git a/js/plugins/vertexai/package.json b/js/plugins/vertexai/package.json index 899515c22..fcc437a20 100644 --- a/js/plugins/vertexai/package.json +++ b/js/plugins/vertexai/package.json @@ -17,7 +17,7 @@ "genai", "generative-ai" ], - "version": "0.9.0-dev.2", + "version": "0.9.0-dev.4", "type": "commonjs", "scripts": { "check": "tsc", @@ -55,7 +55,7 @@ "@types/node": "^20.11.16", "npm-run-all": "^4.1.5", "rimraf": "^6.0.1", - "tsup": "^8.0.2", + "tsup": "^8.3.5", "tsx": "^4.7.0", "typescript": "^4.9.0" }, @@ -66,6 +66,46 @@ "import": "./lib/index.mjs", "types": "./lib/index.d.ts", "default": "./lib/index.js" + }, + "./rerankers": { + "require": "./lib/rerankers/index.js", + "import": "./lib/rerankers/index.mjs", + "types": "./lib/rerankers/index.d.ts", + "default": "./lib/rerankers/index.js" + }, + "./evaluation": { + "require": "./lib/evaluation/index.js", + "import": "./lib/evaluation/index.mjs", + "types": "./lib/evaluation/index.d.ts", + "default": "./lib/evaluation/index.js" + }, + "./modelgarden": { + "require": "./lib/modelgarden/index.js", + "import": "./lib/modelgarden/index.mjs", + "types": "./lib/modelgarden/index.d.ts", + "default": "./lib/modelgarden/index.js" + }, + "./vectorsearch": { + "require": "./lib/vectorsearch/index.js", + "import": "./lib/vectorsearch/index.mjs", + "types": "./lib/vectorsearch/index.d.ts", + "default": "./lib/vectorsearch/index.js" + } + }, + "typesVersions": { + "*": { + "rerankers": [ + "./lib/rerankers/index" + ], + "evaluation": [ + "./lib/evaluation/index" + ], + "modelgarden": [ + "./lib/modelgarden/index" + ], + "vectorsearch": [ + "./lib/vectorsearch/index" + ] } } } diff --git a/js/plugins/vertexai/src/common/constants.ts b/js/plugins/vertexai/src/common/constants.ts new file mode 100644 index 000000000..7395e51c9 --- /dev/null +++ b/js/plugins/vertexai/src/common/constants.ts @@ -0,0 +1,18 @@ +/** + * 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 CLOUD_PLATFORM_OAUTH_SCOPE = + 'https://www.googleapis.com/auth/cloud-platform'; diff --git a/js/plugins/vertexai/src/common/index.ts b/js/plugins/vertexai/src/common/index.ts new file mode 100644 index 000000000..41c7f9ccf --- /dev/null +++ b/js/plugins/vertexai/src/common/index.ts @@ -0,0 +1,91 @@ +/** + * 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 { VertexAI } from '@google-cloud/vertexai'; +import { GenerateRequest } from 'genkit/model'; +import { GoogleAuth } from 'google-auth-library'; +import { GeminiConfigSchema } from '../gemini'; +import { CLOUD_PLATFORM_OAUTH_SCOPE } from './constants'; +import { PluginOptions } from './types'; + +export { PluginOptions }; + +interface DerivedParams { + location: string; + projectId: string; + vertexClientFactory: ( + request: GenerateRequest + ) => VertexAI; + authClient: GoogleAuth; +} + +export async function getDerivedParams( + options?: PluginOptions +): Promise { + let authOptions = options?.googleAuth; + let authClient: GoogleAuth; + + if (process.env.GCLOUD_SERVICE_ACCOUNT_CREDS) { + const serviceAccountCreds = JSON.parse( + process.env.GCLOUD_SERVICE_ACCOUNT_CREDS + ); + authOptions = { + credentials: serviceAccountCreds, + scopes: [CLOUD_PLATFORM_OAUTH_SCOPE], + }; + authClient = new GoogleAuth(authOptions); + } else { + authClient = new GoogleAuth( + authOptions ?? { scopes: [CLOUD_PLATFORM_OAUTH_SCOPE] } + ); + } + + const projectId = options?.projectId || (await authClient.getProjectId()); + const location = options?.location || 'us-central1'; + + if (!location) { + throw new Error( + `VertexAI Plugin is missing the 'location' configuration. Please set the 'GCLOUD_LOCATION' environment variable or explicitly pass 'location' into genkit config.` + ); + } + if (!projectId) { + throw new Error( + `VertexAI Plugin is missing the 'project' configuration. Please set the 'GCLOUD_PROJECT' environment variable or explicitly pass 'project' into genkit config.` + ); + } + + const vertexClientFactoryCache: Record = {}; + const vertexClientFactory = ( + request: GenerateRequest + ): VertexAI => { + const requestLocation = request.config?.location || location; + if (!vertexClientFactoryCache[requestLocation]) { + vertexClientFactoryCache[requestLocation] = new VertexAI({ + project: projectId, + location: requestLocation, + googleAuthOptions: authOptions, + }); + } + return vertexClientFactoryCache[requestLocation]; + }; + + return { + location, + projectId, + vertexClientFactory, + authClient, + }; +} diff --git a/js/plugins/vertexai/src/common/types.ts b/js/plugins/vertexai/src/common/types.ts new file mode 100644 index 000000000..a88fef9e6 --- /dev/null +++ b/js/plugins/vertexai/src/common/types.ts @@ -0,0 +1,30 @@ +/** + * 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 { GoogleAuthOptions } from 'google-auth-library'; + +/** Common options for Vertex AI plugin configuration */ +export interface CommonPluginOptions { + /** The Google Cloud project id to call. */ + projectId?: string; + /** The Google Cloud region to call. */ + location: string; + /** Provide custom authentication configuration for connecting to Vertex AI. */ + googleAuth?: GoogleAuthOptions; +} + +/** Combined plugin options, extending common options with subplugin-specific options */ +export interface PluginOptions extends CommonPluginOptions {} diff --git a/js/plugins/vertexai/src/embedder.ts b/js/plugins/vertexai/src/embedder.ts index 10d2ca18c..40e7a0a21 100644 --- a/js/plugins/vertexai/src/embedder.ts +++ b/js/plugins/vertexai/src/embedder.ts @@ -17,7 +17,7 @@ import { Genkit, z } from 'genkit'; import { EmbedderReference, embedderRef } from 'genkit/embedder'; import { GoogleAuth } from 'google-auth-library'; -import { PluginOptions } from './index.js'; +import { PluginOptions } from './common/types.js'; import { PredictClient, predictModel } from './predict.js'; export const TaskTypeSchema = z.enum([ diff --git a/js/plugins/vertexai/src/evaluation.ts b/js/plugins/vertexai/src/evaluation/evaluation.ts similarity index 100% rename from js/plugins/vertexai/src/evaluation.ts rename to js/plugins/vertexai/src/evaluation/evaluation.ts diff --git a/js/plugins/vertexai/src/evaluator_factory.ts b/js/plugins/vertexai/src/evaluation/evaluator_factory.ts similarity index 100% rename from js/plugins/vertexai/src/evaluator_factory.ts rename to js/plugins/vertexai/src/evaluation/evaluator_factory.ts diff --git a/js/plugins/vertexai/src/evaluation/index.ts b/js/plugins/vertexai/src/evaluation/index.ts new file mode 100644 index 000000000..799c77732 --- /dev/null +++ b/js/plugins/vertexai/src/evaluation/index.ts @@ -0,0 +1,38 @@ +/** + * 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 { Genkit } from 'genkit'; +import { GenkitPlugin, genkitPlugin } from 'genkit/plugin'; +import { getDerivedParams } from '../common/index.js'; +import { vertexEvaluators } from './evaluation.js'; +import { PluginOptions } from './types.js'; +export { VertexAIEvaluationMetricType } from './types.js'; +export { PluginOptions }; +/** + * Add Google Cloud Vertex AI Rerankers API to Genkit. + */ +export function vertexAIEvaluation(options: PluginOptions): GenkitPlugin { + return genkitPlugin('vertexAIEvaluation', async (ai: Genkit) => { + const { projectId, location, authClient } = await getDerivedParams(options); + + const metrics = + options?.evaluation && options.evaluation.metrics.length > 0 + ? options.evaluation.metrics + : []; + + vertexEvaluators(ai, authClient, metrics, projectId, location); + }); +} diff --git a/js/plugins/vertexai/src/evaluation/types.ts b/js/plugins/vertexai/src/evaluation/types.ts new file mode 100644 index 000000000..7ab48c055 --- /dev/null +++ b/js/plugins/vertexai/src/evaluation/types.ts @@ -0,0 +1,54 @@ +/** + * 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 { CommonPluginOptions } from '../common/types.js'; + +export enum VertexAIEvaluationMetricType { + // Update genkit/docs/plugins/vertex-ai.md when modifying the list of enums + BLEU = 'BLEU', + ROUGE = 'ROUGE', + FLUENCY = 'FLEUNCY', + SAFETY = 'SAFETY', + GROUNDEDNESS = 'GROUNDEDNESS', + SUMMARIZATION_QUALITY = 'SUMMARIZATION_QUALITY', + SUMMARIZATION_HELPFULNESS = 'SUMMARIZATION_HELPFULNESS', + SUMMARIZATION_VERBOSITY = 'SUMMARIZATION_VERBOSITY', +} + +/** + * Evaluation metric config. Use `metricSpec` to define the behavior of the metric. + * The value of `metricSpec` will be included in the request to the API. See the API documentation + * for details on the possible values of `metricSpec` for each metric. + * https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/evaluation#parameter-list + */ +export type VertexAIEvaluationMetricConfig = { + type: VertexAIEvaluationMetricType; + metricSpec: any; +}; + +export type VertexAIEvaluationMetric = + | VertexAIEvaluationMetricType + | VertexAIEvaluationMetricConfig; + +/** Options specific to evaluation configuration */ +export interface EvaluationOptions { + /** Configure Vertex AI evaluators */ + evaluation?: { + metrics: VertexAIEvaluationMetric[]; + }; +} + +export interface PluginOptions extends CommonPluginOptions, EvaluationOptions {} diff --git a/js/plugins/vertexai/src/gemini.ts b/js/plugins/vertexai/src/gemini.ts index 384407429..97d106bba 100644 --- a/js/plugins/vertexai/src/gemini.ts +++ b/js/plugins/vertexai/src/gemini.ts @@ -16,6 +16,7 @@ import { Content, + FunctionCallingMode, FunctionDeclaration, FunctionDeclarationSchemaType, Part as GeminiPart, @@ -25,9 +26,10 @@ import { HarmBlockThreshold, HarmCategory, StartChatParams, + ToolConfig, VertexAI, } from '@google-cloud/vertexai'; -import { GENKIT_CLIENT_HEADER, Genkit, z } from 'genkit'; +import { GENKIT_CLIENT_HEADER, Genkit, JSONSchema, z } from 'genkit'; import { CandidateData, GenerateRequest, @@ -46,7 +48,7 @@ import { downloadRequestMedia, simulateSystemPrompt, } from 'genkit/model/middleware'; -import { PluginOptions } from './index.js'; +import { PluginOptions } from './common/types.js'; const SafetySettingsSchema = z.object({ category: z.nativeEnum(HarmCategory), @@ -71,6 +73,12 @@ export const GeminiConfigSchema = GenerationCommonConfigSchema.extend({ location: z.string().optional(), vertexRetrieval: VertexRetrievalSchema.optional(), googleSearchRetrieval: GoogleSearchRetrievalSchema.optional(), + functionCallingConfig: z + .object({ + mode: z.enum(['MODE_UNSPECIFIED', 'AUTO', 'ANY', 'NONE']).optional(), + allowedFunctionNames: z.array(z.string()).optional(), + }) + .optional(), }); export const gemini10Pro = modelRef({ @@ -344,9 +352,9 @@ function fromGeminiFunctionResponsePart(part: GeminiPart): Part { // Converts vertex part to genkit part function fromGeminiPart(part: GeminiPart, jsonMode: boolean): Part { - if (jsonMode && part.text !== undefined) { - return { data: JSON.parse(part.text) }; - } + // if (jsonMode && part.text !== undefined) { + // return { data: JSON.parse(part.text) }; + // } if (part.text !== undefined) return { text: part.text }; if (part.functionCall) return fromGeminiFunctionCallPart(part); if (part.functionResponse) return fromGeminiFunctionResponsePart(part); @@ -407,6 +415,20 @@ const convertSchemaProperty = (property) => { } }; +function cleanSchema(schema: JSONSchema): JSONSchema { + const out = structuredClone(schema); + for (const key in out) { + if (key === '$schema' || key === 'additionalProperties') { + delete out[key]; + continue; + } + if (typeof out[key] === 'object') { + out[key] = cleanSchema(out[key]); + } + } + return out; +} + /** * Define a Vertex AI Gemini model. */ @@ -471,6 +493,18 @@ export function defineGeminiModel( ? [{ functionDeclarations: request.tools?.map(toGeminiTool) }] : []; + let toolConfig: ToolConfig | undefined; + if (request?.config?.functionCallingConfig) { + toolConfig = { + functionCallingConfig: { + allowedFunctionNames: + request.config.functionCallingConfig.allowedFunctionNames, + mode: toGeminiFunctionMode( + request.config.functionCallingConfig.mode + ), + }, + }; + } // Cannot use tools and function calling at the same time const jsonMode = (request.output?.format === 'json' || !!request.output?.schema) && @@ -479,6 +513,7 @@ export function defineGeminiModel( const chatRequest: StartChatParams = { systemInstruction, tools, + toolConfig, history: messages .slice(0, -1) .map((message) => toGeminiMessage(message, model)), @@ -493,6 +528,13 @@ export function defineGeminiModel( }, safetySettings: request.config?.safetySettings, }; + + if (jsonMode && request.output?.constrained) { + chatRequest.generationConfig!.responseSchema = cleanSchema( + request.output.schema + ); + } + if (request.config?.googleSearchRetrieval) { chatRequest.tools?.push({ googleSearchRetrieval: request.config.googleSearchRetrieval, @@ -568,3 +610,27 @@ export function defineGeminiModel( } ); } + +function toGeminiFunctionMode( + genkitMode: string | undefined +): FunctionCallingMode | undefined { + if (genkitMode === undefined) { + return undefined; + } + switch (genkitMode) { + case 'MODE_UNSPECIFIED': { + return FunctionCallingMode.MODE_UNSPECIFIED; + } + case 'ANY': { + return FunctionCallingMode.ANY; + } + case 'AUTO': { + return FunctionCallingMode.AUTO; + } + case 'NONE': { + return FunctionCallingMode.NONE; + } + default: + throw new Error(`unsupported function calling mode: ${genkitMode}`); + } +} diff --git a/js/plugins/vertexai/src/imagen.ts b/js/plugins/vertexai/src/imagen.ts index 12f11fd13..926a9d24d 100644 --- a/js/plugins/vertexai/src/imagen.ts +++ b/js/plugins/vertexai/src/imagen.ts @@ -24,7 +24,7 @@ import { modelRef, } from 'genkit/model'; import { GoogleAuth } from 'google-auth-library'; -import { PluginOptions } from './index.js'; +import { PluginOptions } from './common/types.js'; import { PredictClient, predictModel } from './predict.js'; const ImagenConfigSchema = GenerationCommonConfigSchema.extend({ diff --git a/js/plugins/vertexai/src/index.ts b/js/plugins/vertexai/src/index.ts index 088d9212d..1b6566e1a 100644 --- a/js/plugins/vertexai/src/index.ts +++ b/js/plugins/vertexai/src/index.ts @@ -14,19 +14,10 @@ * limitations under the License. */ -import { VertexAI } from '@google-cloud/vertexai'; -import { Genkit, z } from 'genkit'; -import { GenerateRequest, ModelReference } from 'genkit/model'; +import { Genkit } from 'genkit'; import { GenkitPlugin, genkitPlugin } from 'genkit/plugin'; -import { GoogleAuth, GoogleAuthOptions } from 'google-auth-library'; -import { - SUPPORTED_ANTHROPIC_MODELS, - anthropicModel, - claude35Sonnet, - claude3Haiku, - claude3Opus, - claude3Sonnet, -} from './anthropic.js'; +import { getDerivedParams } from './common/index.js'; +import { PluginOptions } from './common/types.js'; import { SUPPORTED_EMBEDDER_MODELS, defineVertexAIEmbedder, @@ -36,12 +27,6 @@ import { textMultilingualEmbedding002, } from './embedder.js'; import { - VertexAIEvaluationMetric, - VertexAIEvaluationMetricType, - vertexEvaluators, -} from './evaluation.js'; -import { - GeminiConfigSchema, SUPPORTED_GEMINI_MODELS, defineGeminiModel, gemini10Pro, @@ -55,140 +40,26 @@ import { imagen3Fast, imagenModel, } from './imagen.js'; -import { - SUPPORTED_OPENAI_FORMAT_MODELS, - llama3, - llama31, - llama32, - modelGardenOpenaiCompatibleModel, -} from './model_garden.js'; -import { VertexRerankerConfig, vertexAiRerankers } from './reranker.js'; -import { - VectorSearchOptions, - vertexAiIndexers, - vertexAiRetrievers, -} from './vector-search/index.js'; -export { - DocumentIndexer, - DocumentRetriever, - Neighbor, - VectorSearchOptions, - getBigQueryDocumentIndexer, - getBigQueryDocumentRetriever, - getFirestoreDocumentIndexer, - getFirestoreDocumentRetriever, - vertexAiIndexerRef, - vertexAiIndexers, - vertexAiRetrieverRef, - vertexAiRetrievers, -} from './vector-search/index.js'; +export { PluginOptions } from './common/types.js'; export { - VertexAIEvaluationMetricType as VertexAIEvaluationMetricType, - claude35Sonnet, - claude3Haiku, - claude3Opus, - claude3Sonnet, gemini10Pro, gemini15Flash, gemini15Pro, imagen2, imagen3, imagen3Fast, - llama3, - llama31, - llama32, textEmbedding004, textEmbeddingGecko003, textEmbeddingGeckoMultilingual001, textMultilingualEmbedding002, }; - -export interface PluginOptions { - /** The Google Cloud project id to call. */ - projectId?: string; - /** The Google Cloud region to call. */ - location: string; - /** Provide custom authentication configuration for connecting to Vertex AI. */ - googleAuth?: GoogleAuthOptions; - /** Configure Vertex AI evaluators */ - evaluation?: { - metrics: VertexAIEvaluationMetric[]; - }; - /** - * @deprecated use `modelGarden.models` - */ - modelGardenModels?: ModelReference[]; - modelGarden?: { - models: ModelReference[]; - openAiBaseUrlTemplate?: string; - }; - /** Configure Vertex AI vector search index options */ - vectorSearchOptions?: VectorSearchOptions[]; - /** Configure reranker options */ - rerankOptions?: VertexRerankerConfig[]; -} - -const CLOUD_PLATFROM_OAUTH_SCOPE = - 'https://www.googleapis.com/auth/cloud-platform'; - /** * Add Google Cloud Vertex AI to Genkit. Includes Gemini and Imagen models and text embedder. */ export function vertexAI(options?: PluginOptions): GenkitPlugin { return genkitPlugin('vertexai', async (ai: Genkit) => { - let authClient; - let authOptions = options?.googleAuth; - - // Allow customers to pass in cloud credentials from environment variables - // following: https://github.com/googleapis/google-auth-library-nodejs?tab=readme-ov-file#loading-credentials-from-environment-variables - if (process.env.GCLOUD_SERVICE_ACCOUNT_CREDS) { - const serviceAccountCreds = JSON.parse( - process.env.GCLOUD_SERVICE_ACCOUNT_CREDS - ); - authOptions = { - credentials: serviceAccountCreds, - scopes: [CLOUD_PLATFROM_OAUTH_SCOPE], - }; - authClient = new GoogleAuth(authOptions); - } else { - authClient = new GoogleAuth( - authOptions ?? { scopes: [CLOUD_PLATFROM_OAUTH_SCOPE] } - ); - } - - const projectId = options?.projectId || (await authClient.getProjectId()); - - const location = options?.location || 'us-central1'; - const confError = (parameter: string, envVariableName: string) => { - return new Error( - `VertexAI Plugin is missing the '${parameter}' configuration. Please set the '${envVariableName}' environment variable or explicitly pass '${parameter}' into genkit config.` - ); - }; - if (!location) { - throw confError('location', 'GCLOUD_LOCATION'); - } - if (!projectId) { - throw confError('project', 'GCLOUD_PROJECT'); - } - - const vertexClientFactoryCache: Record = {}; - const vertexClientFactory = ( - request: GenerateRequest - ): VertexAI => { - const requestLocation = request.config?.location || location; - if (!vertexClientFactoryCache[requestLocation]) { - vertexClientFactoryCache[requestLocation] = new VertexAI({ - project: projectId, - location: requestLocation, - googleAuthOptions: authOptions, - }); - } - return vertexClientFactoryCache[requestLocation]; - }; - const metrics = - options?.evaluation && options.evaluation.metrics.length > 0 - ? options.evaluation.metrics - : []; + const { projectId, location, vertexClientFactory, authClient } = + await getDerivedParams(options); Object.keys(SUPPORTED_IMAGEN_MODELS).map((name) => imagenModel(ai, name, authClient, { projectId, location }) @@ -197,65 +68,9 @@ export function vertexAI(options?: PluginOptions): GenkitPlugin { defineGeminiModel(ai, name, vertexClientFactory, { projectId, location }) ); - if (options?.modelGardenModels || options?.modelGarden?.models) { - const mgModels = - options?.modelGardenModels || options?.modelGarden?.models; - mgModels!.forEach((m) => { - const anthropicEntry = Object.entries(SUPPORTED_ANTHROPIC_MODELS).find( - ([_, value]) => value.name === m.name - ); - if (anthropicEntry) { - anthropicModel(ai, anthropicEntry[0], projectId, location); - return; - } - const openaiModel = Object.entries(SUPPORTED_OPENAI_FORMAT_MODELS).find( - ([_, value]) => value.name === m.name - ); - if (openaiModel) { - modelGardenOpenaiCompatibleModel( - ai, - openaiModel[0], - projectId, - location, - authClient, - options.modelGarden?.openAiBaseUrlTemplate - ); - return; - } - throw new Error(`Unsupported model garden model: ${m.name}`); - }); - } - - const embedders = Object.keys(SUPPORTED_EMBEDDER_MODELS).map((name) => + Object.keys(SUPPORTED_EMBEDDER_MODELS).map((name) => defineVertexAIEmbedder(ai, name, authClient, { projectId, location }) ); - - if ( - options?.vectorSearchOptions && - options.vectorSearchOptions.length > 0 - ) { - const defaultEmbedder = embedders[0]; - - vertexAiIndexers(ai, { - pluginOptions: options, - authClient, - defaultEmbedder, - }); - - vertexAiRetrievers(ai, { - pluginOptions: options, - authClient, - defaultEmbedder, - }); - } - - const rerankOptions = { - pluginOptions: options, - authClient, - projectId, - }; - await vertexAiRerankers(ai, rerankOptions); - vertexEvaluators(ai, authClient, metrics, projectId, location); }); } diff --git a/js/plugins/vertexai/src/anthropic.ts b/js/plugins/vertexai/src/modelgarden/anthropic.ts similarity index 96% rename from js/plugins/vertexai/src/anthropic.ts rename to js/plugins/vertexai/src/modelgarden/anthropic.ts index a28ea12d4..0c967c31f 100644 --- a/js/plugins/vertexai/src/anthropic.ts +++ b/js/plugins/vertexai/src/modelgarden/anthropic.ts @@ -50,6 +50,22 @@ export const AnthropicConfigSchema = GenerationCommonConfigSchema.extend({ location: z.string().optional(), }); +export const claude35SonnetV2 = modelRef({ + name: 'vertexai/claude-3-5-sonnet-v2', + info: { + label: 'Vertex AI Model Garden - Claude 3.5 Sonnet', + versions: ['claude-3-5-sonnet-v2@20241022'], + supports: { + multiturn: true, + media: true, + tools: true, + systemRole: true, + output: ['text'], + }, + }, + configSchema: AnthropicConfigSchema, +}); + export const claude35Sonnet = modelRef({ name: 'vertexai/claude-3-5-sonnet', info: { @@ -118,6 +134,7 @@ export const SUPPORTED_ANTHROPIC_MODELS: Record< string, ModelReference > = { + 'claude-3-5-sonnet-v2': claude35SonnetV2, 'claude-3-5-sonnet': claude35Sonnet, 'claude-3-sonnet': claude3Sonnet, 'claude-3-opus': claude3Opus, diff --git a/js/plugins/vertexai/src/modelgarden/index.ts b/js/plugins/vertexai/src/modelgarden/index.ts new file mode 100644 index 000000000..8acbe81d4 --- /dev/null +++ b/js/plugins/vertexai/src/modelgarden/index.ts @@ -0,0 +1,69 @@ +/** + * 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 { Genkit } from 'genkit'; +import { GenkitPlugin, genkitPlugin } from 'genkit/plugin'; +import { getDerivedParams } from '../common/index.js'; +import { SUPPORTED_ANTHROPIC_MODELS, anthropicModel } from './anthropic.js'; +import { + SUPPORTED_OPENAI_FORMAT_MODELS, + modelGardenOpenaiCompatibleModel, +} from './model_garden.js'; +import { PluginOptions } from './types.js'; +/** + * Add Google Cloud Vertex AI Rerankers API to Genkit. + */ +export function vertexAIModelGarden(options: PluginOptions): GenkitPlugin { + return genkitPlugin('vertexAIModelGarden', async (ai: Genkit) => { + const { projectId, location, authClient } = await getDerivedParams(options); + + const mgModels = options?.modelGardenModels || options?.modelGarden?.models; + mgModels!.forEach((m) => { + const anthropicEntry = Object.entries(SUPPORTED_ANTHROPIC_MODELS).find( + ([_, value]) => value.name === m.name + ); + if (anthropicEntry) { + anthropicModel(ai, anthropicEntry[0], projectId, location); + return; + } + const openaiModel = Object.entries(SUPPORTED_OPENAI_FORMAT_MODELS).find( + ([_, value]) => value.name === m.name + ); + if (openaiModel) { + modelGardenOpenaiCompatibleModel( + ai, + openaiModel[0], + projectId, + location, + authClient, + options.modelGarden?.openAiBaseUrlTemplate + ); + return; + } + throw new Error(`Unsupported model garden model: ${m.name}`); + }); + }); +} + +export { + claude35Sonnet, + claude35SonnetV2, + claude3Haiku, + claude3Opus, + claude3Sonnet, +} from './anthropic.js'; +export { llama3, llama31, llama32 } from './model_garden.js'; +export type { PluginOptions }; diff --git a/js/plugins/vertexai/src/model_garden.ts b/js/plugins/vertexai/src/modelgarden/model_garden.ts similarity index 100% rename from js/plugins/vertexai/src/model_garden.ts rename to js/plugins/vertexai/src/modelgarden/model_garden.ts diff --git a/js/plugins/vertexai/src/openai_compatibility.ts b/js/plugins/vertexai/src/modelgarden/openai_compatibility.ts similarity index 100% rename from js/plugins/vertexai/src/openai_compatibility.ts rename to js/plugins/vertexai/src/modelgarden/openai_compatibility.ts diff --git a/js/plugins/vertexai/src/modelgarden/types.ts b/js/plugins/vertexai/src/modelgarden/types.ts new file mode 100644 index 000000000..d63dcca33 --- /dev/null +++ b/js/plugins/vertexai/src/modelgarden/types.ts @@ -0,0 +1,52 @@ +/** + * 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 { ModelReference } from 'genkit'; +import { CommonPluginOptions } from '../common/types.js'; + +export enum VertexAIEvaluationMetricType { + // Update genkit/docs/plugins/vertex-ai.md when modifying the list of enums + BLEU = 'BLEU', + ROUGE = 'ROUGE', + FLUENCY = 'FLEUNCY', + SAFETY = 'SAFETY', + GROUNDEDNESS = 'GROUNDEDNESS', + SUMMARIZATION_QUALITY = 'SUMMARIZATION_QUALITY', + SUMMARIZATION_HELPFULNESS = 'SUMMARIZATION_HELPFULNESS', + SUMMARIZATION_VERBOSITY = 'SUMMARIZATION_VERBOSITY', +} + +/** + * Evaluation metric config. Use `metricSpec` to define the behavior of the metric. + * The value of `metricSpec` will be included in the request to the API. See the API documentation + * for details on the possible values of `metricSpec` for each metric. + * https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/evaluation#parameter-list + */ +/** Options specific to Model Garden configuration */ +export interface ModelGardenOptions { + /** + * @deprecated use `modelGarden.models` + */ + modelGardenModels?: ModelReference[]; + modelGarden?: { + models: ModelReference[]; + openAiBaseUrlTemplate?: string; + }; +} + +export interface PluginOptions + extends CommonPluginOptions, + ModelGardenOptions {} diff --git a/js/plugins/vertexai/src/predict.ts b/js/plugins/vertexai/src/predict.ts index dfc538a5b..0d4f39302 100644 --- a/js/plugins/vertexai/src/predict.ts +++ b/js/plugins/vertexai/src/predict.ts @@ -16,7 +16,7 @@ import { GENKIT_CLIENT_HEADER } from 'genkit'; import { GoogleAuth } from 'google-auth-library'; -import { PluginOptions } from '.'; +import { PluginOptions } from './common/types.js'; function endpoint(options: { projectId: string; diff --git a/js/plugins/vertexai/src/reranker.ts b/js/plugins/vertexai/src/reranker.ts deleted file mode 100644 index 95df9b2c9..000000000 --- a/js/plugins/vertexai/src/reranker.ts +++ /dev/null @@ -1,159 +0,0 @@ -/** - * 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 { Genkit, z } from 'genkit'; -import { RankedDocument, RerankerAction, rerankerRef } from 'genkit/reranker'; -import { GoogleAuth } from 'google-auth-library'; -import { PluginOptions } from '.'; - -const DEFAULT_MODEL = 'semantic-ranker-512@latest'; - -const getRerankEndpoint = (projectId: string, location: string) => { - return `https://discoveryengine.googleapis.com/v1/projects/${projectId}/locations/${location}/rankingConfigs/default_ranking_config:rank`; -}; - -// Define the schema for the options used in the Vertex AI reranker -export const VertexAIRerankerOptionsSchema = z.object({ - k: z.number().optional().describe('Number of top documents to rerank'), // Optional: Number of documents to rerank - model: z.string().optional().describe('Model name for reranking'), // Optional: Model name, defaults to a pre-defined model - location: z - .string() - .optional() - .describe('Google Cloud location, e.g., "us-central1"'), // Optional: Location of the reranking model -}); - -// Type alias for the options schema -export type VertexAIRerankerOptions = z.infer< - typeof VertexAIRerankerOptionsSchema ->; - -// Define the structure for each individual reranker configuration -export const VertexRerankerConfigSchema = z.object({ - model: z.string().optional().describe('Model name for reranking'), // Optional: Model name, defaults to a pre-defined model -}); - -export interface VertexRerankerConfig { - name?: string; - model?: string; -} - -export interface VertexRerankPluginOptions { - rerankOptions: VertexRerankerConfig[]; - projectId: string; - location?: string; // Optional: Location of the reranker service -} - -export interface VertexRerankOptions { - authClient: GoogleAuth; - pluginOptions?: PluginOptions; -} - -/** - * Creates Vertex AI rerankers. - * - * This function returns a list of reranker actions for Vertex AI based on the provided - * rerank options and configuration. - * - * @param {VertexRerankOptions} params - The parameters for creating the rerankers. - * @returns {RerankerAction[]} - An array of reranker actions. - */ -export async function vertexAiRerankers( - ai: Genkit, - params: VertexRerankOptions -): Promise[]> { - if (!params.pluginOptions) { - return []; - } - const pluginOptions = params.pluginOptions; - if (!params.pluginOptions.rerankOptions) { - return []; - } - - const rerankOptions = params.pluginOptions.rerankOptions; - const rerankers: RerankerAction[] = []; - - if (!rerankOptions || rerankOptions.length === 0) { - return rerankers; - } - const auth = new GoogleAuth(); - const client = await auth.getClient(); - const projectId = await auth.getProjectId(); - - for (const rerankOption of rerankOptions) { - const reranker = ai.defineReranker( - { - name: `vertexai/${rerankOption.name || rerankOption.model}`, - configSchema: VertexAIRerankerOptionsSchema.optional(), - }, - async (query, documents, _options) => { - const response = await client.request({ - method: 'POST', - url: getRerankEndpoint( - projectId, - pluginOptions.location ?? 'us-central1' - ), - data: { - model: rerankOption.model || DEFAULT_MODEL, // Use model from config or default - query: query.text, - records: documents.map((doc, idx) => ({ - id: `${idx}`, - content: doc.text, - })), - }, - }); - - const rankedDocuments: RankedDocument[] = ( - response.data as any - ).records.map((record: any) => { - const doc = documents[record.id]; - return new RankedDocument({ - content: doc.content, - metadata: { - ...doc.metadata, - score: record.score, - }, - }); - }); - - return { documents: rankedDocuments }; - } - ); - - rerankers.push(reranker); - } - - return rerankers; -} - -/** - * Creates a reference to a Vertex AI reranker. - * - * @param {Object} params - The parameters for the reranker reference. - * @param {string} [params.displayName] - An optional display name for the reranker. - * @returns {Object} - The reranker reference object. - */ -export const vertexAiRerankerRef = (params: { - name: string; - displayName?: string; -}) => { - return rerankerRef({ - name: `vertexai/${name}`, - info: { - label: params.displayName ?? `Vertex AI Reranker`, - }, - configSchema: VertexAIRerankerOptionsSchema.optional(), - }); -}; diff --git a/js/plugins/vertexai/src/rerankers/constants.ts b/js/plugins/vertexai/src/rerankers/constants.ts new file mode 100644 index 000000000..fc32392be --- /dev/null +++ b/js/plugins/vertexai/src/rerankers/constants.ts @@ -0,0 +1,21 @@ +/** + * 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 DEFAULT_MODEL = 'semantic-ranker-512@latest'; + +export const getRerankEndpoint = (projectId: string, location: string) => { + return `https://discoveryengine.googleapis.com/v1/projects/${projectId}/locations/${location}/rankingConfigs/default_ranking_config:rank`; +}; diff --git a/js/plugins/vertexai/src/rerankers/index.ts b/js/plugins/vertexai/src/rerankers/index.ts new file mode 100644 index 000000000..33cd8cf52 --- /dev/null +++ b/js/plugins/vertexai/src/rerankers/index.ts @@ -0,0 +1,42 @@ +/** + * 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 { Genkit } from 'genkit'; +import { GenkitPlugin, genkitPlugin } from 'genkit/plugin'; +import { CommonPluginOptions } from '../common/types.js'; +import { RerankerOptions } from './types.js'; + +import { getDerivedParams } from '../common/index.js'; + +import { vertexAiRerankers } from './reranker.js'; + +export interface PluginOptions extends CommonPluginOptions, RerankerOptions {} + +/** + * Add Google Cloud Vertex AI Rerankers API to Genkit. + */ +export function vertexAIRerankers(options: PluginOptions): GenkitPlugin { + return genkitPlugin('vertexAIRerankers', async (ai: Genkit) => { + const { projectId, location, authClient } = await getDerivedParams(options); + + await vertexAiRerankers(ai, { + projectId, + location, + authClient, + rerankOptions: options.rerankOptions, + }); + }); +} diff --git a/js/plugins/vertexai/src/rerankers/reranker.ts b/js/plugins/vertexai/src/rerankers/reranker.ts new file mode 100644 index 000000000..cc807cb75 --- /dev/null +++ b/js/plugins/vertexai/src/rerankers/reranker.ts @@ -0,0 +1,101 @@ +/** + * 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 { Genkit } from 'genkit'; +import { RankedDocument, rerankerRef } from 'genkit/reranker'; +import { DEFAULT_MODEL, getRerankEndpoint } from './constants.js'; +import { VertexAIRerankerOptionsSchema, VertexRerankOptions } from './types.js'; + +/** + * Creates Vertex AI rerankers. + * + * This function creates and registers rerankers for the specified models. + * + * @param {VertexRerankOptions} options - The parameters for creating the rerankers. + * @returns {Promise} + */ +export async function vertexAiRerankers( + ai: Genkit, + options: VertexRerankOptions +): Promise { + const rerankOptions = options.rerankOptions; + + if (rerankOptions.length === 0) { + return; + } + + const auth = options.authClient; + const client = await auth.getClient(); + const projectId = options.projectId; + + for (const rerankOption of rerankOptions) { + ai.defineReranker( + { + name: `vertexai/${rerankOption.name || rerankOption.model}`, + configSchema: VertexAIRerankerOptionsSchema.optional(), + }, + async (query, documents, _options) => { + const response = await client.request({ + method: 'POST', + url: getRerankEndpoint(projectId, options.location ?? 'us-central1'), + data: { + model: rerankOption.model || DEFAULT_MODEL, // Use model from config or default + query: query.text, + records: documents.map((doc, idx) => ({ + id: `${idx}`, + content: doc.text, + })), + }, + }); + + const rankedDocuments: RankedDocument[] = ( + response.data as any + ).records.map((record: any) => { + const doc = documents[record.id]; + return new RankedDocument({ + content: doc.content, + metadata: { + ...doc.metadata, + score: record.score, + }, + }); + }); + + return { documents: rankedDocuments }; + } + ); + } +} + +/** + * Creates a reference to a Vertex AI reranker. + * + * @param {Object} params - The parameters for the reranker reference. + * @param {string} [params.displayName] - An optional display name for the reranker. + * @returns {Object} - The reranker reference object. + */ +export const vertexAiRerankerRef = (params: { + rerankerName: string; + displayName?: string; +}) => { + return rerankerRef({ + name: `vertexai/${params.rerankerName}`, + info: { + label: params.displayName ?? `Vertex AI Reranker`, + }, + configSchema: VertexAIRerankerOptionsSchema.optional(), + }); +}; diff --git a/js/plugins/vertexai/src/rerankers/types.ts b/js/plugins/vertexai/src/rerankers/types.ts new file mode 100644 index 000000000..4f5b00d5f --- /dev/null +++ b/js/plugins/vertexai/src/rerankers/types.ts @@ -0,0 +1,56 @@ +/** + * 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 { z } from 'genkit'; +import { GoogleAuth } from 'google-auth-library'; +import { CommonPluginOptions } from '../common/types.js'; + +// Define the schema for the options used in the Vertex AI reranker +export const VertexAIRerankerOptionsSchema = z.object({ + k: z.number().optional().describe('Number of top documents to rerank'), // Optional: Number of documents to rerank + model: z.string().optional().describe('Model name for reranking'), // Optional: Model name, defaults to a pre-defined model + location: z + .string() + .optional() + .describe('Google Cloud location, e.g., "us-central1"'), // Optional: Location of the reranking model +}); + +// Type alias for the options schema +export type VertexAIRerankerOptions = z.infer< + typeof VertexAIRerankerOptionsSchema +>; + +// Define the structure for each individual reranker configuration +export const VertexRerankerConfigSchema = z.object({ + name: z.string().optional().describe('Name of the reranker'), // Optional: Name of the reranker + model: z.string().optional().describe('Model name for reranking'), // Optional: Model name, defaults to a pre-defined model +}); + +export type VertexRerankerConfig = z.infer; + +export interface VertexRerankOptions { + authClient: GoogleAuth; + location: string; + projectId: string; + rerankOptions: VertexRerankerConfig[]; +} + +export interface RerankerOptions { + /** Configure reranker options */ + rerankOptions: VertexRerankerConfig[]; +} + +export interface PluginOptions extends CommonPluginOptions, RerankerOptions {} diff --git a/js/plugins/vertexai/src/vectorsearch/index.ts b/js/plugins/vertexai/src/vectorsearch/index.ts new file mode 100644 index 000000000..e372660e5 --- /dev/null +++ b/js/plugins/vertexai/src/vectorsearch/index.ts @@ -0,0 +1,62 @@ +/** + * 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 { Genkit } from 'genkit'; +import { GenkitPlugin, genkitPlugin } from 'genkit/plugin'; +import { getDerivedParams } from '../common/index.js'; +import { PluginOptions } from './types.js'; +import { vertexAiIndexers, vertexAiRetrievers } from './vector_search/index.js'; +export { PluginOptions } from '../common/types.js'; +export { + DocumentIndexer, + DocumentRetriever, + Neighbor, + VectorSearchOptions, + getBigQueryDocumentIndexer, + getBigQueryDocumentRetriever, + getFirestoreDocumentIndexer, + getFirestoreDocumentRetriever, + vertexAiIndexerRef, + vertexAiIndexers, + vertexAiRetrieverRef, + vertexAiRetrievers, +} from './vector_search/index.js'; +/** + * Add Google Cloud Vertex AI to Genkit. Includes Gemini and Imagen models and text embedder. + */ +export function vertexAIVectorSearch(options?: PluginOptions): GenkitPlugin { + return genkitPlugin('vertexAIVectorSearch', async (ai: Genkit) => { + const { projectId, location, vertexClientFactory, authClient } = + await getDerivedParams(options); + + if ( + options?.vectorSearchOptions && + options.vectorSearchOptions.length > 0 + ) { + vertexAiIndexers(ai, { + pluginOptions: options, + authClient, + defaultEmbedder: options.embedder, + }); + + vertexAiRetrievers(ai, { + pluginOptions: options, + authClient, + defaultEmbedder: options.embedder, + }); + } + }); +} diff --git a/js/plugins/vertexai/src/vectorsearch/types.ts b/js/plugins/vertexai/src/vectorsearch/types.ts new file mode 100644 index 000000000..9b5a934b3 --- /dev/null +++ b/js/plugins/vertexai/src/vectorsearch/types.ts @@ -0,0 +1,30 @@ +/** + * 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 { EmbedderReference, z } from 'genkit'; +import { CommonPluginOptions } from '../common/types.js'; +import { VectorSearchOptions } from './vector_search/index.js'; + +/** Options specific to vector search configuration */ +export interface VectorSearchOptionsConfig { + /** Configure Vertex AI vector search index options */ + vectorSearchOptions?: VectorSearchOptions[]; + embedder?: EmbedderReference; +} + +export interface PluginOptions + extends CommonPluginOptions, + VectorSearchOptionsConfig {} diff --git a/js/plugins/vertexai/src/vector-search/bigquery.ts b/js/plugins/vertexai/src/vectorsearch/vector_search/bigquery.ts similarity index 100% rename from js/plugins/vertexai/src/vector-search/bigquery.ts rename to js/plugins/vertexai/src/vectorsearch/vector_search/bigquery.ts diff --git a/js/plugins/vertexai/src/vector-search/firestore.ts b/js/plugins/vertexai/src/vectorsearch/vector_search/firestore.ts similarity index 100% rename from js/plugins/vertexai/src/vector-search/firestore.ts rename to js/plugins/vertexai/src/vectorsearch/vector_search/firestore.ts diff --git a/js/plugins/vertexai/src/vector-search/index.ts b/js/plugins/vertexai/src/vectorsearch/vector_search/index.ts similarity index 100% rename from js/plugins/vertexai/src/vector-search/index.ts rename to js/plugins/vertexai/src/vectorsearch/vector_search/index.ts diff --git a/js/plugins/vertexai/src/vector-search/indexers.ts b/js/plugins/vertexai/src/vectorsearch/vector_search/indexers.ts similarity index 93% rename from js/plugins/vertexai/src/vector-search/indexers.ts rename to js/plugins/vertexai/src/vectorsearch/vector_search/indexers.ts index 66a00e913..aabd23a1f 100644 --- a/js/plugins/vertexai/src/vector-search/indexers.ts +++ b/js/plugins/vertexai/src/vectorsearch/vector_search/indexers.ts @@ -58,7 +58,6 @@ export function vertexAiIndexers( params: VertexVectorSearchOptions ): IndexerAction[] { const vectorSearchOptions = params.pluginOptions.vectorSearchOptions; - const defaultEmbedder = params.defaultEmbedder; const indexers: IndexerAction[] = []; if (!vectorSearchOptions || vectorSearchOptions.length === 0) { @@ -67,7 +66,14 @@ export function vertexAiIndexers( for (const vectorSearchOption of vectorSearchOptions) { const { documentIndexer, indexId } = vectorSearchOption; - const embedder = vectorSearchOption.embedder ?? defaultEmbedder; + const embedderReference = + vectorSearchOption.embedder ?? params.defaultEmbedder; + + if (!embedderReference) { + throw new Error( + 'Embedder reference is required to define Vertex AI retriever' + ); + } const embedderOptions = vectorSearchOption.embedderOptions; const indexer = ai.defineIndexer( @@ -87,7 +93,7 @@ export function vertexAiIndexers( } const embeddings = await ai.embedMany({ - embedder, + embedder: embedderReference, content: docs, options: embedderOptions, }); diff --git a/js/plugins/vertexai/src/vector-search/query_public_endpoint.ts b/js/plugins/vertexai/src/vectorsearch/vector_search/query_public_endpoint.ts similarity index 100% rename from js/plugins/vertexai/src/vector-search/query_public_endpoint.ts rename to js/plugins/vertexai/src/vectorsearch/vector_search/query_public_endpoint.ts diff --git a/js/plugins/vertexai/src/vector-search/retrievers.ts b/js/plugins/vertexai/src/vectorsearch/vector_search/retrievers.ts similarity index 93% rename from js/plugins/vertexai/src/vector-search/retrievers.ts rename to js/plugins/vertexai/src/vectorsearch/vector_search/retrievers.ts index 67f47f33d..0f8a64024 100644 --- a/js/plugins/vertexai/src/vector-search/retrievers.ts +++ b/js/plugins/vertexai/src/vectorsearch/vector_search/retrievers.ts @@ -57,8 +57,17 @@ export function vertexAiRetrievers( configSchema: VertexAIVectorRetrieverOptionsSchema.optional(), }, async (content, options) => { + const embedderReference = + vectorSearchOption.embedder ?? defaultEmbedder; + + if (!embedderReference) { + throw new Error( + 'Embedder reference is required to define Vertex AI retriever' + ); + } + const queryEmbeddings = await ai.embed({ - embedder, + embedder: embedderReference, options: embedderOptions, content, }); diff --git a/js/plugins/vertexai/src/vector-search/types.ts b/js/plugins/vertexai/src/vectorsearch/vector_search/types.ts similarity index 96% rename from js/plugins/vertexai/src/vector-search/types.ts rename to js/plugins/vertexai/src/vectorsearch/vector_search/types.ts index 6b58e4f34..ef0c000ad 100644 --- a/js/plugins/vertexai/src/vector-search/types.ts +++ b/js/plugins/vertexai/src/vectorsearch/vector_search/types.ts @@ -16,10 +16,10 @@ import * as aiplatform from '@google-cloud/aiplatform'; import { z } from 'genkit'; -import { EmbedderArgument } from 'genkit/embedder'; +import { EmbedderReference } from 'genkit/embedder'; import { CommonRetrieverOptionsSchema, Document } from 'genkit/retriever'; import { GoogleAuth } from 'google-auth-library'; -import { PluginOptions } from '..'; +import { PluginOptions } from '../types.js'; // This internal interface will be passed to the vertexIndexers and vertexRetrievers functions export interface VertexVectorSearchOptions< @@ -27,7 +27,7 @@ export interface VertexVectorSearchOptions< > { pluginOptions: PluginOptions; authClient: GoogleAuth; - defaultEmbedder: EmbedderArgument; + defaultEmbedder?: EmbedderReference; } export type IIndexDatapoint = @@ -184,6 +184,6 @@ export interface VectorSearchOptions< documentRetriever: DocumentRetriever; documentIndexer: DocumentIndexer; // Embedder and default options to use for indexing and retrieval - embedder?: EmbedderArgument; + embedder?: EmbedderReference; embedderOptions?: z.infer; } diff --git a/js/plugins/vertexai/src/vector-search/upsert_datapoints.ts b/js/plugins/vertexai/src/vectorsearch/vector_search/upsert_datapoints.ts similarity index 100% rename from js/plugins/vertexai/src/vector-search/upsert_datapoints.ts rename to js/plugins/vertexai/src/vectorsearch/vector_search/upsert_datapoints.ts diff --git a/js/plugins/vertexai/src/vector-search/utils.ts b/js/plugins/vertexai/src/vectorsearch/vector_search/utils.ts similarity index 100% rename from js/plugins/vertexai/src/vector-search/utils.ts rename to js/plugins/vertexai/src/vectorsearch/vector_search/utils.ts diff --git a/js/plugins/vertexai/tests/anthropic_test.ts b/js/plugins/vertexai/tests/anthropic_test.ts index f5870e6a1..dc4328a48 100644 --- a/js/plugins/vertexai/tests/anthropic_test.ts +++ b/js/plugins/vertexai/tests/anthropic_test.ts @@ -25,7 +25,7 @@ import { AnthropicConfigSchema, fromAnthropicResponse, toAnthropicRequest, -} from '../src/anthropic.js'; +} from '../src/modelgarden/anthropic'; const MODEL_ID = 'modelid'; diff --git a/js/plugins/vertexai/tests/gemini_test.ts b/js/plugins/vertexai/tests/gemini_test.ts index c6156b4be..4b7ba137c 100644 --- a/js/plugins/vertexai/tests/gemini_test.ts +++ b/js/plugins/vertexai/tests/gemini_test.ts @@ -339,7 +339,10 @@ describe('fromGeminiCandidate', () => { for (const test of testCases) { it(test.should, () => { assert.deepEqual( - fromGeminiCandidate(test.geminiCandidate as GenerateContentCandidate), + fromGeminiCandidate( + test.geminiCandidate as GenerateContentCandidate, + false + ), test.expectedOutput ); }); diff --git a/js/plugins/vertexai/tests/vector-search/bigquery_test.ts b/js/plugins/vertexai/tests/vectorsearch/bigquery_test.ts similarity index 98% rename from js/plugins/vertexai/tests/vector-search/bigquery_test.ts rename to js/plugins/vertexai/tests/vectorsearch/bigquery_test.ts index 1cbc54314..cf94ed8de 100644 --- a/js/plugins/vertexai/tests/vector-search/bigquery_test.ts +++ b/js/plugins/vertexai/tests/vectorsearch/bigquery_test.ts @@ -18,7 +18,7 @@ import { BigQuery } from '@google-cloud/bigquery'; import { Document } from 'genkit/retriever'; import assert from 'node:assert'; import { describe, it } from 'node:test'; -import { getBigQueryDocumentRetriever } from '../../src'; +import { getBigQueryDocumentRetriever } from '../../src/vectorsearch'; class MockBigQuery { query: Function; diff --git a/js/plugins/vertexai/tests/vector-search/query_public_endpoint_test.ts b/js/plugins/vertexai/tests/vectorsearch/query_public_endpoint_test.ts similarity index 96% rename from js/plugins/vertexai/tests/vector-search/query_public_endpoint_test.ts rename to js/plugins/vertexai/tests/vectorsearch/query_public_endpoint_test.ts index 9419f2916..9147428c7 100644 --- a/js/plugins/vertexai/tests/vector-search/query_public_endpoint_test.ts +++ b/js/plugins/vertexai/tests/vectorsearch/query_public_endpoint_test.ts @@ -16,7 +16,7 @@ import assert from 'assert'; import { describe, it, Mock } from 'node:test'; -import { queryPublicEndpoint } from '../../src/vector-search/query_public_endpoint'; +import { queryPublicEndpoint } from '../../src/vectorsearch/vector_search/query_public_endpoint'; describe('queryPublicEndpoint', () => { // FIXME -- t.mock.method is not supported node above 20 diff --git a/js/plugins/vertexai/tests/vector-search/upsert_datapoints_test.ts b/js/plugins/vertexai/tests/vectorsearch/upsert_datapoints_test.ts similarity index 93% rename from js/plugins/vertexai/tests/vector-search/upsert_datapoints_test.ts rename to js/plugins/vertexai/tests/vectorsearch/upsert_datapoints_test.ts index 5b36a47d0..07c0b1883 100644 --- a/js/plugins/vertexai/tests/vector-search/upsert_datapoints_test.ts +++ b/js/plugins/vertexai/tests/vectorsearch/upsert_datapoints_test.ts @@ -17,8 +17,8 @@ import assert from 'assert'; import { GoogleAuth } from 'google-auth-library'; import { describe, it, Mock } from 'node:test'; -import { IIndexDatapoint } from '../../src/vector-search/types'; -import { upsertDatapoints } from '../../src/vector-search/upsert_datapoints'; +import { IIndexDatapoint } from '../../src/vectorsearch/vector_search/types'; +import { upsertDatapoints } from '../../src/vectorsearch/vector_search/upsert_datapoints'; describe('upsertDatapoints', () => { // FIXME -- t.mock.method is not supported node above 20 diff --git a/js/plugins/vertexai/tests/vector-search/utils_test.ts b/js/plugins/vertexai/tests/vectorsearch/utils_test.ts similarity index 97% rename from js/plugins/vertexai/tests/vector-search/utils_test.ts rename to js/plugins/vertexai/tests/vectorsearch/utils_test.ts index 38b130b3a..7bf35b659 100644 --- a/js/plugins/vertexai/tests/vector-search/utils_test.ts +++ b/js/plugins/vertexai/tests/vectorsearch/utils_test.ts @@ -20,7 +20,7 @@ import { describe, it } from 'node:test'; import { getAccessToken, getProjectNumber, -} from '../../src/vector-search/utils'; +} from '../../src/vectorsearch/vector_search/utils'; // Mocking the google.auth.getClient method google.auth.getClient = async () => { diff --git a/js/pnpm-lock.yaml b/js/pnpm-lock.yaml index 9aac0204a..8bcce37f4 100644 --- a/js/pnpm-lock.yaml +++ b/js/pnpm-lock.yaml @@ -41,6 +41,9 @@ importers: partial-json: specifier: ^0.1.7 version: 0.1.7 + uuid: + specifier: ^10.0.0 + version: 10.0.0 devDependencies: npm-run-all: specifier: ^4.1.5 @@ -170,6 +173,9 @@ importers: '@genkit-ai/dotprompt': specifier: workspace:* version: link:../plugins/dotprompt + uuid: + specifier: ^10.0.0 + version: 10.0.0 devDependencies: '@types/body-parser': specifier: ^1.19.5 @@ -195,9 +201,6 @@ importers: typescript: specifier: ^4.9.0 version: 4.9.5 - uuid: - specifier: ^10.0.0 - version: 10.0.0 plugins/chroma: dependencies: @@ -484,8 +487,8 @@ importers: plugins/googleai: dependencies: '@google/generative-ai': - specifier: ^0.16.0 - version: 0.16.0 + specifier: ^0.21.0 + version: 0.21.0 genkit: specifier: workspace:* version: link:../../genkit @@ -655,8 +658,8 @@ importers: specifier: ^6.0.1 version: 6.0.1 tsup: - specifier: ^8.0.2 - version: 8.0.2(postcss@8.4.47)(typescript@4.9.5) + specifier: ^8.3.5 + version: 8.3.5(postcss@8.4.47)(tsx@4.7.1)(typescript@4.9.5) tsx: specifier: ^4.7.0 version: 4.7.1 @@ -1039,6 +1042,31 @@ importers: specifier: ^5.3.3 version: 5.4.5 + testapps/format-tester: + dependencies: + '@genkit-ai/googleai': + specifier: workspace:* + version: link:../../plugins/googleai + '@genkit-ai/vertexai': + specifier: workspace:* + version: link:../../plugins/vertexai + '@opentelemetry/sdk-trace-base': + specifier: ^1.25.0 + version: 1.26.0(@opentelemetry/api@1.9.0) + genkit: + specifier: workspace:* + version: link:../../genkit + devDependencies: + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + tsx: + specifier: ^4.19.2 + version: 4.19.2 + typescript: + specifier: ^5.3.3 + version: 5.6.2 + testapps/google-ai-code-execution: dependencies: '@genkit-ai/google-cloud': @@ -1160,7 +1188,7 @@ importers: version: link:../../plugins/ollama genkitx-openai: specifier: ^0.10.1 - version: 0.10.1(@genkit-ai/ai@0.9.0-dev.2)(@genkit-ai/core@0.9.0-dev.2) + version: 0.10.1(@genkit-ai/ai@0.9.0-dev.4)(@genkit-ai/core@0.9.0-dev.4) devDependencies: rimraf: specifier: ^6.0.1 @@ -1627,6 +1655,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.24.0': + resolution: {integrity: sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.19.12': resolution: {integrity: sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==} engines: {node: '>=12'} @@ -1639,6 +1673,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.24.0': + resolution: {integrity: sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.19.12': resolution: {integrity: sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==} engines: {node: '>=12'} @@ -1651,6 +1691,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.24.0': + resolution: {integrity: sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.19.12': resolution: {integrity: sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==} engines: {node: '>=12'} @@ -1663,6 +1709,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.24.0': + resolution: {integrity: sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.19.12': resolution: {integrity: sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==} engines: {node: '>=12'} @@ -1675,6 +1727,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.24.0': + resolution: {integrity: sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.19.12': resolution: {integrity: sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==} engines: {node: '>=12'} @@ -1687,6 +1745,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.24.0': + resolution: {integrity: sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.19.12': resolution: {integrity: sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==} engines: {node: '>=12'} @@ -1699,6 +1763,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.24.0': + resolution: {integrity: sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.19.12': resolution: {integrity: sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==} engines: {node: '>=12'} @@ -1711,6 +1781,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.24.0': + resolution: {integrity: sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.19.12': resolution: {integrity: sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==} engines: {node: '>=12'} @@ -1723,6 +1799,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.24.0': + resolution: {integrity: sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.19.12': resolution: {integrity: sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==} engines: {node: '>=12'} @@ -1735,6 +1817,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.24.0': + resolution: {integrity: sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.19.12': resolution: {integrity: sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==} engines: {node: '>=12'} @@ -1747,6 +1835,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.24.0': + resolution: {integrity: sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.19.12': resolution: {integrity: sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==} engines: {node: '>=12'} @@ -1759,6 +1853,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.24.0': + resolution: {integrity: sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.19.12': resolution: {integrity: sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==} engines: {node: '>=12'} @@ -1771,6 +1871,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.24.0': + resolution: {integrity: sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.19.12': resolution: {integrity: sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==} engines: {node: '>=12'} @@ -1783,6 +1889,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.24.0': + resolution: {integrity: sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.19.12': resolution: {integrity: sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==} engines: {node: '>=12'} @@ -1795,6 +1907,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.24.0': + resolution: {integrity: sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.19.12': resolution: {integrity: sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==} engines: {node: '>=12'} @@ -1807,6 +1925,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.24.0': + resolution: {integrity: sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.19.12': resolution: {integrity: sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==} engines: {node: '>=12'} @@ -1819,6 +1943,12 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.24.0': + resolution: {integrity: sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-x64@0.19.12': resolution: {integrity: sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==} engines: {node: '>=12'} @@ -1831,12 +1961,24 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.24.0': + resolution: {integrity: sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.23.1': resolution: {integrity: sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.24.0': + resolution: {integrity: sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.19.12': resolution: {integrity: sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==} engines: {node: '>=12'} @@ -1849,6 +1991,12 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.24.0': + resolution: {integrity: sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/sunos-x64@0.19.12': resolution: {integrity: sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==} engines: {node: '>=12'} @@ -1861,6 +2009,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.24.0': + resolution: {integrity: sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.19.12': resolution: {integrity: sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==} engines: {node: '>=12'} @@ -1873,6 +2027,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.24.0': + resolution: {integrity: sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.19.12': resolution: {integrity: sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==} engines: {node: '>=12'} @@ -1885,6 +2045,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.24.0': + resolution: {integrity: sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.19.12': resolution: {integrity: sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==} engines: {node: '>=12'} @@ -1897,6 +2063,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.24.0': + resolution: {integrity: sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@fastify/busboy@3.0.0': resolution: {integrity: sha512-83rnH2nCvclWaPQQKvkJ2pdOjG4TZyEVuFDnlOF6KP08lDaaceVyw/W63mDuafQT+MKHCvXIPpE5uYWeM0rT4w==} @@ -1927,11 +2099,11 @@ packages: '@firebase/util@1.9.5': resolution: {integrity: sha512-PP4pAFISDxsf70l3pEy34Mf3GkkUcVQ3MdKp6aSVb7tcpfUQxnsdV7twDd8EkfB6zZylH6wpUAoangQDmCUMqw==} - '@genkit-ai/ai@0.9.0-dev.2': - resolution: {integrity: sha512-O5l5H5v2Lb78HjZVg105iUGb8MR1Cc4snGB3iAac2uKGB24IjIlxTs2vHlDO0QB5Yi56woZ4ZsWbbYg7gmMmIA==} + '@genkit-ai/ai@0.9.0-dev.4': + resolution: {integrity: sha512-j7mCfJnPupK9tqkESV+SVtwGAfGFB6CnIr/NXeTZleU6cupocP0uFkZKi72HbdMYk2VI38spplp5aIt4jW/wNA==} - '@genkit-ai/core@0.9.0-dev.2': - resolution: {integrity: sha512-xqTFEV/XTYswlBa3l0zL+k/OfexxxHLs3zIDR0TdSBKBdau5KgW0Xcf2RgAjakDp4LaxSs2+0qbWxwZzy/apmg==} + '@genkit-ai/core@0.9.0-dev.4': + resolution: {integrity: sha512-v6QpSedACJU/jKJGukJKHM5sPJdyYKPoyzAMyztWvVD12t2bkvXYL7+QyCeB/cUE7cijyO4w/2lRNyZciyAgMw==} '@google-cloud/aiplatform@3.25.0': resolution: {integrity: sha512-qKnJgbyCENjed8e1G5zZGFTxxNKhhaKQN414W2KIVHrLxMFmlMuG+3QkXPOWwXBnT5zZ7aMxypt5og0jCirpHg==} @@ -2019,8 +2191,8 @@ packages: resolution: {integrity: sha512-zs37judcTYFJf1U7tnuqnh7gdzF6dcWj9pNRxjA5JTONRoiQ0htrRdbefRFiewOIfXwhun5t9hbd2ray7812eQ==} engines: {node: '>=18.0.0'} - '@google/generative-ai@0.16.0': - resolution: {integrity: sha512-doB5ZNxS6m+jUZqaLCeYXfBZCdq6Ho0ibkq5/17xe1qAUZpCLWlvCDGtqFPqqO+yezNmvGatS0KhV22yiOT3DA==} + '@google/generative-ai@0.21.0': + resolution: {integrity: sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg==} engines: {node: '>=18.0.0'} '@grpc/grpc-js@1.10.10': @@ -2882,10 +3054,6 @@ packages: peerDependencies: '@opentelemetry/api': '>=1.0.0 <1.10.0' - '@opentelemetry/semantic-conventions@1.22.0': - resolution: {integrity: sha512-CAOgFOKLybd02uj/GhCdEeeBjOS0yeoDeo/CA7ASBSmenpZHAKGB3iDm/rv3BQLcabb/OprDEsSQ1y0P8A7Siw==} - engines: {node: '>=14'} - '@opentelemetry/semantic-conventions@1.25.1': resolution: {integrity: sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==} engines: {node: '>=14'} @@ -2952,6 +3120,11 @@ packages: cpu: [arm] os: [android] + '@rollup/rollup-android-arm-eabi@4.25.0': + resolution: {integrity: sha512-CC/ZqFZwlAIbU1wUPisHyV/XRc5RydFrNLtgl3dGYskdwPZdt4HERtKm50a/+DtTlKeCq9IXFEWR+P6blwjqBA==} + cpu: [arm] + os: [android] + '@rollup/rollup-android-arm64@4.13.2': resolution: {integrity: sha512-GdxxXbAuM7Y/YQM9/TwwP+L0omeE/lJAR1J+olu36c3LqqZEBdsIWeQ91KBe6nxwOnb06Xh7JS2U5ooWU5/LgQ==} cpu: [arm64] @@ -2962,6 +3135,11 @@ packages: cpu: [arm64] os: [android] + '@rollup/rollup-android-arm64@4.25.0': + resolution: {integrity: sha512-/Y76tmLGUJqVBXXCfVS8Q8FJqYGhgH4wl4qTA24E9v/IJM0XvJCGQVSW1QZ4J+VURO9h8YCa28sTFacZXwK7Rg==} + cpu: [arm64] + os: [android] + '@rollup/rollup-darwin-arm64@4.13.2': resolution: {integrity: sha512-mCMlpzlBgOTdaFs83I4XRr8wNPveJiJX1RLfv4hggyIVhfB5mJfN4P8Z6yKh+oE4Luz+qq1P3kVdWrCKcMYrrA==} cpu: [arm64] @@ -2972,6 +3150,11 @@ packages: cpu: [arm64] os: [darwin] + '@rollup/rollup-darwin-arm64@4.25.0': + resolution: {integrity: sha512-YVT6L3UrKTlC0FpCZd0MGA7NVdp7YNaEqkENbWQ7AOVOqd/7VzyHpgIpc1mIaxRAo1ZsJRH45fq8j4N63I/vvg==} + cpu: [arm64] + os: [darwin] + '@rollup/rollup-darwin-x64@4.13.2': resolution: {integrity: sha512-yUoEvnH0FBef/NbB1u6d3HNGyruAKnN74LrPAfDQL3O32e3k3OSfLrPgSJmgb3PJrBZWfPyt6m4ZhAFa2nZp2A==} cpu: [x64] @@ -2982,6 +3165,21 @@ packages: cpu: [x64] os: [darwin] + '@rollup/rollup-darwin-x64@4.25.0': + resolution: {integrity: sha512-ZRL+gexs3+ZmmWmGKEU43Bdn67kWnMeWXLFhcVv5Un8FQcx38yulHBA7XR2+KQdYIOtD0yZDWBCudmfj6lQJoA==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.25.0': + resolution: {integrity: sha512-xpEIXhiP27EAylEpreCozozsxWQ2TJbOLSivGfXhU4G1TBVEYtUPi2pOZBnvGXHyOdLAUUhPnJzH3ah5cqF01g==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.25.0': + resolution: {integrity: sha512-sC5FsmZGlJv5dOcURrsnIK7ngc3Kirnx3as2XU9uER+zjfyqIjdcMVgzy4cOawhsssqzoAX19qmxgJ8a14Qrqw==} + cpu: [x64] + os: [freebsd] + '@rollup/rollup-linux-arm-gnueabihf@4.13.2': resolution: {integrity: sha512-GYbLs5ErswU/Xs7aGXqzc3RrdEjKdmoCrgzhJWyFL0r5fL3qd1NPcDKDowDnmcoSiGJeU68/Vy+OMUluRxPiLQ==} cpu: [arm] @@ -2992,11 +3190,21 @@ packages: cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-gnueabihf@4.25.0': + resolution: {integrity: sha512-uD/dbLSs1BEPzg564TpRAQ/YvTnCds2XxyOndAO8nJhaQcqQGFgv/DAVko/ZHap3boCvxnzYMa3mTkV/B/3SWA==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.22.4': resolution: {integrity: sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==} cpu: [arm] os: [linux] + '@rollup/rollup-linux-arm-musleabihf@4.25.0': + resolution: {integrity: sha512-ZVt/XkrDlQWegDWrwyC3l0OfAF7yeJUF4fq5RMS07YM72BlSfn2fQQ6lPyBNjt+YbczMguPiJoCfaQC2dnflpQ==} + cpu: [arm] + os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.13.2': resolution: {integrity: sha512-L1+D8/wqGnKQIlh4Zre9i4R4b4noxzH5DDciyahX4oOz62CphY7WDWqJoQ66zNR4oScLNOqQJfNSIAe/6TPUmQ==} cpu: [arm64] @@ -3007,6 +3215,11 @@ packages: cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-gnu@4.25.0': + resolution: {integrity: sha512-qboZ+T0gHAW2kkSDPHxu7quaFaaBlynODXpBVnPxUgvWYaE84xgCKAPEYE+fSMd3Zv5PyFZR+L0tCdYCMAtG0A==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-arm64-musl@4.13.2': resolution: {integrity: sha512-tK5eoKFkXdz6vjfkSTCupUzCo40xueTOiOO6PeEIadlNBkadH1wNOH8ILCPIl8by/Gmb5AGAeQOFeLev7iZDOA==} cpu: [arm64] @@ -3017,6 +3230,11 @@ packages: cpu: [arm64] os: [linux] + '@rollup/rollup-linux-arm64-musl@4.25.0': + resolution: {integrity: sha512-ndWTSEmAaKr88dBuogGH2NZaxe7u2rDoArsejNslugHZ+r44NfWiwjzizVS1nUOHo+n1Z6qV3X60rqE/HlISgw==} + cpu: [arm64] + os: [linux] + '@rollup/rollup-linux-powerpc64le-gnu@4.13.2': resolution: {integrity: sha512-zvXvAUGGEYi6tYhcDmb9wlOckVbuD+7z3mzInCSTACJ4DQrdSLPNUeDIcAQW39M3q6PDquqLWu7pnO39uSMRzQ==} cpu: [ppc64le] @@ -3027,6 +3245,11 @@ packages: cpu: [ppc64] os: [linux] + '@rollup/rollup-linux-powerpc64le-gnu@4.25.0': + resolution: {integrity: sha512-BVSQvVa2v5hKwJSy6X7W1fjDex6yZnNKy3Kx1JGimccHft6HV0THTwNtC2zawtNXKUu+S5CjXslilYdKBAadzA==} + cpu: [ppc64] + os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.13.2': resolution: {integrity: sha512-C3GSKvMtdudHCN5HdmAMSRYR2kkhgdOfye4w0xzyii7lebVr4riCgmM6lRiSCnJn2w1Xz7ZZzHKuLrjx5620kw==} cpu: [riscv64] @@ -3037,6 +3260,11 @@ packages: cpu: [riscv64] os: [linux] + '@rollup/rollup-linux-riscv64-gnu@4.25.0': + resolution: {integrity: sha512-G4hTREQrIdeV0PE2JruzI+vXdRnaK1pg64hemHq2v5fhv8C7WjVaeXc9P5i4Q5UC06d/L+zA0mszYIKl+wY8oA==} + cpu: [riscv64] + os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.13.2': resolution: {integrity: sha512-l4U0KDFwzD36j7HdfJ5/TveEQ1fUTjFFQP5qIt9gBqBgu1G8/kCaq5Ok05kd5TG9F8Lltf3MoYsUMw3rNlJ0Yg==} cpu: [s390x] @@ -3047,6 +3275,11 @@ packages: cpu: [s390x] os: [linux] + '@rollup/rollup-linux-s390x-gnu@4.25.0': + resolution: {integrity: sha512-9T/w0kQ+upxdkFL9zPVB6zy9vWW1deA3g8IauJxojN4bnz5FwSsUAD034KpXIVX5j5p/rn6XqumBMxfRkcHapQ==} + cpu: [s390x] + os: [linux] + '@rollup/rollup-linux-x64-gnu@4.13.2': resolution: {integrity: sha512-xXMLUAMzrtsvh3cZ448vbXqlUa7ZL8z0MwHp63K2IIID2+DeP5iWIT6g1SN7hg1VxPzqx0xZdiDM9l4n9LRU1A==} cpu: [x64] @@ -3057,6 +3290,11 @@ packages: cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-gnu@4.25.0': + resolution: {integrity: sha512-ThcnU0EcMDn+J4B9LD++OgBYxZusuA7iemIIiz5yzEcFg04VZFzdFjuwPdlURmYPZw+fgVrFzj4CA64jSTG4Ig==} + cpu: [x64] + os: [linux] + '@rollup/rollup-linux-x64-musl@4.13.2': resolution: {integrity: sha512-M/JYAWickafUijWPai4ehrjzVPKRCyDb1SLuO+ZyPfoXgeCEAlgPkNXewFZx0zcnoIe3ay4UjXIMdXQXOZXWqA==} cpu: [x64] @@ -3067,6 +3305,11 @@ packages: cpu: [x64] os: [linux] + '@rollup/rollup-linux-x64-musl@4.25.0': + resolution: {integrity: sha512-zx71aY2oQxGxAT1JShfhNG79PnjYhMC6voAjzpu/xmMjDnKNf6Nl/xv7YaB/9SIa9jDYf8RBPWEnjcdlhlv1rQ==} + cpu: [x64] + os: [linux] + '@rollup/rollup-win32-arm64-msvc@4.13.2': resolution: {integrity: sha512-2YWwoVg9KRkIKaXSh0mz3NmfurpmYoBBTAXA9qt7VXk0Xy12PoOP40EFuau+ajgALbbhi4uTj3tSG3tVseCjuA==} cpu: [arm64] @@ -3077,6 +3320,11 @@ packages: cpu: [arm64] os: [win32] + '@rollup/rollup-win32-arm64-msvc@4.25.0': + resolution: {integrity: sha512-JT8tcjNocMs4CylWY/CxVLnv8e1lE7ff1fi6kbGocWwxDq9pj30IJ28Peb+Y8yiPNSF28oad42ApJB8oUkwGww==} + cpu: [arm64] + os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.13.2': resolution: {integrity: sha512-2FSsE9aQ6OWD20E498NYKEQLneShWes0NGMPQwxWOdws35qQXH+FplabOSP5zEe1pVjurSDOGEVCE2agFwSEsw==} cpu: [ia32] @@ -3087,6 +3335,11 @@ packages: cpu: [ia32] os: [win32] + '@rollup/rollup-win32-ia32-msvc@4.25.0': + resolution: {integrity: sha512-dRLjLsO3dNOfSN6tjyVlG+Msm4IiZnGkuZ7G5NmpzwF9oOc582FZG05+UdfTbz5Jd4buK/wMb6UeHFhG18+OEg==} + cpu: [ia32] + os: [win32] + '@rollup/rollup-win32-x64-msvc@4.13.2': resolution: {integrity: sha512-7h7J2nokcdPePdKykd8wtc8QqqkqxIrUz7MHj6aNr8waBRU//NLDVnNjQnqQO6fqtjrtCdftpbTuOKAyrAQETQ==} cpu: [x64] @@ -3097,6 +3350,11 @@ packages: cpu: [x64] os: [win32] + '@rollup/rollup-win32-x64-msvc@4.25.0': + resolution: {integrity: sha512-/RqrIFtLB926frMhZD0a5oDa4eFIbyNEwLLloMTEjmqfwZWXywwVVOVmwTsuyhC9HKkVEZcOOi+KV4U9wmOdlg==} + cpu: [x64] + os: [win32] + '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} @@ -3152,6 +3410,9 @@ packages: '@types/estree@1.0.5': resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/express-serve-static-core@4.17.43': resolution: {integrity: sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==} @@ -3548,6 +3809,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.1: + resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} + engines: {node: '>= 14.16.0'} + chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} @@ -3910,6 +4175,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.24.0: + resolution: {integrity: sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==} + engines: {node: '>=18'} + hasBin: true + escalade@3.1.2: resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} engines: {node: '>=6'} @@ -4017,6 +4287,14 @@ packages: picomatch: optional: true + fdir@6.4.2: + resolution: {integrity: sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} @@ -5417,6 +5695,9 @@ packages: picocolors@1.1.0: resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@2.3.1: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} @@ -5585,6 +5866,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.0.2: + resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} + engines: {node: '>= 14.16.0'} + regexp.prototype.flags@1.5.2: resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} engines: {node: '>= 0.4'} @@ -5652,6 +5937,11 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rollup@4.25.0: + resolution: {integrity: sha512-uVbClXmR6wvx5R1M3Od4utyLUxrmOcEm3pAtMphn73Apq19PDtHpgZoEvqH2YnnaNUuvKmg2DgRd2Sqv+odyqg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -5920,6 +6210,13 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tinyexec@0.3.1: + resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + + tinyglobby@0.2.10: + resolution: {integrity: sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew==} + engines: {node: '>=12.0.0'} + tinyglobby@0.2.6: resolution: {integrity: sha512-NbBoFBpqfcgd1tCiO8Lkfdk+xrA7mlLR9zgvZcZWQQwU63XAfUePyd6wZBaU93Hqw347lHnwFzttAkemHzzz4g==} engines: {node: '>=12.0.0'} @@ -6029,11 +6326,35 @@ packages: typescript: optional: true + tsup@8.3.5: + resolution: {integrity: sha512-Tunf6r6m6tnZsG9GYWndg0z8dEV7fD733VBFzFJ5Vcm1FtlXB8xBD/rtrBi2a3YKEV7hHtxiZtW5EAVADoe1pA==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + tsx@4.19.1: resolution: {integrity: sha512-0flMz1lh74BR4wOvBjuh9olbnwqCPc35OOlfyzHba0Dc+QNUeWX/Gq2YTbnwcWPO3BMd8fkzRVrHcsR+a7z7rA==} engines: {node: '>=18.0.0'} hasBin: true + tsx@4.19.2: + resolution: {integrity: sha512-pOUl6Vo2LUq/bSa8S5q7b91cgNSjctn9ugq/+Mvow99qW6x/UZYwzxy/3NmqoT66eHYfCVvFvACC58UBPFf28g==} + engines: {node: '>=18.0.0'} + hasBin: true + tsx@4.7.1: resolution: {integrity: sha512-8d6VuibXHtlN5E3zFkgY8u4DX7Y3Z27zvvPKVmLon/D4AjuKzarkUBTLDBgj9iTQ0hg5xM7c/mYiRVM+HETf0g==} engines: {node: '>=18.0.0'} @@ -6558,141 +6879,213 @@ snapshots: '@esbuild/aix-ppc64@0.23.1': optional: true + '@esbuild/aix-ppc64@0.24.0': + optional: true + '@esbuild/android-arm64@0.19.12': optional: true '@esbuild/android-arm64@0.23.1': optional: true + '@esbuild/android-arm64@0.24.0': + optional: true + '@esbuild/android-arm@0.19.12': optional: true '@esbuild/android-arm@0.23.1': optional: true + '@esbuild/android-arm@0.24.0': + optional: true + '@esbuild/android-x64@0.19.12': optional: true '@esbuild/android-x64@0.23.1': optional: true + '@esbuild/android-x64@0.24.0': + optional: true + '@esbuild/darwin-arm64@0.19.12': optional: true '@esbuild/darwin-arm64@0.23.1': optional: true + '@esbuild/darwin-arm64@0.24.0': + optional: true + '@esbuild/darwin-x64@0.19.12': optional: true '@esbuild/darwin-x64@0.23.1': optional: true + '@esbuild/darwin-x64@0.24.0': + optional: true + '@esbuild/freebsd-arm64@0.19.12': optional: true '@esbuild/freebsd-arm64@0.23.1': optional: true + '@esbuild/freebsd-arm64@0.24.0': + optional: true + '@esbuild/freebsd-x64@0.19.12': optional: true '@esbuild/freebsd-x64@0.23.1': optional: true + '@esbuild/freebsd-x64@0.24.0': + optional: true + '@esbuild/linux-arm64@0.19.12': optional: true '@esbuild/linux-arm64@0.23.1': optional: true + '@esbuild/linux-arm64@0.24.0': + optional: true + '@esbuild/linux-arm@0.19.12': optional: true '@esbuild/linux-arm@0.23.1': optional: true + '@esbuild/linux-arm@0.24.0': + optional: true + '@esbuild/linux-ia32@0.19.12': optional: true '@esbuild/linux-ia32@0.23.1': optional: true + '@esbuild/linux-ia32@0.24.0': + optional: true + '@esbuild/linux-loong64@0.19.12': optional: true '@esbuild/linux-loong64@0.23.1': optional: true + '@esbuild/linux-loong64@0.24.0': + optional: true + '@esbuild/linux-mips64el@0.19.12': optional: true '@esbuild/linux-mips64el@0.23.1': optional: true + '@esbuild/linux-mips64el@0.24.0': + optional: true + '@esbuild/linux-ppc64@0.19.12': optional: true '@esbuild/linux-ppc64@0.23.1': optional: true + '@esbuild/linux-ppc64@0.24.0': + optional: true + '@esbuild/linux-riscv64@0.19.12': optional: true '@esbuild/linux-riscv64@0.23.1': optional: true + '@esbuild/linux-riscv64@0.24.0': + optional: true + '@esbuild/linux-s390x@0.19.12': optional: true '@esbuild/linux-s390x@0.23.1': optional: true + '@esbuild/linux-s390x@0.24.0': + optional: true + '@esbuild/linux-x64@0.19.12': optional: true '@esbuild/linux-x64@0.23.1': optional: true + '@esbuild/linux-x64@0.24.0': + optional: true + '@esbuild/netbsd-x64@0.19.12': optional: true '@esbuild/netbsd-x64@0.23.1': optional: true + '@esbuild/netbsd-x64@0.24.0': + optional: true + '@esbuild/openbsd-arm64@0.23.1': optional: true + '@esbuild/openbsd-arm64@0.24.0': + optional: true + '@esbuild/openbsd-x64@0.19.12': optional: true '@esbuild/openbsd-x64@0.23.1': optional: true + '@esbuild/openbsd-x64@0.24.0': + optional: true + '@esbuild/sunos-x64@0.19.12': optional: true '@esbuild/sunos-x64@0.23.1': optional: true + '@esbuild/sunos-x64@0.24.0': + optional: true + '@esbuild/win32-arm64@0.19.12': optional: true '@esbuild/win32-arm64@0.23.1': optional: true + '@esbuild/win32-arm64@0.24.0': + optional: true + '@esbuild/win32-ia32@0.19.12': optional: true '@esbuild/win32-ia32@0.23.1': optional: true + '@esbuild/win32-ia32@0.24.0': + optional: true + '@esbuild/win32-x64@0.19.12': optional: true '@esbuild/win32-x64@0.23.1': optional: true + '@esbuild/win32-x64@0.24.0': + optional: true + '@fastify/busboy@3.0.0': {} '@firebase/app-check-interop-types@0.3.1': {} @@ -6738,9 +7131,9 @@ snapshots: dependencies: tslib: 2.6.2 - '@genkit-ai/ai@0.9.0-dev.2': + '@genkit-ai/ai@0.9.0-dev.4': dependencies: - '@genkit-ai/core': 0.9.0-dev.2 + '@genkit-ai/core': 0.9.0-dev.4 '@opentelemetry/api': 1.9.0 '@types/node': 20.16.9 colorette: 2.0.20 @@ -6750,7 +7143,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@genkit-ai/core@0.9.0-dev.2': + '@genkit-ai/core@0.9.0-dev.4': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/context-async-hooks': 1.26.0(@opentelemetry/api@1.9.0) @@ -6947,7 +7340,7 @@ snapshots: '@google/generative-ai@0.15.0': {} - '@google/generative-ai@0.16.0': {} + '@google/generative-ai@0.21.0': {} '@grpc/grpc-js@1.10.10': dependencies: @@ -7367,9 +7760,9 @@ snapshots: '@opentelemetry/instrumentation-amqplib@0.41.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -7378,8 +7771,8 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/propagator-aws-xray': 1.3.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 '@types/aws-lambda': 8.10.122 transitivePeerDependencies: - supports-color @@ -7387,10 +7780,10 @@ snapshots: '@opentelemetry/instrumentation-aws-sdk@0.43.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/propagation-utils': 0.30.10(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -7407,16 +7800,16 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color '@opentelemetry/instrumentation-connect@0.38.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 '@types/connect': 3.4.36 transitivePeerDependencies: - supports-color @@ -7425,7 +7818,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -7447,25 +7840,25 @@ snapshots: '@opentelemetry/instrumentation-express@0.41.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color '@opentelemetry/instrumentation-fastify@0.38.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color '@opentelemetry/instrumentation-fs@0.14.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color @@ -7495,9 +7888,9 @@ snapshots: '@opentelemetry/instrumentation-hapi@0.40.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -7516,7 +7909,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common': 0.36.2 - '@opentelemetry/semantic-conventions': 1.26.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -7524,7 +7917,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.26.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -7532,16 +7925,16 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color '@opentelemetry/instrumentation-koa@0.42.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -7556,7 +7949,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.26.0 + '@opentelemetry/semantic-conventions': 1.27.0 '@types/memcached': 2.2.10 transitivePeerDependencies: - supports-color @@ -7566,16 +7959,16 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/sdk-metrics': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color '@opentelemetry/instrumentation-mongoose@0.40.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -7583,7 +7976,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color @@ -7592,7 +7985,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 '@types/mysql': 2.15.22 transitivePeerDependencies: - supports-color @@ -7601,7 +7994,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.26.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -7609,7 +8002,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.26.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -7617,7 +8010,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 '@opentelemetry/sql-common': 0.40.1(@opentelemetry/api@1.9.0) '@types/pg': 8.6.1 '@types/pg-pool': 2.0.4 @@ -7638,7 +8031,7 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common': 0.36.2 - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -7647,16 +8040,16 @@ snapshots: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) '@opentelemetry/redis-common': 0.36.2 - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color '@opentelemetry/instrumentation-restify@0.40.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -7664,7 +8057,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -7672,7 +8065,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 transitivePeerDependencies: - supports-color @@ -7680,7 +8073,7 @@ snapshots: dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/semantic-conventions': 1.27.0 '@types/tedious': 4.0.14 transitivePeerDependencies: - supports-color @@ -7688,7 +8081,7 @@ snapshots: '@opentelemetry/instrumentation-undici@0.4.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0) transitivePeerDependencies: - supports-color @@ -7745,7 +8138,7 @@ snapshots: '@opentelemetry/propagator-aws-xray@1.3.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) '@opentelemetry/propagator-b3@1.25.1(@opentelemetry/api@1.9.0)': dependencies: @@ -7762,34 +8155,34 @@ snapshots: '@opentelemetry/resource-detector-alibaba-cloud@0.29.0(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 '@opentelemetry/resource-detector-aws@1.5.2(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 '@opentelemetry/resource-detector-azure@0.2.9(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 '@opentelemetry/resource-detector-container@0.3.11(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 '@opentelemetry/resource-detector-gcp@0.29.10(@opentelemetry/api@1.9.0)(encoding@0.1.13)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0) - '@opentelemetry/semantic-conventions': 1.22.0 + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.26.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.27.0 gcp-metadata: 6.1.0(encoding@0.1.13) transitivePeerDependencies: - encoding @@ -7870,8 +8263,6 @@ snapshots: '@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0) semver: 7.6.0 - '@opentelemetry/semantic-conventions@1.22.0': {} - '@opentelemetry/semantic-conventions@1.25.1': {} '@opentelemetry/semantic-conventions@1.26.0': {} @@ -7881,7 +8272,7 @@ snapshots: '@opentelemetry/sql-common@0.40.1(@opentelemetry/api@1.9.0)': dependencies: '@opentelemetry/api': 1.9.0 - '@opentelemetry/core': 1.25.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.26.0(@opentelemetry/api@1.9.0) '@pinecone-database/pinecone@2.2.0': dependencies: @@ -7922,93 +8313,147 @@ snapshots: '@rollup/rollup-android-arm-eabi@4.22.4': optional: true + '@rollup/rollup-android-arm-eabi@4.25.0': + optional: true + '@rollup/rollup-android-arm64@4.13.2': optional: true '@rollup/rollup-android-arm64@4.22.4': optional: true + '@rollup/rollup-android-arm64@4.25.0': + optional: true + '@rollup/rollup-darwin-arm64@4.13.2': optional: true '@rollup/rollup-darwin-arm64@4.22.4': optional: true + '@rollup/rollup-darwin-arm64@4.25.0': + optional: true + '@rollup/rollup-darwin-x64@4.13.2': optional: true '@rollup/rollup-darwin-x64@4.22.4': optional: true + '@rollup/rollup-darwin-x64@4.25.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.25.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.25.0': + optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.13.2': optional: true '@rollup/rollup-linux-arm-gnueabihf@4.22.4': optional: true + '@rollup/rollup-linux-arm-gnueabihf@4.25.0': + optional: true + '@rollup/rollup-linux-arm-musleabihf@4.22.4': optional: true + '@rollup/rollup-linux-arm-musleabihf@4.25.0': + optional: true + '@rollup/rollup-linux-arm64-gnu@4.13.2': optional: true '@rollup/rollup-linux-arm64-gnu@4.22.4': optional: true + '@rollup/rollup-linux-arm64-gnu@4.25.0': + optional: true + '@rollup/rollup-linux-arm64-musl@4.13.2': optional: true '@rollup/rollup-linux-arm64-musl@4.22.4': optional: true + '@rollup/rollup-linux-arm64-musl@4.25.0': + optional: true + '@rollup/rollup-linux-powerpc64le-gnu@4.13.2': optional: true '@rollup/rollup-linux-powerpc64le-gnu@4.22.4': optional: true + '@rollup/rollup-linux-powerpc64le-gnu@4.25.0': + optional: true + '@rollup/rollup-linux-riscv64-gnu@4.13.2': optional: true '@rollup/rollup-linux-riscv64-gnu@4.22.4': optional: true + '@rollup/rollup-linux-riscv64-gnu@4.25.0': + optional: true + '@rollup/rollup-linux-s390x-gnu@4.13.2': optional: true '@rollup/rollup-linux-s390x-gnu@4.22.4': optional: true + '@rollup/rollup-linux-s390x-gnu@4.25.0': + optional: true + '@rollup/rollup-linux-x64-gnu@4.13.2': optional: true '@rollup/rollup-linux-x64-gnu@4.22.4': optional: true + '@rollup/rollup-linux-x64-gnu@4.25.0': + optional: true + '@rollup/rollup-linux-x64-musl@4.13.2': optional: true '@rollup/rollup-linux-x64-musl@4.22.4': optional: true + '@rollup/rollup-linux-x64-musl@4.25.0': + optional: true + '@rollup/rollup-win32-arm64-msvc@4.13.2': optional: true '@rollup/rollup-win32-arm64-msvc@4.22.4': optional: true + '@rollup/rollup-win32-arm64-msvc@4.25.0': + optional: true + '@rollup/rollup-win32-ia32-msvc@4.13.2': optional: true '@rollup/rollup-win32-ia32-msvc@4.22.4': optional: true + '@rollup/rollup-win32-ia32-msvc@4.25.0': + optional: true + '@rollup/rollup-win32-x64-msvc@4.13.2': optional: true '@rollup/rollup-win32-x64-msvc@4.22.4': optional: true + '@rollup/rollup-win32-x64-msvc@4.25.0': + optional: true + '@sinclair/typebox@0.27.8': {} '@sinclair/typebox@0.29.6': {} @@ -8076,6 +8521,8 @@ snapshots: '@types/estree@1.0.5': {} + '@types/estree@1.0.6': {} + '@types/express-serve-static-core@4.17.43': dependencies: '@types/node': 20.16.9 @@ -8502,6 +8949,11 @@ snapshots: esbuild: 0.23.1 load-tsconfig: 0.2.5 + bundle-require@5.0.0(esbuild@0.24.0): + dependencies: + esbuild: 0.24.0 + load-tsconfig: 0.2.5 + bytes@3.1.2: {} cac@6.7.14: {} @@ -8559,6 +9011,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.1: + dependencies: + readdirp: 4.0.2 + chownr@2.0.0: optional: true @@ -8977,6 +9433,33 @@ snapshots: '@esbuild/win32-ia32': 0.23.1 '@esbuild/win32-x64': 0.23.1 + esbuild@0.24.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.24.0 + '@esbuild/android-arm': 0.24.0 + '@esbuild/android-arm64': 0.24.0 + '@esbuild/android-x64': 0.24.0 + '@esbuild/darwin-arm64': 0.24.0 + '@esbuild/darwin-x64': 0.24.0 + '@esbuild/freebsd-arm64': 0.24.0 + '@esbuild/freebsd-x64': 0.24.0 + '@esbuild/linux-arm': 0.24.0 + '@esbuild/linux-arm64': 0.24.0 + '@esbuild/linux-ia32': 0.24.0 + '@esbuild/linux-loong64': 0.24.0 + '@esbuild/linux-mips64el': 0.24.0 + '@esbuild/linux-ppc64': 0.24.0 + '@esbuild/linux-riscv64': 0.24.0 + '@esbuild/linux-s390x': 0.24.0 + '@esbuild/linux-x64': 0.24.0 + '@esbuild/netbsd-x64': 0.24.0 + '@esbuild/openbsd-arm64': 0.24.0 + '@esbuild/openbsd-x64': 0.24.0 + '@esbuild/sunos-x64': 0.24.0 + '@esbuild/win32-arm64': 0.24.0 + '@esbuild/win32-ia32': 0.24.0 + '@esbuild/win32-x64': 0.24.0 + escalade@3.1.2: {} escalade@3.2.0: {} @@ -9137,6 +9620,10 @@ snapshots: optionalDependencies: picomatch: 4.0.2 + fdir@6.4.2(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + fecha@4.2.3: {} fetch-blob@3.2.0: @@ -9334,10 +9821,10 @@ snapshots: - encoding - supports-color - genkitx-openai@0.10.1(@genkit-ai/ai@0.9.0-dev.2)(@genkit-ai/core@0.9.0-dev.2): + genkitx-openai@0.10.1(@genkit-ai/ai@0.9.0-dev.4)(@genkit-ai/core@0.9.0-dev.4): dependencies: - '@genkit-ai/ai': 0.9.0-dev.2 - '@genkit-ai/core': 0.9.0-dev.2 + '@genkit-ai/ai': 0.9.0-dev.4 + '@genkit-ai/core': 0.9.0-dev.4 openai: 4.53.0(encoding@0.1.13) zod: 3.23.8 transitivePeerDependencies: @@ -10848,6 +11335,8 @@ snapshots: picocolors@1.1.0: {} + picocolors@1.1.1: {} + picomatch@2.3.1: {} picomatch@4.0.2: {} @@ -10878,10 +11367,17 @@ snapshots: postcss: 8.4.47 tsx: 4.19.1 + postcss-load-config@6.0.1(postcss@8.4.47)(tsx@4.7.1): + dependencies: + lilconfig: 3.1.2 + optionalDependencies: + postcss: 8.4.47 + tsx: 4.7.1 + postcss@8.4.47: dependencies: nanoid: 3.3.7 - picocolors: 1.1.0 + picocolors: 1.1.1 source-map-js: 1.2.1 optional: true @@ -11013,6 +11509,8 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@4.0.2: {} + regexp.prototype.flags@1.5.2: dependencies: call-bind: 1.0.7 @@ -11114,6 +11612,30 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.22.4 fsevents: 2.3.3 + rollup@4.25.0: + dependencies: + '@types/estree': 1.0.6 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.25.0 + '@rollup/rollup-android-arm64': 4.25.0 + '@rollup/rollup-darwin-arm64': 4.25.0 + '@rollup/rollup-darwin-x64': 4.25.0 + '@rollup/rollup-freebsd-arm64': 4.25.0 + '@rollup/rollup-freebsd-x64': 4.25.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.25.0 + '@rollup/rollup-linux-arm-musleabihf': 4.25.0 + '@rollup/rollup-linux-arm64-gnu': 4.25.0 + '@rollup/rollup-linux-arm64-musl': 4.25.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.25.0 + '@rollup/rollup-linux-riscv64-gnu': 4.25.0 + '@rollup/rollup-linux-s390x-gnu': 4.25.0 + '@rollup/rollup-linux-x64-gnu': 4.25.0 + '@rollup/rollup-linux-x64-musl': 4.25.0 + '@rollup/rollup-win32-arm64-msvc': 4.25.0 + '@rollup/rollup-win32-ia32-msvc': 4.25.0 + '@rollup/rollup-win32-x64-msvc': 4.25.0 + fsevents: 2.3.3 + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -11440,6 +11962,13 @@ snapshots: dependencies: any-promise: 1.3.0 + tinyexec@0.3.1: {} + + tinyglobby@0.2.10: + dependencies: + fdir: 6.4.2(picomatch@4.0.2) + picomatch: 4.0.2 + tinyglobby@0.2.6: dependencies: fdir: 6.3.0(picomatch@4.0.2) @@ -11544,6 +12073,33 @@ snapshots: - tsx - yaml + tsup@8.3.5(postcss@8.4.47)(tsx@4.7.1)(typescript@4.9.5): + dependencies: + bundle-require: 5.0.0(esbuild@0.24.0) + cac: 6.7.14 + chokidar: 4.0.1 + consola: 3.2.3 + debug: 4.3.7 + esbuild: 0.24.0 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(postcss@8.4.47)(tsx@4.7.1) + resolve-from: 5.0.0 + rollup: 4.25.0 + source-map: 0.8.0-beta.0 + sucrase: 3.35.0 + tinyexec: 0.3.1 + tinyglobby: 0.2.10 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.4.47 + typescript: 4.9.5 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + tsx@4.19.1: dependencies: esbuild: 0.23.1 @@ -11551,6 +12107,13 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tsx@4.19.2: + dependencies: + esbuild: 0.23.1 + get-tsconfig: 4.8.1 + optionalDependencies: + fsevents: 2.3.3 + tsx@4.7.1: dependencies: esbuild: 0.19.12 diff --git a/js/testapps/anthropic-models/src/index.ts b/js/testapps/anthropic-models/src/index.ts index d88bf4a34..2c5bb8582 100644 --- a/js/testapps/anthropic-models/src/index.ts +++ b/js/testapps/anthropic-models/src/index.ts @@ -16,7 +16,11 @@ // Import models from the Vertex AI plugin. The Vertex AI API provides access to // several generative models. Here, we import Gemini 1.5 Flash. -import { claude35Sonnet, vertexAI } from '@genkit-ai/vertexai'; +import { vertexAI } from '@genkit-ai/vertexai'; +import { + claude35Sonnet, + vertexAIModelGarden, +} from '@genkit-ai/vertexai/modelgarden'; // Import the Genkit core libraries and plugins. import { genkit, z } from 'genkit'; @@ -30,7 +34,12 @@ const ai = genkit({ // the value from the GCLOUD_PROJECT environment variable. vertexAI({ location: 'europe-west1', - modelGardenModels: [claude35Sonnet], + }), + vertexAIModelGarden({ + location: 'europe-west1', + modelGarden: { + models: [claude35Sonnet], + }, }), ], }); diff --git a/js/testapps/dev-ui-gallery/src/genkit.ts b/js/testapps/dev-ui-gallery/src/genkit.ts index e73c01830..e22fc439f 100644 --- a/js/testapps/dev-ui-gallery/src/genkit.ts +++ b/js/testapps/dev-ui-gallery/src/genkit.ts @@ -17,14 +17,15 @@ import { devLocalVectorstore } from '@genkit-ai/dev-local-vectorstore'; import { genkitEval, GenkitMetric } from '@genkit-ai/evaluator'; import { gemini15Flash, googleAI } from '@genkit-ai/googleai'; +import { textEmbedding004, vertexAI } from '@genkit-ai/vertexai'; import { - claude3Haiku, - claude3Opus, - claude3Sonnet, - textEmbedding004, - vertexAI, + vertexAIEvaluation, VertexAIEvaluationMetricType, -} from '@genkit-ai/vertexai'; +} from '@genkit-ai/vertexai/evaluation'; +import { + claude35Sonnet, + vertexAIModelGarden, +} from '@genkit-ai/vertexai/modelgarden'; import { genkit } from 'genkit'; import { logger } from 'genkit/logging'; import { chroma } from 'genkitx-chromadb'; @@ -74,7 +75,15 @@ export const ai = genkit({ }), vertexAI({ location: 'us-central1', - modelGardenModels: [claude3Haiku, claude3Sonnet, claude3Opus], + }), + vertexAIModelGarden({ + location: 'us-central1', + modelGarden: { + models: [claude35Sonnet], + }, + }), + vertexAIEvaluation({ + location: 'us-central1', evaluation: { metrics: [ VertexAIEvaluationMetricType.BLEU, diff --git a/js/testapps/dev-ui-gallery/src/main/prompts.ts b/js/testapps/dev-ui-gallery/src/main/prompts.ts index a05ad674d..f9261405c 100644 --- a/js/testapps/dev-ui-gallery/src/main/prompts.ts +++ b/js/testapps/dev-ui-gallery/src/main/prompts.ts @@ -137,7 +137,7 @@ ai.defineFlow( outputSchema: z.string(), }, async (input) => { - const hello = await ai.prompt('functionalPrompt'); + const hello = ai.prompt('functionalPrompt'); return (await hello(input)).text; } ); @@ -153,7 +153,7 @@ ai.defineFlow( outputSchema: z.string(), }, async (input) => { - const hello = await ai.prompt('hello'); + const hello = ai.prompt('hello'); return (await hello(input)).text; } ); @@ -169,7 +169,7 @@ ai.defineFlow( outputSchema: z.string(), }, async (input) => { - const hello = await ai.prompt('hello', { + const hello = ai.prompt('hello', { variant: 'first-last-name', }); return (await hello(input)).text; @@ -187,7 +187,7 @@ ai.defineFlow( outputSchema: z.any(), }, async (input) => { - const hello = await ai.prompt('hello', { + const hello = ai.prompt('hello', { variant: 'json-output', }); return (await hello(input)).output; @@ -205,7 +205,7 @@ ai.defineFlow( outputSchema: z.any(), }, async (input) => { - const hello = await ai.prompt('hello', { + const hello = ai.prompt('hello', { variant: 'system', }); return (await hello(input)).text; @@ -223,7 +223,7 @@ ai.defineFlow( outputSchema: z.any(), }, async (input) => { - const hello = await ai.prompt('hello', { + const hello = ai.prompt('hello', { variant: 'history', }); return (await hello(input)).text; diff --git a/js/testapps/flow-simple-ai/src/index.ts b/js/testapps/flow-simple-ai/src/index.ts index f845323ab..29304c045 100644 --- a/js/testapps/flow-simple-ai/src/index.ts +++ b/js/testapps/flow-simple-ai/src/index.ts @@ -27,8 +27,11 @@ import { AlwaysOnSampler } from '@opentelemetry/sdk-trace-base'; import { initializeApp } from 'firebase-admin/app'; import { getFirestore } from 'firebase-admin/firestore'; import { MessageSchema, genkit, run, z } from 'genkit'; +import { logger } from 'genkit/logging'; import { Allow, parse } from 'partial-json'; +logger.setLogLevel('debug'); + enableGoogleCloudTelemetry({ // These are configured for demonstration purposes. Sensible defaults are // in place in the event that telemetryConfig is absent. @@ -335,9 +338,7 @@ export const dotpromptContext = ai.defineFlow( }, ]; - const result = await ( - await ai.prompt('dotpromptContext') - ).generate({ + const result = await ai.prompt('dotpromptContext').generate({ input: { question: question }, docs, }); @@ -476,3 +477,70 @@ export const toolTester = ai.defineFlow( return result.messages; } ); + +export const arrayStreamTester = ai.defineStreamingFlow( + { + name: 'arrayStreamTester', + inputSchema: z.string().nullish(), + outputSchema: z.any(), + streamSchema: z.any(), + }, + async (input, streamingCallback) => { + try { + const { stream, response } = await ai.generateStream({ + model: gemini15Flash, + config: { + safetySettings: [ + { + category: 'HARM_CATEGORY_HATE_SPEECH', + threshold: 'BLOCK_NONE', + }, + { + category: 'HARM_CATEGORY_DANGEROUS_CONTENT', + threshold: 'BLOCK_NONE', + }, + { + category: 'HARM_CATEGORY_HARASSMENT', + threshold: 'BLOCK_NONE', + }, + { + category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', + threshold: 'BLOCK_NONE', + }, + ], + }, + prompt: `Generate a list of 20 characters from ${input || 'Futurama'}`, + output: { + format: 'array', + schema: z.array( + z.object({ + name: z.string(), + description: z.string(), + friends: z.array(z.string()), + enemies: z.array(z.string()), + }) + ), + }, + }); + + for await (const { output, text } of stream) { + streamingCallback?.({ text, output }); + } + + const result = await response; + console.log(result.parser); + return result.output; + } catch (e: any) { + return 'Error: ' + e.message; + } + } +); + +// async function main() { +// const { stream, output } = arrayStreamTester(); +// for await (const chunk of stream) { +// console.log(chunk); +// } +// console.log(await output); +// } +// main(); diff --git a/js/testapps/format-tester/package.json b/js/testapps/format-tester/package.json new file mode 100644 index 000000000..0b9e2c0e7 --- /dev/null +++ b/js/testapps/format-tester/package.json @@ -0,0 +1,28 @@ +{ + "name": "format-tester", + "version": "1.0.0", + "description": "", + "main": "lib/index.js", + "scripts": { + "start": "node lib/index.js", + "compile": "tsc", + "build": "pnpm build:clean && pnpm compile", + "build:clean": "rimraf ./lib", + "build:watch": "tsc --watch", + "build-and-run": "pnpm build && node lib/index.js" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@genkit-ai/googleai": "workspace:*", + "@genkit-ai/vertexai": "workspace:*", + "@opentelemetry/sdk-trace-base": "^1.25.0", + "genkit": "workspace:*" + }, + "devDependencies": { + "rimraf": "^6.0.1", + "tsx": "^4.19.2", + "typescript": "^5.3.3" + } +} diff --git a/js/testapps/format-tester/src/index.ts b/js/testapps/format-tester/src/index.ts new file mode 100644 index 000000000..857487e38 --- /dev/null +++ b/js/testapps/format-tester/src/index.ts @@ -0,0 +1,178 @@ +/** + * 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 { googleAI } from '@genkit-ai/googleai'; +import { vertexAI } from '@genkit-ai/vertexai'; +import { + claude35Sonnet, + claude35SonnetV2, + vertexAIModelGarden, +} from '@genkit-ai/vertexai/modelgarden'; +import { CallableFlow, GenerateOptions, genkit, z } from 'genkit'; +import { logger } from 'genkit/logging'; + +logger.setLogLevel('debug'); + +const ai = genkit({ + plugins: [ + googleAI(), + vertexAI({ + location: 'us-east5', + }), + vertexAIModelGarden({ + location: 'us-east5', + modelGarden: { models: [claude35Sonnet, claude35SonnetV2] }, + }), + ], +}); + +function formatFlow(name: string, options: GenerateOptions) { + return ai.defineFlow( + { + name, + inputSchema: z.string(), + }, + async (model) => { + console.log('\n===', name, 'with model', model); + options.model = model; + if (model.includes('gemini')) { + options.config = { + safetySettings: [ + { + category: 'HARM_CATEGORY_HATE_SPEECH', + threshold: 'BLOCK_NONE', + }, + { + category: 'HARM_CATEGORY_DANGEROUS_CONTENT', + threshold: 'BLOCK_NONE', + }, + { + category: 'HARM_CATEGORY_HARASSMENT', + threshold: 'BLOCK_NONE', + }, + { + category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', + threshold: 'BLOCK_NONE', + }, + ], + }; + } + const { stream, response } = await ai.generateStream(options); + for await (const chunk of stream) { + console.log('text:', chunk.text); + console.log('output:', chunk.output); + } + console.log(); + console.log('final output:', (await response).output); + } + ); +} + +const prompts: Record = { + text: { + prompt: 'tell me a short story about pirates', + output: { format: 'text' }, + }, + json: { + prompt: 'generate a creature for an RPG game', + output: { + format: 'json', + schema: z.object({ + name: z.string().describe('the name of the creature'), + backstory: z + .string() + .describe('a one-paragraph backstory for the creature'), + hitPoints: z.number(), + attacks: z.array(z.string()).describe('named attacks'), + }), + }, + }, + array: { + prompt: 'generate a list of characters from Futurama', + output: { + // @ts-ignore + format: 'array', + schema: z.array( + z.object({ + name: z.string(), + description: z.string(), + friends: z.array(z.string()), + enemies: z.array(z.string()), + }) + ), + }, + }, + jsonl: { + prompt: 'generate fake products for an online pet store', + output: { + // @ts-ignore + format: 'array', + schema: z.array( + z.object({ + name: z.string(), + description: z.string(), + price: z.number(), + stock: z.number(), + color: z.string(), + tags: z.array(z.string()), + }) + ), + }, + }, + enum: { + prompt: 'how risky is skydiving?', + output: { + // @ts-ignore + format: 'enum', + schema: z.enum(['VERY_LOW', 'LOW', 'MEDIUM', 'HIGH', 'VERY_HIGH']), + }, + }, +}; + +const flows: CallableFlow[] = []; +for (const format in prompts) { + flows.push(formatFlow(format, prompts[format])); +} + +let models = process.argv.slice(2); +if (!models.length) { + models = [ + 'vertexai/gemini-1.5-pro', + 'vertexai/gemini-1.5-flash', + 'googleai/gemini-1.5-pro', + 'googleai/gemini-1.5-flash', + ]; +} + +async function main() { + const fails: { model: string; flow: string; error: string }[] = []; + for (const model of models) { + for (const flow of flows) { + try { + await flow(model); + } catch (e: any) { + console.error('ERROR:', e.stack); + fails.push({ model, flow: flow.flow.name, error: e.message }); + } + } + } + + console.log('!!!', fails.length, 'errors'); + for (const fail of fails) { + console.log(`${fail.model}: ${fail.flow}: ${fail.error}`); + } +} +main(); diff --git a/js/testapps/format-tester/src/tools.ts b/js/testapps/format-tester/src/tools.ts new file mode 100644 index 000000000..80ed923af --- /dev/null +++ b/js/testapps/format-tester/src/tools.ts @@ -0,0 +1,54 @@ +/** + * 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 { gemini15Flash, googleAI } from '@genkit-ai/googleai'; +import { genkit, z } from 'genkit'; + +const ai = genkit({ plugins: [googleAI()], model: gemini15Flash }); + +const lookupUsers = ai.defineTool( + { + name: 'lookupUsers', + description: 'use this tool to list users', + outputSchema: z.array(z.object({ name: z.string(), id: z.number() })), + }, + async () => [ + { id: 123, name: 'Michael Bleigh' }, + { id: 456, name: 'Pavel Jbanov' }, + { id: 789, name: 'Chris Gill' }, + { id: 1122, name: 'Marissa Christy' }, + ] +); + +async function main() { + const { stream } = await ai.generateStream({ + prompt: + 'use the lookupUsers tool and generate silly nicknames for each, then generate 50 fake users in the same format. return a JSON array.', + output: { + format: 'json', + schema: z.array( + z.object({ id: z.number(), name: z.string(), nickname: z.string() }) + ), + }, + tools: [lookupUsers], + }); + + for await (const chunk of stream) { + console.log('raw:', chunk); + console.log('output:', chunk.output); + } +} +main(); diff --git a/js/testapps/format-tester/tsconfig.json b/js/testapps/format-tester/tsconfig.json new file mode 100644 index 000000000..e51f33ae3 --- /dev/null +++ b/js/testapps/format-tester/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "module": "NodeNext", + "noImplicitReturns": true, + "noUnusedLocals": false, + "outDir": "lib", + "sourceMap": true, + "strict": true, + "target": "es2017", + "skipLibCheck": true, + "esModuleInterop": true + }, + "compileOnSave": true, + "include": ["src"] +} diff --git a/js/testapps/model-tester/src/index.ts b/js/testapps/model-tester/src/index.ts index cc9fb5361..7c1fe1fcb 100644 --- a/js/testapps/model-tester/src/index.ts +++ b/js/testapps/model-tester/src/index.ts @@ -15,7 +15,12 @@ */ import { googleAI } from '@genkit-ai/googleai'; -import { claude3Sonnet, llama31, vertexAI } from '@genkit-ai/vertexai'; +import { vertexAI } from '@genkit-ai/vertexai'; +import { + claude3Sonnet, + llama31, + vertexAIModelGarden, +} from '@genkit-ai/vertexai/modelgarden'; import * as clc from 'colorette'; import { genkit } from 'genkit'; import { testModels } from 'genkit/testing'; @@ -27,6 +32,9 @@ export const ai = genkit({ googleAI(), vertexAI({ location: 'us-central1', + }), + vertexAIModelGarden({ + location: 'us-central1', modelGarden: { models: [claude3Sonnet, llama31], }, diff --git a/js/testapps/prompt-file/src/index.ts b/js/testapps/prompt-file/src/index.ts index 9e6a1a966..4d1003727 100644 --- a/js/testapps/prompt-file/src/index.ts +++ b/js/testapps/prompt-file/src/index.ts @@ -51,60 +51,57 @@ ai.defineHelper('list', (data: any) => { return data.map((item) => `- ${item}`).join('\n'); }); -ai.prompt('recipe').then((recipePrompt) => { - ai.defineFlow( - { - name: 'chefFlow', - inputSchema: z.object({ - food: z.string(), - }), - outputSchema: RecipeSchema, - }, - async (input) => - (await recipePrompt.generate({ input: input })) - .output! - ); -}); +ai.defineFlow( + { + name: 'chefFlow', + inputSchema: z.object({ + food: z.string(), + }), + outputSchema: RecipeSchema, + }, + async (input) => + (await ai.prompt('recipe').generate({ input: input })) + .output! +); -ai.prompt('recipe', { variant: 'robot' }).then((recipePrompt) => { - ai.defineFlow( - { - name: 'robotChefFlow', - inputSchema: z.object({ - food: z.string(), - }), - outputSchema: z.any(), - }, - async (input) => (await recipePrompt.generate({ input: input })).output - ); -}); +ai.defineFlow( + { + name: 'robotChefFlow', + inputSchema: z.object({ + food: z.string(), + }), + outputSchema: z.any(), + }, + async (input) => + (await ai.prompt('recipe', { variant: 'robot' }).generate({ input: input })) + .output +); // A variation that supports streaming, optionally -ai.prompt('story').then((storyPrompt) => { - ai.defineStreamingFlow( - { - name: 'tellStory', - inputSchema: z.object({ - subject: z.string(), - personality: z.string().optional(), - }), - outputSchema: z.string(), - streamSchema: z.string(), - }, - async ({ subject, personality }, streamingCallback) => { - if (streamingCallback) { - const { response, stream } = await storyPrompt.generateStream({ - input: { subject, personality }, - }); - for await (const chunk of stream) { - streamingCallback(chunk.content[0]?.text!); - } - return (await response).text; - } else { - const response = await storyPrompt.generate({ input: { subject } }); - return response.text; +ai.defineStreamingFlow( + { + name: 'tellStory', + inputSchema: z.object({ + subject: z.string(), + personality: z.string().optional(), + }), + outputSchema: z.string(), + streamSchema: z.string(), + }, + async ({ subject, personality }, streamingCallback) => { + const storyPrompt = ai.prompt('story'); + if (streamingCallback) { + const { response, stream } = await storyPrompt.generateStream({ + input: { subject, personality }, + }); + for await (const chunk of stream) { + streamingCallback(chunk.content[0]?.text!); } + return (await response).text; + } else { + const response = await storyPrompt.generate({ input: { subject } }); + return response.text; } - ); -}); + } +); diff --git a/js/testapps/rag/src/genkit.ts b/js/testapps/rag/src/genkit.ts index 5e2cd4163..b8436f752 100644 --- a/js/testapps/rag/src/genkit.ts +++ b/js/testapps/rag/src/genkit.ts @@ -17,12 +17,12 @@ import { devLocalVectorstore } from '@genkit-ai/dev-local-vectorstore'; import { genkitEval, GenkitMetric } from '@genkit-ai/evaluator'; import { gemini15Flash, googleAI } from '@genkit-ai/googleai'; +import { textEmbedding004, vertexAI } from '@genkit-ai/vertexai'; import { claude3Sonnet, llama31, - textEmbedding004, - vertexAI, -} from '@genkit-ai/vertexai'; + vertexAIModelGarden, +} from '@genkit-ai/vertexai/modelgarden'; import { genkit } from 'genkit'; import { chroma } from 'genkitx-chromadb'; import { langchain } from 'genkitx-langchain'; @@ -76,6 +76,9 @@ export const ai = genkit({ }), vertexAI({ location: 'us-central1', + }), + vertexAIModelGarden({ + location: 'us-central1', modelGarden: { models: [claude3Sonnet, llama31], }, diff --git a/js/testapps/vertexai-reranker/src/index.ts b/js/testapps/vertexai-reranker/src/index.ts index 1759b70a5..5ed30acaa 100644 --- a/js/testapps/vertexai-reranker/src/index.ts +++ b/js/testapps/vertexai-reranker/src/index.ts @@ -19,6 +19,7 @@ import { Document, genkit, z } from 'genkit'; // important imports for this sample: import { vertexAI } from '@genkit-ai/vertexai'; +import { vertexAIRerankers } from '@genkit-ai/vertexai/rerankers'; import { LOCATION, PROJECT_ID } from './config'; // Configure Genkit with Vertex AI plugin @@ -30,9 +31,13 @@ const ai = genkit({ googleAuth: { scopes: ['https://www.googleapis.com/auth/cloud-platform'], }, + }), + vertexAIRerankers({ + projectId: PROJECT_ID, + location: LOCATION, rerankOptions: [ { - model: 'vertexai/semantic-ranker-512', + model: 'vertexai/reranker', }, ], }), diff --git a/js/testapps/vertexai-vector-search-bigquery/src/index.ts b/js/testapps/vertexai-vector-search-bigquery/src/index.ts index 59c7763ee..a2b13b6ee 100644 --- a/js/testapps/vertexai-vector-search-bigquery/src/index.ts +++ b/js/testapps/vertexai-vector-search-bigquery/src/index.ts @@ -18,16 +18,16 @@ import { Document, genkit, z } from 'genkit'; // important imports for this sample: +import { textEmbedding004, vertexAI } from '@genkit-ai/vertexai'; import { + DocumentIndexer, + DocumentRetriever, getBigQueryDocumentIndexer, getBigQueryDocumentRetriever, - vertexAI, + vertexAIVectorSearch, vertexAiIndexerRef, vertexAiRetrieverRef, - type DocumentIndexer, - type DocumentRetriever, -} from '@genkit-ai/vertexai'; - +} from '@genkit-ai/vertexai/vectorsearch'; // // Environment variables set with dotenv for simplicity of sample import { BIGQUERY_DATASET, @@ -81,6 +81,11 @@ const ai = genkit({ googleAuth: { scopes: ['https://www.googleapis.com/auth/cloud-platform'], }, + }), + vertexAIVectorSearch({ + location: LOCATION, + projectId: PROJECT_ID, + embedder: textEmbedding004, vectorSearchOptions: [ { publicDomainName: VECTOR_SEARCH_PUBLIC_DOMAIN_NAME, @@ -95,7 +100,6 @@ const ai = genkit({ ], }); -// // Define indexing flow export const indexFlow = ai.defineFlow( { name: 'indexFlow', diff --git a/js/testapps/vertexai-vector-search-custom/src/index.ts b/js/testapps/vertexai-vector-search-custom/src/index.ts index 96a708d4a..581fa5079 100644 --- a/js/testapps/vertexai-vector-search-custom/src/index.ts +++ b/js/testapps/vertexai-vector-search-custom/src/index.ts @@ -18,14 +18,15 @@ import { Document, genkit, z } from 'genkit'; // important imports for this sample: +import { textEmbedding004, vertexAI } from '@genkit-ai/vertexai'; import { - vertexAI, + vertexAIVectorSearch, vertexAiIndexerRef, vertexAiRetrieverRef, type DocumentIndexer, type DocumentRetriever, type Neighbor, -} from '@genkit-ai/vertexai'; +} from '@genkit-ai/vertexai/vectorsearch'; // // Environment variables set with dotenv for simplicity of sample import { @@ -148,6 +149,10 @@ const ai = genkit({ googleAuth: { scopes: ['https://www.googleapis.com/auth/cloud-platform'], }, + }), + vertexAIVectorSearch({ + location: LOCATION, + projectId: PROJECT_ID, vectorSearchOptions: [ { publicDomainName: VECTOR_SEARCH_PUBLIC_DOMAIN_NAME, @@ -156,6 +161,7 @@ const ai = genkit({ deployedIndexId: VECTOR_SEARCH_DEPLOYED_INDEX_ID, documentRetriever: localDocumentRetriever, documentIndexer: localDocumentIndexer, + embedder: textEmbedding004, }, ], }), diff --git a/js/testapps/vertexai-vector-search-firestore/src/index.ts b/js/testapps/vertexai-vector-search-firestore/src/index.ts index fda773418..bad5aba88 100644 --- a/js/testapps/vertexai-vector-search-firestore/src/index.ts +++ b/js/testapps/vertexai-vector-search-firestore/src/index.ts @@ -19,15 +19,18 @@ import { initializeApp } from 'firebase-admin/app'; import { Document, genkit, z } from 'genkit'; // important imports for this sample: + +import { textEmbedding004, vertexAI } from '@genkit-ai/vertexai'; + import { DocumentIndexer, DocumentRetriever, getFirestoreDocumentIndexer, getFirestoreDocumentRetriever, - vertexAI, vertexAiIndexerRef, vertexAiRetrieverRef, -} from '@genkit-ai/vertexai'; + vertexAIVectorSearch, +} from '@genkit-ai/vertexai/vectorsearch'; // // Environment variables set with dotenv for simplicity of sample import { getFirestore } from 'firebase-admin/firestore'; @@ -80,6 +83,10 @@ const ai = genkit({ googleAuth: { scopes: ['https://www.googleapis.com/auth/cloud-platform'], }, + }), + vertexAIVectorSearch({ + projectId: PROJECT_ID, + location: LOCATION, vectorSearchOptions: [ { publicDomainName: VECTOR_SEARCH_PUBLIC_DOMAIN_NAME, @@ -88,6 +95,7 @@ const ai = genkit({ deployedIndexId: VECTOR_SEARCH_DEPLOYED_INDEX_ID, documentRetriever: firestoreDocumentRetriever, documentIndexer: firestoreDocumentIndexer, + embedder: textEmbedding004, }, ], }), diff --git a/package.json b/package.json index f14d1f6ce..a6a47e3c4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,6 @@ { "name": "genkit", "private": true, - "version": "0.5.10", "scripts": { "preinstall": "npx only-allow pnpm", "setup": "npm-run-all pnpm-install-js pnpm-install-genkit-tools build link-genkit-cli", diff --git a/scripts/copyright.ts b/scripts/copyright.ts index 5cf7bd7ca..ee9f15537 100644 --- a/scripts/copyright.ts +++ b/scripts/copyright.ts @@ -44,6 +44,15 @@ const COPYRIGHT = ` Copyright ${new Date().getFullYear()} Google LLC See the License for the specific language governing permissions and limitations under the License.`; +async function fileExists(path: string): Promise { + try { + await readFile(path); + return true; + } catch { + return false; + } +} + async function getSourceFilesToUpdate() { const paths = ( execSync('git ls-files', FILE_OPTS) + @@ -52,14 +61,22 @@ async function getSourceFilesToUpdate() { ) .split('\n') .filter((p) => !!p); + + const existingPaths = ( + await Promise.all( + paths.map(async (path) => ((await fileExists(path)) ? path : null)) + ) + ).filter((path) => path !== null) as string[]; + const fileContents = await Promise.all( - paths.map((path) => readFile(path, { encoding: 'utf-8' })) + existingPaths.map((path) => readFile(path, { encoding: 'utf-8' })) ); + return fileContents .map((contents, idx) => ({ contents, - path: paths[idx], - format: FORMAT_TYPES.find(({ regex }) => regex.test(paths[idx])), + path: existingPaths[idx], + format: FORMAT_TYPES.find(({ regex }) => regex.test(existingPaths[idx])), })) .filter( ({ contents, format }) => format && !/Copyright \d\d\d\d/.test(contents)