UNPKG

@genkit-ai/core

Version:

Genkit AI framework core libraries.

345 lines (322 loc) 10.5 kB
/** * 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 express from 'express'; import z from 'zod'; import { runWithStreamingCallback, Status, StatusCodes } from './action.js'; import { config } from './config.js'; import { logger } from './logging.js'; import * as registry from './registry.js'; import { toJsonSchema } from './schema.js'; import { cleanUpTracing, flushTracing, newTrace, setCustomMetadataAttribute, } from './tracing.js'; export const RunActionResponseSchema = z.object({ result: z.unknown().optional(), error: z.unknown().optional(), telemetry: z .object({ traceId: z.string().optional(), }) .optional(), }); export type RunActionResponse = z.infer<typeof RunActionResponseSchema>; let server; const GLOBAL_REFLECTION_API_PORT_KEY = 'genkit__reflectionApiPort'; /** * Starts a Reflection API that will be used by the Runner to call and control actions and flows. * @param port port on which to listen */ export async function startReflectionApi(port?: number | undefined) { if (global[GLOBAL_REFLECTION_API_PORT_KEY] !== undefined) { logger.warn( `Reflection API is already running on port ${global[GLOBAL_REFLECTION_API_PORT_KEY]}` ); return; } if (!port) { port = Number(process.env.GENKIT_REFLECTION_PORT) || 3100; } global[GLOBAL_REFLECTION_API_PORT_KEY] = port; const api = express(); api.use(express.json({ limit: '30mb' })); api.get('/api/__health', async (_, response) => { await registry.listActions(); response.status(200).send('OK'); }); api.get('/api/__quitquitquit', async (_, response) => { logger.debug('Received quitquitquit'); response.status(200).send('OK'); await stopReflectionApi(); }); api.get('/api/actions', async (_, response, next) => { logger.debug('Fetching actions.'); const actions = await registry.listActions(); const convertedActions = {}; Object.keys(actions).forEach((key) => { const action = actions[key].__action; convertedActions[key] = { key, name: action.name, description: action.description, metadata: action.metadata, }; if (action.inputSchema || action.inputJsonSchema) { convertedActions[key].inputSchema = toJsonSchema({ schema: action.inputSchema, jsonSchema: action.inputJsonSchema, }); } if (action.outputSchema || action.outputJsonSchema) { convertedActions[key].outputSchema = toJsonSchema({ schema: action.outputSchema, jsonSchema: action.outputJsonSchema, }); } }); // TODO: Remove try/catch when upgrading to Express 5; error is sent to `next` automatically // in that version try { response.send(convertedActions); } catch (err) { const { message, stack } = err as Error; next({ message, stack }); } }); api.post('/api/runAction', async (request, response, next) => { const { key, input } = request.body; const { stream } = request.query; logger.debug(`Running action \`${key}\`...`); let traceId; try { const action = await registry.lookupAction(key); if (!action) { response.status(404).send(`action ${key} not found`); return; } if (stream === 'true') { const result = await newTrace( { name: 'dev-run-action-wrapper' }, async (_, span) => { setCustomMetadataAttribute('genkit-dev-internal', 'true'); traceId = span.spanContext().traceId; return await runWithStreamingCallback( (chunk) => { response.write(JSON.stringify(chunk) + '\n'); }, async () => await action(input) ); } ); await flushTracing(); response.write( JSON.stringify({ result, telemetry: traceId ? { traceId, } : undefined, } as RunActionResponse) ); response.end(); } else { const result = await newTrace( { name: 'dev-run-action-wrapper' }, async (_, span) => { setCustomMetadataAttribute('genkit-dev-internal', 'true'); traceId = span.spanContext().traceId; return await action(input); } ); response.send({ result, telemetry: traceId ? { traceId, } : undefined, } as RunActionResponse); } } catch (err) { const { message, stack } = err as Error; next({ message, stack, traceId }); } }); api.get('/api/envs', async (_, response) => { response.json(config.configuredEnvs); }); api.get('/api/envs/:env/traces/:traceId', async (request, response) => { const { env, traceId } = request.params; logger.debug(`Fetching trace \`${traceId}\` for env \`${env}\`.`); const tracestore = await registry.lookupTraceStore(env); if (!tracestore) { return response.status(500).send({ code: StatusCodes.FAILED_PRECONDITION, message: `${env} trace store not found`, }); } // TODO: Remove try/catch when upgrading to Express 5; error is sent to `next` automatically // in that version try { const trace = await tracestore?.load(traceId); return trace ? response.json(trace) : response.status(404).send({ code: StatusCodes.NOT_FOUND, message: `Trace with traceId=${traceId} not found.`, }); } catch (err) { const error = err as Error; const { message, stack } = error; const errorResponse: Status = { code: StatusCodes.INTERNAL, message, details: { stack, }, }; return response.status(500).json(errorResponse); } }); api.get('/api/envs/:env/traces', async (request, response, next) => { const { env } = request.params; const { limit, continuationToken } = request.query; logger.debug(`Fetching traces for env \`${env}\`.`); const tracestore = await registry.lookupTraceStore(env); if (!tracestore) { return response.status(500).send({ code: StatusCodes.FAILED_PRECONDITION, message: `${env} trace store not found`, }); } // TODO: Remove try/catch when upgrading to Express 5; error is sent to `next` automatically // in that version try { response.json( await tracestore.list({ limit: limit ? parseInt(limit.toString()) : undefined, continuationToken: continuationToken ? continuationToken.toString() : undefined, }) ); } catch (err) { const { message, stack } = err as Error; next({ message, stack }); } }); api.get( '/api/envs/:env/flowStates/:flowId', async (request, response, next) => { const { env, flowId } = request.params; logger.debug(`Fetching flow state \`${flowId}\` for env \`${env}\`.`); const flowStateStore = await registry.lookupFlowStateStore(env); if (!flowStateStore) { return response.status(500).send({ code: StatusCodes.FAILED_PRECONDITION, message: `${env} flow state store not found`, }); } // TODO: Remove try/catch when upgrading to Express 5; error is sent to `next` automatically // in that version try { response.json(await flowStateStore?.load(flowId)); } catch (err) { const { message, stack } = err as Error; next({ message, stack }); } } ); api.get('/api/envs/:env/flowStates', async (request, response, next) => { const { env } = request.params; const { limit, continuationToken } = request.query; logger.debug(`Fetching traces for env \`${env}\`.`); const flowStateStore = await registry.lookupFlowStateStore(env); if (!flowStateStore) { return response.status(500).send({ code: StatusCodes.FAILED_PRECONDITION, message: `${env} flow state store not found`, }); } // TODO: Remove try/catch when upgrading to Express 5; error is sent to `next` automatically // in that version try { response.json( await flowStateStore?.list({ limit: limit ? parseInt(limit.toString()) : undefined, continuationToken: continuationToken ? continuationToken.toString() : undefined, }) ); } catch (err) { const { message, stack } = err as Error; next({ message, stack }); } }); api.use((err, req, res, next) => { logger.error(err.stack); const error = err as Error; const { message, stack } = error; const errorResponse: Status = { code: StatusCodes.INTERNAL, message, details: { stack, }, }; if (err.traceId) { errorResponse.details.traceId = err.traceId; } res.status(500).json(errorResponse); }); server = api.listen(port, () => { console.log(`Reflection API running on http://localhost:${port}`); }); server.on('error', (error) => { if (process.env.GENKIT_REFLECTION_ON_STARTUP_FAILURE === 'ignore') { logger.warn( `Failed to start the Reflection API on port ${port}, ignoring the error.` ); logger.debug(error); } else { throw error; } }); process.on('SIGTERM', async () => await stopReflectionApi()); } /** * Stops Reflection API and any running dependencies. */ async function stopReflectionApi() { await Promise.all([ new Promise<void>((resolve) => { if (server) { server.close(() => { logger.info('Reflection API has succesfully shut down.'); resolve(); }); } else { resolve(); } }), cleanUpTracing(), ]); process.exit(0); }