diff --git a/genkit-tools/common/src/types/model.ts b/genkit-tools/common/src/types/model.ts index a36d9f288f..9d1f1e0cd4 100644 --- a/genkit-tools/common/src/types/model.ts +++ b/genkit-tools/common/src/types/model.ts @@ -20,7 +20,6 @@ import { DataPartSchema, MediaPartSchema, MultipartToolResponseSchema, - PartSchema, ReasoningPartSchema, ResourcePartSchema, TextPartSchema, @@ -30,7 +29,6 @@ import { type DataPart, type MediaPart, type MultipartToolResponse, - type Part, type ReasoningPart, type ResourcePart, type TextPart, @@ -42,7 +40,6 @@ export { DataPartSchema, MediaPartSchema, MultipartToolResponseSchema, - PartSchema, ReasoningPartSchema, ResourcePartSchema, TextPartSchema, @@ -52,7 +49,6 @@ export { type DataPart, type MediaPart, type MultipartToolResponse, - type Part, type ReasoningPart, type ResourcePart, type TextPart, @@ -61,11 +57,53 @@ export { }; // -// IMPORTANT: Keep this file in sync with genkit/ai/src/model.ts! +// IMPORTANT: Keep this file in sync with genkit/ai/src/model-types.ts! // +/** Descriptor for a registered middleware, returned by reflection API. */ +export const MiddlewareDescSchema = z.object({ + /** Unique name of the middleware. */ + name: z.string(), + /** Human-readable description of what the middleware does. */ + description: z.string().optional(), + /** JSON Schema for the middleware's configuration. */ + configSchema: z.record(z.any()).nullish(), + /** User defined metadata for the middleware. */ + metadata: z.record(z.any()).nullish(), +}); +export type MiddlewareDesc = z.infer; + +/** + * Zod schema of middleware reference. + */ +export const MiddlewareRefSchema = z.object({ + name: z.string(), + config: z.any().optional(), +}); + +/** + * Middleware reference. + */ +export type MiddlewareRef = z.infer; + +/** + * Zod schema of an opration representing a model reference. + */ +export const ModelReferenceSchema = z.object({ + name: z.string(), + configSchema: z.any().optional(), + info: z.any().optional(), + version: z.string().optional(), + config: z.any().optional(), +}); + +/** + * Model Reference + */ +export type ModelReference = z.infer; + /** - * Zod schema of an opration representing a background task. + * Zod schema of an operation representing a background task. */ export const OperationSchema = z.object({ action: z.string().optional(), @@ -81,6 +119,25 @@ export const OperationSchema = z.object({ */ export type OperationData = z.infer; +/** + * Zod schema of message part. + */ +export const PartSchema = z.union([ + TextPartSchema, + MediaPartSchema, + ToolRequestPartSchema, + ToolResponsePartSchema, + DataPartSchema, + CustomPartSchema, + ReasoningPartSchema, + ResourcePartSchema, +]); + +/** + * Message part. + */ +export type Part = z.infer; + /** * Zod schema of a message role. */ @@ -136,7 +193,7 @@ 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 operations. */ + /** Model can perform long-running operations. */ longRunning: z.boolean().optional(), }) .optional(), @@ -182,18 +239,65 @@ export const ToolDefinitionSchema = z.object({ */ export type ToolDefinition = z.infer; +/** + * Configuration parameter descriptions. + */ +export const GenerationCommonConfigDescriptions = { + temperature: + 'Controls the degree of randomness in token selection. A lower value is ' + + 'good for a more predictable response. A higher value leads to more ' + + 'diverse or unexpected results.', + maxOutputTokens: 'The maximum number of tokens to include in the response.', + topK: 'The maximum number of tokens to consider when sampling.', + topP: + 'Decides how many possible words to consider. A higher value means ' + + 'that the model looks at more possible words, even the less likely ' + + 'ones, which makes the generated text more diverse.', +}; + /** * Zod schema of a common config object. */ -export const GenerationCommonConfigSchema = z.object({ - /** A specific version of a model family, e.g. `gemini-1.0-pro-001` for the `gemini-1.0-pro` family. */ - version: z.string().optional(), - temperature: z.number().optional(), - maxOutputTokens: z.number().optional(), - topK: z.number().optional(), - topP: z.number().optional(), - stopSequences: z.array(z.string()).optional(), -}); +export const GenerationCommonConfigSchema = z + .object({ + version: z + .string() + .describe( + 'A specific version of a model family, e.g. `gemini-2.5-flash` ' + + 'for the `googleai` family.' + ) + .optional(), + temperature: z + .number() + .describe(GenerationCommonConfigDescriptions.temperature) + .optional(), + maxOutputTokens: z + .number() + .describe(GenerationCommonConfigDescriptions.maxOutputTokens) + .optional(), + topK: z + .number() + .describe(GenerationCommonConfigDescriptions.topK) + .optional(), + topP: z + .number() + .describe(GenerationCommonConfigDescriptions.topP) + .optional(), + stopSequences: z + .array(z.string()) + .max(5) + .describe( + 'Set of character sequences (up to 5) that will stop output generation.' + ) + .optional(), + apiKey: z + .string() + .describe( + 'API Key to use for the model call, overrides API key provided in plugin config.' + ) + .optional(), + }) + .passthrough(); /** * Common config object. @@ -379,6 +483,8 @@ export const GenerateActionOptionsSchema = z.object({ messages: z.array(MessageSchema), /** List of registered tool names for this generation if supported by the underlying model. */ tools: z.array(z.string()).optional(), + /** List of registered resource names for this generation if supported by the underlying model. */ + resources: z.array(z.string()).optional(), /** Tool calling mode. `auto` lets the model decide whether to use tools, `required` forces the model to choose a tool, and `none` forces the model not to use any tools. Defaults to `auto`. */ toolChoice: z.enum(['auto', 'required', 'none']).optional(), /** Configuration for the generation request. */ @@ -399,5 +505,7 @@ export const GenerateActionOptionsSchema = z.object({ maxTurns: z.number().optional(), /** Custom step name for this generate call to display in trace views. Defaults to "generate". */ stepName: z.string().optional(), + /** Middleware to apply to this generation. */ + use: z.array(MiddlewareRefSchema).optional(), }); export type GenerateActionOptions = z.infer; diff --git a/genkit-tools/common/src/types/parts.ts b/genkit-tools/common/src/types/parts.ts index eef420cc7b..1c23a905f0 100644 --- a/genkit-tools/common/src/types/parts.ts +++ b/genkit-tools/common/src/types/parts.ts @@ -32,7 +32,7 @@ const EmptyPartSchema = z.object({ * Zod schema for a text part. */ export const TextPartSchema = EmptyPartSchema.extend({ - /** The text of the document. */ + /** The text of the message. */ text: z.string(), }); @@ -89,6 +89,7 @@ export const ToolRequestSchema = z.object({ /** Whether the request is a partial chunk. */ partial: z.boolean().optional(), }); +export type ToolRequest = z.infer; /** * Zod schema of a tool request part. @@ -103,6 +104,9 @@ export const ToolRequestPartSchema = EmptyPartSchema.extend({ */ export type ToolRequestPart = z.infer; +/** + * Zod schema of a tool response. + */ const ToolResponseSchemaBase = z.object({ /** The call id or reference for a specific request. */ ref: z.string().optional(), @@ -116,14 +120,14 @@ const ToolResponseSchemaBase = z.object({ * Tool response part. */ export type ToolResponse = z.infer & { - content?: Part[]; + content?: TextOrMediaPart[]; }; export const ToolResponseSchema: z.ZodType = ToolResponseSchemaBase.extend({ content: z.array(z.any()).optional(), // TODO: switch to this once we have effective recursive schema support across the board. - // content: z.array(z.lazy(() => PartSchema)).optional(), + // content: z.array(z.lazy(() => DocumentPartSchema)).optional(), }); /** @@ -134,6 +138,9 @@ export const ToolResponsePartSchema = EmptyPartSchema.extend({ toolResponse: ToolResponseSchema, }); +/** + * Tool response part. + */ export type ToolResponsePart = z.infer; /** @@ -174,28 +181,17 @@ export const ResourcePartSchema = EmptyPartSchema.extend({ */ export type ResourcePart = z.infer; -/** - * Zod schema of message part. - */ -export const PartSchema = z.union([ - TextPartSchema, - MediaPartSchema, - ToolRequestPartSchema, - ToolResponsePartSchema, - DataPartSchema, - CustomPartSchema, - ReasoningPartSchema, - ResourcePartSchema, -]); - -/** - * Message part. - */ -export type Part = z.infer; +// Disclaimer: genkit/js/ai/parts.ts defines the following schema, type pair +// as PartSchema and Part, respectively. genkit-tools cannot retain those names +// due to it clashing with similar schema in model.ts, and genkit-tools +// exporting all types at root. We use a different name here and updated +// coresponding the imports. +export const TextOrMediaPartSchema = z.union([TextPartSchema, MediaPartSchema]); +export type TextOrMediaPart = z.infer; export const MultipartToolResponseSchema = z.object({ output: z.unknown().optional(), - content: z.array(PartSchema).optional(), + content: z.array(TextOrMediaPartSchema).optional(), }); export type MultipartToolResponse = z.infer; diff --git a/genkit-tools/common/src/types/reranker.ts b/genkit-tools/common/src/types/reranker.ts index 6fbe2bfc22..d9ca395350 100644 --- a/genkit-tools/common/src/types/reranker.ts +++ b/genkit-tools/common/src/types/reranker.ts @@ -22,7 +22,8 @@ * JSON schema) but until then this file must be manually kept in sync */ import { z } from 'zod'; -import { DocumentDataSchema, DocumentPartSchema } from './document'; +import { DocumentDataSchema } from './document'; +import { DocumentPartSchema } from './parts'; // // IMPORTANT: Keep this file in sync with genkit/ai/src/reranker.ts! diff --git a/genkit-tools/genkit-schema.json b/genkit-tools/genkit-schema.json index 26cc4fbf4f..85c310159f 100644 --- a/genkit-tools/genkit-schema.json +++ b/genkit-tools/genkit-schema.json @@ -20,16 +20,6 @@ ], "additionalProperties": false }, - "DocumentPart": { - "anyOf": [ - { - "$ref": "#/$defs/TextPart" - }, - { - "$ref": "#/$defs/MediaPart" - } - ] - }, "EmbedRequest": { "type": "object", "properties": { @@ -424,6 +414,12 @@ "type": "string" } }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, "toolChoice": { "type": "string", "enum": [ @@ -466,6 +462,12 @@ }, "stepName": { "type": "string" + }, + "use": { + "type": "array", + "items": { + "$ref": "#/$defs/MiddlewareRef" + } } }, "required": [ @@ -601,28 +603,39 @@ "type": "object", "properties": { "version": { - "type": "string" + "type": "string", + "description": "A specific version of a model family, e.g. `gemini-2.5-flash` for the `googleai` family." }, "temperature": { - "type": "number" + "type": "number", + "description": "Controls the degree of randomness in token selection. A lower value is good for a more predictable response. A higher value leads to more diverse or unexpected results." }, "maxOutputTokens": { - "type": "number" + "type": "number", + "description": "The maximum number of tokens to include in the response." }, "topK": { - "type": "number" + "type": "number", + "description": "The maximum number of tokens to consider when sampling." }, "topP": { - "type": "number" + "type": "number", + "description": "Decides how many possible words to consider. A higher value means that the model looks at more possible words, even the less likely ones, which makes the generated text more diverse." }, "stopSequences": { "type": "array", "items": { "type": "string" - } + }, + "maxItems": 5, + "description": "Set of character sequences (up to 5) that will stop output generation." + }, + "apiKey": { + "type": "string", + "description": "API Key to use for the model call, overrides API key provided in plugin config." } }, - "additionalProperties": false + "additionalProperties": true }, "GenerationUsage": { "type": "object", @@ -734,6 +747,56 @@ ], "additionalProperties": false }, + "MiddlewareDesc": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "configSchema": { + "anyOf": [ + { + "type": "object", + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "metadata": { + "anyOf": [ + { + "type": "object", + "additionalProperties": {} + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "MiddlewareRef": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "config": {} + }, + "required": [ + "name" + ], + "additionalProperties": false + }, "ModelInfo": { "type": "object", "properties": { @@ -810,6 +873,24 @@ }, "additionalProperties": false }, + "ModelReference": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "configSchema": {}, + "info": {}, + "version": { + "type": "string" + }, + "config": {} + }, + "required": [ + "name" + ], + "additionalProperties": false + }, "ModelRequest": { "type": "object", "properties": { @@ -904,7 +985,7 @@ "content": { "type": "array", "items": { - "$ref": "#/$defs/Part" + "$ref": "#/$defs/DocumentPart" } } }, @@ -1237,6 +1318,16 @@ ], "additionalProperties": false }, + "DocumentPart": { + "anyOf": [ + { + "$ref": "#/$defs/TextPart" + }, + { + "$ref": "#/$defs/MediaPart" + } + ] + }, "Media": { "type": "object", "properties": { diff --git a/js/ai/src/model-types.ts b/js/ai/src/model-types.ts index d76a555c9a..1bf3da805b 100644 --- a/js/ai/src/model-types.ts +++ b/js/ai/src/model-types.ts @@ -106,6 +106,8 @@ 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 can perform long-running operations. */ + longRunning: z.boolean().optional(), }) .optional(), /** At which stage of development this model is. diff --git a/js/ai/src/model.ts b/js/ai/src/model.ts index 442576e3d8..5ed77f3b8e 100644 --- a/js/ai/src/model.ts +++ b/js/ai/src/model.ts @@ -57,12 +57,18 @@ import { CustomPartSchema, DataPartSchema, MediaPartSchema, + MultipartToolResponseSchema, + ReasoningPartSchema, + ResourcePartSchema, TextPartSchema, ToolRequestPartSchema, ToolResponsePartSchema, type CustomPart, type DataPart, type MediaPart, + type MultipartToolResponse, + type ReasoningPart, + type ResourcePart, type TextPart, type ToolRequestPart, type ToolResponsePart, @@ -73,6 +79,9 @@ export { CustomPartSchema, DataPartSchema, MediaPartSchema, + MultipartToolResponseSchema, + ReasoningPartSchema, + ResourcePartSchema, TextPartSchema, ToolRequestPartSchema, ToolResponsePartSchema, @@ -80,6 +89,9 @@ export { type CustomPart, type DataPart, type MediaPart, + type MultipartToolResponse, + type ReasoningPart, + type ResourcePart, type TextPart, type ToolRequestPart, type ToolResponsePart, diff --git a/js/ai/src/parts.ts b/js/ai/src/parts.ts index 3a81cd9660..53fe2f086f 100644 --- a/js/ai/src/parts.ts +++ b/js/ai/src/parts.ts @@ -36,6 +36,11 @@ export const TextPartSchema = EmptyPartSchema.extend({ text: z.string(), }); +/** + * Text part. + */ +export type TextPart = z.infer; + /** * Zod schema for a reasoning part. */ @@ -45,9 +50,9 @@ export const ReasoningPartSchema = EmptyPartSchema.extend({ }); /** - * Text part. + * Reasoning part. */ -export type TextPart = z.infer; +export type ReasoningPart = z.infer; /** * Zod schema of media. diff --git a/js/plugins/google-genai/src/googleai/veo.ts b/js/plugins/google-genai/src/googleai/veo.ts index 80f81cb3cc..e563541ab8 100644 --- a/js/plugins/google-genai/src/googleai/veo.ts +++ b/js/plugins/google-genai/src/googleai/veo.ts @@ -102,18 +102,16 @@ function commonRef( return modelRef({ name: `googleai/${name}`, configSchema, - info: - info ?? - ({ - supports: { - media: true, - multiturn: false, - tools: false, - systemRole: false, - output: ['media'], - longRunning: true, - }, - } as ModelInfo), // TODO(ifielker): Remove this cast if we fix longRunning + info: info ?? { + supports: { + media: true, + multiturn: false, + tools: false, + systemRole: false, + output: ['media'], + longRunning: true, + }, + }, }); } diff --git a/js/plugins/google-genai/src/vertexai/veo.ts b/js/plugins/google-genai/src/vertexai/veo.ts index 969da00c69..c3a7537491 100644 --- a/js/plugins/google-genai/src/vertexai/veo.ts +++ b/js/plugins/google-genai/src/vertexai/veo.ts @@ -129,18 +129,16 @@ function commonRef( return modelRef({ name: `vertexai/${name}`, configSchema, - info: - info ?? - ({ - supports: { - media: true, - multiturn: false, - tools: false, - systemRole: false, - output: ['media'], - longRunning: true, - }, - } as ModelInfo), // TODO(ifielker): Remove this cast if we fix longRunning + info: info ?? { + supports: { + media: true, + multiturn: false, + tools: false, + systemRole: false, + output: ['media'], + longRunning: true, + }, + }, }); } diff --git a/py/packages/genkit/src/genkit/ai/_aio.py b/py/packages/genkit/src/genkit/ai/_aio.py index 7feaf06738..fb8e6ee65a 100644 --- a/py/packages/genkit/src/genkit/ai/_aio.py +++ b/py/packages/genkit/src/genkit/ai/_aio.py @@ -153,7 +153,7 @@ async def generate( return_tool_requests: bool | None = None, tool_choice: ToolChoice | None = None, tool_responses: list[Part] | None = None, - config: GenerationCommonConfig | dict[str, object] | None = None, + config: dict[str, object] | GenerationCommonConfig | None = None, max_turns: int | None = None, on_chunk: ModelStreamingCallback | None = None, context: dict[str, object] | None = None, @@ -178,7 +178,7 @@ async def generate( return_tool_requests: bool | None = None, tool_choice: ToolChoice | None = None, tool_responses: list[Part] | None = None, - config: GenerationCommonConfig | dict[str, object] | None = None, + config: dict[str, object] | GenerationCommonConfig | None = None, max_turns: int | None = None, on_chunk: ModelStreamingCallback | None = None, context: dict[str, object] | None = None, @@ -201,7 +201,7 @@ async def generate( return_tool_requests: bool | None = None, tool_choice: ToolChoice | None = None, tool_responses: list[Part] | None = None, - config: GenerationCommonConfig | dict[str, object] | None = None, + config: dict[str, object] | GenerationCommonConfig | None = None, max_turns: int | None = None, on_chunk: ModelStreamingCallback | None = None, context: dict[str, object] | None = None, @@ -358,7 +358,7 @@ def generate_stream( tools: list[str] | None = None, return_tool_requests: bool | None = None, tool_choice: ToolChoice | None = None, - config: GenerationCommonConfig | dict[str, object] | None = None, + config: dict[str, object] | GenerationCommonConfig | None = None, max_turns: int | None = None, context: dict[str, object] | None = None, output_format: str | None = None, @@ -385,7 +385,7 @@ def generate_stream( tools: list[str] | None = None, return_tool_requests: bool | None = None, tool_choice: ToolChoice | None = None, - config: GenerationCommonConfig | dict[str, object] | None = None, + config: dict[str, object] | GenerationCommonConfig | None = None, max_turns: int | None = None, context: dict[str, object] | None = None, output_format: str | None = None, @@ -410,7 +410,7 @@ def generate_stream( tools: list[str] | None = None, return_tool_requests: bool | None = None, tool_choice: ToolChoice | None = None, - config: GenerationCommonConfig | dict[str, object] | None = None, + config: dict[str, object] | GenerationCommonConfig | None = None, max_turns: int | None = None, context: dict[str, object] | None = None, output_format: str | None = None, @@ -1003,7 +1003,7 @@ async def generate_operation( tools: list[str] | None = None, return_tool_requests: bool | None = None, tool_choice: ToolChoice | None = None, - config: GenerationCommonConfig | dict[str, object] | None = None, + config: dict[str, object] | GenerationCommonConfig | None = None, max_turns: int | None = None, context: dict[str, object] | None = None, output_format: str | None = None, diff --git a/py/packages/genkit/src/genkit/ai/_registry.py b/py/packages/genkit/src/genkit/ai/_registry.py index 12f6f57d1a..d5cc80bc54 100644 --- a/py/packages/genkit/src/genkit/ai/_registry.py +++ b/py/packages/genkit/src/genkit/ai/_registry.py @@ -1186,7 +1186,7 @@ def define_prompt( name: str | None = None, variant: str | None = None, model: str | None = None, - config: GenerationCommonConfig | dict[str, object] | None = None, + config: dict[str, object] | GenerationCommonConfig | None = None, description: str | None = None, input_schema: type | dict[str, object] | str | None = None, system: str | Part | list[Part] | Callable[..., Any] | None = None, @@ -1216,7 +1216,7 @@ def define_prompt( name: str | None = None, variant: str | None = None, model: str | None = None, - config: GenerationCommonConfig | dict[str, object] | None = None, + config: dict[str, object] | GenerationCommonConfig | None = None, description: str | None = None, input_schema: type | dict[str, object] | str | None = None, system: str | Part | list[Part] | Callable[..., Any] | None = None, @@ -1246,7 +1246,7 @@ def define_prompt( name: str | None = None, variant: str | None = None, model: str | None = None, - config: GenerationCommonConfig | dict[str, object] | None = None, + config: dict[str, object] | GenerationCommonConfig | None = None, description: str | None = None, input_schema: type | dict[str, object] | str | None = None, system: str | Part | list[Part] | Callable[..., Any] | None = None, @@ -1276,7 +1276,7 @@ def define_prompt( name: str | None = None, variant: str | None = None, model: str | None = None, - config: GenerationCommonConfig | dict[str, object] | None = None, + config: dict[str, object] | GenerationCommonConfig | None = None, description: str | None = None, input_schema: type | dict[str, object] | str | None = None, system: str | Part | list[Part] | Callable[..., Any] | None = None, @@ -1303,7 +1303,7 @@ def define_prompt( name: str | None = None, variant: str | None = None, model: str | None = None, - config: GenerationCommonConfig | dict[str, object] | None = None, + config: dict[str, object] | GenerationCommonConfig | None = None, description: str | None = None, input_schema: type | dict[str, object] | str | None = None, system: str | Part | list[Part] | Callable[..., Any] | None = None, diff --git a/py/packages/genkit/src/genkit/blocks/prompt.py b/py/packages/genkit/src/genkit/blocks/prompt.py index 4fd19a3d8e..838580c73c 100644 --- a/py/packages/genkit/src/genkit/blocks/prompt.py +++ b/py/packages/genkit/src/genkit/blocks/prompt.py @@ -394,7 +394,7 @@ def on_chunk(chunk): """ model: str | None - config: GenerationCommonConfig | dict[str, Any] | None + config: dict[str, Any] | GenerationCommonConfig | None messages: list[Message] | None docs: list[DocumentData] | None tools: list[str] | None @@ -554,7 +554,7 @@ class PromptConfig(BaseModel): variant: str | None = None model: str | None = None - config: GenerationCommonConfig | dict[str, Any] | None = None + config: dict[str, Any] | GenerationCommonConfig | None = None description: str | None = None input_schema: type | dict[str, Any] | str | None = None system: str | Part | list[Part] | Callable[..., Any] | None = None @@ -677,7 +677,7 @@ def __init__( registry: Registry, variant: str | None = None, model: str | None = None, - config: GenerationCommonConfig | dict[str, Any] | None = None, + config: dict[str, Any] | GenerationCommonConfig | None = None, description: str | None = None, input_schema: type | dict[str, Any] | str | None = None, system: str | Part | list[Part] | Callable[..., Any] | None = None, @@ -731,7 +731,7 @@ def __init__( self._registry: Registry = registry self._variant: str | None = variant self._model: str | None = model - self._config: GenerationCommonConfig | dict[str, Any] | None = config + self._config: dict[str, Any] | GenerationCommonConfig | None = config self._description: str | None = description self._input_schema: type | dict[str, Any] | str | None = input_schema self._system: str | Part | list[Part] | Callable[..., Any] | None = system @@ -1185,7 +1185,7 @@ def define_prompt( name: str | None = None, variant: str | None = None, model: str | None = None, - config: GenerationCommonConfig | dict[str, Any] | None = None, + config: dict[str, Any] | GenerationCommonConfig | None = None, description: str | None = None, input_schema: type | dict[str, Any] | str | None = None, system: str | Part | list[Part] | Callable[..., Any] | None = None, @@ -1216,7 +1216,7 @@ def define_prompt( name: str | None = None, variant: str | None = None, model: str | None = None, - config: GenerationCommonConfig | dict[str, Any] | None = None, + config: dict[str, Any] | GenerationCommonConfig | None = None, description: str | None = None, input_schema: type | dict[str, Any] | str | None = None, system: str | Part | list[Part] | Callable[..., Any] | None = None, @@ -1247,7 +1247,7 @@ def define_prompt( name: str | None = None, variant: str | None = None, model: str | None = None, - config: GenerationCommonConfig | dict[str, Any] | None = None, + config: dict[str, Any] | GenerationCommonConfig | None = None, description: str | None = None, input_schema: type | dict[str, Any] | str | None = None, system: str | Part | list[Part] | Callable[..., Any] | None = None, @@ -1278,7 +1278,7 @@ def define_prompt( name: str | None = None, variant: str | None = None, model: str | None = None, - config: GenerationCommonConfig | dict[str, Any] | None = None, + config: dict[str, Any] | GenerationCommonConfig | None = None, description: str | None = None, input_schema: type | dict[str, Any] | str | None = None, system: str | Part | list[Part] | Callable[..., Any] | None = None, @@ -1307,7 +1307,7 @@ def define_prompt( name: str | None = None, variant: str | None = None, model: str | None = None, - config: GenerationCommonConfig | dict[str, Any] | None = None, + config: dict[str, Any] | GenerationCommonConfig | None = None, description: str | None = None, input_schema: type | dict[str, Any] | str | None = None, system: str | Part | list[Part] | Callable[..., Any] | None = None, diff --git a/py/packages/genkit/src/genkit/codec.py b/py/packages/genkit/src/genkit/codec.py index 9bbc8063d3..70d69849a8 100644 --- a/py/packages/genkit/src/genkit/codec.py +++ b/py/packages/genkit/src/genkit/codec.py @@ -150,4 +150,5 @@ def dump_json( if isinstance(obj, BaseModel): return obj.model_dump_json(by_alias=True, exclude_none=True, indent=indent, fallback=fallback) else: - return json.dumps(obj, indent=indent, default=fallback or default_serializer) + separators = (',', ':') if indent is None else None + return json.dumps(obj, indent=indent, default=fallback or default_serializer, separators=separators) diff --git a/py/packages/genkit/src/genkit/core/typing.py b/py/packages/genkit/src/genkit/core/typing.py index 02f5927450..757594b4cd 100644 --- a/py/packages/genkit/src/genkit/core/typing.py +++ b/py/packages/genkit/src/genkit/core/typing.py @@ -187,13 +187,29 @@ class GenerateActionOutputConfig(BaseModel): class GenerationCommonConfig(BaseModel): """Model for generationcommonconfig data.""" - model_config: ClassVar[ConfigDict] = ConfigDict(alias_generator=to_camel, extra='forbid', populate_by_name=True) - version: str | None = None - temperature: float | None = None - max_output_tokens: float | None = Field(default=None) - top_k: float | None = Field(default=None) - top_p: float | None = Field(default=None) - stop_sequences: list[str] | None = Field(default=None) + model_config: ClassVar[ConfigDict] = ConfigDict(alias_generator=to_camel, extra='allow', populate_by_name=True) + version: str | None = Field( + default=None, + description='A specific version of a model family, e.g. `gemini-2.5-flash` for the `googleai` family.', + ) + temperature: float | None = Field( + default=None, + description='Controls the degree of randomness in token selection. A lower value is good for a more predictable response. A higher value leads to more diverse or unexpected results.', + ) + max_output_tokens: float | None = Field( + default=None, description='The maximum number of tokens to include in the response.' + ) + top_k: float | None = Field(default=None, description='The maximum number of tokens to consider when sampling.') + top_p: float | None = Field( + default=None, + description='Decides how many possible words to consider. A higher value means that the model looks at more possible words, even the less likely ones, which makes the generated text more diverse.', + ) + stop_sequences: list[str] | None = Field( + default=None, description='Set of character sequences (up to 5) that will stop output generation.', max_length=5 + ) + api_key: str | None = Field( + default=None, description='API Key to use for the model call, overrides API key provided in plugin config.' + ) class GenerationUsage(BaseModel): @@ -216,6 +232,24 @@ class GenerationUsage(BaseModel): cached_content_tokens: float | None = Field(default=None) +class MiddlewareDesc(BaseModel): + """Model for middlewaredesc data.""" + + model_config: ClassVar[ConfigDict] = ConfigDict(alias_generator=to_camel, extra='forbid', populate_by_name=True) + name: str + description: str | None = None + config_schema: dict[str, Any] | None = Field(default=None) + metadata: dict[str, Any] | None = None + + +class MiddlewareRef(BaseModel): + """Model for middlewareref data.""" + + model_config: ClassVar[ConfigDict] = ConfigDict(alias_generator=to_camel, extra='forbid', populate_by_name=True) + name: str + config: Any | None = None + + class Constrained(StrEnum): """Constrained data type class.""" @@ -261,6 +295,17 @@ class ModelInfo(BaseModel): stage: Stage | None = None +class ModelReference(BaseModel): + """Model for modelreference data.""" + + model_config: ClassVar[ConfigDict] = ConfigDict(alias_generator=to_camel, extra='forbid', populate_by_name=True) + name: str + config_schema: Any | None = Field(default=None) + info: Any | None = None + version: str | None = None + config: Any | None = None + + class Error(BaseModel): """Model for error data.""" @@ -756,6 +801,27 @@ class ToolResponsePart(BaseModel): resource: Resource | None = None +class DocumentPart(RootModel[TextPart | MediaPart]): + """Root model for documentpart.""" + + root: TextPart | MediaPart + + +class RankedDocumentData(BaseModel): + """Model for rankeddocumentdata data.""" + + model_config: ClassVar[ConfigDict] = ConfigDict(alias_generator=to_camel, extra='forbid', populate_by_name=True) + content: list[DocumentPart] + metadata: RankedDocumentMetadata + + +class RerankerResponse(BaseModel): + """Model for rerankerresponse data.""" + + model_config: ClassVar[ConfigDict] = ConfigDict(alias_generator=to_camel, extra='forbid', populate_by_name=True) + documents: list[RankedDocumentData] + + class Link(BaseModel): """Model for link data.""" @@ -835,10 +901,20 @@ class TraceEvent(RootModel[SpanStartEvent | SpanEndEvent]): root: SpanStartEvent | SpanEndEvent -class DocumentPart(RootModel[TextPart | MediaPart]): - """Root model for documentpart.""" +class DocumentData(BaseModel): + """Model for documentdata data.""" - root: TextPart | MediaPart + model_config: ClassVar[ConfigDict] = ConfigDict(alias_generator=to_camel, extra='forbid', populate_by_name=True) + content: list[DocumentPart] + metadata: dict[str, Any] | None = None + + +class EmbedRequest(BaseModel): + """Model for embedrequest data.""" + + model_config: ClassVar[ConfigDict] = ConfigDict(alias_generator=to_camel, extra='forbid', populate_by_name=True) + input: list[DocumentData] + options: Any | None = None class Resume(BaseModel): @@ -850,6 +926,14 @@ class Resume(BaseModel): metadata: dict[str, Any] | None = None +class MultipartToolResponse(BaseModel): + """Model for multiparttoolresponse data.""" + + model_config: ClassVar[ConfigDict] = ConfigDict(alias_generator=to_camel, extra='forbid', populate_by_name=True) + output: Any | None = None + content: list[DocumentPart] | None = None + + class Part( RootModel[ TextPart | MediaPart | ToolRequestPart | ToolResponsePart | DataPart | CustomPart | ReasoningPart | ResourcePart @@ -862,41 +946,40 @@ class Part( ) -class RankedDocumentData(BaseModel): - """Model for rankeddocumentdata data.""" +class RerankerRequest(BaseModel): + """Model for rerankerrequest data.""" model_config: ClassVar[ConfigDict] = ConfigDict(alias_generator=to_camel, extra='forbid', populate_by_name=True) - content: list[DocumentPart] - metadata: RankedDocumentMetadata + query: DocumentData + documents: list[DocumentData] + options: Any | None = None -class RerankerResponse(BaseModel): - """Model for rerankerresponse data.""" +class RetrieverRequest(BaseModel): + """Model for retrieverrequest data.""" model_config: ClassVar[ConfigDict] = ConfigDict(alias_generator=to_camel, extra='forbid', populate_by_name=True) - documents: list[RankedDocumentData] + query: DocumentData + options: Any | None = None -class Content(RootModel[list[Part]]): - """Root model for content.""" +class RetrieverResponse(BaseModel): + """Model for retrieverresponse data.""" - root: list[Part] + model_config: ClassVar[ConfigDict] = ConfigDict(alias_generator=to_camel, extra='forbid', populate_by_name=True) + documents: list[DocumentData] -class DocumentData(BaseModel): - """Model for documentdata data.""" +class Docs(RootModel[list[DocumentData]]): + """Root model for docs.""" - model_config: ClassVar[ConfigDict] = ConfigDict(alias_generator=to_camel, extra='forbid', populate_by_name=True) - content: list[DocumentPart] - metadata: dict[str, Any] | None = None + root: list[DocumentData] -class EmbedRequest(BaseModel): - """Model for embedrequest data.""" +class Content(RootModel[list[Part]]): + """Root model for content.""" - model_config: ClassVar[ConfigDict] = ConfigDict(alias_generator=to_camel, extra='forbid', populate_by_name=True) - input: list[DocumentData] - options: Any | None = None + root: list[Part] class GenerateResponseChunk(BaseModel): @@ -930,44 +1013,6 @@ class ModelResponseChunk(BaseModel): aggregated: Aggregated | None = None -class MultipartToolResponse(BaseModel): - """Model for multiparttoolresponse data.""" - - model_config: ClassVar[ConfigDict] = ConfigDict(alias_generator=to_camel, extra='forbid', populate_by_name=True) - output: Any | None = None - content: list[Part] | None = None - - -class RerankerRequest(BaseModel): - """Model for rerankerrequest data.""" - - model_config: ClassVar[ConfigDict] = ConfigDict(alias_generator=to_camel, extra='forbid', populate_by_name=True) - query: DocumentData - documents: list[DocumentData] - options: Any | None = None - - -class RetrieverRequest(BaseModel): - """Model for retrieverrequest data.""" - - model_config: ClassVar[ConfigDict] = ConfigDict(alias_generator=to_camel, extra='forbid', populate_by_name=True) - query: DocumentData - options: Any | None = None - - -class RetrieverResponse(BaseModel): - """Model for retrieverresponse data.""" - - model_config: ClassVar[ConfigDict] = ConfigDict(alias_generator=to_camel, extra='forbid', populate_by_name=True) - documents: list[DocumentData] - - -class Docs(RootModel[list[DocumentData]]): - """Root model for docs.""" - - root: list[DocumentData] - - class Messages(RootModel[list[Message]]): """Root model for messages.""" @@ -1002,6 +1047,7 @@ class GenerateActionOptions(BaseModel): return_tool_requests: bool | None = Field(default=None) max_turns: float | None = Field(default=None) step_name: str | None = Field(default=None) + use: list[MiddlewareRef] | None = None class GenerateRequest(BaseModel): diff --git a/py/packages/genkit/src/genkit/model_types.py b/py/packages/genkit/src/genkit/model_types.py index 874ba12479..2bf9cad3b3 100644 --- a/py/packages/genkit/src/genkit/model_types.py +++ b/py/packages/genkit/src/genkit/model_types.py @@ -38,7 +38,7 @@ class GenerationCommonConfig(CoreGenerationCommonConfig): ) -def get_request_api_key(config: GenerationCommonConfig | Mapping[str, object] | object | None) -> str | None: +def get_request_api_key(config: Mapping[str, object] | GenerationCommonConfig | object | None) -> str | None: """Extract a request-scoped API key from config. Supports both typed config objects and dict payloads with either snake_case @@ -66,7 +66,7 @@ def get_request_api_key(config: GenerationCommonConfig | Mapping[str, object] | def get_effective_api_key( - config: GenerationCommonConfig | Mapping[str, object] | object | None, + config: Mapping[str, object] | GenerationCommonConfig | object | None, plugin_api_key: str | None, ) -> str | None: """Resolve effective API key using request-over-plugin precedence.""" diff --git a/py/packages/genkit/tests/genkit/blocks/prompt_test.py b/py/packages/genkit/tests/genkit/blocks/prompt_test.py index 25434bed2a..01b5e54538 100644 --- a/py/packages/genkit/tests/genkit/blocks/prompt_test.py +++ b/py/packages/genkit/tests/genkit/blocks/prompt_test.py @@ -64,7 +64,7 @@ async def test_simple_prompt() -> None: """Test simple prompt rendering.""" ai, *_ = setup_test() - want_txt = '[ECHO] user: "hi" {"temperature":11.0}' + want_txt = '[ECHO] user: "hi" {"temperature":11}' my_prompt = ai.define_prompt(prompt='hi', config={'temperature': 11}) @@ -87,7 +87,7 @@ async def test_simple_prompt_with_override_config() -> None: ai, *_ = setup_test() # Config is MERGED: prompt config (banana: true) + opts config (temperature: 12) - want_txt = '[ECHO] user: "hi" {"banana": true, "temperature": 12}' + want_txt = '[ECHO] user: "hi" {"banana":true,"temperature":12}' my_prompt = ai.define_prompt(prompt='hi', config={'banana': True}) @@ -236,7 +236,7 @@ async def docs_resolver(input: dict[str, Any], context: object) -> list[Document GenerationCommonConfig.model_validate({'temperature': 11}), {}, # Config is MERGED: prompt config (banana: ripe) + opts config (temperature: 11) - """[ECHO] system: "hello foo (bar)" {"banana": "ripe", "temperature": 11.0}""", + """[ECHO] system: "hello foo (bar)" {"banana":"ripe","temperature":11.0}""", ), ( 'renders user prompt', @@ -256,7 +256,7 @@ async def docs_resolver(input: dict[str, Any], context: object) -> list[Document GenerationCommonConfig.model_validate({'temperature': 11}), {}, # Config is MERGED: prompt config (banana: ripe) + opts config (temperature: 11) - """[ECHO] user: "hello foo (bar_system)" {"banana": "ripe", "temperature": 11.0}""", + """[ECHO] user: "hello foo (bar_system)" {"banana":"ripe","temperature":11.0}""", ), ( 'renders user prompt with context', @@ -276,7 +276,7 @@ async def docs_resolver(input: dict[str, Any], context: object) -> list[Document GenerationCommonConfig.model_validate({'temperature': 11}), {'auth': {'email': 'a@b.c'}}, # Config is MERGED: prompt config (banana: ripe) + opts config (temperature: 11) - """[ECHO] user: "hello foo (bar, a@b.c)" {"banana": "ripe", "temperature": 11.0}""", + """[ECHO] user: "hello foo (bar, a@b.c)" {"banana":"ripe","temperature":11.0}""", ), ] diff --git a/py/packages/genkit/tests/genkit/codec_test.py b/py/packages/genkit/tests/genkit/codec_test.py index f4a407fd93..97e61f870e 100644 --- a/py/packages/genkit/tests/genkit/codec_test.py +++ b/py/packages/genkit/tests/genkit/codec_test.py @@ -24,13 +24,13 @@ def test_dump_json_basic() -> None: """Test basic JSON serialization.""" # Test dictionary - assert dump_json({'a': 1, 'b': 'test'}) == '{"a": 1, "b": "test"}' + assert dump_json({'a': 1, 'b': 'test'}) == '{"a":1,"b":"test"}' # Test list - assert dump_json([1, 2, 3]) == '[1, 2, 3]' + assert dump_json([1, 2, 3]) == '[1,2,3]' # Test nested structures - assert dump_json({'a': [1, 2], 'b': {'c': 3}}) == '{"a": [1, 2], "b": {"c": 3}}' + assert dump_json({'a': [1, 2], 'b': {'c': 3}}) == '{"a":[1,2],"b":{"c":3}}' def test_dump_json_special_types() -> None: diff --git a/py/packages/genkit/tests/genkit/core/action_test.py b/py/packages/genkit/tests/genkit/core/action_test.py index 6476a69593..406857be7c 100644 --- a/py/packages/genkit/tests/genkit/core/action_test.py +++ b/py/packages/genkit/tests/genkit/core/action_test.py @@ -275,8 +275,8 @@ async def baz() -> str: first = baz_action.arun(context={'foo': 'bar'}) second = baz_action.arun(context={'bar': 'baz'}) - assert (await second).response == '{"bar": "baz"}' - assert (await first).response == '{"foo": "bar"}' + assert (await second).response == '{"bar":"baz"}' + assert (await first).response == '{"foo":"bar"}' @pytest.mark.asyncio diff --git a/py/packages/genkit/tests/genkit/veneer/veneer_test.py b/py/packages/genkit/tests/genkit/veneer/veneer_test.py index 8d7ee00e3e..ef06e03502 100644 --- a/py/packages/genkit/tests/genkit/veneer/veneer_test.py +++ b/py/packages/genkit/tests/genkit/veneer/veneer_test.py @@ -73,7 +73,7 @@ async def test_generate_uses_default_model(setup_test: SetupFixture) -> None: """Test that the generate function uses the default model.""" ai, *_ = setup_test - want_txt = '[ECHO] user: "hi" {"temperature":11.0}' + want_txt = '[ECHO] user: "hi" {"temperature":11}' response = await ai.generate(prompt='hi', config={'temperature': 11}) @@ -123,11 +123,11 @@ async def test_generate_with_explicit_model(setup_test: SetupFixture) -> None: response = await ai.generate(model='echoModel', prompt='hi', config={'temperature': 11}) - assert response.text == '[ECHO] user: "hi" {"temperature":11.0}' + assert response.text == '[ECHO] user: "hi" {"temperature":11}' _, response = ai.generate_stream(model='echoModel', prompt='hi', config={'temperature': 11}) - assert (await response).text == '[ECHO] user: "hi" {"temperature":11.0}' + assert (await response).text == '[ECHO] user: "hi" {"temperature":11}' @pytest.mark.asyncio @@ -137,7 +137,7 @@ async def test_generate_with_str_prompt(setup_test: SetupFixture) -> None: response = await ai.generate(prompt='hi', config={'temperature': 11}) - assert response.text == '[ECHO] user: "hi" {"temperature":11.0}' + assert response.text == '[ECHO] user: "hi" {"temperature":11}' @pytest.mark.asyncio @@ -145,7 +145,7 @@ async def test_generate_with_part_prompt(setup_test: SetupFixture) -> None: """Test that the generate function with a part prompt works.""" ai, *_ = setup_test - want_txt = '[ECHO] user: "hi" {"temperature":11.0}' + want_txt = '[ECHO] user: "hi" {"temperature":11}' response = await ai.generate(prompt=Part(root=TextPart(text='hi')), config={'temperature': 11}) @@ -161,7 +161,7 @@ async def test_generate_with_part_list_prompt(setup_test: SetupFixture) -> None: """Test that the generate function with a list of parts prompt works.""" ai, *_ = setup_test - want_txt = '[ECHO] user: "hello","world" {"temperature":11.0}' + want_txt = '[ECHO] user: "hello","world" {"temperature":11}' response = await ai.generate( prompt=[Part(root=TextPart(text='hello')), Part(root=TextPart(text='world'))], @@ -183,7 +183,7 @@ async def test_generate_with_str_system(setup_test: SetupFixture) -> None: """Test that the generate function with a string system works.""" ai, *_ = setup_test - want_txt = '[ECHO] system: "talk like pirate" user: "hi" {"temperature":11.0}' + want_txt = '[ECHO] system: "talk like pirate" user: "hi" {"temperature":11}' response = await ai.generate(system='talk like pirate', prompt='hi', config={'temperature': 11}) @@ -199,7 +199,7 @@ async def test_generate_with_part_system(setup_test: SetupFixture) -> None: """Test that the generate function with a part system works.""" ai, *_ = setup_test - want_txt = '[ECHO] system: "talk like pirate" user: "hi" {"temperature":11.0}' + want_txt = '[ECHO] system: "talk like pirate" user: "hi" {"temperature":11}' response = await ai.generate( system=Part(root=TextPart(text='talk like pirate')), @@ -223,7 +223,7 @@ async def test_generate_with_part_list_system(setup_test: SetupFixture) -> None: """Test that the generate function with a list of parts system works.""" ai, *_ = setup_test - want_txt = '[ECHO] system: "talk","like pirate" user: "hi" {"temperature":11.0}' + want_txt = '[ECHO] system: "talk","like pirate" user: "hi" {"temperature":11}' response = await ai.generate( system=[Part(root=TextPart(text='talk')), Part(root=TextPart(text='like pirate'))], @@ -257,7 +257,7 @@ async def test_generate_with_messages(setup_test: SetupFixture) -> None: config={'temperature': 11}, ) - assert response.text == '[ECHO] user: "hi" {"temperature":11.0}' + assert response.text == '[ECHO] user: "hi" {"temperature":11}' _, response = ai.generate_stream( messages=[ @@ -269,7 +269,7 @@ async def test_generate_with_messages(setup_test: SetupFixture) -> None: config={'temperature': 11}, ) - assert (await response).text == '[ECHO] user: "hi" {"temperature":11.0}' + assert (await response).text == '[ECHO] user: "hi" {"temperature":11}' @pytest.mark.asyncio diff --git a/py/plugins/flask/tests/flask_test.py b/py/plugins/flask/tests/flask_test.py index 5bf38d53f0..99f0fd841b 100644 --- a/py/plugins/flask/tests/flask_test.py +++ b/py/plugins/flask/tests/flask_test.py @@ -81,8 +81,8 @@ def test_streaming() -> None: chunks.append(chunk) assert chunks == [ - b'data: {"message": 1}\n\n', - b'data: {"message": {"username": "Pavel"}}\n\n', - b'data: {"message": {"foo": "bar"}}\n\n', - b'data: {"result": {"bar": "baz"}}\n\n', + b'data: {"message":1}\n\n', + b'data: {"message":{"username":"Pavel"}}\n\n', + b'data: {"message":{"foo":"bar"}}\n\n', + b'data: {"result":{"bar":"baz"}}\n\n', ]