@genkit-ai/express 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -44,6 +44,12 @@ const streamDelimiter = "\n\n";
44
44
  function expressHandler(action, opts) {
45
45
  return async (request, response) => {
46
46
  const { stream } = request.query;
47
+ if (!request.body) {
48
+ const errMsg = `Error: request.body is undefined. Possible reasons: missing 'content-type: application/json' in request headers or misconfigured JSON middleware ('app.use(express.json()')? `;
49
+ import_logging.logger.error(errMsg);
50
+ response.status(400).json({ message: errMsg, status: "INVALID ARGUMENT" }).end();
51
+ return;
52
+ }
47
53
  let input = request.body.data;
48
54
  let context;
49
55
  try {
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport bodyParser from 'body-parser';\nimport cors, { CorsOptions } from 'cors';\nimport express from 'express';\nimport {\n Action,\n ActionContext,\n Flow,\n runWithStreamingCallback,\n z,\n} from 'genkit';\nimport {\n ContextProvider,\n RequestData,\n getCallableJSON,\n getHttpStatus,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport { Server } from 'http';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n opts?: {\n contextProvider?: ContextProvider<C, I>;\n }\n): express.RequestHandler {\n return async (\n request: express.Request,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n let input = request.body.data as z.infer<I>;\n let context: Record<string, any>;\n\n try {\n context =\n (await opts?.contextProvider?.({\n method: request.method as RequestData['method'],\n headers: Object.fromEntries(\n Object.entries(request.headers).map(([key, value]) => [\n key.toLowerCase(),\n Array.isArray(value) ? value.join(' ') : String(value),\n ])\n ),\n input,\n })) || {};\n } catch (e: any) {\n logger.error(\n `Auth policy failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n return;\n }\n\n if (request.get('Accept') === 'text/event-stream' || stream === 'true') {\n response.writeHead(200, {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n });\n try {\n const onChunk = (chunk: z.infer<S>) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n const result = await runWithStreamingCallback(\n action.__registry,\n onChunk,\n () =>\n action.run(input, {\n onChunk,\n context,\n })\n );\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n } catch (e) {\n logger.error(\n `Streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.write(\n `error: ${JSON.stringify({ error: getCallableJSON(e) })}${streamDelimiter}`\n );\n response.end();\n }\n } else {\n try {\n const result = await action.run(input, { context });\n response.setHeader('x-genkit-trace-id', result.telemetry.traceId);\n response.setHeader('x-genkit-span-id', result.telemetry.spanId);\n // Responses for non-streaming flows are passed back with the flow result stored in a field called \"result.\"\n response\n .status(200)\n .json({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n logger.error(\n `Non-streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n }\n }\n };\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n */\nexport type FlowWithContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n context: ContextProvider<C, I>;\n};\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n context: ContextProvider<C, I>\n): FlowWithContextProvider<C, I, O, S> {\n return {\n flow,\n context,\n };\n}\n\n/**\n * Options to configure the flow server.\n */\nexport interface FlowServerOptions {\n /** List of flows to expose via the flow server. */\n flows: (Flow<any, any, any> | FlowWithContextProvider<any, any, any>)[];\n /** Port to run the server on. Defaults to env.PORT or 3400. */\n port?: number;\n /** CORS options for the server. */\n cors?: CorsOptions;\n /** HTTP method path prefix for the exposed flows. */\n pathPrefix?: string;\n /** JSON body parser options. */\n jsonParserOptions?: bodyParser.OptionsJson;\n}\n\n/**\n * Starts an express server with the provided flows and options.\n */\nexport function startFlowServer(options: FlowServerOptions): FlowServer {\n const server = new FlowServer(options);\n server.start();\n return server;\n}\n\n/**\n * Flow server exposes registered flows as HTTP endpoints.\n *\n * This is for use in production environments.\n *\n * @hidden\n */\nexport class FlowServer {\n /** List of all running servers needed to be cleaned up on process exit. */\n private static RUNNING_SERVERS: FlowServer[] = [];\n\n /** Options for the flow server configured by the developer. */\n private options: FlowServerOptions;\n /** Port the server is actually running on. This may differ from `options.port` if the original was occupied. Null is server is not running. */\n private port: number | null = null;\n /** Express server instance. Null if server is not running. */\n private server: Server | null = null;\n\n constructor(options: FlowServerOptions) {\n this.options = {\n ...options,\n };\n }\n\n /**\n * Starts the server and adds it to the list of running servers to clean up on exit.\n */\n async start() {\n const server = express();\n\n server.use(bodyParser.json(this.options.jsonParserOptions));\n server.use(cors(this.options.cors));\n\n logger.debug('Running flow server with flow paths:');\n const pathPrefix = this.options.pathPrefix ?? '';\n this.options.flows?.forEach((flow) => {\n if ('context' in flow) {\n const flowPath = `/${pathPrefix}${flow.flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(\n flowPath,\n expressHandler(flow.flow, { contextProvider: flow.context })\n );\n } else {\n const flowPath = `/${pathPrefix}${flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(flow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":"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","names":["express","bodyParser","cors"]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport bodyParser from 'body-parser';\nimport cors, { CorsOptions } from 'cors';\nimport express from 'express';\nimport {\n Action,\n ActionContext,\n Flow,\n runWithStreamingCallback,\n z,\n} from 'genkit';\nimport {\n ContextProvider,\n RequestData,\n getCallableJSON,\n getHttpStatus,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport { Server } from 'http';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n opts?: {\n contextProvider?: ContextProvider<C, I>;\n }\n): express.RequestHandler {\n return async (\n request: express.Request,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n if (!request.body) {\n const errMsg =\n `Error: request.body is undefined. ` +\n `Possible reasons: missing 'content-type: application/json' in request ` +\n `headers or misconfigured JSON middleware ('app.use(express.json()')? `;\n logger.error(errMsg);\n response\n .status(400)\n .json({ message: errMsg, status: 'INVALID ARGUMENT' })\n .end();\n return;\n }\n\n let input = request.body.data as z.infer<I>;\n let context: Record<string, any>;\n\n try {\n context =\n (await opts?.contextProvider?.({\n method: request.method as RequestData['method'],\n headers: Object.fromEntries(\n Object.entries(request.headers).map(([key, value]) => [\n key.toLowerCase(),\n Array.isArray(value) ? value.join(' ') : String(value),\n ])\n ),\n input,\n })) || {};\n } catch (e: any) {\n logger.error(\n `Auth policy failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n return;\n }\n\n if (request.get('Accept') === 'text/event-stream' || stream === 'true') {\n response.writeHead(200, {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n });\n try {\n const onChunk = (chunk: z.infer<S>) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n const result = await runWithStreamingCallback(\n action.__registry,\n onChunk,\n () =>\n action.run(input, {\n onChunk,\n context,\n })\n );\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n } catch (e) {\n logger.error(\n `Streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.write(\n `error: ${JSON.stringify({ error: getCallableJSON(e) })}${streamDelimiter}`\n );\n response.end();\n }\n } else {\n try {\n const result = await action.run(input, { context });\n response.setHeader('x-genkit-trace-id', result.telemetry.traceId);\n response.setHeader('x-genkit-span-id', result.telemetry.spanId);\n // Responses for non-streaming flows are passed back with the flow result stored in a field called \"result.\"\n response\n .status(200)\n .json({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n logger.error(\n `Non-streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n }\n }\n };\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n */\nexport type FlowWithContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n context: ContextProvider<C, I>;\n};\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n context: ContextProvider<C, I>\n): FlowWithContextProvider<C, I, O, S> {\n return {\n flow,\n context,\n };\n}\n\n/**\n * Options to configure the flow server.\n */\nexport interface FlowServerOptions {\n /** List of flows to expose via the flow server. */\n flows: (Flow<any, any, any> | FlowWithContextProvider<any, any, any>)[];\n /** Port to run the server on. Defaults to env.PORT or 3400. */\n port?: number;\n /** CORS options for the server. */\n cors?: CorsOptions;\n /** HTTP method path prefix for the exposed flows. */\n pathPrefix?: string;\n /** JSON body parser options. */\n jsonParserOptions?: bodyParser.OptionsJson;\n}\n\n/**\n * Starts an express server with the provided flows and options.\n */\nexport function startFlowServer(options: FlowServerOptions): FlowServer {\n const server = new FlowServer(options);\n server.start();\n return server;\n}\n\n/**\n * Flow server exposes registered flows as HTTP endpoints.\n *\n * This is for use in production environments.\n *\n * @hidden\n */\nexport class FlowServer {\n /** List of all running servers needed to be cleaned up on process exit. */\n private static RUNNING_SERVERS: FlowServer[] = [];\n\n /** Options for the flow server configured by the developer. */\n private options: FlowServerOptions;\n /** Port the server is actually running on. This may differ from `options.port` if the original was occupied. Null is server is not running. */\n private port: number | null = null;\n /** Express server instance. Null if server is not running. */\n private server: Server | null = null;\n\n constructor(options: FlowServerOptions) {\n this.options = {\n ...options,\n };\n }\n\n /**\n * Starts the server and adds it to the list of running servers to clean up on exit.\n */\n async start() {\n const server = express();\n\n server.use(bodyParser.json(this.options.jsonParserOptions));\n server.use(cors(this.options.cors));\n\n logger.debug('Running flow server with flow paths:');\n const pathPrefix = this.options.pathPrefix ?? '';\n this.options.flows?.forEach((flow) => {\n if ('context' in flow) {\n const flowPath = `/${pathPrefix}${flow.flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(\n flowPath,\n expressHandler(flow.flow, { contextProvider: flow.context })\n );\n } else {\n const flowPath = `/${pathPrefix}${flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(flow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":"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","names":["express","bodyParser","cors"]}
package/lib/index.mjs CHANGED
@@ -13,6 +13,12 @@ const streamDelimiter = "\n\n";
13
13
  function expressHandler(action, opts) {
14
14
  return async (request, response) => {
15
15
  const { stream } = request.query;
16
+ if (!request.body) {
17
+ const errMsg = `Error: request.body is undefined. Possible reasons: missing 'content-type: application/json' in request headers or misconfigured JSON middleware ('app.use(express.json()')? `;
18
+ logger.error(errMsg);
19
+ response.status(400).json({ message: errMsg, status: "INVALID ARGUMENT" }).end();
20
+ return;
21
+ }
16
22
  let input = request.body.data;
17
23
  let context;
18
24
  try {
package/lib/index.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport bodyParser from 'body-parser';\nimport cors, { CorsOptions } from 'cors';\nimport express from 'express';\nimport {\n Action,\n ActionContext,\n Flow,\n runWithStreamingCallback,\n z,\n} from 'genkit';\nimport {\n ContextProvider,\n RequestData,\n getCallableJSON,\n getHttpStatus,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport { Server } from 'http';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n opts?: {\n contextProvider?: ContextProvider<C, I>;\n }\n): express.RequestHandler {\n return async (\n request: express.Request,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n let input = request.body.data as z.infer<I>;\n let context: Record<string, any>;\n\n try {\n context =\n (await opts?.contextProvider?.({\n method: request.method as RequestData['method'],\n headers: Object.fromEntries(\n Object.entries(request.headers).map(([key, value]) => [\n key.toLowerCase(),\n Array.isArray(value) ? value.join(' ') : String(value),\n ])\n ),\n input,\n })) || {};\n } catch (e: any) {\n logger.error(\n `Auth policy failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n return;\n }\n\n if (request.get('Accept') === 'text/event-stream' || stream === 'true') {\n response.writeHead(200, {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n });\n try {\n const onChunk = (chunk: z.infer<S>) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n const result = await runWithStreamingCallback(\n action.__registry,\n onChunk,\n () =>\n action.run(input, {\n onChunk,\n context,\n })\n );\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n } catch (e) {\n logger.error(\n `Streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.write(\n `error: ${JSON.stringify({ error: getCallableJSON(e) })}${streamDelimiter}`\n );\n response.end();\n }\n } else {\n try {\n const result = await action.run(input, { context });\n response.setHeader('x-genkit-trace-id', result.telemetry.traceId);\n response.setHeader('x-genkit-span-id', result.telemetry.spanId);\n // Responses for non-streaming flows are passed back with the flow result stored in a field called \"result.\"\n response\n .status(200)\n .json({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n logger.error(\n `Non-streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n }\n }\n };\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n */\nexport type FlowWithContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n context: ContextProvider<C, I>;\n};\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n context: ContextProvider<C, I>\n): FlowWithContextProvider<C, I, O, S> {\n return {\n flow,\n context,\n };\n}\n\n/**\n * Options to configure the flow server.\n */\nexport interface FlowServerOptions {\n /** List of flows to expose via the flow server. */\n flows: (Flow<any, any, any> | FlowWithContextProvider<any, any, any>)[];\n /** Port to run the server on. Defaults to env.PORT or 3400. */\n port?: number;\n /** CORS options for the server. */\n cors?: CorsOptions;\n /** HTTP method path prefix for the exposed flows. */\n pathPrefix?: string;\n /** JSON body parser options. */\n jsonParserOptions?: bodyParser.OptionsJson;\n}\n\n/**\n * Starts an express server with the provided flows and options.\n */\nexport function startFlowServer(options: FlowServerOptions): FlowServer {\n const server = new FlowServer(options);\n server.start();\n return server;\n}\n\n/**\n * Flow server exposes registered flows as HTTP endpoints.\n *\n * This is for use in production environments.\n *\n * @hidden\n */\nexport class FlowServer {\n /** List of all running servers needed to be cleaned up on process exit. */\n private static RUNNING_SERVERS: FlowServer[] = [];\n\n /** Options for the flow server configured by the developer. */\n private options: FlowServerOptions;\n /** Port the server is actually running on. This may differ from `options.port` if the original was occupied. Null is server is not running. */\n private port: number | null = null;\n /** Express server instance. Null if server is not running. */\n private server: Server | null = null;\n\n constructor(options: FlowServerOptions) {\n this.options = {\n ...options,\n };\n }\n\n /**\n * Starts the server and adds it to the list of running servers to clean up on exit.\n */\n async start() {\n const server = express();\n\n server.use(bodyParser.json(this.options.jsonParserOptions));\n server.use(cors(this.options.cors));\n\n logger.debug('Running flow server with flow paths:');\n const pathPrefix = this.options.pathPrefix ?? '';\n this.options.flows?.forEach((flow) => {\n if ('context' in flow) {\n const flowPath = `/${pathPrefix}${flow.flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(\n flowPath,\n expressHandler(flow.flow, { contextProvider: flow.context })\n );\n } else {\n const flowPath = `/${pathPrefix}${flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(flow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport bodyParser from 'body-parser';\nimport cors, { CorsOptions } from 'cors';\nimport express from 'express';\nimport {\n Action,\n ActionContext,\n Flow,\n runWithStreamingCallback,\n z,\n} from 'genkit';\nimport {\n ContextProvider,\n RequestData,\n getCallableJSON,\n getHttpStatus,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport { Server } from 'http';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n opts?: {\n contextProvider?: ContextProvider<C, I>;\n }\n): express.RequestHandler {\n return async (\n request: express.Request,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n if (!request.body) {\n const errMsg =\n `Error: request.body is undefined. ` +\n `Possible reasons: missing 'content-type: application/json' in request ` +\n `headers or misconfigured JSON middleware ('app.use(express.json()')? `;\n logger.error(errMsg);\n response\n .status(400)\n .json({ message: errMsg, status: 'INVALID ARGUMENT' })\n .end();\n return;\n }\n\n let input = request.body.data as z.infer<I>;\n let context: Record<string, any>;\n\n try {\n context =\n (await opts?.contextProvider?.({\n method: request.method as RequestData['method'],\n headers: Object.fromEntries(\n Object.entries(request.headers).map(([key, value]) => [\n key.toLowerCase(),\n Array.isArray(value) ? value.join(' ') : String(value),\n ])\n ),\n input,\n })) || {};\n } catch (e: any) {\n logger.error(\n `Auth policy failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n return;\n }\n\n if (request.get('Accept') === 'text/event-stream' || stream === 'true') {\n response.writeHead(200, {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n });\n try {\n const onChunk = (chunk: z.infer<S>) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n const result = await runWithStreamingCallback(\n action.__registry,\n onChunk,\n () =>\n action.run(input, {\n onChunk,\n context,\n })\n );\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n } catch (e) {\n logger.error(\n `Streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.write(\n `error: ${JSON.stringify({ error: getCallableJSON(e) })}${streamDelimiter}`\n );\n response.end();\n }\n } else {\n try {\n const result = await action.run(input, { context });\n response.setHeader('x-genkit-trace-id', result.telemetry.traceId);\n response.setHeader('x-genkit-span-id', result.telemetry.spanId);\n // Responses for non-streaming flows are passed back with the flow result stored in a field called \"result.\"\n response\n .status(200)\n .json({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n logger.error(\n `Non-streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n }\n }\n };\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n */\nexport type FlowWithContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n context: ContextProvider<C, I>;\n};\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n context: ContextProvider<C, I>\n): FlowWithContextProvider<C, I, O, S> {\n return {\n flow,\n context,\n };\n}\n\n/**\n * Options to configure the flow server.\n */\nexport interface FlowServerOptions {\n /** List of flows to expose via the flow server. */\n flows: (Flow<any, any, any> | FlowWithContextProvider<any, any, any>)[];\n /** Port to run the server on. Defaults to env.PORT or 3400. */\n port?: number;\n /** CORS options for the server. */\n cors?: CorsOptions;\n /** HTTP method path prefix for the exposed flows. */\n pathPrefix?: string;\n /** JSON body parser options. */\n jsonParserOptions?: bodyParser.OptionsJson;\n}\n\n/**\n * Starts an express server with the provided flows and options.\n */\nexport function startFlowServer(options: FlowServerOptions): FlowServer {\n const server = new FlowServer(options);\n server.start();\n return server;\n}\n\n/**\n * Flow server exposes registered flows as HTTP endpoints.\n *\n * This is for use in production environments.\n *\n * @hidden\n */\nexport class FlowServer {\n /** List of all running servers needed to be cleaned up on process exit. */\n private static RUNNING_SERVERS: FlowServer[] = [];\n\n /** Options for the flow server configured by the developer. */\n private options: FlowServerOptions;\n /** Port the server is actually running on. This may differ from `options.port` if the original was occupied. Null is server is not running. */\n private port: number | null = null;\n /** Express server instance. Null if server is not running. */\n private server: Server | null = null;\n\n constructor(options: FlowServerOptions) {\n this.options = {\n ...options,\n };\n }\n\n /**\n * Starts the server and adds it to the list of running servers to clean up on exit.\n */\n async start() {\n const server = express();\n\n server.use(bodyParser.json(this.options.jsonParserOptions));\n server.use(cors(this.options.cors));\n\n logger.debug('Running flow server with flow paths:');\n const pathPrefix = this.options.pathPrefix ?? '';\n this.options.flows?.forEach((flow) => {\n if ('context' in flow) {\n const flowPath = `/${pathPrefix}${flow.flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(\n flowPath,\n expressHandler(flow.flow, { contextProvider: flow.context })\n );\n } else {\n const flowPath = `/${pathPrefix}${flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(flow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":"AAgBA,OAAO,gBAAgB;AACvB,OAAO,UAA2B;AAClC,OAAO,aAAa;AACpB;AAAA,EAIE;AAAA,OAEK;AACP;AAAA,EAGE;AAAA,EACA;AAAA,OACK;AACP,SAAS,cAAc;AAGvB,MAAM,kBAAkB;AAKjB,SAAS,eAMd,QACA,MAGwB;AACxB,SAAO,OACL,SACA,aACkB;AAClB,UAAM,EAAE,OAAO,IAAI,QAAQ;AAC3B,QAAI,CAAC,QAAQ,MAAM;AACjB,YAAM,SACJ;AAGF,aAAO,MAAM,MAAM;AACnB,eACG,OAAO,GAAG,EACV,KAAK,EAAE,SAAS,QAAQ,QAAQ,mBAAmB,CAAC,EACpD,IAAI;AACP;AAAA,IACF;AAEA,QAAI,QAAQ,QAAQ,KAAK;AACzB,QAAI;AAEJ,QAAI;AACF,gBACG,MAAM,MAAM,kBAAkB;AAAA,QAC7B,QAAQ,QAAQ;AAAA,QAChB,SAAS,OAAO;AAAA,UACd,OAAO,QAAQ,QAAQ,OAAO,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM;AAAA,YACpD,IAAI,YAAY;AAAA,YAChB,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,GAAG,IAAI,OAAO,KAAK;AAAA,UACvD,CAAC;AAAA,QACH;AAAA,QACA;AAAA,MACF,CAAC,KAAM,CAAC;AAAA,IACZ,SAAS,GAAQ;AACf,aAAO;AAAA,QACL,kCAAmC,EAAY,OAAO;AAAA,EAAM,EAAY,KAAK;AAAA,MAC/E;AACA,eAAS,OAAO,cAAc,CAAC,CAAC,EAAE,KAAK,gBAAgB,CAAC,CAAC,EAAE,IAAI;AAC/D;AAAA,IACF;AAEA,QAAI,QAAQ,IAAI,QAAQ,MAAM,uBAAuB,WAAW,QAAQ;AACtE,eAAS,UAAU,KAAK;AAAA,QACtB,gBAAgB;AAAA,QAChB,qBAAqB;AAAA,MACvB,CAAC;AACD,UAAI;AACF,cAAM,UAAU,CAAC,UAAsB;AACrC,mBAAS;AAAA,YACP,WAAW,KAAK,UAAU,EAAE,SAAS,MAAM,CAAC,IAAI;AAAA,UAClD;AAAA,QACF;AACA,cAAM,SAAS,MAAM;AAAA,UACnB,OAAO;AAAA,UACP;AAAA,UACA,MACE,OAAO,IAAI,OAAO;AAAA,YAChB;AAAA,YACA;AAAA,UACF,CAAC;AAAA,QACL;AACA,iBAAS;AAAA,UACP,WAAW,KAAK,UAAU,EAAE,QAAQ,OAAO,OAAO,CAAC,IAAI;AAAA,QACzD;AACA,iBAAS,IAAI;AAAA,MACf,SAAS,GAAG;AACV,eAAO;AAAA,UACL,wCAAyC,EAAY,OAAO;AAAA,EAAM,EAAY,KAAK;AAAA,QACrF;AACA,iBAAS;AAAA,UACP,UAAU,KAAK,UAAU,EAAE,OAAO,gBAAgB,CAAC,EAAE,CAAC,CAAC,GAAG,eAAe;AAAA,QAC3E;AACA,iBAAS,IAAI;AAAA,MACf;AAAA,IACF,OAAO;AACL,UAAI;AACF,cAAM,SAAS,MAAM,OAAO,IAAI,OAAO,EAAE,QAAQ,CAAC;AAClD,iBAAS,UAAU,qBAAqB,OAAO,UAAU,OAAO;AAChE,iBAAS,UAAU,oBAAoB,OAAO,UAAU,MAAM;AAE9D,iBACG,OAAO,GAAG,EACV,KAAK;AAAA,UACJ,QAAQ,OAAO;AAAA,QACjB,CAAC,EACA,IAAI;AAAA,MACT,SAAS,GAAG;AAEV,eAAO;AAAA,UACL,4CAA6C,EAAY,OAAO;AAAA,EAAM,EAAY,KAAK;AAAA,QACzF;AACA,iBAAS,OAAO,cAAc,CAAC,CAAC,EAAE,KAAK,gBAAgB,CAAC,CAAC,EAAE,IAAI;AAAA,MACjE;AAAA,IACF;AAAA,EACF;AACF;AAkBO,SAAS,oBAMd,MACA,SACqC;AACrC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;AAqBO,SAAS,gBAAgB,SAAwC;AACtE,QAAM,SAAS,IAAI,WAAW,OAAO;AACrC,SAAO,MAAM;AACb,SAAO;AACT;AASO,MAAM,WAAW;AAAA;AAAA,EAEtB,OAAe,kBAAgC,CAAC;AAAA;AAAA,EAGxC;AAAA;AAAA,EAEA,OAAsB;AAAA;AAAA,EAEtB,SAAwB;AAAA,EAEhC,YAAY,SAA4B;AACtC,SAAK,UAAU;AAAA,MACb,GAAG;AAAA,IACL;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAQ;AACZ,UAAM,SAAS,QAAQ;AAEvB,WAAO,IAAI,WAAW,KAAK,KAAK,QAAQ,iBAAiB,CAAC;AAC1D,WAAO,IAAI,KAAK,KAAK,QAAQ,IAAI,CAAC;AAElC,WAAO,MAAM,sCAAsC;AACnD,UAAM,aAAa,KAAK,QAAQ,cAAc;AAC9C,SAAK,QAAQ,OAAO,QAAQ,CAAC,SAAS;AACpC,UAAI,aAAa,MAAM;AACrB,cAAM,WAAW,IAAI,UAAU,GAAG,KAAK,KAAK,SAAS,IAAI;AACzD,eAAO,MAAM,MAAM,QAAQ,EAAE;AAC7B,eAAO;AAAA,UACL;AAAA,UACA,eAAe,KAAK,MAAM,EAAE,iBAAiB,KAAK,QAAQ,CAAC;AAAA,QAC7D;AAAA,MACF,OAAO;AACL,cAAM,WAAW,IAAI,UAAU,GAAG,KAAK,SAAS,IAAI;AACpD,eAAO,MAAM,MAAM,QAAQ,EAAE;AAC7B,eAAO,KAAK,UAAU,eAAe,IAAI,CAAC;AAAA,MAC5C;AAAA,IACF,CAAC;AACD,SAAK,OACH,KAAK,SAAS,SACb,QAAQ,IAAI,OAAO,SAAS,QAAQ,IAAI,IAAI,IAAI,MACjD;AACF,SAAK,SAAS,OAAO,OAAO,KAAK,MAAM,MAAM;AAC3C,aAAO,MAAM,2CAA2C,KAAK,IAAI,EAAE;AACnE,iBAAW,gBAAgB,KAAK,IAAI;AAAA,IACtC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAsB;AAC1B,QAAI,CAAC,KAAK,QAAQ;AAChB;AAAA,IACF;AACA,WAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,WAAK,OAAQ,MAAM,CAAC,QAAQ;AAC1B,YAAI,KAAK;AACP,iBAAO;AAAA,YACL,2CAA2C,KAAK,IAAI,KAAK,GAAG;AAAA,UAC9D;AACA,iBAAO,GAAG;AAAA,QACZ;AACA,cAAM,QAAQ,WAAW,gBAAgB,QAAQ,IAAI;AACrD,YAAI,QAAQ,IAAI;AACd,qBAAW,gBAAgB,OAAO,OAAO,CAAC;AAAA,QAC5C;AACA,eAAO;AAAA,UACL,uBAAuB,KAAK,IAAI;AAAA,QAClC;AACA,aAAK,OAAO;AACZ,aAAK,SAAS;AACd,gBAAQ;AAAA,MACV,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa,UAAU;AACrB,WAAO,QAAQ;AAAA,MACb,WAAW,gBAAgB,IAAI,CAAC,WAAW,OAAO,KAAK,CAAC;AAAA,IAC1D;AAAA,EACF;AACF;","names":[]}
package/package.json CHANGED
@@ -9,7 +9,7 @@
9
9
  "genai",
10
10
  "generative-ai"
11
11
  ],
12
- "version": "1.3.0",
12
+ "version": "1.5.0",
13
13
  "type": "commonjs",
14
14
  "repository": {
15
15
  "type": "git",
@@ -24,8 +24,8 @@
24
24
  },
25
25
  "peerDependencies": {
26
26
  "express": "^4.21.1",
27
- "@genkit-ai/core": "1.3.0",
28
- "genkit": "^1.3.0"
27
+ "@genkit-ai/core": "1.5.0",
28
+ "genkit": "^1.5.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "get-port": "^5.1.0",
package/src/index.ts CHANGED
@@ -54,6 +54,19 @@ export function expressHandler<
54
54
  response: express.Response
55
55
  ): Promise<void> => {
56
56
  const { stream } = request.query;
57
+ if (!request.body) {
58
+ const errMsg =
59
+ `Error: request.body is undefined. ` +
60
+ `Possible reasons: missing 'content-type: application/json' in request ` +
61
+ `headers or misconfigured JSON middleware ('app.use(express.json()')? `;
62
+ logger.error(errMsg);
63
+ response
64
+ .status(400)
65
+ .json({ message: errMsg, status: 'INVALID ARGUMENT' })
66
+ .end();
67
+ return;
68
+ }
69
+
57
70
  let input = request.body.data as z.infer<I>;
58
71
  let context: Record<string, any>;
59
72