@genkit-ai/express 1.13.0 → 1.14.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 +13 -2
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +13 -2
- package/lib/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +14 -1
- package/tests/express_test.ts +50 -0
package/lib/index.js
CHANGED
|
@@ -71,6 +71,13 @@ ${e.stack}`
|
|
|
71
71
|
response.status((0, import_context.getHttpStatus)(e)).json((0, import_context.getCallableJSON)(e)).end();
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
|
+
const abortController = new AbortController();
|
|
75
|
+
request.on("close", () => {
|
|
76
|
+
abortController.abort();
|
|
77
|
+
});
|
|
78
|
+
request.on("timeout", () => {
|
|
79
|
+
abortController.abort();
|
|
80
|
+
});
|
|
74
81
|
if (request.get("Accept") === "text/event-stream" || stream === "true") {
|
|
75
82
|
response.writeHead(200, {
|
|
76
83
|
"Content-Type": "text/plain",
|
|
@@ -87,7 +94,8 @@ ${e.stack}`
|
|
|
87
94
|
onChunk,
|
|
88
95
|
() => action.run(input, {
|
|
89
96
|
onChunk,
|
|
90
|
-
context
|
|
97
|
+
context,
|
|
98
|
+
abortSignal: abortController.signal
|
|
91
99
|
})
|
|
92
100
|
);
|
|
93
101
|
response.write(
|
|
@@ -106,7 +114,10 @@ ${e.stack}`
|
|
|
106
114
|
}
|
|
107
115
|
} else {
|
|
108
116
|
try {
|
|
109
|
-
const result = await action.run(input, {
|
|
117
|
+
const result = await action.run(input, {
|
|
118
|
+
context,
|
|
119
|
+
abortSignal: abortController.signal
|
|
120
|
+
});
|
|
110
121
|
response.setHeader("x-genkit-trace-id", result.telemetry.traceId);
|
|
111
122
|
response.setHeader("x-genkit-span-id", result.telemetry.spanId);
|
|
112
123
|
response.status(200).json({
|
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, { type CorsOptions } from 'cors';\nimport express from 'express';\nimport {\n runWithStreamingCallback,\n type Action,\n type ActionContext,\n type Flow,\n type z,\n} from 'genkit';\nimport {\n getCallableJSON,\n getHttpStatus,\n type ContextProvider,\n type RequestData,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport type { 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 const 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 ? Number.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, { type CorsOptions } from 'cors';\nimport express from 'express';\nimport {\n runWithStreamingCallback,\n type Action,\n type ActionContext,\n type Flow,\n type z,\n} from 'genkit';\nimport {\n getCallableJSON,\n getHttpStatus,\n type ContextProvider,\n type RequestData,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport type { 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 const 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 const abortController = new AbortController();\n request.on('close', () => {\n abortController.abort();\n });\n // when/if using timeout middleware, it will emit 'timeout' event.\n request.on('timeout', () => {\n abortController.abort();\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 abortSignal: abortController.signal,\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, {\n context,\n abortSignal: abortController.signal,\n });\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 ? Number.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
|
@@ -40,6 +40,13 @@ ${e.stack}`
|
|
|
40
40
|
response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
|
+
const abortController = new AbortController();
|
|
44
|
+
request.on("close", () => {
|
|
45
|
+
abortController.abort();
|
|
46
|
+
});
|
|
47
|
+
request.on("timeout", () => {
|
|
48
|
+
abortController.abort();
|
|
49
|
+
});
|
|
43
50
|
if (request.get("Accept") === "text/event-stream" || stream === "true") {
|
|
44
51
|
response.writeHead(200, {
|
|
45
52
|
"Content-Type": "text/plain",
|
|
@@ -56,7 +63,8 @@ ${e.stack}`
|
|
|
56
63
|
onChunk,
|
|
57
64
|
() => action.run(input, {
|
|
58
65
|
onChunk,
|
|
59
|
-
context
|
|
66
|
+
context,
|
|
67
|
+
abortSignal: abortController.signal
|
|
60
68
|
})
|
|
61
69
|
);
|
|
62
70
|
response.write(
|
|
@@ -75,7 +83,10 @@ ${e.stack}`
|
|
|
75
83
|
}
|
|
76
84
|
} else {
|
|
77
85
|
try {
|
|
78
|
-
const result = await action.run(input, {
|
|
86
|
+
const result = await action.run(input, {
|
|
87
|
+
context,
|
|
88
|
+
abortSignal: abortController.signal
|
|
89
|
+
});
|
|
79
90
|
response.setHeader("x-genkit-trace-id", result.telemetry.traceId);
|
|
80
91
|
response.setHeader("x-genkit-span-id", result.telemetry.spanId);
|
|
81
92
|
response.status(200).json({
|
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, { type CorsOptions } from 'cors';\nimport express from 'express';\nimport {\n runWithStreamingCallback,\n type Action,\n type ActionContext,\n type Flow,\n type z,\n} from 'genkit';\nimport {\n getCallableJSON,\n getHttpStatus,\n type ContextProvider,\n type RequestData,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport type { 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 const 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 ? Number.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, { type CorsOptions } from 'cors';\nimport express from 'express';\nimport {\n runWithStreamingCallback,\n type Action,\n type ActionContext,\n type Flow,\n type z,\n} from 'genkit';\nimport {\n getCallableJSON,\n getHttpStatus,\n type ContextProvider,\n type RequestData,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport type { 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 const 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 const abortController = new AbortController();\n request.on('close', () => {\n abortController.abort();\n });\n // when/if using timeout middleware, it will emit 'timeout' event.\n request.on('timeout', () => {\n abortController.abort();\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 abortSignal: abortController.signal,\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, {\n context,\n abortSignal: abortController.signal,\n });\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 ? Number.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":[]}
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"genai",
|
|
10
10
|
"generative-ai"
|
|
11
11
|
],
|
|
12
|
-
"version": "1.
|
|
12
|
+
"version": "1.14.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.
|
|
28
|
-
"genkit": "^1.
|
|
27
|
+
"@genkit-ai/core": "1.14.0",
|
|
28
|
+
"genkit": "^1.14.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"get-port": "^5.1.0",
|
package/src/index.ts
CHANGED
|
@@ -90,6 +90,15 @@ export function expressHandler<
|
|
|
90
90
|
return;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
const abortController = new AbortController();
|
|
94
|
+
request.on('close', () => {
|
|
95
|
+
abortController.abort();
|
|
96
|
+
});
|
|
97
|
+
// when/if using timeout middleware, it will emit 'timeout' event.
|
|
98
|
+
request.on('timeout', () => {
|
|
99
|
+
abortController.abort();
|
|
100
|
+
});
|
|
101
|
+
|
|
93
102
|
if (request.get('Accept') === 'text/event-stream' || stream === 'true') {
|
|
94
103
|
response.writeHead(200, {
|
|
95
104
|
'Content-Type': 'text/plain',
|
|
@@ -108,6 +117,7 @@ export function expressHandler<
|
|
|
108
117
|
action.run(input, {
|
|
109
118
|
onChunk,
|
|
110
119
|
context,
|
|
120
|
+
abortSignal: abortController.signal,
|
|
111
121
|
})
|
|
112
122
|
);
|
|
113
123
|
response.write(
|
|
@@ -125,7 +135,10 @@ export function expressHandler<
|
|
|
125
135
|
}
|
|
126
136
|
} else {
|
|
127
137
|
try {
|
|
128
|
-
const result = await action.run(input, {
|
|
138
|
+
const result = await action.run(input, {
|
|
139
|
+
context,
|
|
140
|
+
abortSignal: abortController.signal,
|
|
141
|
+
});
|
|
129
142
|
response.setHeader('x-genkit-trace-id', result.telemetry.traceId);
|
|
130
143
|
response.setHeader('x-genkit-span-id', result.telemetry.spanId);
|
|
131
144
|
// Responses for non-streaming flows are passed back with the flow result stored in a field called "result."
|
package/tests/express_test.ts
CHANGED
|
@@ -61,8 +61,10 @@ const contextProvider: ContextProvider<Context> = (req: RequestData) => {
|
|
|
61
61
|
describe('expressHandler', async () => {
|
|
62
62
|
let server: http.Server;
|
|
63
63
|
let port;
|
|
64
|
+
let abortableFlowResult;
|
|
64
65
|
|
|
65
66
|
beforeEach(async () => {
|
|
67
|
+
abortableFlowResult = undefined;
|
|
66
68
|
const ai = genkit({});
|
|
67
69
|
const echoModel = defineEchoModel(ai);
|
|
68
70
|
|
|
@@ -114,6 +116,27 @@ describe('expressHandler', async () => {
|
|
|
114
116
|
}
|
|
115
117
|
);
|
|
116
118
|
|
|
119
|
+
const abortableFlow = ai.defineFlow(
|
|
120
|
+
{
|
|
121
|
+
name: 'abortableFlow',
|
|
122
|
+
},
|
|
123
|
+
async (_, { abortSignal, sendChunk }) => {
|
|
124
|
+
let itersLeft = 20;
|
|
125
|
+
while (itersLeft > 0 && !abortSignal.aborted) {
|
|
126
|
+
await new Promise((r) => {
|
|
127
|
+
setTimeout(r, 100);
|
|
128
|
+
});
|
|
129
|
+
sendChunk(itersLeft);
|
|
130
|
+
itersLeft--;
|
|
131
|
+
}
|
|
132
|
+
abortableFlowResult = [
|
|
133
|
+
itersLeft > 0 ? 'success' : 'failure',
|
|
134
|
+
abortSignal.aborted,
|
|
135
|
+
];
|
|
136
|
+
return itersLeft > 0 ? 'success' : 'failure';
|
|
137
|
+
}
|
|
138
|
+
);
|
|
139
|
+
|
|
117
140
|
const app = express();
|
|
118
141
|
app.use(express.json());
|
|
119
142
|
port = await getPort();
|
|
@@ -132,6 +155,7 @@ describe('expressHandler', async () => {
|
|
|
132
155
|
'/echoModelWithAuth',
|
|
133
156
|
expressHandler(echoModel, { contextProvider })
|
|
134
157
|
);
|
|
158
|
+
app.post('/abortableFlow', expressHandler(abortableFlow));
|
|
135
159
|
|
|
136
160
|
server = app.listen(port, () => {
|
|
137
161
|
console.log(`Example app listening on port ${port}`);
|
|
@@ -258,6 +282,32 @@ describe('expressHandler', async () => {
|
|
|
258
282
|
return (err as Error).message.includes('not authorized');
|
|
259
283
|
});
|
|
260
284
|
});
|
|
285
|
+
|
|
286
|
+
it('should abort a flow with auth', async () => {
|
|
287
|
+
const controller = new AbortController();
|
|
288
|
+
const response = fetch(`http://localhost:${port}/abortableFlow`, {
|
|
289
|
+
method: 'POST',
|
|
290
|
+
signal: controller.signal,
|
|
291
|
+
});
|
|
292
|
+
// TODO: make this work instead of direct fetch. For some reason doesn't work in a test,
|
|
293
|
+
// even though appears to be doing exactly the same thing.
|
|
294
|
+
/*
|
|
295
|
+
const response = runFlow({
|
|
296
|
+
url: `http://localhost:${port}/abortableFlow`,
|
|
297
|
+
abortSignal: controller.signal,
|
|
298
|
+
});
|
|
299
|
+
*/
|
|
300
|
+
|
|
301
|
+
setTimeout(() => controller.abort(), 10);
|
|
302
|
+
|
|
303
|
+
await assert.rejects(response); // abort error
|
|
304
|
+
|
|
305
|
+
await new Promise((r) => {
|
|
306
|
+
setTimeout(r, 200);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
assert.deepStrictEqual(abortableFlowResult, ['success', true]);
|
|
310
|
+
});
|
|
261
311
|
});
|
|
262
312
|
|
|
263
313
|
describe('streamFlow', () => {
|