Skip to content

Commit e580e7a

Browse files
committed
feat: resolve named schemas in prompt Picoschema references
Pass `defineSchema`-registered schemas to Picoschema conversion when loading `.prompt` files, so input and output schemas can reference named schemas by name (e.g. `schema: MyAddress`). Without this, prompts using named schema references could not be resolved.
1 parent 7501d8f commit e580e7a

2 files changed

Lines changed: 65 additions & 8 deletions

File tree

packages/genkit/lib/src/ai/prompt_loader_io.dart

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,20 +137,31 @@ void _loadPrompt(
137137
final config = metadata.config;
138138
final tools = metadata.tools;
139139

140+
// Named schemas registered via `defineSchema`. Picoschema may reference these
141+
// by name (e.g. `schema: MyAddress`), so they are passed through to the
142+
// converter to resolve, mirroring what `_resolveMetadata` does internally.
143+
// `listValues` keys are registry paths (`/schema/<name>`); Picoschema looks
144+
// schemas up by bare name, so strip the prefix.
145+
final schemas = {
146+
for (final entry
147+
in registry.listValues<Map<String, dynamic>>('schema').entries)
148+
entry.key.split('/').last: entry.value,
149+
};
150+
140151
// Build the input schema from the frontmatter `input.schema`. The raw
141152
// metadata from `parse` is not schema-resolved, so Picoschema is converted
142153
// to JSON Schema here (mirroring what `renderMetadata` does internally).
143154
// Without this the action has no input schema, so the Developer UI cannot
144155
// render an input form for the prompt.
145-
final inputSchema = _toInputSchema(metadata.input?.schema);
156+
final inputSchema = _toInputSchema(metadata.input?.schema, schemas);
146157

147158
// Build output config from parsed metadata. As with the input schema, the
148159
// output schema may be Picoschema and must be converted to JSON Schema
149160
// before it reaches the model, otherwise the raw Picoschema is sent as the
150161
// response schema and the request fails or is ignored.
151162
GenerateActionOutputConfig? outputConfig;
152163
if (metadata.output != null) {
153-
final outputSchema = _toJsonSchema(metadata.output!.schema);
164+
final outputSchema = _toJsonSchema(metadata.output!.schema, schemas);
154165
outputConfig = GenerateActionOutputConfig.fromJson({
155166
'format': ?metadata.output!.format,
156167
'jsonSchema': ?outputSchema,
@@ -210,10 +221,16 @@ String _registryDefinitionKey(String name, String? variant, String? ns) {
210221
/// not recognize the `type, description` form (e.g. `name: string, the person
211222
/// to greet`) that the docs and examples use. [_isJsonSchema] is used instead
212223
/// so that form is converted rather than passed through raw.
213-
Map<String, dynamic>? _toJsonSchema(Map<String, dynamic>? schema) {
224+
///
225+
/// [schemas] holds named schemas registered via `defineSchema`, so Picoschema
226+
/// references to them by name can be resolved during conversion.
227+
Map<String, dynamic>? _toJsonSchema(
228+
Map<String, dynamic>? schema,
229+
Map<String, Map<String, dynamic>> schemas,
230+
) {
214231
if (schema == null) return null;
215232
if (_isJsonSchema(schema)) return schema;
216-
return Picoschema.toJsonSchema(schema);
233+
return Picoschema.toJsonSchema(schema, schemas: schemas);
217234
}
218235

219236
/// Whether [schema] is already a JSON Schema (as opposed to Picoschema).
@@ -255,10 +272,14 @@ bool _isJsonSchema(Map<String, dynamic> schema) {
255272
///
256273
/// Returns `null` when no input schema is declared, in which case the prompt
257274
/// accepts free-form input as before.
275+
///
276+
/// [schemas] holds named schemas registered via `defineSchema`, so Picoschema
277+
/// references to them by name can be resolved during conversion.
258278
SchemanticType<Map<String, dynamic>>? _toInputSchema(
259279
Map<String, dynamic>? schema,
280+
Map<String, Map<String, dynamic>> schemas,
260281
) {
261-
final jsonSchema = _toJsonSchema(schema);
282+
final jsonSchema = _toJsonSchema(schema, schemas);
262283
if (jsonSchema == null) return null;
263284
return SchemanticType.from<Map<String, dynamic>>(
264285
jsonSchema: jsonSchema,

packages/genkit/test/ai/prompt_test.dart

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1324,7 +1324,8 @@ Hello {{name}}!
13241324
expect(
13251325
action.inputSchema,
13261326
isNotNull,
1327-
reason: 'input.schema in the .prompt file should be surfaced so the '
1327+
reason:
1328+
'input.schema in the .prompt file should be surfaced so the '
13281329
'Developer UI can render a form and input can be validated',
13291330
);
13301331

@@ -1439,14 +1440,49 @@ Classify: {{text}}
14391440

14401441
// The output schema reaching the model must be JSON Schema, not the raw
14411442
// Picoschema strings.
1442-
final outputSchema =
1443-
options.output!.jsonSchema as Map<String, dynamic>;
1443+
final outputSchema = options.output!.jsonSchema as Map<String, dynamic>;
14441444
expect(outputSchema['type'], equals('object'));
14451445
final properties = outputSchema['properties'] as Map<String, dynamic>;
14461446
expect(properties['category'], containsPair('type', 'string'));
14471447
expect(properties['confidence'], containsPair('type', 'number'));
14481448
expect(options.output!.format, equals('json'));
14491449
});
1450+
1451+
test('resolves a named schema referenced in input.schema', () async {
1452+
// A schema registered via `defineSchema` is referenced by name in the
1453+
// .prompt frontmatter. The loader must pass registered schemas to the
1454+
// Picoschema converter so the reference resolves to the real schema.
1455+
registry.registerValue('schema', 'Address', {
1456+
'type': 'object',
1457+
'properties': {
1458+
'street': {'type': 'string'},
1459+
'city': {'type': 'string'},
1460+
},
1461+
'required': ['street', 'city'],
1462+
});
1463+
1464+
File(p.join(tempDir.path, 'shipto.prompt')).writeAsStringSync('''
1465+
---
1466+
input:
1467+
schema:
1468+
address: Address
1469+
---
1470+
Ship to {{address.city}}.
1471+
''');
1472+
1473+
loadPromptFolder(registry, dpRegistry, dir: tempDir.path);
1474+
1475+
final action =
1476+
await registry.lookupAction('executable-prompt', 'shipto')
1477+
as PromptAction;
1478+
final jsonSchema = action.inputSchema!.jsonSchema();
1479+
final addressSchema = (jsonSchema['properties'] as Map)['address'] as Map;
1480+
// If the named reference were not resolved, `address` would not be an
1481+
// object schema with the registered properties.
1482+
expect(addressSchema['type'], equals('object'));
1483+
expect(addressSchema['properties'], contains('street'));
1484+
expect(addressSchema['properties'], contains('city'));
1485+
});
14501486
});
14511487

14521488
group('DotpromptRegistry', () {

0 commit comments

Comments
 (0)