UNPKG

@genkit-ai/core

Version:

Genkit AI framework core libraries.

304 lines (269 loc) 8.23 kB
/** * Copyright 2025 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 * as assert from 'assert'; import { beforeEach, describe, it } from 'node:test'; import { z } from 'zod'; import { defineBidiAction } from '../src/action.js'; import { initNodeFeatures } from '../src/node.js'; import { Registry } from '../src/registry.js'; initNodeFeatures(); describe('bidi action', () => { var registry: Registry; beforeEach(() => { registry = new Registry(); }); it('streamBidi ergonomic (push)', async () => { const act = defineBidiAction( registry, { name: 'chat', actionType: 'custom', outputSchema: z.string(), inputSchema: z.string(), // Option 1: messages are strings }, async function* ({ inputStream }) { for await (const chunk of inputStream) { yield `echo ${chunk}`; } return 'done'; } ); const session = act.streamBidi(); session.send('1'); session.send('2'); session.close(); const chunks: string[] = []; for await (const chunk of session.stream) { chunks.push(chunk); } assert.deepStrictEqual(chunks, ['echo 1', 'echo 2']); assert.strictEqual(await session.output, 'done'); }); it('streamBidi pull (generator)', async () => { const act = defineBidiAction( registry, { name: 'chat', actionType: 'custom', outputSchema: z.string(), inputSchema: z.string(), }, async function* ({ inputStream }) { for await (const chunk of inputStream) { yield `echo ${chunk}`; } return 'done'; } ); async function* inputGen() { yield '1'; yield '2'; } const session = act.streamBidi(undefined, { inputStream: inputGen() }); // Pass stream in options! const chunks: string[] = []; for await (const chunk of session.stream) { chunks.push(chunk); } assert.deepStrictEqual(chunks, ['echo 1', 'echo 2']); assert.strictEqual(await session.output, 'done'); }); it('classic run works on bidi action', async () => { const act = defineBidiAction( registry, { name: 'chat', actionType: 'custom', outputSchema: z.string(), inputSchema: z.string(), }, async function* ({ inputStream }) { const inputs: string[] = []; for await (const chunk of inputStream) { inputs.push(chunk); } return `done: ${inputs.join(', ')}`; } ); const result = await act.run('1'); // Pass single message as input! assert.strictEqual(result.result, 'done: 1'); }); it('classic run works on bidi action with streaming', async () => { const act = defineBidiAction( registry, { name: 'chat', actionType: 'custom', outputSchema: z.string(), inputSchema: z.string(), }, async function* ({ inputStream }) { for await (const chunk of inputStream) { yield `echo ${chunk}`; } return 'done'; } ); const chunks: string[] = []; const result = await act.run('1', { onChunk: (c) => chunks.push(c), }); assert.deepStrictEqual(chunks, ['echo 1']); assert.strictEqual(result.result, 'done'); }); it('bidi action receives init data', async () => { const act = defineBidiAction( registry, { name: 'chatWithInit', actionType: 'custom', outputSchema: z.string(), inputSchema: z.string(), initSchema: z.object({ prefix: z.string() }), }, async function* ({ inputStream, init }) { const prefix = init?.prefix || ''; for await (const chunk of inputStream) { yield `${prefix}${chunk}`; } return 'done'; } ); const session = act.streamBidi({ prefix: '>> ' }); // Pass init as first argument! session.send('1'); session.send('2'); session.close(); const chunks: string[] = []; for await (const chunk of session.stream) { chunks.push(chunk); } assert.deepStrictEqual(chunks, ['>> 1', '>> 2']); assert.strictEqual(await session.output, 'done'); }); it('classic run works on bidi action with init', async () => { const act = defineBidiAction( registry, { name: 'chatWithInitRun', actionType: 'custom', outputSchema: z.string(), inputSchema: z.string(), initSchema: z.object({ prefix: z.string() }), }, async function* ({ inputStream, init }) { const prefix = init?.prefix || ''; const inputs: string[] = []; for await (const chunk of inputStream) { inputs.push(chunk); } return `${prefix}${inputs.join(', ')}`; } ); const result = await act.run('1', { init: { prefix: '>> ' } }); assert.strictEqual(result.result, '>> 1'); }); it('classic stream works on bidi action with init', async () => { const act = defineBidiAction( registry, { name: 'chatWithInitStream', actionType: 'custom', outputSchema: z.string(), inputSchema: z.string(), initSchema: z.object({ prefix: z.string() }), }, async function* ({ inputStream, init }) { const prefix = init?.prefix || ''; for await (const chunk of inputStream) { yield `${prefix}${chunk}`; } return 'done'; } ); const session = act.stream('1', { init: { prefix: '>> ' } }); const chunks: string[] = []; for await (const chunk of session.stream) { chunks.push(chunk); } assert.deepStrictEqual(chunks, ['>> 1']); assert.strictEqual(await session.output, 'done'); }); it('bidi action generator is closed on sendChunk error', async () => { let generatorClosed = false; const act = defineBidiAction( registry, { name: 'chatCleanup', actionType: 'custom', outputSchema: z.string(), inputSchema: z.string(), }, async function* ({ inputStream }) { try { for await (const chunk of inputStream) { yield `echo ${chunk}`; } } finally { generatorClosed = true; } return 'done'; } ); try { await act.run('1', { onChunk: (c) => { throw new Error('abort'); }, }); assert.fail('Expected error to be thrown'); } catch (e: any) { assert.strictEqual(e.message, 'abort'); } assert.strictEqual(generatorClosed, true, 'Generator should be closed'); }); it('bidi action generator is closed on early break by consumer', async () => { let generatorClosed = false; const act = defineBidiAction( registry, { name: 'chatCleanupBreak', actionType: 'custom', outputSchema: z.string(), inputSchema: z.string(), }, async function* ({ inputStream }) { try { for await (const chunk of inputStream) { yield `echo ${chunk}`; } } finally { generatorClosed = true; } return 'done'; } ); const session = act.streamBidi(); session.send('1'); session.send('2'); // Read only one chunk and break for await (const chunk of session.stream) { assert.strictEqual(chunk, 'echo 1'); break; // Stop reading early } // Give the stream some time to propagate the close. await new Promise((resolve) => setTimeout(resolve, 10)); assert.strictEqual(generatorClosed, true, 'Generator should be closed'); }); });