diff --git a/genkit-tools/common/src/types/model.ts b/genkit-tools/common/src/types/model.ts index a3fef4b9d9..ca273ac80a 100644 --- a/genkit-tools/common/src/types/model.ts +++ b/genkit-tools/common/src/types/model.ts @@ -49,6 +49,18 @@ export { // IMPORTANT: Keep this file in sync with genkit/ai/src/model.ts! // +/** + * Zod schema of an opration representing a background task. + */ +export const OperationSchema = z.object({ + action: z.string().optional(), + id: z.string(), + done: z.boolean().optional(), + output: z.any().optional(), + error: z.object({ message: z.string() }).passthrough().optional(), + metadata: z.record(z.string(), z.any()).optional(), +}); + /** * Zod schema of message part. */ @@ -122,8 +134,6 @@ export const ModelInfoSchema = z.object({ constrained: z.enum(['none', 'all', 'no-tools']).optional(), /** Model supports controlling tool choice, e.g. forced tool calling. */ toolChoice: z.boolean().optional(), - /** Model supports long running operation interface. */ - longRunning: z.boolean().optional(), }) .optional(), /** At which stage of development this model is. @@ -196,40 +206,6 @@ export const OutputConfigSchema = z.object({ contentType: z.string().optional(), }); -/** Model response finish reason enum. */ -export const FinishReasonSchema = z.enum([ - 'stop', - 'length', - 'blocked', - 'interrupted', - 'pending', - 'other', - 'unknown', -]); - -/** - * Zod schema of a long running operation. - */ -export const ModelOperationSchema = z - .object({ - name: z.string(), - done: z.boolean().optional(), - request: z - .object({ - model: z.string(), - config: z.record(z.string(), z.any()).optional(), - }) - .optional(), - response: z - .object({ - message: MessageSchema.optional(), - finishReason: FinishReasonSchema, - raw: z.unknown(), - }) - .optional(), - }) - .passthrough(); - /** * Output config. */ @@ -243,9 +219,7 @@ export const ModelRequestSchema = z.object({ toolChoice: z.enum(['auto', 'required', 'none']).optional(), output: OutputConfigSchema.optional(), docs: z.array(DocumentDataSchema).optional(), - operation: ModelOperationSchema.optional(), }); - /** ModelRequest represents the parameters that are passed to a model when generating content. */ export interface ModelRequest< CustomOptionsSchema extends z.ZodTypeAny = z.ZodTypeAny, @@ -299,6 +273,16 @@ export const GenerationUsageSchema = z.object({ */ export type GenerationUsage = z.infer; +/** Model response finish reason enum. */ +export const FinishReasonSchema = z.enum([ + 'stop', + 'length', + 'blocked', + 'interrupted', + 'other', + 'unknown', +]); + /** @deprecated All responses now return a single candidate. Only the first candidate will be used if supplied. */ export const CandidateSchema = z.object({ index: z.number(), @@ -333,7 +317,7 @@ export const ModelResponseSchema = z.object({ custom: z.unknown(), raw: z.unknown(), request: GenerateRequestSchema.optional(), - operation: ModelOperationSchema.optional(), + operation: OperationSchema.optional(), }); /** diff --git a/genkit-tools/genkit-schema.json b/genkit-tools/genkit-schema.json index dbaf8ad1b0..bc637c7551 100644 --- a/genkit-tools/genkit-schema.json +++ b/genkit-tools/genkit-schema.json @@ -602,7 +602,6 @@ "length", "blocked", "interrupted", - "pending", "other", "unknown" ] @@ -733,9 +732,6 @@ "$ref": "#/$defs/DocumentData" } }, - "operation": { - "$ref": "#/$defs/ModelOperation" - }, "candidates": { "type": "number" } @@ -794,7 +790,7 @@ "$ref": "#/$defs/GenerateRequest" }, "operation": { - "$ref": "#/$defs/ModelOperation" + "$ref": "#/$defs/Operation" }, "candidates": { "type": "array", @@ -962,9 +958,6 @@ }, "toolChoice": { "type": "boolean" - }, - "longRunning": { - "type": "boolean" } }, "additionalProperties": false @@ -982,53 +975,6 @@ }, "additionalProperties": false }, - "ModelOperation": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "done": { - "type": "boolean" - }, - "request": { - "type": "object", - "properties": { - "model": { - "type": "string" - }, - "config": { - "type": "object", - "additionalProperties": {} - } - }, - "required": [ - "model" - ], - "additionalProperties": false - }, - "response": { - "type": "object", - "properties": { - "message": { - "$ref": "#/$defs/Message" - }, - "finishReason": { - "$ref": "#/$defs/FinishReason" - }, - "raw": {} - }, - "required": [ - "finishReason" - ], - "additionalProperties": false - } - }, - "required": [ - "name" - ], - "additionalProperties": true - }, "ModelRequest": { "type": "object", "properties": { @@ -1049,9 +995,6 @@ }, "docs": { "$ref": "#/$defs/GenerateRequest/properties/docs" - }, - "operation": { - "$ref": "#/$defs/GenerateRequest/properties/operation" } }, "required": [ @@ -1119,6 +1062,41 @@ ], "additionalProperties": false }, + "Operation": { + "type": "object", + "properties": { + "action": { + "type": "string" + }, + "id": { + "type": "string" + }, + "done": { + "type": "boolean" + }, + "output": {}, + "error": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + }, + "required": [ + "message" + ], + "additionalProperties": true + }, + "metadata": { + "type": "object", + "additionalProperties": {} + } + }, + "required": [ + "id" + ], + "additionalProperties": false + }, "OutputConfig": { "type": "object", "properties": { diff --git a/js/ai/src/check-operation.ts b/js/ai/src/check-operation.ts index e383c119ee..14c62d984a 100644 --- a/js/ai/src/check-operation.ts +++ b/js/ai/src/check-operation.ts @@ -14,42 +14,27 @@ * limitations under the License. */ -import { GenkitError } from '@genkit-ai/core'; +import { GenkitError, Operation } from '@genkit-ai/core'; import { Registry } from '@genkit-ai/core/registry'; -import { GenerateRequest, ModelAction, ModelOperation } from './model'; -export async function checkOperation( +export async function checkOperation( registry: Registry, - operation: ModelOperation -): Promise { - if (!operation.request?.model) { + operation: Operation +): Promise> { + if (!operation.action) { throw new GenkitError({ status: 'INVALID_ARGUMENT', message: 'Provided operation is missing original request information', }); } - const model = (await registry.lookupAction( - `/model/${operation.request?.model}` - )) as ModelAction; - if (!model) { + const backgroundAction = await registry.lookupBackgroundAction( + operation.action + ); + if (!backgroundAction) { throw new GenkitError({ status: 'INVALID_ARGUMENT', - message: `Failed to resolve model from original request: ${operation.request?.model}`, + message: `Failed to resolve background action from original request: ${operation.action}`, }); } - const request = { - operation, - messages: [], - } as GenerateRequest; - const rawResponse = await model(request); - if (!rawResponse.operation) { - throw new GenkitError({ - status: 'FAILED_PRECONDITION', - message: `The model did not return expected operation information: ${JSON.stringify(rawResponse)}`, - }); - } - return { - ...rawResponse.operation!, - request: operation.request, - }; + return await backgroundAction.check(operation); } diff --git a/js/ai/src/generate.ts b/js/ai/src/generate.ts index f795ea4e14..4fcdf288a6 100755 --- a/js/ai/src/generate.ts +++ b/js/ai/src/generate.ts @@ -19,6 +19,7 @@ import { GenkitError, isAction, isDetachedAction, + Operation, runWithContext, runWithStreamingCallback, sentinelNoopStreamingCallback, @@ -44,7 +45,7 @@ import { GenerateResponseChunk } from './generate/chunk.js'; import { GenerateResponse } from './generate/response.js'; import { Message } from './message.js'; import { - ModelOperation, + GenerateResponseData, resolveModel, type GenerateActionOptions, type GenerateRequest, @@ -342,9 +343,6 @@ export async function generate< registry = maybeRegisterDynamicTools(registry, resolvedOptions); const params = await toGenerateActionOptions(registry, resolvedOptions); - const model = await resolveModel(registry, resolvedOptions.model, { - warnDeprecated: true, - }); const tools = await toolsToActionRefs(registry, resolvedOptions.tools); return await runWithStreamingCallback( @@ -365,7 +363,6 @@ export async function generate< tools, }); return new GenerateResponse(response, { - model: model.modelAction.__action.name, request: response.request ?? request, parser: resolvedFormat?.handler(request.output?.schema).parseMessage, }); @@ -381,7 +378,7 @@ export async function generateOperation< options: | GenerateOptions | PromiseLike> -): Promise { +): Promise> { assertUnstable(registry, 'beta', 'generateOperation is a beta feature.'); options = await options; diff --git a/js/ai/src/generate/action.ts b/js/ai/src/generate/action.ts index 4da91c3f38..16d298e969 100644 --- a/js/ai/src/generate/action.ts +++ b/js/ai/src/generate/action.ts @@ -322,26 +322,30 @@ async function generate( ); }; - const rawResponse = await dispatch(0, request); - if (!rawResponse.model) { - rawResponse.model = model.__action.name; + const modelResponse = await dispatch(0, request); + + if (model.__action.actionType === 'background-model') { + return new GenerateResponse( + { operation: modelResponse }, + { + request, + parser: format?.handler(request.output?.schema).parseMessage, + } + ); } - return new GenerateResponse(rawResponse, { - model: model.__action.name, + return new GenerateResponse(modelResponse, { request, parser: format?.handler(request.output?.schema).parseMessage, }); } ); - - // Throw an error if the response is not usable. - response.assertValid(); - - if (response.operation) { + if (model.__action.actionType === 'background-model') { return response.toJSON(); } + // Throw an error if the response is not usable. + response.assertValid(); const generatedMessage = response.message!; // would have thrown if no message const toolRequests = generatedMessage.content.filter( diff --git a/js/ai/src/generate/response.ts b/js/ai/src/generate/response.ts index 32e135a153..54fcb7c1b7 100644 --- a/js/ai/src/generate/response.ts +++ b/js/ai/src/generate/response.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { GenkitError } from '@genkit-ai/core'; +import { Operation } from '@genkit-ai/core'; import { parseSchema } from '@genkit-ai/core/schema'; import { GenerationBlockedError, @@ -26,7 +26,6 @@ import type { GenerateResponseData, GenerationUsage, MessageData, - ModelOperation, ModelResponseData, ToolRequestPart, } from '../model.js'; @@ -51,7 +50,7 @@ export class GenerateResponse implements ModelResponseData { /** The request that generated this response. */ request?: GenerateRequest; /** Model generation long running operation. */ - operation?: ModelOperation; + operation?: Operation; /** Name of the model used. */ model?: string; /** The parser for output parsing of this response. */ @@ -60,7 +59,6 @@ export class GenerateResponse implements ModelResponseData { constructor( response: GenerateResponseData, options?: { - model?: string; request?: GenerateRequest; parser?: MessageParser; } @@ -81,20 +79,7 @@ export class GenerateResponse implements ModelResponseData { this.custom = response.custom || {}; this.raw = response.raw || this.custom; this.request = options?.request; - this.operation = response.operation; - this.model = options?.model; - if (this.operation) { - if (!this.model) { - throw new GenkitError({ - status: 'INVALID_ARGUMENT', - message: 'Must provide model for responses with pending operations.', - }); - } - this.operation.request = { - model: this.model, - config: this.request?.config, - }; - } + this.operation = response?.operation; } /** diff --git a/js/ai/src/model.ts b/js/ai/src/model.ts index 2fe5f36e82..e8d506f7f2 100644 --- a/js/ai/src/model.ts +++ b/js/ai/src/model.ts @@ -15,8 +15,12 @@ */ import { + BackgroundAction, GenkitError, + Operation, + OperationSchema, defineAction, + defineBackgroundAction, getStreamingCallback, z, type Action, @@ -145,8 +149,6 @@ export const ModelInfoSchema = z.object({ constrained: z.enum(['none', 'all', 'no-tools']).optional(), /** Model supports controlling tool choice, e.g. forced tool calling. */ toolChoice: z.boolean().optional(), - /** Model supports long running operation interface. */ - longRunning: z.boolean().optional(), }) .optional(), /** At which stage of development this model is. @@ -259,42 +261,6 @@ export const OutputConfigSchema = z.object({ constrained: z.boolean().optional(), contentType: z.string().optional(), }); -/** Model response finish reason enum. */ -export const FinishReasonSchema = z.enum([ - 'stop', - 'length', - 'blocked', - 'interrupted', - 'pending', - 'other', - 'unknown', -]); - -/** - * Zod schema of a long running operation. - */ -export const ModelOperationSchema = z.object({ - name: z.string(), - done: z.boolean().optional(), - request: z - .object({ - model: z.string(), - config: z.record(z.string(), z.any()).optional(), - }) - .optional(), - response: z - .object({ - message: MessageSchema.optional(), - finishReason: FinishReasonSchema, - raw: z.unknown(), - }) - .optional(), -}); - -/** - * Model operation data. - */ -export type ModelOperation = z.infer; /** * Output config. @@ -309,9 +275,7 @@ export const ModelRequestSchema = z.object({ toolChoice: z.enum(['auto', 'required', 'none']).optional(), output: OutputConfigSchema.optional(), docs: z.array(DocumentDataSchema).optional(), - operation: ModelOperationSchema.optional(), }); - /** ModelRequest represents the parameters that are passed to a model when generating content. */ export interface ModelRequest< CustomOptionsSchema extends z.ZodTypeAny = z.ZodTypeAny, @@ -365,6 +329,16 @@ export const GenerationUsageSchema = z.object({ */ export type GenerationUsage = z.infer; +/** Model response finish reason enum. */ +export const FinishReasonSchema = z.enum([ + 'stop', + 'length', + 'blocked', + 'interrupted', + 'other', + 'unknown', +]); + /** @deprecated All responses now return a single candidate. Only the first candidate will be used if supplied. */ export const CandidateSchema = z.object({ index: z.number(), @@ -399,7 +373,7 @@ export const ModelResponseSchema = z.object({ custom: z.unknown(), raw: z.unknown(), request: GenerateRequestSchema.optional(), - operation: ModelOperationSchema.optional(), + operation: OperationSchema.optional(), }); /** @@ -450,6 +424,15 @@ export type ModelAction< __configSchema: CustomOptionsSchema; }; +export type BackgroundModelAction< + CustomOptionsSchema extends z.ZodTypeAny = z.ZodTypeAny, +> = BackgroundAction< + typeof GenerateRequestSchema, + typeof GenerateResponseSchema +> & { + __configSchema: CustomOptionsSchema; +}; + export type ModelMiddleware = SimpleMiddleware< z.infer, z.infer @@ -485,23 +468,7 @@ export function defineModel< ) => Promise ): ModelAction { const label = options.label || options.name; - const middleware: ModelMiddleware[] = [ - ...(options.use || []), - validateSupport(options), - ]; - if (!options?.supports?.context) middleware.push(augmentWithContext()); - const constratedSimulator = simulateConstrainedGeneration(); - middleware.push((req, next) => { - if ( - !options?.supports?.constrained || - options?.supports?.constrained === 'none' || - (options?.supports?.constrained === 'no-tools' && - (req.tools?.length ?? 0) > 0) - ) { - return constratedSimulator(req, next); - } - return next(req); - }); + const middleware = getModelMiddleware(options); const act = defineAction( registry, { @@ -540,6 +507,103 @@ export function defineModel< return act as ModelAction; } +export type DefineBackgroundModelOptions< + CustomOptionsSchema extends z.ZodTypeAny = z.ZodTypeAny, +> = DefineModelOptions & { + start: ( + request: GenerateRequest + ) => Promise>; + check: ( + operation: Operation + ) => Promise>; + cancel?: ( + operation: Operation + ) => Promise>; +}; + +/** + * Defines a new model that runs in the background. + */ +export function defineBackgroundModel< + CustomOptionsSchema extends z.ZodTypeAny = z.ZodTypeAny, +>( + registry: Registry, + options: DefineBackgroundModelOptions +): BackgroundModelAction { + const label = options.label || options.name; + const middleware = getModelMiddleware(options); + const act = defineBackgroundAction(registry, { + actionType: 'background-model', + name: options.name, + description: label, + inputSchema: GenerateRequestSchema, + outputSchema: GenerateResponseSchema, + metadata: { + model: { + label, + customOptions: options.configSchema + ? toJsonSchema({ schema: options.configSchema }) + : undefined, + versions: options.versions, + supports: options.supports, + }, + }, + use: middleware, + async start(request) { + const startTimeMs = performance.now(); + const response = await options.start(request); + Object.assign(response, { + latencyMs: performance.now() - startTimeMs, + }); + return response; + }, + async check(op) { + return options.check(op); + }, + cancel: options.cancel + ? async (op) => { + if (!options.cancel) { + throw new GenkitError({ + status: 'UNIMPLEMENTED', + message: 'cancel not implemented', + }); + } + return options.cancel(op); + } + : undefined, + }) as BackgroundModelAction; + Object.assign(act, { + __configSchema: options.configSchema || z.unknown(), + }); + return act; +} + +function getModelMiddleware(options: { + use?: ModelMiddleware[]; + name: string; + supports?: ModelInfo['supports']; +}) { + const middleware: ModelMiddleware[] = [ + ...(options.use || []), + validateSupport(options), + ]; + if (!options?.supports?.context) middleware.push(augmentWithContext()); + const constratedSimulator = simulateConstrainedGeneration(); + middleware.push((req, next) => { + if ( + !options?.supports?.constrained || + options?.supports?.constrained === 'none' || + (options?.supports?.constrained === 'no-tools' && + (req.tools?.length ?? 0) > 0) + ) { + return constratedSimulator(req, next); + } + return next(req); + }); + + return middleware; +} + export interface ModelReference { name: string; configSchema?: CustomOptions; @@ -694,7 +758,7 @@ export async function resolveModel( } if (typeof model === 'string') { modelId = model; - out = { modelAction: await registry.lookupAction(`/model/${model}`) }; + out = { modelAction: await lookupModel(registry, model) }; } else if (model.hasOwnProperty('__action')) { modelId = (model as ModelAction).__action.name; out = { modelAction: model as ModelAction }; @@ -702,9 +766,7 @@ export async function resolveModel( const ref = model as ModelReference; modelId = ref.name; out = { - modelAction: (await registry.lookupAction( - `/model/${ref.name}` - )) as ModelAction, + modelAction: await lookupModel(registry, ref.name), config: { ...ref.config, }, @@ -731,6 +793,16 @@ export async function resolveModel( return out; } +async function lookupModel( + registry: Registry, + model: string +): Promise { + return ( + (await registry.lookupAction(`/model/${model}`)) || + (await registry.lookupAction(`/background-model/${model}`)) + ); +} + export const GenerateActionOutputConfig = z.object({ format: z.string().optional(), contentType: z.string().optional(), diff --git a/js/core/src/background-action.ts b/js/core/src/background-action.ts new file mode 100644 index 0000000000..3b665ff511 --- /dev/null +++ b/js/core/src/background-action.ts @@ -0,0 +1,316 @@ +/** + * 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 type { JSONSchema7 } from 'json-schema'; +import * as z from 'zod'; +import { Action, ActionMetadata, defineAction, Middleware } from './action.js'; +import { ActionContext } from './context.js'; +import { GenkitError } from './error.js'; +import { ActionType, Registry } from './registry.js'; +import { toJsonSchema } from './schema.js'; + +/** + * Zod schema of an opration representing a background task. + */ +export const OperationSchema = z.object({ + action: z.string().optional(), + id: z.string(), + done: z.boolean().optional(), + output: z.any().optional(), + error: z.object({ message: z.string() }).passthrough().optional(), + metadata: z.record(z.string(), z.any()).optional(), +}); + +/** + * Background operation. + */ +export interface Operation { + action?: string; + id: string; + done?: boolean; + output?: O; + error?: { message: string; [key: string]: unknown }; + metadata?: Record; +} + +/** + * Background action. Unlike regular action, background action can run for a long time in the background. + * The returned operation can used to check the status of the background operation and retrieve the response. + */ +export interface BackgroundAction< + I extends z.ZodTypeAny = z.ZodTypeAny, + O extends z.ZodTypeAny = z.ZodTypeAny, + RunOptions extends BackgroundActionRunOptions = BackgroundActionRunOptions, +> { + __action: ActionMetadata; + readonly supportsCancel: boolean; + + start( + input?: z.infer, + options?: RunOptions + ): Promise>>; + + check(operation: Operation>): Promise>>; + + cancel(operation: Operation>): Promise>>; +} + +export async function lookupBackgroundAction< + I extends z.ZodTypeAny = z.ZodTypeAny, + O extends z.ZodTypeAny = z.ZodTypeAny, +>( + registry: Registry, + key: string +): Promise | undefined> { + const root: Action = await registry.lookupAction< + I, + typeof OperationSchema, + Action + >(key); + if (!root) return undefined; + const actionName = key.substring(key.indexOf('/', 1) + 1); + return new BackgroundActionImpl( + root, + await registry.lookupAction< + typeof OperationSchema, + typeof OperationSchema, + Action + >(`/check-operation/${actionName}/check`), + await registry.lookupAction< + typeof OperationSchema, + typeof OperationSchema, + Action + >(`/cancel-operation/${actionName}/cancel`) + ); +} + +class BackgroundActionImpl< + I extends z.ZodTypeAny = z.ZodTypeAny, + O extends z.ZodTypeAny = z.ZodTypeAny, + RunOptions extends BackgroundActionRunOptions = BackgroundActionRunOptions, +> implements BackgroundAction +{ + __action: ActionMetadata; + + readonly startAction: Action; + readonly checkAction: Action; + readonly cancelAction?: Action< + typeof OperationSchema, + typeof OperationSchema + >; + + constructor( + startAction: Action, + checkAction: Action, + cancelAction: + | Action + | undefined + ) { + this.__action = { + name: startAction.__action.name, + description: startAction.__action.description, + inputSchema: startAction.__action.inputSchema, + inputJsonSchema: startAction.__action.inputJsonSchema, + metadata: startAction.__action.metadata, + actionType: startAction.__action.actionType, + }; + this.startAction = startAction; + this.checkAction = checkAction; + this.cancelAction = cancelAction; + } + + async start( + input?: z.infer, + options?: RunOptions + ): Promise>> { + return await this.startAction(input, options); + } + + async check( + operation: Operation> + ): Promise>> { + return await this.checkAction(operation); + } + + get supportsCancel(): boolean { + return !!this.cancelAction; + } + + async cancel( + operation: Operation> + ): Promise>> { + if (!this.cancelAction) { + return operation; + } + return await this.cancelAction(operation); + } +} + +/** + * Options (side channel) data to pass to the model. + */ +export interface BackgroundActionRunOptions { + /** + * Additional runtime context data (ex. auth context data). + */ + context?: ActionContext; + + /** + * Additional span attributes to apply to OT spans. + */ + telemetryLabels?: Record; +} + +/** + * Options (side channel) data to pass to the model. + */ +export interface BackgroundActionFnArg { + /** + * Additional runtime context data (ex. auth context data). + */ + context?: ActionContext; + + /** + * Trace context containing trace and span IDs. + */ + trace: { + traceId: string; + spanId: string; + }; +} + +/** + * Action factory params. + */ +export type BackgroundActionParams< + I extends z.ZodTypeAny, + O extends z.ZodTypeAny, + S extends z.ZodTypeAny = z.ZodTypeAny, +> = { + name: string; + start: ( + input: z.infer, + options: BackgroundActionFnArg> + ) => Promise>>; + check: (input: Operation>) => Promise>>; + cancel?: (input: Operation>) => Promise>>; + actionType: ActionType; + + description?: string; + inputSchema?: I; + inputJsonSchema?: JSONSchema7; + outputSchema?: O; + outputJsonSchema?: JSONSchema7; + metadata?: Record; + use?: Middleware, z.infer>[]; + streamSchema?: S; +}; + +/** + * Defines an action with the given config and registers it in the registry. + */ +export function defineBackgroundAction< + I extends z.ZodTypeAny, + O extends z.ZodTypeAny, + S extends z.ZodTypeAny = z.ZodTypeAny, +>( + registry: Registry, + config: BackgroundActionParams +): BackgroundAction { + const startAction = defineAction( + registry, + { + actionType: config.actionType, + name: config.name, + description: config.description, + inputSchema: config.inputSchema, + inputJsonSchema: config.inputJsonSchema, + outputSchema: OperationSchema, + metadata: { + ...config.metadata, + outputSchema: toJsonSchema({ + schema: config.outputSchema, + jsonSchema: config.outputJsonSchema, + }), + }, + use: config.use, + }, + async (input, options) => { + const operation = await config.start(input, options); + operation.action = `/${config.actionType}/${config.name}`; + return operation; + } + ); + const checkAction = defineAction( + registry, + { + actionType: 'check-operation', + name: `${config.name}/check`, + description: config.description, + inputSchema: OperationSchema, + inputJsonSchema: config.inputJsonSchema, + outputSchema: OperationSchema, + metadata: { + ...config.metadata, + outputSchema: toJsonSchema({ + schema: config.outputSchema, + jsonSchema: config.outputJsonSchema, + }), + }, + }, + async (input) => { + const operation = await config.check(input); + operation.action = `/${config.actionType}/${config.name}`; + return operation; + } + ); + let cancelAction: + | Action + | undefined = undefined; + if (config.cancel) { + cancelAction = defineAction( + registry, + { + actionType: 'cancel-operation', + name: `${config.name}/cancel`, + description: config.description, + inputSchema: OperationSchema, + inputJsonSchema: config.inputJsonSchema, + outputSchema: OperationSchema, + metadata: { + ...config.metadata, + outputSchema: toJsonSchema({ + schema: config.outputSchema, + jsonSchema: config.outputJsonSchema, + }), + }, + }, + async (input) => { + if (!config.cancel) { + throw new GenkitError({ + status: 'UNAVAILABLE', + message: `${config.name} does not support cancellation.`, + }); + } + const operation = await config.cancel(input); + operation.action = `/${config.actionType}/${config.name}`; + return operation; + } + ); + } + + return new BackgroundActionImpl(startAction, checkAction, cancelAction); +} diff --git a/js/core/src/index.ts b/js/core/src/index.ts index 3933ddb53f..c78b783b3a 100644 --- a/js/core/src/index.ts +++ b/js/core/src/index.ts @@ -29,6 +29,15 @@ export const GENKIT_REFLECTION_API_SPEC_VERSION = 1; export { z } from 'zod'; export * from './action.js'; +export { + OperationSchema, + defineBackgroundAction, + type BackgroundAction, + type BackgroundActionFnArg, + type BackgroundActionParams, + type BackgroundActionRunOptions, + type Operation, +} from './background-action.js'; export { apiKey, getContext, diff --git a/js/core/src/registry.ts b/js/core/src/registry.ts index 45a5a769fb..7dec5b520b 100644 --- a/js/core/src/registry.ts +++ b/js/core/src/registry.ts @@ -22,6 +22,10 @@ import { type Action, type ActionMetadata, } from './action.js'; +import { + BackgroundAction, + lookupBackgroundAction, +} from './background-action.js'; import { ActionContext } from './context.js'; import { GenkitError } from './error.js'; import { logger } from './logging.js'; @@ -41,6 +45,9 @@ export type ActionType = | 'flow' | 'indexer' | 'model' + | 'background-model' + | 'check-operation' + | 'cancel-operation' | 'prompt' | 'reranker' | 'retriever' @@ -185,6 +192,17 @@ export class Registry { ); } + /** + * Looks up a background action from the registry. + * @param key The key of the action to lookup. + * @returns The action. + */ + async lookupBackgroundAction( + key: string + ): Promise { + return lookupBackgroundAction(this, key); + } + /** * Registers an action in the registry. * @param type The type of the action to register. diff --git a/js/genkit/src/common.ts b/js/genkit/src/common.ts index 5c5f40b6c9..52fa249d44 100644 --- a/js/genkit/src/common.ts +++ b/js/genkit/src/common.ts @@ -117,6 +117,7 @@ export { GENKIT_CLIENT_HEADER, GENKIT_VERSION, GenkitError, + OperationSchema, ReflectionServer, StatusCodes, StatusSchema, @@ -138,6 +139,7 @@ export { type JSONSchema, type JSONSchema7, type Middleware, + type Operation, type ReflectionServerOptions, type RunActionResponse, type Status, diff --git a/js/genkit/src/genkit-beta.ts b/js/genkit/src/genkit-beta.ts index 02114ed780..26fa5f0056 100644 --- a/js/genkit/src/genkit-beta.ts +++ b/js/genkit/src/genkit-beta.ts @@ -18,6 +18,7 @@ import { defineInterrupt, generateOperation, GenerateOptions, + GenerateResponseData, GenerationCommonConfigSchema, isExecutablePrompt, type ExecutablePrompt, @@ -26,7 +27,6 @@ import { } from '@genkit-ai/ai'; import type { Chat, ChatOptions } from '@genkit-ai/ai/chat'; import { defineFormat } from '@genkit-ai/ai/formats'; -import { ModelOperation } from '@genkit-ai/ai/model'; import { getCurrentSession, Session, @@ -34,10 +34,10 @@ import { type SessionData, type SessionOptions, } from '@genkit-ai/ai/session'; -import type { z } from '@genkit-ai/core'; +import type { Operation, z } from '@genkit-ai/core'; import { v4 as uuidv4 } from 'uuid'; -import type { Formatter } from './formats.js'; -import { Genkit, type GenkitOptions } from './genkit.js'; +import type { Formatter } from './formats'; +import { Genkit, type GenkitOptions } from './genkit'; export type { GenkitOptions as GenkitBetaOptions }; // in case they drift later @@ -263,7 +263,7 @@ export class GenkitBeta extends Genkit { opts: | GenerateOptions | PromiseLike> - ): Promise { + ): Promise> { return generateOperation(this.registry, opts); } } diff --git a/js/genkit/src/genkit.ts b/js/genkit/src/genkit.ts index 1e7c687b82..19a5cdcbb4 100644 --- a/js/genkit/src/genkit.ts +++ b/js/genkit/src/genkit.ts @@ -72,9 +72,11 @@ import { } from '@genkit-ai/ai/evaluator'; import { configureFormats } from '@genkit-ai/ai/formats'; import { - ModelOperation, + defineBackgroundModel, defineGenerateAction, defineModel, + type BackgroundModelAction, + type DefineBackgroundModelOptions, type DefineModelOptions, type GenerateResponseChunkData, type ModelAction, @@ -98,6 +100,7 @@ import { import { dynamicTool, type ToolFn } from '@genkit-ai/ai/tool'; import { GenkitError, + Operation, ReflectionServer, defineFlow, defineJsonSchema, @@ -251,6 +254,17 @@ export class Genkit implements HasRegistry { return defineModel(this.registry, options, runner); } + /** + * Defines a new background model and adds it to the registry. + */ + defineBackgroundModel< + CustomOptionsSchema extends z.ZodTypeAny = z.ZodTypeAny, + >( + options: DefineBackgroundModelOptions + ): BackgroundModelAction { + return defineBackgroundModel(this.registry, options); + } + /** * Looks up a prompt by `name` (and optionally `variant`). Can be used to lookup * .prompt files or prompts previously defined with {@link Genkit.definePrompt} @@ -787,7 +801,7 @@ export class Genkit implements HasRegistry { * @param operation * @returns */ - checkOperation(operation: ModelOperation): Promise { + checkOperation(operation: Operation): Promise> { return checkOperation(this.registry, operation); } diff --git a/js/genkit/src/model.ts b/js/genkit/src/model.ts index 05a48b7989..09f8c7bd65 100644 --- a/js/genkit/src/model.ts +++ b/js/genkit/src/model.ts @@ -28,7 +28,6 @@ export { MediaPartSchema, MessageSchema, ModelInfoSchema, - ModelOperationSchema, ModelRequestSchema, ModelResponseSchema, PartSchema, @@ -40,10 +39,12 @@ export { getBasicUsageStats, modelRef, simulateConstrainedGeneration, + type BackgroundModelAction, type CandidateData, type CandidateError, type CustomPart, type DataPart, + type DefineBackgroundModelOptions, type DefineModelOptions, type GenerateRequest, type GenerateRequestData, @@ -57,7 +58,6 @@ export { type ModelArgument, type ModelInfo, type ModelMiddleware, - type ModelOperation, type ModelReference, type ModelRequest, type ModelResponseChunkData, diff --git a/js/genkit/tests/generate_test.ts b/js/genkit/tests/generate_test.ts index 7aed19712d..41b8ef7167 100644 --- a/js/genkit/tests/generate_test.ts +++ b/js/genkit/tests/generate_test.ts @@ -15,10 +15,10 @@ */ import type { GenerateResponseChunkData, MessageData } from '@genkit-ai/ai'; -import { z, type JSONSchema7 } from '@genkit-ai/core'; +import { Operation, z, type JSONSchema7 } from '@genkit-ai/core'; import * as assert from 'assert'; import { beforeEach, describe, it } from 'node:test'; -import { ModelOperation, modelRef } from '../../ai/src/model'; +import { modelRef } from '../../ai/src/model'; import { dynamicTool, genkit, type GenkitBeta } from '../src/beta'; import { defineEchoModel, @@ -1267,82 +1267,66 @@ describe('generate', () => { async () => 'tool called' ); - pm.handleResponse = async (req, sc) => { - return { - finishReason: 'pending', - operation: { name: '123' }, - }; - }; + ai.defineBackgroundModel({ + name: 'bkg-model', + async start(_) { + return { + id: '123', + }; + }, + async check(operation) { + return { + id: '123', + }; + }, + }); const { operation } = await ai.generate({ + model: 'bkg-model', prompt: 'call the tool', tools: ['testTool'], }); + delete (operation as any).latencyMs; assert.deepStrictEqual(operation, { - name: '123', - request: { - config: { - version: undefined, - }, - model: 'programmableModel', - }, + action: '/background-model/bkg-model', + id: '123', }); }); it('checks operation status', async () => { - ai.defineTool( - { name: 'testTool', description: 'description' }, - async () => 'tool called' - ); - const newOp = { - name: '123', + id: '123', done: true, - response: { + output: { finishReason: 'stop', message: { role: 'model', content: [{ text: 'done' }], }, }, - } as ModelOperation; + } as Operation; - pm.handleResponse = async (req, sc) => { - return { - finishReason: 'stop', - operation: newOp, - }; - }; + ai.defineBackgroundModel({ + name: 'bkg-model', + async start(_) { + return { + id: '123', + }; + }, + async check(operation) { + return { ...newOp }; + }, + }); const operation = await ai.checkOperation({ - name: '123', - request: { - config: { - version: undefined, - }, - model: 'programmableModel', - }, + action: '/background-model/bkg-model', + id: '123', }); assert.deepStrictEqual(operation, { ...newOp, - request: { - config: { - version: undefined, - }, - model: 'programmableModel', - }, - }); - assert.deepStrictEqual(pm.lastRequest, { - messages: [], - operation: { - name: '123', - request: { - config: {}, - model: 'programmableModel', - }, - }, + action: '/background-model/bkg-model', }); }); }); diff --git a/js/plugins/googleai/src/predict.ts b/js/plugins/googleai/src/predict.ts index 60c9483d15..e25c5bfcc5 100644 --- a/js/plugins/googleai/src/predict.ts +++ b/js/plugins/googleai/src/predict.ts @@ -21,6 +21,9 @@ export type PredictMethod = 'predict' | 'predictLongRunning'; export interface Operation { name: string; done?: boolean; + error?: { + message: string; + }; response?: { generateVideoResponse: { generatedSamples: { video: { uri: string } }[]; diff --git a/js/plugins/googleai/src/veo.ts b/js/plugins/googleai/src/veo.ts index cab485bcf7..31921d5a8c 100644 --- a/js/plugins/googleai/src/veo.ts +++ b/js/plugins/googleai/src/veo.ts @@ -14,13 +14,18 @@ * limitations under the License. */ -import { GenkitError, z, type Genkit } from 'genkit'; import { + GenerateResponseData, + GenkitError, + Operation, + z, + type Genkit, +} from 'genkit'; +import { + BackgroundModelAction, modelRef, type GenerateRequest, - type ModelAction, type ModelInfo, - type ModelOperation, type ModelReference, } from 'genkit/model'; import { getApiKeyFromEnvVar } from './common.js'; @@ -117,7 +122,7 @@ export function defineVeoModel( ai: Genkit, name: string, apiKey?: string | false -): ModelAction { +): BackgroundModelAction { if (apiKey !== false) { apiKey = apiKey || getApiKeyFromEnvVar(); if (!apiKey) { @@ -139,21 +144,11 @@ export function defineVeoModel( configSchema: VeoConfigSchema, }); - return ai.defineModel( - { - name: modelName, - ...model.info, - configSchema: VeoConfigSchema, - }, - async (request) => { - if (request.operation) { - const newOp = await checkOp(request.operation.name, apiKey as string); - return { - finishReason: request.operation.done ? 'stop' : 'pending', - operation: toGenkitOp(newOp), - }; - } - + return ai.defineBackgroundModel({ + name: modelName, + ...model.info, + configSchema: VeoConfigSchema, + async start(request) { const instance: VeoInstance = { prompt: extractText(request), }; @@ -169,26 +164,31 @@ export function defineVeoModel( >(model.version || name, apiKey as string, 'predictLongRunning'); const response = await predictClient([instance], toParameters(request)); - return { - finishReason: response.done ? 'stop' : 'pending', - operation: toGenkitOp(response), - custom: response, - }; - } - ); + return toGenkitOp(response); + }, + async check(operation) { + const newOp = await checkOp(operation.id, apiKey as string); + return toGenkitOp(newOp); + }, + }); } -function toGenkitOp(apiOp: ApiOperation): ModelOperation { - const res = { name: apiOp.name } as ModelOperation; +function toGenkitOp(apiOp: ApiOperation): Operation { + const res = { id: apiOp.name } as Operation; if (apiOp.done !== undefined) { res.done = apiOp.done; } + + if (apiOp.error) { + res.error = { message: apiOp.error.message }; + } + if ( apiOp.response && apiOp.response.generateVideoResponse && apiOp.response.generateVideoResponse.generatedSamples ) { - res.response = { + res.output = { finishReason: 'stop', raw: apiOp.response, message: { diff --git a/js/pnpm-lock.yaml b/js/pnpm-lock.yaml index cae06e501d..afc417e53c 100644 --- a/js/pnpm-lock.yaml +++ b/js/pnpm-lock.yaml @@ -903,7 +903,7 @@ importers: version: link:../../plugins/compat-oai '@genkit-ai/express': specifier: ^1.1.0 - version: 1.8.0(@genkit-ai/core@1.11.1)(express@5.0.1)(genkit@genkit) + version: 1.8.0(@genkit-ai/core@1.12.0)(express@5.0.1)(genkit@genkit) genkit: specifier: workspace:* version: link:../../genkit @@ -1465,7 +1465,7 @@ importers: version: link:../../plugins/ollama genkitx-openai: specifier: ^0.10.1 - version: 0.10.1(@genkit-ai/ai@1.11.1)(@genkit-ai/core@1.11.1) + version: 0.10.1(@genkit-ai/ai@1.12.0)(@genkit-ai/core@1.12.0) devDependencies: rimraf: specifier: ^6.0.1 @@ -2747,11 +2747,11 @@ packages: '@firebase/webchannel-wrapper@1.0.3': resolution: {integrity: sha512-2xCRM9q9FlzGZCdgDMJwc0gyUkWFtkosy7Xxr6sFgQwn+wMNIWd7xIvYNauU1r64B5L5rsGKy/n9TKJ0aAFeqQ==} - '@genkit-ai/ai@1.11.1': - resolution: {integrity: sha512-E1rlzaShKmvTHxyYGqVaXCsekK9r516wh4XoKv+pcWVG71aWxV05qoI8zMIKFEg8SDj5oLOBYMsyoDDgyfpFqQ==} + '@genkit-ai/ai@1.12.0': + resolution: {integrity: sha512-0xNVb90JsgxmY4zEtHf4y6qgUXUCa3NfCsirEXKMSjgPYG/HRjxEQD9/qOL4LXL0WZmESJZZ5A+eTNv2TTJO1w==} - '@genkit-ai/core@1.11.1': - resolution: {integrity: sha512-Dfp22tsx3HDA0ZAA5gRoLdWhZzwcoVC42WMPBYCV/0WAToTuXjuwI2cv1D3bMMNZcNuuTTk4uKLuUhILaT/7QQ==} + '@genkit-ai/core@1.12.0': + resolution: {integrity: sha512-DyS47N+rzqOvVTnQOJTiRkm9ksLxnn1cR3ehRoV1AeT6q335pIpGxilelD9jEpCCKEFXTk2dtaowWWLC3Mw6uw==} '@genkit-ai/express@1.8.0': resolution: {integrity: sha512-Cq5BdxslixkrEJujtMQh8WoSy/gHCBE/hCjqfa6MCqy6CwYGf9p/Y5P8C00gsaf4ymnfTK4HTfLfm60GunA+Mg==} @@ -8450,9 +8450,9 @@ snapshots: '@firebase/webchannel-wrapper@1.0.3': {} - '@genkit-ai/ai@1.11.1': + '@genkit-ai/ai@1.12.0': dependencies: - '@genkit-ai/core': 1.11.1 + '@genkit-ai/core': 1.12.0 '@opentelemetry/api': 1.9.0 '@types/node': 20.17.17 colorette: 2.0.20 @@ -8464,7 +8464,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@genkit-ai/core@1.11.1': + '@genkit-ai/core@1.12.0': dependencies: '@opentelemetry/api': 1.9.0 '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) @@ -8487,9 +8487,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@genkit-ai/express@1.8.0(@genkit-ai/core@1.11.1)(express@5.0.1)(genkit@genkit)': + '@genkit-ai/express@1.8.0(@genkit-ai/core@1.12.0)(express@5.0.1)(genkit@genkit)': dependencies: - '@genkit-ai/core': 1.11.1 + '@genkit-ai/core': 1.12.0 body-parser: 1.20.3 cors: 2.8.5 express: 5.0.1 @@ -11562,10 +11562,10 @@ snapshots: - encoding - supports-color - genkitx-openai@0.10.1(@genkit-ai/ai@1.11.1)(@genkit-ai/core@1.11.1): + genkitx-openai@0.10.1(@genkit-ai/ai@1.12.0)(@genkit-ai/core@1.12.0): dependencies: - '@genkit-ai/ai': 1.11.1 - '@genkit-ai/core': 1.11.1 + '@genkit-ai/ai': 1.12.0 + '@genkit-ai/core': 1.12.0 openai: 4.53.0(encoding@0.1.13) zod: 3.24.1 transitivePeerDependencies: diff --git a/js/testapps/flow-simple-ai/src/index.ts b/js/testapps/flow-simple-ai/src/index.ts index f268e1a2d4..60c8df6639 100644 --- a/js/testapps/flow-simple-ai/src/index.ts +++ b/js/testapps/flow-simple-ai/src/index.ts @@ -1155,14 +1155,14 @@ ai.defineFlow('meme-of-the-day', async () => { } while (!operation.done) { - console.log('check status', operation.name); + console.log('check status', operation.id); operation = await ai.checkOperation(operation); await new Promise((resolve) => setTimeout(resolve, 5000)); } // operation done, download generated video to Firebae Storage - const video = operation.response?.message?.content.find((p) => !!p.media); + const video = operation.output?.message?.content.find((p) => !!p.media); if (!video) { throw new Error('Failed to find the generated video'); } diff --git a/py/packages/genkit/src/genkit/core/typing.py b/py/packages/genkit/src/genkit/core/typing.py index 89f2313bf9..4f7a8877df 100644 --- a/py/packages/genkit/src/genkit/core/typing.py +++ b/py/packages/genkit/src/genkit/core/typing.py @@ -189,7 +189,6 @@ class FinishReason(StrEnum): LENGTH = 'length' BLOCKED = 'blocked' INTERRUPTED = 'interrupted' - PENDING = 'pending' OTHER = 'other' UNKNOWN = 'unknown' @@ -266,7 +265,6 @@ class Supports(BaseModel): context: bool | None = None constrained: Constrained | None = None tool_choice: bool | None = Field(None, alias='toolChoice') - long_running: bool | None = Field(None, alias='longRunning') class Stage(StrEnum): @@ -290,12 +288,23 @@ class ModelInfo(BaseModel): stage: Stage | None = None -class Request(BaseModel): - """Model for request data.""" +class Error(BaseModel): + """Model for error data.""" model_config = ConfigDict(extra='forbid', populate_by_name=True) - model: str - config: dict[str, Any] | None = None + message: str + + +class Operation(BaseModel): + """Model for operation data.""" + + model_config = ConfigDict(extra='forbid', populate_by_name=True) + action: str | None = None + id: str + done: bool | None = None + output: Any | None = None + error: Error | None = None + metadata: dict[str, Any] | None = None class OutputConfig(BaseModel): @@ -798,25 +807,6 @@ class Message(BaseModel): metadata: dict[str, Any] | None = None -class Response(BaseModel): - """Model for response data.""" - - model_config = ConfigDict(extra='forbid', populate_by_name=True) - message: Message | None = None - finish_reason: FinishReason = Field(..., alias='finishReason') - raw: Any | None = None - - -class ModelOperation(BaseModel): - """Model for modeloperation data.""" - - model_config = ConfigDict(extra='forbid', populate_by_name=True) - name: str - done: bool | None = None - request: Request | None = None - response: Response | None = None - - class ModelResponseChunk(BaseModel): """Model for modelresponsechunk data.""" @@ -849,12 +839,6 @@ class Messages(RootModel[list[Message]]): root: list[Message] -class Operation(RootModel[ModelOperation]): - """Root model for operation.""" - - root: ModelOperation - - class DocumentData(BaseModel): """Model for documentdata data.""" @@ -909,7 +893,6 @@ class GenerateRequest(BaseModel): tool_choice: ToolChoice | None = Field(None, alias='toolChoice') output: OutputConfig | None = None docs: list[DocumentData] | None = None - operation: ModelOperation | None = None candidates: float | None = None @@ -925,7 +908,7 @@ class GenerateResponse(BaseModel): custom: Any | None = None raw: Any | None = None request: GenerateRequest | None = None - operation: ModelOperation | None = None + operation: Operation | None = None candidates: list[Candidate] | None = None @@ -959,8 +942,8 @@ class Docs(RootModel[list[DocumentData]]): root: list[DocumentData] -class RequestModel(RootModel[GenerateRequest]): - """Root model for requestmodel.""" +class Request(RootModel[GenerateRequest]): + """Root model for request.""" root: GenerateRequest @@ -975,7 +958,6 @@ class ModelRequest(BaseModel): tool_choice: ToolChoice | None = Field(None, alias='toolChoice') output: OutputModel | None = None docs: Docs | None = None - operation: Operation | None = None class ModelResponse(BaseModel): @@ -989,5 +971,5 @@ class ModelResponse(BaseModel): usage: Usage | None = None custom: CustomModel | None = None raw: Raw | None = None - request: RequestModel | None = None + request: Request | None = None operation: Operation | None = None diff --git a/py/uv.lock b/py/uv.lock index 04beefa010..bb1cf4a8e7 100644 --- a/py/uv.lock +++ b/py/uv.lock @@ -2191,9 +2191,9 @@ wheels = [ name = "jsonata-python" version = "0.5.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ab/d1/9455e5ef09044a550500b32ff276a30f44928ab84b2db9ef13352bffd154/jsonata_python-0.5.3.tar.gz", hash = "sha256:c83f45127f8dc45e5ca5f20fd8b8635f094ef6bbb2d203a75bdde11ffece61e2", size = 338742, upload-time = "2025-03-16T00:35:03.076Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/d1/9455e5ef09044a550500b32ff276a30f44928ab84b2db9ef13352bffd154/jsonata_python-0.5.3.tar.gz", hash = "sha256:c83f45127f8dc45e5ca5f20fd8b8635f094ef6bbb2d203a75bdde11ffece61e2", size = 338742 } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/70/e7e92230b832e8b4ee2ee1ed0725be39991ef10bd0983e753226115a3bcb/jsonata_python-0.5.3-py3-none-any.whl", hash = "sha256:e7bd58020441fec198858fd1bc22bb9837a5ce281c41899f79750519fa6cef9f", size = 83045, upload-time = "2025-03-16T00:35:01.437Z" }, + { url = "https://files.pythonhosted.org/packages/24/70/e7e92230b832e8b4ee2ee1ed0725be39991ef10bd0983e753226115a3bcb/jsonata_python-0.5.3-py3-none-any.whl", hash = "sha256:e7bd58020441fec198858fd1bc22bb9837a5ce281c41899f79750519fa6cef9f", size = 83045 }, ] [[package]]