@genkit-ai/express 1.24.0 → 1.26.0-rc.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/README.md CHANGED
@@ -59,6 +59,44 @@ app.post(
59
59
  );
60
60
  ```
61
61
 
62
+ ### Durable Streaming (Beta)
63
+
64
+ You can configure flows to use a `StreamManager` to persist their state. This allows clients to disconnect and reconnect to a stream without losing its state.
65
+
66
+ To enable durable streaming, provide a `streamManager` in the `expressHandler` options. The `InMemoryStreamManager` is useful for development and testing:
67
+
68
+ ```ts
69
+ import { InMemoryStreamManager } from 'genkit/beta';
70
+
71
+ // ...
72
+
73
+ app.post(
74
+ '/myDurableFlow',
75
+ expressHandler(myFlow, {
76
+ streamManager: new InMemoryStreamManager(),
77
+ })
78
+ );
79
+ ```
80
+
81
+ For production environments, you should use a durable `StreamManager` implementation, such as `FirestoreStreamManager` or `RtdbStreamManager` from the `@genkit-ai/firebase` plugin, or a custom implementation.
82
+
83
+ Clients can then connect and reconnect to the stream using the `streamId`:
84
+
85
+ ```ts
86
+ // Start a new stream
87
+ const result = streamFlow({
88
+ url: `http://localhost:8080/myDurableFlow`,
89
+ input: 'tell me a long story',
90
+ });
91
+ const streamId = await result.streamId; // Save this ID
92
+
93
+ // ... later, reconnect if needed ...
94
+ const reconnectedResult = streamFlow({
95
+ url: `http://localhost:8080/myDurableFlow`,
96
+ streamId: streamId,
97
+ });
98
+ ```
99
+
62
100
  Flows and actions exposed using the `expressHandler` function can be accessed using `genkit/beta/client` library:
63
101
 
64
102
  ```ts
package/lib/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import bodyParser from 'body-parser';
2
2
  import { CorsOptions } from 'cors';
3
3
  import express from 'express';
4
- import { ActionContext, z, Action, Flow } from 'genkit';
4
+ import { ActionContext, z, Action, StreamManager, Flow } from 'genkit/beta';
5
5
  import { ContextProvider } from 'genkit/context';
6
6
 
7
7
  /**
@@ -25,24 +25,46 @@ import { ContextProvider } from 'genkit/context';
25
25
  */
26
26
  declare function expressHandler<C extends ActionContext = ActionContext, I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny>(action: Action<I, O, S>, opts?: {
27
27
  contextProvider?: ContextProvider<C, I>;
28
+ streamManager?: StreamManager;
28
29
  }): express.RequestHandler;
29
30
  /**
30
31
  * A wrapper object containing a flow with its associated auth policy.
32
+ * @deprecated Use `withFlowOptions` instead.
31
33
  */
32
34
  type FlowWithContextProvider<C extends ActionContext = ActionContext, I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> = {
33
35
  flow: Flow<I, O, S>;
34
36
  context: ContextProvider<C, I>;
35
37
  };
38
+ /**
39
+ * A wrapper object containing a flow with its associated options.
40
+ */
41
+ type FlowWithOptions<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> = {
42
+ flow: Flow<I, O, S>;
43
+ options: {
44
+ contextProvider?: ContextProvider<any, I>;
45
+ streamManager?: StreamManager;
46
+ path?: string;
47
+ };
48
+ };
36
49
  /**
37
50
  * Adds an auth policy to the flow.
51
+ * @deprecated Use `withFlowOptions` instead.
38
52
  */
39
53
  declare function withContextProvider<C extends ActionContext = ActionContext, I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny>(flow: Flow<I, O, S>, context: ContextProvider<C, I>): FlowWithContextProvider<C, I, O, S>;
54
+ /**
55
+ * Adds an auth policy to the flow.
56
+ */
57
+ declare function withFlowOptions<I extends z.ZodTypeAny, O extends z.ZodTypeAny, S extends z.ZodTypeAny>(flow: Flow<I, O, S>, options: {
58
+ contextProvider?: ContextProvider<any, I>;
59
+ streamManager?: StreamManager;
60
+ path?: string;
61
+ }): FlowWithOptions<I, O, S>;
40
62
  /**
41
63
  * Options to configure the flow server.
42
64
  */
43
65
  interface FlowServerOptions {
44
66
  /** List of flows to expose via the flow server. */
45
- flows: (Flow<any, any, any> | FlowWithContextProvider<any, any, any>)[];
67
+ flows: (Flow<any, any, any> | FlowWithContextProvider<any, any, any> | FlowWithOptions<any, any, any>)[];
46
68
  /** Port to run the server on. Defaults to env.PORT or 3400. */
47
69
  port?: number;
48
70
  /** CORS options for the server. */
@@ -87,4 +109,4 @@ declare class FlowServer {
87
109
  static stopAll(): Promise<void[]>;
88
110
  }
89
111
 
90
- export { FlowServer, type FlowServerOptions, type FlowWithContextProvider, expressHandler, startFlowServer, withContextProvider };
112
+ export { FlowServer, type FlowServerOptions, type FlowWithContextProvider, type FlowWithOptions, expressHandler, startFlowServer, withContextProvider, withFlowOptions };
package/lib/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import bodyParser from 'body-parser';
2
2
  import { CorsOptions } from 'cors';
3
3
  import express from 'express';
4
- import { ActionContext, z, Action, Flow } from 'genkit';
4
+ import { ActionContext, z, Action, StreamManager, Flow } from 'genkit/beta';
5
5
  import { ContextProvider } from 'genkit/context';
6
6
 
7
7
  /**
@@ -25,24 +25,46 @@ import { ContextProvider } from 'genkit/context';
25
25
  */
26
26
  declare function expressHandler<C extends ActionContext = ActionContext, I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny>(action: Action<I, O, S>, opts?: {
27
27
  contextProvider?: ContextProvider<C, I>;
28
+ streamManager?: StreamManager;
28
29
  }): express.RequestHandler;
29
30
  /**
30
31
  * A wrapper object containing a flow with its associated auth policy.
32
+ * @deprecated Use `withFlowOptions` instead.
31
33
  */
32
34
  type FlowWithContextProvider<C extends ActionContext = ActionContext, I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> = {
33
35
  flow: Flow<I, O, S>;
34
36
  context: ContextProvider<C, I>;
35
37
  };
38
+ /**
39
+ * A wrapper object containing a flow with its associated options.
40
+ */
41
+ type FlowWithOptions<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> = {
42
+ flow: Flow<I, O, S>;
43
+ options: {
44
+ contextProvider?: ContextProvider<any, I>;
45
+ streamManager?: StreamManager;
46
+ path?: string;
47
+ };
48
+ };
36
49
  /**
37
50
  * Adds an auth policy to the flow.
51
+ * @deprecated Use `withFlowOptions` instead.
38
52
  */
39
53
  declare function withContextProvider<C extends ActionContext = ActionContext, I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny>(flow: Flow<I, O, S>, context: ContextProvider<C, I>): FlowWithContextProvider<C, I, O, S>;
54
+ /**
55
+ * Adds an auth policy to the flow.
56
+ */
57
+ declare function withFlowOptions<I extends z.ZodTypeAny, O extends z.ZodTypeAny, S extends z.ZodTypeAny>(flow: Flow<I, O, S>, options: {
58
+ contextProvider?: ContextProvider<any, I>;
59
+ streamManager?: StreamManager;
60
+ path?: string;
61
+ }): FlowWithOptions<I, O, S>;
40
62
  /**
41
63
  * Options to configure the flow server.
42
64
  */
43
65
  interface FlowServerOptions {
44
66
  /** List of flows to expose via the flow server. */
45
- flows: (Flow<any, any, any> | FlowWithContextProvider<any, any, any>)[];
67
+ flows: (Flow<any, any, any> | FlowWithContextProvider<any, any, any> | FlowWithOptions<any, any, any>)[];
46
68
  /** Port to run the server on. Defaults to env.PORT or 3400. */
47
69
  port?: number;
48
70
  /** CORS options for the server. */
@@ -87,4 +109,4 @@ declare class FlowServer {
87
109
  static stopAll(): Promise<void[]>;
88
110
  }
89
111
 
90
- export { FlowServer, type FlowServerOptions, type FlowWithContextProvider, expressHandler, startFlowServer, withContextProvider };
112
+ export { FlowServer, type FlowServerOptions, type FlowWithContextProvider, type FlowWithOptions, expressHandler, startFlowServer, withContextProvider, withFlowOptions };
package/lib/index.js CHANGED
@@ -31,18 +31,23 @@ __export(index_exports, {
31
31
  FlowServer: () => FlowServer,
32
32
  expressHandler: () => expressHandler,
33
33
  startFlowServer: () => startFlowServer,
34
- withContextProvider: () => withContextProvider
34
+ withContextProvider: () => withContextProvider,
35
+ withFlowOptions: () => withFlowOptions
35
36
  });
36
37
  module.exports = __toCommonJS(index_exports);
37
38
  var import_body_parser = __toESM(require("body-parser"));
38
39
  var import_cors = __toESM(require("cors"));
40
+ var import_crypto = require("crypto");
39
41
  var import_express = __toESM(require("express"));
42
+ var import_beta = require("genkit/beta");
40
43
  var import_context = require("genkit/context");
41
44
  var import_logging = require("genkit/logging");
42
45
  const streamDelimiter = "\n\n";
43
46
  function expressHandler(action, opts) {
44
47
  return async (request, response) => {
45
48
  const { stream } = request.query;
49
+ const streamIdHeader = request.headers["x-genkit-stream-id"];
50
+ const streamId = Array.isArray(streamIdHeader) ? streamIdHeader[0] : streamIdHeader;
46
51
  if (!request.body) {
47
52
  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()')? `;
48
53
  import_logging.logger.error(errMsg);
@@ -78,35 +83,29 @@ ${e.stack}`
78
83
  abortController.abort();
79
84
  });
80
85
  if (request.get("Accept") === "text/event-stream" || stream === "true") {
81
- response.writeHead(200, {
86
+ const streamManager = opts?.streamManager;
87
+ if (streamManager && streamId) {
88
+ await subscribeToStream(streamManager, streamId, response);
89
+ return;
90
+ }
91
+ const streamIdToUse = (0, import_crypto.randomUUID)();
92
+ const headers = {
82
93
  "Content-Type": "text/plain",
83
94
  "Transfer-Encoding": "chunked"
84
- });
85
- try {
86
- const onChunk = (chunk) => {
87
- response.write(
88
- "data: " + JSON.stringify({ message: chunk }) + streamDelimiter
89
- );
90
- };
91
- const result = await action.run(input, {
92
- onChunk,
93
- context,
94
- abortSignal: abortController.signal
95
- });
96
- response.write(
97
- "data: " + JSON.stringify({ result: result.result }) + streamDelimiter
98
- );
99
- response.end();
100
- } catch (e) {
101
- import_logging.logger.error(
102
- `Streaming request failed with error: ${e.message}
103
- ${e.stack}`
104
- );
105
- response.write(
106
- `error: ${JSON.stringify({ error: (0, import_context.getCallableJSON)(e) })}${streamDelimiter}`
107
- );
108
- response.end();
95
+ };
96
+ if (streamManager) {
97
+ headers["x-genkit-stream-id"] = streamIdToUse;
109
98
  }
99
+ response.writeHead(200, headers);
100
+ runActionWithDurableStreaming(
101
+ action,
102
+ streamManager,
103
+ streamIdToUse,
104
+ input,
105
+ context,
106
+ response,
107
+ abortController.signal
108
+ );
110
109
  } else {
111
110
  try {
112
111
  const result = await action.run(input, {
@@ -128,12 +127,112 @@ ${e.stack}`
128
127
  }
129
128
  };
130
129
  }
130
+ async function runActionWithDurableStreaming(action, streamManager, streamId, input, context, response, abortSignal) {
131
+ let taskQueue;
132
+ let durableStream;
133
+ if (streamManager) {
134
+ taskQueue = new import_beta.AsyncTaskQueue();
135
+ durableStream = await streamManager.open(streamId);
136
+ }
137
+ try {
138
+ let onChunk = (chunk) => {
139
+ response.write(
140
+ "data: " + JSON.stringify({ message: chunk }) + streamDelimiter
141
+ );
142
+ };
143
+ if (streamManager) {
144
+ const originalOnChunk = onChunk;
145
+ onChunk = (chunk) => {
146
+ originalOnChunk(chunk);
147
+ taskQueue.enqueue(() => durableStream.write(chunk));
148
+ };
149
+ }
150
+ const result = await action.run(input, {
151
+ onChunk,
152
+ context,
153
+ abortSignal
154
+ });
155
+ if (streamManager) {
156
+ taskQueue.enqueue(() => durableStream.done(result.result));
157
+ await taskQueue.merge();
158
+ }
159
+ response.write(
160
+ "data: " + JSON.stringify({ result: result.result }) + streamDelimiter
161
+ );
162
+ response.end();
163
+ } catch (e) {
164
+ if (durableStream) {
165
+ taskQueue.enqueue(() => durableStream.error(e));
166
+ await taskQueue.merge();
167
+ }
168
+ import_logging.logger.error(
169
+ `Streaming request failed with error: ${e.message}
170
+ ${e.stack}`
171
+ );
172
+ response.write(
173
+ `error: ${JSON.stringify({
174
+ error: (0, import_context.getCallableJSON)(e)
175
+ })}${streamDelimiter}`
176
+ );
177
+ response.end();
178
+ }
179
+ }
180
+ async function subscribeToStream(streamManager, streamId, response) {
181
+ try {
182
+ await streamManager.subscribe(streamId, {
183
+ onChunk: (chunk) => {
184
+ response.write(
185
+ "data: " + JSON.stringify({ message: chunk }) + streamDelimiter
186
+ );
187
+ },
188
+ onDone: (output) => {
189
+ response.write(
190
+ "data: " + JSON.stringify({ result: output }) + streamDelimiter
191
+ );
192
+ response.end();
193
+ },
194
+ onError: (err) => {
195
+ import_logging.logger.error(
196
+ `Streaming request failed with error: ${err.message}
197
+ ${err.stack}`
198
+ );
199
+ response.write(
200
+ `error: ${JSON.stringify({
201
+ error: (0, import_context.getCallableJSON)(err)
202
+ })}${streamDelimiter}`
203
+ );
204
+ response.end();
205
+ }
206
+ });
207
+ } catch (e) {
208
+ if (e instanceof import_beta.StreamNotFoundError) {
209
+ response.status(204).end();
210
+ return;
211
+ }
212
+ if (e.status === "DEADLINE_EXCEEDED") {
213
+ response.write(
214
+ `error: ${JSON.stringify({
215
+ error: (0, import_context.getCallableJSON)(e)
216
+ })}${streamDelimiter}`
217
+ );
218
+ response.end();
219
+ return;
220
+ }
221
+ throw e;
222
+ }
223
+ }
131
224
  function withContextProvider(flow, context) {
132
225
  return {
133
226
  flow,
134
227
  context
135
228
  };
136
229
  }
230
+ function withFlowOptions(flow, options) {
231
+ return {
232
+ flow,
233
+ options
234
+ };
235
+ }
137
236
  function startFlowServer(options) {
138
237
  const server = new FlowServer(options);
139
238
  server.start();
@@ -163,13 +262,11 @@ class FlowServer {
163
262
  import_logging.logger.debug("Running flow server with flow paths:");
164
263
  const pathPrefix = this.options.pathPrefix ?? "";
165
264
  this.options.flows?.forEach((flow) => {
166
- if ("context" in flow) {
167
- const flowPath = `/${pathPrefix}${flow.flow.__action.name}`;
265
+ if ("flow" in flow) {
266
+ const flowPath = `/${pathPrefix}${"options" in flow && flow.options.path || flow.flow.__action.name}`;
168
267
  import_logging.logger.debug(` - ${flowPath}`);
169
- server.post(
170
- flowPath,
171
- expressHandler(flow.flow, { contextProvider: flow.context })
172
- );
268
+ const options = "options" in flow ? flow.options : { contextProvider: flow.context };
269
+ server.post(flowPath, expressHandler(flow.flow, options));
173
270
  } else {
174
271
  const flowPath = `/${pathPrefix}${flow.__action.name}`;
175
272
  import_logging.logger.debug(` - ${flowPath}`);
@@ -224,6 +321,7 @@ class FlowServer {
224
321
  FlowServer,
225
322
  expressHandler,
226
323
  startFlowServer,
227
- withContextProvider
324
+ withContextProvider,
325
+ withFlowOptions
228
326
  });
229
327
  //# sourceMappingURL=index.js.map
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 { type Action, type ActionContext, type Flow, type z } 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 action.run(input, {\n onChunk,\n context,\n abortSignal: abortController.signal,\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"]}
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 { randomUUID } from 'crypto';\nimport express from 'express';\nimport {\n Action,\n ActionStreamInput,\n AsyncTaskQueue,\n Flow,\n StreamNotFoundError,\n type ActionContext,\n type StreamManager,\n type z,\n} from 'genkit/beta';\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 streamManager?: StreamManager;\n }\n): express.RequestHandler {\n return async (\n request: express.Request,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n const streamIdHeader = request.headers['x-genkit-stream-id'];\n const streamId = Array.isArray(streamIdHeader)\n ? streamIdHeader[0]\n : streamIdHeader;\n\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 const streamManager = opts?.streamManager;\n if (streamManager && streamId) {\n await subscribeToStream(streamManager, streamId, response);\n return;\n }\n const streamIdToUse = randomUUID();\n const headers = {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n };\n if (streamManager) {\n headers['x-genkit-stream-id'] = streamIdToUse;\n }\n response.writeHead(200, headers);\n runActionWithDurableStreaming(\n action,\n streamManager,\n streamIdToUse,\n input,\n context,\n response,\n abortController.signal\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\nasync function runActionWithDurableStreaming<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n streamManager: StreamManager | undefined,\n streamId: string,\n input: z.infer<I>,\n context: ActionContext,\n response: express.Response,\n abortSignal: AbortSignal\n) {\n let taskQueue: AsyncTaskQueue | undefined;\n let durableStream: ActionStreamInput<any, any> | undefined;\n if (streamManager) {\n taskQueue = new AsyncTaskQueue();\n durableStream = await streamManager.open(streamId);\n }\n try {\n let onChunk = (chunk: z.infer<S>) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n if (streamManager) {\n const originalOnChunk = onChunk;\n onChunk = (chunk: z.infer<S>) => {\n originalOnChunk(chunk);\n taskQueue!.enqueue(() => durableStream!.write(chunk));\n };\n }\n const result = await action.run(input, {\n onChunk,\n context,\n abortSignal,\n });\n if (streamManager) {\n taskQueue!.enqueue(() => durableStream!.done(result.result));\n await taskQueue!.merge();\n }\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n } catch (e) {\n if (durableStream) {\n taskQueue!.enqueue(() => durableStream!.error(e));\n await taskQueue!.merge();\n }\n logger.error(\n `Streaming request failed with error: ${(e as Error).message}\\n${\n (e as Error).stack\n }`\n );\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n }\n}\n\nasync function subscribeToStream(\n streamManager: StreamManager,\n streamId: string,\n response: express.Response\n): Promise<void> {\n try {\n await streamManager.subscribe(streamId, {\n onChunk: (chunk) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n },\n onDone: (output) => {\n response.write(\n 'data: ' + JSON.stringify({ result: output }) + streamDelimiter\n );\n response.end();\n },\n onError: (err) => {\n logger.error(\n `Streaming request failed with error: ${(err as Error).message}\\n${\n (err as Error).stack\n }`\n );\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(err),\n })}${streamDelimiter}`\n );\n response.end();\n },\n });\n } catch (e: any) {\n if (e instanceof StreamNotFoundError) {\n response.status(204).end();\n return;\n }\n if (e.status === 'DEADLINE_EXCEEDED') {\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n return;\n }\n throw e;\n }\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n * @deprecated Use `withFlowOptions` instead.\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 * A wrapper object containing a flow with its associated options.\n */\nexport type FlowWithOptions<\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 options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n };\n};\n\n/**\n * Adds an auth policy to the flow.\n * @deprecated Use `withFlowOptions` instead.\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 * Adds an auth policy to the flow.\n */\nexport function withFlowOptions<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n }\n): FlowWithOptions<I, O, S> {\n return {\n flow,\n options,\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: (\n | Flow<any, any, any>\n | FlowWithContextProvider<any, any, any>\n | FlowWithOptions<any, any, any>\n )[];\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 ('flow' in flow) {\n const flowPath = `/${pathPrefix}${\n ('options' in flow && flow.options.path) || flow.flow.__action.name\n }`;\n logger.debug(` - ${flowPath}`);\n const options =\n 'options' in flow ? flow.options : { contextProvider: flow.context };\n server.post(flowPath, expressHandler(flow.flow, options));\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
@@ -1,6 +1,11 @@
1
1
  import bodyParser from "body-parser";
2
2
  import cors from "cors";
3
+ import { randomUUID } from "crypto";
3
4
  import express from "express";
5
+ import {
6
+ AsyncTaskQueue,
7
+ StreamNotFoundError
8
+ } from "genkit/beta";
4
9
  import {
5
10
  getCallableJSON,
6
11
  getHttpStatus
@@ -10,6 +15,8 @@ const streamDelimiter = "\n\n";
10
15
  function expressHandler(action, opts) {
11
16
  return async (request, response) => {
12
17
  const { stream } = request.query;
18
+ const streamIdHeader = request.headers["x-genkit-stream-id"];
19
+ const streamId = Array.isArray(streamIdHeader) ? streamIdHeader[0] : streamIdHeader;
13
20
  if (!request.body) {
14
21
  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()')? `;
15
22
  logger.error(errMsg);
@@ -45,35 +52,29 @@ ${e.stack}`
45
52
  abortController.abort();
46
53
  });
47
54
  if (request.get("Accept") === "text/event-stream" || stream === "true") {
48
- response.writeHead(200, {
55
+ const streamManager = opts?.streamManager;
56
+ if (streamManager && streamId) {
57
+ await subscribeToStream(streamManager, streamId, response);
58
+ return;
59
+ }
60
+ const streamIdToUse = randomUUID();
61
+ const headers = {
49
62
  "Content-Type": "text/plain",
50
63
  "Transfer-Encoding": "chunked"
51
- });
52
- try {
53
- const onChunk = (chunk) => {
54
- response.write(
55
- "data: " + JSON.stringify({ message: chunk }) + streamDelimiter
56
- );
57
- };
58
- const result = await action.run(input, {
59
- onChunk,
60
- context,
61
- abortSignal: abortController.signal
62
- });
63
- response.write(
64
- "data: " + JSON.stringify({ result: result.result }) + streamDelimiter
65
- );
66
- response.end();
67
- } catch (e) {
68
- logger.error(
69
- `Streaming request failed with error: ${e.message}
70
- ${e.stack}`
71
- );
72
- response.write(
73
- `error: ${JSON.stringify({ error: getCallableJSON(e) })}${streamDelimiter}`
74
- );
75
- response.end();
64
+ };
65
+ if (streamManager) {
66
+ headers["x-genkit-stream-id"] = streamIdToUse;
76
67
  }
68
+ response.writeHead(200, headers);
69
+ runActionWithDurableStreaming(
70
+ action,
71
+ streamManager,
72
+ streamIdToUse,
73
+ input,
74
+ context,
75
+ response,
76
+ abortController.signal
77
+ );
77
78
  } else {
78
79
  try {
79
80
  const result = await action.run(input, {
@@ -95,12 +96,112 @@ ${e.stack}`
95
96
  }
96
97
  };
97
98
  }
99
+ async function runActionWithDurableStreaming(action, streamManager, streamId, input, context, response, abortSignal) {
100
+ let taskQueue;
101
+ let durableStream;
102
+ if (streamManager) {
103
+ taskQueue = new AsyncTaskQueue();
104
+ durableStream = await streamManager.open(streamId);
105
+ }
106
+ try {
107
+ let onChunk = (chunk) => {
108
+ response.write(
109
+ "data: " + JSON.stringify({ message: chunk }) + streamDelimiter
110
+ );
111
+ };
112
+ if (streamManager) {
113
+ const originalOnChunk = onChunk;
114
+ onChunk = (chunk) => {
115
+ originalOnChunk(chunk);
116
+ taskQueue.enqueue(() => durableStream.write(chunk));
117
+ };
118
+ }
119
+ const result = await action.run(input, {
120
+ onChunk,
121
+ context,
122
+ abortSignal
123
+ });
124
+ if (streamManager) {
125
+ taskQueue.enqueue(() => durableStream.done(result.result));
126
+ await taskQueue.merge();
127
+ }
128
+ response.write(
129
+ "data: " + JSON.stringify({ result: result.result }) + streamDelimiter
130
+ );
131
+ response.end();
132
+ } catch (e) {
133
+ if (durableStream) {
134
+ taskQueue.enqueue(() => durableStream.error(e));
135
+ await taskQueue.merge();
136
+ }
137
+ logger.error(
138
+ `Streaming request failed with error: ${e.message}
139
+ ${e.stack}`
140
+ );
141
+ response.write(
142
+ `error: ${JSON.stringify({
143
+ error: getCallableJSON(e)
144
+ })}${streamDelimiter}`
145
+ );
146
+ response.end();
147
+ }
148
+ }
149
+ async function subscribeToStream(streamManager, streamId, response) {
150
+ try {
151
+ await streamManager.subscribe(streamId, {
152
+ onChunk: (chunk) => {
153
+ response.write(
154
+ "data: " + JSON.stringify({ message: chunk }) + streamDelimiter
155
+ );
156
+ },
157
+ onDone: (output) => {
158
+ response.write(
159
+ "data: " + JSON.stringify({ result: output }) + streamDelimiter
160
+ );
161
+ response.end();
162
+ },
163
+ onError: (err) => {
164
+ logger.error(
165
+ `Streaming request failed with error: ${err.message}
166
+ ${err.stack}`
167
+ );
168
+ response.write(
169
+ `error: ${JSON.stringify({
170
+ error: getCallableJSON(err)
171
+ })}${streamDelimiter}`
172
+ );
173
+ response.end();
174
+ }
175
+ });
176
+ } catch (e) {
177
+ if (e instanceof StreamNotFoundError) {
178
+ response.status(204).end();
179
+ return;
180
+ }
181
+ if (e.status === "DEADLINE_EXCEEDED") {
182
+ response.write(
183
+ `error: ${JSON.stringify({
184
+ error: getCallableJSON(e)
185
+ })}${streamDelimiter}`
186
+ );
187
+ response.end();
188
+ return;
189
+ }
190
+ throw e;
191
+ }
192
+ }
98
193
  function withContextProvider(flow, context) {
99
194
  return {
100
195
  flow,
101
196
  context
102
197
  };
103
198
  }
199
+ function withFlowOptions(flow, options) {
200
+ return {
201
+ flow,
202
+ options
203
+ };
204
+ }
104
205
  function startFlowServer(options) {
105
206
  const server = new FlowServer(options);
106
207
  server.start();
@@ -130,13 +231,11 @@ class FlowServer {
130
231
  logger.debug("Running flow server with flow paths:");
131
232
  const pathPrefix = this.options.pathPrefix ?? "";
132
233
  this.options.flows?.forEach((flow) => {
133
- if ("context" in flow) {
134
- const flowPath = `/${pathPrefix}${flow.flow.__action.name}`;
234
+ if ("flow" in flow) {
235
+ const flowPath = `/${pathPrefix}${"options" in flow && flow.options.path || flow.flow.__action.name}`;
135
236
  logger.debug(` - ${flowPath}`);
136
- server.post(
137
- flowPath,
138
- expressHandler(flow.flow, { contextProvider: flow.context })
139
- );
237
+ const options = "options" in flow ? flow.options : { contextProvider: flow.context };
238
+ server.post(flowPath, expressHandler(flow.flow, options));
140
239
  } else {
141
240
  const flowPath = `/${pathPrefix}${flow.__action.name}`;
142
241
  logger.debug(` - ${flowPath}`);
@@ -190,6 +289,7 @@ export {
190
289
  FlowServer,
191
290
  expressHandler,
192
291
  startFlowServer,
193
- withContextProvider
292
+ withContextProvider,
293
+ withFlowOptions
194
294
  };
195
295
  //# sourceMappingURL=index.mjs.map
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 { type Action, type ActionContext, type Flow, type z } 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 action.run(input, {\n onChunk,\n context,\n abortSignal: abortController.signal,\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":[]}
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 { randomUUID } from 'crypto';\nimport express from 'express';\nimport {\n Action,\n ActionStreamInput,\n AsyncTaskQueue,\n Flow,\n StreamNotFoundError,\n type ActionContext,\n type StreamManager,\n type z,\n} from 'genkit/beta';\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 streamManager?: StreamManager;\n }\n): express.RequestHandler {\n return async (\n request: express.Request,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n const streamIdHeader = request.headers['x-genkit-stream-id'];\n const streamId = Array.isArray(streamIdHeader)\n ? streamIdHeader[0]\n : streamIdHeader;\n\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 const streamManager = opts?.streamManager;\n if (streamManager && streamId) {\n await subscribeToStream(streamManager, streamId, response);\n return;\n }\n const streamIdToUse = randomUUID();\n const headers = {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n };\n if (streamManager) {\n headers['x-genkit-stream-id'] = streamIdToUse;\n }\n response.writeHead(200, headers);\n runActionWithDurableStreaming(\n action,\n streamManager,\n streamIdToUse,\n input,\n context,\n response,\n abortController.signal\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\nasync function runActionWithDurableStreaming<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n streamManager: StreamManager | undefined,\n streamId: string,\n input: z.infer<I>,\n context: ActionContext,\n response: express.Response,\n abortSignal: AbortSignal\n) {\n let taskQueue: AsyncTaskQueue | undefined;\n let durableStream: ActionStreamInput<any, any> | undefined;\n if (streamManager) {\n taskQueue = new AsyncTaskQueue();\n durableStream = await streamManager.open(streamId);\n }\n try {\n let onChunk = (chunk: z.infer<S>) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n if (streamManager) {\n const originalOnChunk = onChunk;\n onChunk = (chunk: z.infer<S>) => {\n originalOnChunk(chunk);\n taskQueue!.enqueue(() => durableStream!.write(chunk));\n };\n }\n const result = await action.run(input, {\n onChunk,\n context,\n abortSignal,\n });\n if (streamManager) {\n taskQueue!.enqueue(() => durableStream!.done(result.result));\n await taskQueue!.merge();\n }\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n } catch (e) {\n if (durableStream) {\n taskQueue!.enqueue(() => durableStream!.error(e));\n await taskQueue!.merge();\n }\n logger.error(\n `Streaming request failed with error: ${(e as Error).message}\\n${\n (e as Error).stack\n }`\n );\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n }\n}\n\nasync function subscribeToStream(\n streamManager: StreamManager,\n streamId: string,\n response: express.Response\n): Promise<void> {\n try {\n await streamManager.subscribe(streamId, {\n onChunk: (chunk) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n },\n onDone: (output) => {\n response.write(\n 'data: ' + JSON.stringify({ result: output }) + streamDelimiter\n );\n response.end();\n },\n onError: (err) => {\n logger.error(\n `Streaming request failed with error: ${(err as Error).message}\\n${\n (err as Error).stack\n }`\n );\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(err),\n })}${streamDelimiter}`\n );\n response.end();\n },\n });\n } catch (e: any) {\n if (e instanceof StreamNotFoundError) {\n response.status(204).end();\n return;\n }\n if (e.status === 'DEADLINE_EXCEEDED') {\n response.write(\n `error: ${JSON.stringify({\n error: getCallableJSON(e),\n })}${streamDelimiter}`\n );\n response.end();\n return;\n }\n throw e;\n }\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n * @deprecated Use `withFlowOptions` instead.\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 * A wrapper object containing a flow with its associated options.\n */\nexport type FlowWithOptions<\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 options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n };\n};\n\n/**\n * Adds an auth policy to the flow.\n * @deprecated Use `withFlowOptions` instead.\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 * Adds an auth policy to the flow.\n */\nexport function withFlowOptions<\n I extends z.ZodTypeAny,\n O extends z.ZodTypeAny,\n S extends z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n options: {\n contextProvider?: ContextProvider<any, I>;\n streamManager?: StreamManager;\n path?: string;\n }\n): FlowWithOptions<I, O, S> {\n return {\n flow,\n options,\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: (\n | Flow<any, any, any>\n | FlowWithContextProvider<any, any, any>\n | FlowWithOptions<any, any, any>\n )[];\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 ('flow' in flow) {\n const flowPath = `/${pathPrefix}${\n ('options' in flow && flow.options.path) || flow.flow.__action.name\n }`;\n logger.debug(` - ${flowPath}`);\n const options =\n 'options' in flow ? flow.options : { contextProvider: flow.context };\n server.post(flowPath, expressHandler(flow.flow, options));\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.24.0",
12
+ "version": "1.26.0-rc.0",
13
13
  "type": "commonjs",
14
14
  "repository": {
15
15
  "type": "git",
@@ -23,8 +23,8 @@
23
23
  "body-parser": "^1.20.3"
24
24
  },
25
25
  "peerDependencies": {
26
- "express": "^4.21.1",
27
- "genkit": "^1.24.0"
26
+ "express": "^4.21.1 || ^5",
27
+ "genkit": "^1.26.0-rc.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "get-port": "^5.1.0",
package/src/index.ts CHANGED
@@ -16,8 +16,18 @@
16
16
 
17
17
  import bodyParser from 'body-parser';
18
18
  import cors, { type CorsOptions } from 'cors';
19
+ import { randomUUID } from 'crypto';
19
20
  import express from 'express';
20
- import { type Action, type ActionContext, type Flow, type z } from 'genkit';
21
+ import {
22
+ Action,
23
+ ActionStreamInput,
24
+ AsyncTaskQueue,
25
+ Flow,
26
+ StreamNotFoundError,
27
+ type ActionContext,
28
+ type StreamManager,
29
+ type z,
30
+ } from 'genkit/beta';
21
31
  import {
22
32
  getCallableJSON,
23
33
  getHttpStatus,
@@ -41,6 +51,7 @@ export function expressHandler<
41
51
  action: Action<I, O, S>,
42
52
  opts?: {
43
53
  contextProvider?: ContextProvider<C, I>;
54
+ streamManager?: StreamManager;
44
55
  }
45
56
  ): express.RequestHandler {
46
57
  return async (
@@ -48,6 +59,11 @@ export function expressHandler<
48
59
  response: express.Response
49
60
  ): Promise<void> => {
50
61
  const { stream } = request.query;
62
+ const streamIdHeader = request.headers['x-genkit-stream-id'];
63
+ const streamId = Array.isArray(streamIdHeader)
64
+ ? streamIdHeader[0]
65
+ : streamIdHeader;
66
+
51
67
  if (!request.body) {
52
68
  const errMsg =
53
69
  `Error: request.body is undefined. ` +
@@ -94,34 +110,29 @@ export function expressHandler<
94
110
  });
95
111
 
96
112
  if (request.get('Accept') === 'text/event-stream' || stream === 'true') {
97
- response.writeHead(200, {
113
+ const streamManager = opts?.streamManager;
114
+ if (streamManager && streamId) {
115
+ await subscribeToStream(streamManager, streamId, response);
116
+ return;
117
+ }
118
+ const streamIdToUse = randomUUID();
119
+ const headers = {
98
120
  'Content-Type': 'text/plain',
99
121
  'Transfer-Encoding': 'chunked',
100
- });
101
- try {
102
- const onChunk = (chunk: z.infer<S>) => {
103
- response.write(
104
- 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter
105
- );
106
- };
107
- const result = await action.run(input, {
108
- onChunk,
109
- context,
110
- abortSignal: abortController.signal,
111
- });
112
- response.write(
113
- 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter
114
- );
115
- response.end();
116
- } catch (e) {
117
- logger.error(
118
- `Streaming request failed with error: ${(e as Error).message}\n${(e as Error).stack}`
119
- );
120
- response.write(
121
- `error: ${JSON.stringify({ error: getCallableJSON(e) })}${streamDelimiter}`
122
- );
123
- response.end();
122
+ };
123
+ if (streamManager) {
124
+ headers['x-genkit-stream-id'] = streamIdToUse;
124
125
  }
126
+ response.writeHead(200, headers);
127
+ runActionWithDurableStreaming(
128
+ action,
129
+ streamManager,
130
+ streamIdToUse,
131
+ input,
132
+ context,
133
+ response,
134
+ abortController.signal
135
+ );
125
136
  } else {
126
137
  try {
127
138
  const result = await action.run(input, {
@@ -148,8 +159,123 @@ export function expressHandler<
148
159
  };
149
160
  }
150
161
 
162
+ async function runActionWithDurableStreaming<
163
+ I extends z.ZodTypeAny,
164
+ O extends z.ZodTypeAny,
165
+ S extends z.ZodTypeAny,
166
+ >(
167
+ action: Action<I, O, S>,
168
+ streamManager: StreamManager | undefined,
169
+ streamId: string,
170
+ input: z.infer<I>,
171
+ context: ActionContext,
172
+ response: express.Response,
173
+ abortSignal: AbortSignal
174
+ ) {
175
+ let taskQueue: AsyncTaskQueue | undefined;
176
+ let durableStream: ActionStreamInput<any, any> | undefined;
177
+ if (streamManager) {
178
+ taskQueue = new AsyncTaskQueue();
179
+ durableStream = await streamManager.open(streamId);
180
+ }
181
+ try {
182
+ let onChunk = (chunk: z.infer<S>) => {
183
+ response.write(
184
+ 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter
185
+ );
186
+ };
187
+ if (streamManager) {
188
+ const originalOnChunk = onChunk;
189
+ onChunk = (chunk: z.infer<S>) => {
190
+ originalOnChunk(chunk);
191
+ taskQueue!.enqueue(() => durableStream!.write(chunk));
192
+ };
193
+ }
194
+ const result = await action.run(input, {
195
+ onChunk,
196
+ context,
197
+ abortSignal,
198
+ });
199
+ if (streamManager) {
200
+ taskQueue!.enqueue(() => durableStream!.done(result.result));
201
+ await taskQueue!.merge();
202
+ }
203
+ response.write(
204
+ 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter
205
+ );
206
+ response.end();
207
+ } catch (e) {
208
+ if (durableStream) {
209
+ taskQueue!.enqueue(() => durableStream!.error(e));
210
+ await taskQueue!.merge();
211
+ }
212
+ logger.error(
213
+ `Streaming request failed with error: ${(e as Error).message}\n${
214
+ (e as Error).stack
215
+ }`
216
+ );
217
+ response.write(
218
+ `error: ${JSON.stringify({
219
+ error: getCallableJSON(e),
220
+ })}${streamDelimiter}`
221
+ );
222
+ response.end();
223
+ }
224
+ }
225
+
226
+ async function subscribeToStream(
227
+ streamManager: StreamManager,
228
+ streamId: string,
229
+ response: express.Response
230
+ ): Promise<void> {
231
+ try {
232
+ await streamManager.subscribe(streamId, {
233
+ onChunk: (chunk) => {
234
+ response.write(
235
+ 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter
236
+ );
237
+ },
238
+ onDone: (output) => {
239
+ response.write(
240
+ 'data: ' + JSON.stringify({ result: output }) + streamDelimiter
241
+ );
242
+ response.end();
243
+ },
244
+ onError: (err) => {
245
+ logger.error(
246
+ `Streaming request failed with error: ${(err as Error).message}\n${
247
+ (err as Error).stack
248
+ }`
249
+ );
250
+ response.write(
251
+ `error: ${JSON.stringify({
252
+ error: getCallableJSON(err),
253
+ })}${streamDelimiter}`
254
+ );
255
+ response.end();
256
+ },
257
+ });
258
+ } catch (e: any) {
259
+ if (e instanceof StreamNotFoundError) {
260
+ response.status(204).end();
261
+ return;
262
+ }
263
+ if (e.status === 'DEADLINE_EXCEEDED') {
264
+ response.write(
265
+ `error: ${JSON.stringify({
266
+ error: getCallableJSON(e),
267
+ })}${streamDelimiter}`
268
+ );
269
+ response.end();
270
+ return;
271
+ }
272
+ throw e;
273
+ }
274
+ }
275
+
151
276
  /**
152
277
  * A wrapper object containing a flow with its associated auth policy.
278
+ * @deprecated Use `withFlowOptions` instead.
153
279
  */
154
280
  export type FlowWithContextProvider<
155
281
  C extends ActionContext = ActionContext,
@@ -161,8 +287,25 @@ export type FlowWithContextProvider<
161
287
  context: ContextProvider<C, I>;
162
288
  };
163
289
 
290
+ /**
291
+ * A wrapper object containing a flow with its associated options.
292
+ */
293
+ export type FlowWithOptions<
294
+ I extends z.ZodTypeAny = z.ZodTypeAny,
295
+ O extends z.ZodTypeAny = z.ZodTypeAny,
296
+ S extends z.ZodTypeAny = z.ZodTypeAny,
297
+ > = {
298
+ flow: Flow<I, O, S>;
299
+ options: {
300
+ contextProvider?: ContextProvider<any, I>;
301
+ streamManager?: StreamManager;
302
+ path?: string;
303
+ };
304
+ };
305
+
164
306
  /**
165
307
  * Adds an auth policy to the flow.
308
+ * @deprecated Use `withFlowOptions` instead.
166
309
  */
167
310
  export function withContextProvider<
168
311
  C extends ActionContext = ActionContext,
@@ -179,12 +322,37 @@ export function withContextProvider<
179
322
  };
180
323
  }
181
324
 
325
+ /**
326
+ * Adds an auth policy to the flow.
327
+ */
328
+ export function withFlowOptions<
329
+ I extends z.ZodTypeAny,
330
+ O extends z.ZodTypeAny,
331
+ S extends z.ZodTypeAny,
332
+ >(
333
+ flow: Flow<I, O, S>,
334
+ options: {
335
+ contextProvider?: ContextProvider<any, I>;
336
+ streamManager?: StreamManager;
337
+ path?: string;
338
+ }
339
+ ): FlowWithOptions<I, O, S> {
340
+ return {
341
+ flow,
342
+ options,
343
+ };
344
+ }
345
+
182
346
  /**
183
347
  * Options to configure the flow server.
184
348
  */
185
349
  export interface FlowServerOptions {
186
350
  /** List of flows to expose via the flow server. */
187
- flows: (Flow<any, any, any> | FlowWithContextProvider<any, any, any>)[];
351
+ flows: (
352
+ | Flow<any, any, any>
353
+ | FlowWithContextProvider<any, any, any>
354
+ | FlowWithOptions<any, any, any>
355
+ )[];
188
356
  /** Port to run the server on. Defaults to env.PORT or 3400. */
189
357
  port?: number;
190
358
  /** CORS options for the server. */
@@ -240,13 +408,14 @@ export class FlowServer {
240
408
  logger.debug('Running flow server with flow paths:');
241
409
  const pathPrefix = this.options.pathPrefix ?? '';
242
410
  this.options.flows?.forEach((flow) => {
243
- if ('context' in flow) {
244
- const flowPath = `/${pathPrefix}${flow.flow.__action.name}`;
411
+ if ('flow' in flow) {
412
+ const flowPath = `/${pathPrefix}${
413
+ ('options' in flow && flow.options.path) || flow.flow.__action.name
414
+ }`;
245
415
  logger.debug(` - ${flowPath}`);
246
- server.post(
247
- flowPath,
248
- expressHandler(flow.flow, { contextProvider: flow.context })
249
- );
416
+ const options =
417
+ 'options' in flow ? flow.options : { contextProvider: flow.context };
418
+ server.post(flowPath, expressHandler(flow.flow, options));
250
419
  } else {
251
420
  const flowPath = `/${pathPrefix}${flow.__action.name}`;
252
421
  logger.debug(` - ${flowPath}`);
@@ -23,6 +23,7 @@ import {
23
23
  type GenerateResponseData,
24
24
  type Genkit,
25
25
  } from 'genkit';
26
+ import { InMemoryStreamManager } from 'genkit/beta';
26
27
  import { runFlow, streamFlow } from 'genkit/beta/client';
27
28
  import type { ContextProvider, RequestData } from 'genkit/context';
28
29
  import type { GenerateResponseChunkData, ModelAction } from 'genkit/model';
@@ -32,7 +33,7 @@ import { afterEach, beforeEach, describe, it } from 'node:test';
32
33
  import {
33
34
  expressHandler,
34
35
  startFlowServer,
35
- withContextProvider,
36
+ withFlowOptions,
36
37
  type FlowServer,
37
38
  } from '../src/index.js';
38
39
 
@@ -144,6 +145,12 @@ describe('expressHandler', async () => {
144
145
  app.post('/stringInput', expressHandler(stringInput));
145
146
  app.post('/objectInput', expressHandler(objectInput));
146
147
  app.post('/streamingFlow', expressHandler(streamingFlow));
148
+ app.post(
149
+ '/streamingFlowDurable',
150
+ expressHandler(streamingFlow, {
151
+ streamManager: new InMemoryStreamManager(),
152
+ })
153
+ );
147
154
  app.post(
148
155
  '/flowWithAuth',
149
156
  expressHandler(flowWithAuth, { contextProvider })
@@ -333,6 +340,88 @@ describe('expressHandler', async () => {
333
340
  assert.strictEqual(await result.output, 'Echo: olleh');
334
341
  });
335
342
 
343
+ it('should create and subscribe to a durable stream', async () => {
344
+ const result = streamFlow({
345
+ url: `http://localhost:${port}/streamingFlowDurable`,
346
+ input: {
347
+ question: 'durable',
348
+ },
349
+ });
350
+
351
+ const streamId = await result.streamId;
352
+ assert.ok(streamId);
353
+
354
+ const subscription = streamFlow({
355
+ url: `http://localhost:${port}/streamingFlowDurable`,
356
+ input: {
357
+ question: 'durable',
358
+ },
359
+ streamId: streamId!,
360
+ });
361
+
362
+ const gotChunks: GenerateResponseChunkData[] = [];
363
+ for await (const chunk of subscription.stream) {
364
+ gotChunks.push(chunk);
365
+ }
366
+
367
+ // of note here is that we're consuming the original stream after re-subscription
368
+ // which should still work fine.
369
+ const originalChunks: GenerateResponseChunkData[] = [];
370
+ for await (const chunk of result.stream) {
371
+ originalChunks.push(chunk);
372
+ }
373
+
374
+ assert.deepStrictEqual(gotChunks, originalChunks);
375
+ assert.strictEqual(await subscription.output, 'Echo: durable');
376
+ assert.strictEqual(await result.output, 'Echo: durable');
377
+ });
378
+
379
+ it('should subscribe to a stream in progress', async () => {
380
+ const result = streamFlow({
381
+ url: `http://localhost:${port}/streamingFlowDurable`,
382
+ input: {
383
+ question: 'durable',
384
+ },
385
+ });
386
+
387
+ const streamId = await result.streamId;
388
+ assert.ok(streamId);
389
+
390
+ // Don't wait for the original stream to finish.
391
+ const subscription = streamFlow({
392
+ url: `http://localhost:${port}/streamingFlowDurable`,
393
+ input: {
394
+ question: 'durable',
395
+ },
396
+ streamId: streamId!,
397
+ });
398
+
399
+ const gotChunks: GenerateResponseChunkData[] = [];
400
+ for await (const chunk of subscription.stream) {
401
+ gotChunks.push(chunk);
402
+ }
403
+
404
+ assert.deepStrictEqual(gotChunks.length, 3);
405
+ assert.strictEqual(await subscription.output, 'Echo: durable');
406
+ });
407
+
408
+ it.only('should return 204 for a non-existent stream', async () => {
409
+ try {
410
+ const result = streamFlow({
411
+ url: `http://localhost:${port}/streamingFlowDurable`,
412
+ input: {
413
+ question: 'durable',
414
+ },
415
+ streamId: 'non-existent-stream-id',
416
+ });
417
+ for await (const _ of result.stream) {
418
+ }
419
+ assert.fail('should have thrown');
420
+ } catch (err: any) {
421
+ assert.strictEqual(err.message, 'NOT_FOUND: Stream not found.');
422
+ }
423
+ });
424
+
336
425
  it('stream a model', async () => {
337
426
  const result = streamFlow({
338
427
  url: `http://localhost:${port}/echoModel`,
@@ -447,8 +536,12 @@ describe('startFlowServer', async () => {
447
536
  stringInput,
448
537
  objectInput,
449
538
  streamingFlow,
539
+ withFlowOptions(streamingFlow, {
540
+ streamManager: new InMemoryStreamManager(),
541
+ path: 'streamingFlowDurable',
542
+ }),
450
543
  streamingFlowV2,
451
- withContextProvider(flowWithAuth, contextProvider),
544
+ withFlowOptions(flowWithAuth, { contextProvider }),
452
545
  ],
453
546
  port,
454
547
  });
@@ -547,6 +640,88 @@ describe('startFlowServer', async () => {
547
640
 
548
641
  assert.strictEqual(await result.output, 'Echo: olleh');
549
642
  });
643
+
644
+ it('should create and subscribe to a durable stream', async () => {
645
+ const result = streamFlow({
646
+ url: `http://localhost:${port}/streamingFlowDurable`,
647
+ input: {
648
+ question: 'durable',
649
+ },
650
+ });
651
+
652
+ const streamId = await result.streamId;
653
+ assert.ok(streamId);
654
+
655
+ const subscription = streamFlow({
656
+ url: `http://localhost:${port}/streamingFlowDurable`,
657
+ input: {
658
+ question: 'durable',
659
+ },
660
+ streamId: streamId!,
661
+ });
662
+
663
+ const gotChunks: GenerateResponseChunkData[] = [];
664
+ for await (const chunk of subscription.stream) {
665
+ gotChunks.push(chunk);
666
+ }
667
+
668
+ // of note here is that we're consuming the original stream after re-subscription
669
+ // which should still work fine.
670
+ const originalChunks: GenerateResponseChunkData[] = [];
671
+ for await (const chunk of result.stream) {
672
+ originalChunks.push(chunk);
673
+ }
674
+
675
+ assert.deepStrictEqual(gotChunks, originalChunks);
676
+ assert.strictEqual(await subscription.output, 'Echo: durable');
677
+ assert.strictEqual(await result.output, 'Echo: durable');
678
+ });
679
+
680
+ it('should subscribe to a stream in progress', async () => {
681
+ const result = streamFlow({
682
+ url: `http://localhost:${port}/streamingFlowDurable`,
683
+ input: {
684
+ question: 'durable',
685
+ },
686
+ });
687
+
688
+ const streamId = await result.streamId;
689
+ assert.ok(streamId);
690
+
691
+ // Don't wait for the original stream to finish.
692
+ const subscription = streamFlow({
693
+ url: `http://localhost:${port}/streamingFlowDurable`,
694
+ input: {
695
+ question: 'durable',
696
+ },
697
+ streamId: streamId!,
698
+ });
699
+
700
+ const gotChunks: GenerateResponseChunkData[] = [];
701
+ for await (const chunk of subscription.stream) {
702
+ gotChunks.push(chunk);
703
+ }
704
+
705
+ assert.deepStrictEqual(gotChunks.length, 3);
706
+ assert.strictEqual(await subscription.output, 'Echo: durable');
707
+ });
708
+
709
+ it('should return 204 for a non-existent stream', async () => {
710
+ try {
711
+ const result = streamFlow({
712
+ url: `http://localhost:${port}/streamingFlowDurable`,
713
+ input: {
714
+ question: 'durable',
715
+ },
716
+ streamId: 'non-existent-stream-id',
717
+ });
718
+ for await (const _ of result.stream) {
719
+ }
720
+ assert.fail('should have thrown');
721
+ } catch (err: any) {
722
+ assert.strictEqual(err.message, 'NOT_FOUND: Stream not found.');
723
+ }
724
+ });
550
725
  });
551
726
 
552
727
  it('stream a flow (v2 model)', async () => {