@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 +38 -0
- package/lib/index.d.mts +25 -3
- package/lib/index.d.ts +25 -3
- package/lib/index.js +132 -34
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +133 -33
- package/lib/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +202 -33
- package/tests/express_test.ts +177 -2
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
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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 ("
|
|
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
|
-
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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 ("
|
|
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
|
-
|
|
137
|
-
|
|
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":"AAgBA,OAAO,gBAAgB;AACvB,OAAO,UAAgC;AACvC,OAAO,aAAa;AAEpB;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP,SAAS,cAAc;AAGvB,MAAM,kBAAkB;AAKjB,SAAS,eAMd,QACA,MAGwB;AACxB,SAAO,OACL,SACA,aACkB;AAClB,UAAM,EAAE,OAAO,IAAI,QAAQ;AAC3B,QAAI,CAAC,QAAQ,MAAM;AACjB,YAAM,SACJ;AAGF,aAAO,MAAM,MAAM;AACnB,eACG,OAAO,GAAG,EACV,KAAK,EAAE,SAAS,QAAQ,QAAQ,mBAAmB,CAAC,EACpD,IAAI;AACP;AAAA,IACF;AAEA,UAAM,QAAQ,QAAQ,KAAK;AAC3B,QAAI;AAEJ,QAAI;AACF,gBACG,MAAM,MAAM,kBAAkB;AAAA,QAC7B,QAAQ,QAAQ;AAAA,QAChB,SAAS,OAAO;AAAA,UACd,OAAO,QAAQ,QAAQ,OAAO,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,MAAM;AAAA,YACpD,IAAI,YAAY;AAAA,YAChB,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,GAAG,IAAI,OAAO,KAAK;AAAA,UACvD,CAAC;AAAA,QACH;AAAA,QACA;AAAA,MACF,CAAC,KAAM,CAAC;AAAA,IACZ,SAAS,GAAQ;AACf,aAAO;AAAA,QACL,kCAAmC,EAAY,OAAO;AAAA,EAAM,EAAY,KAAK;AAAA,MAC/E;AACA,eAAS,OAAO,cAAc,CAAC,CAAC,EAAE,KAAK,gBAAgB,CAAC,CAAC,EAAE,IAAI;AAC/D;AAAA,IACF;AAEA,UAAM,kBAAkB,IAAI,gBAAgB;AAC5C,YAAQ,GAAG,SAAS,MAAM;AACxB,sBAAgB,MAAM;AAAA,IACxB,CAAC;AAED,YAAQ,GAAG,WAAW,MAAM;AAC1B,sBAAgB,MAAM;AAAA,IACxB,CAAC;AAED,QAAI,QAAQ,IAAI,QAAQ,MAAM,uBAAuB,WAAW,QAAQ;AACtE,eAAS,UAAU,KAAK;AAAA,QACtB,gBAAgB;AAAA,QAChB,qBAAqB;AAAA,MACvB,CAAC;AACD,UAAI;AACF,cAAM,UAAU,CAAC,UAAsB;AACrC,mBAAS;AAAA,YACP,WAAW,KAAK,UAAU,EAAE,SAAS,MAAM,CAAC,IAAI;AAAA,UAClD;AAAA,QACF;AACA,cAAM,SAAS,MAAM,OAAO,IAAI,OAAO;AAAA,UACrC;AAAA,UACA;AAAA,UACA,aAAa,gBAAgB;AAAA,QAC/B,CAAC;AACD,iBAAS;AAAA,UACP,WAAW,KAAK,UAAU,EAAE,QAAQ,OAAO,OAAO,CAAC,IAAI;AAAA,QACzD;AACA,iBAAS,IAAI;AAAA,MACf,SAAS,GAAG;AACV,eAAO;AAAA,UACL,wCAAyC,EAAY,OAAO;AAAA,EAAM,EAAY,KAAK;AAAA,QACrF;AACA,iBAAS;AAAA,UACP,UAAU,KAAK,UAAU,EAAE,OAAO,gBAAgB,CAAC,EAAE,CAAC,CAAC,GAAG,eAAe;AAAA,QAC3E;AACA,iBAAS,IAAI;AAAA,MACf;AAAA,IACF,OAAO;AACL,UAAI;AACF,cAAM,SAAS,MAAM,OAAO,IAAI,OAAO;AAAA,UACrC;AAAA,UACA,aAAa,gBAAgB;AAAA,QAC/B,CAAC;AACD,iBAAS,UAAU,qBAAqB,OAAO,UAAU,OAAO;AAChE,iBAAS,UAAU,oBAAoB,OAAO,UAAU,MAAM;AAE9D,iBACG,OAAO,GAAG,EACV,KAAK;AAAA,UACJ,QAAQ,OAAO;AAAA,QACjB,CAAC,EACA,IAAI;AAAA,MACT,SAAS,GAAG;AAEV,eAAO;AAAA,UACL,4CAA6C,EAAY,OAAO;AAAA,EAAM,EAAY,KAAK;AAAA,QACzF;AACA,iBAAS,OAAO,cAAc,CAAC,CAAC,EAAE,KAAK,gBAAgB,CAAC,CAAC,EAAE,IAAI;AAAA,MACjE;AAAA,IACF;AAAA,EACF;AACF;AAkBO,SAAS,oBAMd,MACA,SACqC;AACrC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;AAqBO,SAAS,gBAAgB,SAAwC;AACtE,QAAM,SAAS,IAAI,WAAW,OAAO;AACrC,SAAO,MAAM;AACb,SAAO;AACT;AASO,MAAM,WAAW;AAAA;AAAA,EAEtB,OAAe,kBAAgC,CAAC;AAAA;AAAA,EAGxC;AAAA;AAAA,EAEA,OAAsB;AAAA;AAAA,EAEtB,SAAwB;AAAA,EAEhC,YAAY,SAA4B;AACtC,SAAK,UAAU;AAAA,MACb,GAAG;AAAA,IACL;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,QAAQ;AACZ,UAAM,SAAS,QAAQ;AAEvB,WAAO,IAAI,WAAW,KAAK,KAAK,QAAQ,iBAAiB,CAAC;AAC1D,WAAO,IAAI,KAAK,KAAK,QAAQ,IAAI,CAAC;AAElC,WAAO,MAAM,sCAAsC;AACnD,UAAM,aAAa,KAAK,QAAQ,cAAc;AAC9C,SAAK,QAAQ,OAAO,QAAQ,CAAC,SAAS;AACpC,UAAI,aAAa,MAAM;AACrB,cAAM,WAAW,IAAI,UAAU,GAAG,KAAK,KAAK,SAAS,IAAI;AACzD,eAAO,MAAM,MAAM,QAAQ,EAAE;AAC7B,eAAO;AAAA,UACL;AAAA,UACA,eAAe,KAAK,MAAM,EAAE,iBAAiB,KAAK,QAAQ,CAAC;AAAA,QAC7D;AAAA,MACF,OAAO;AACL,cAAM,WAAW,IAAI,UAAU,GAAG,KAAK,SAAS,IAAI;AACpD,eAAO,MAAM,MAAM,QAAQ,EAAE;AAC7B,eAAO,KAAK,UAAU,eAAe,IAAI,CAAC;AAAA,MAC5C;AAAA,IACF,CAAC;AACD,SAAK,OACH,KAAK,SAAS,SACb,QAAQ,IAAI,OAAO,OAAO,SAAS,QAAQ,IAAI,IAAI,IAAI,MACxD;AACF,SAAK,SAAS,OAAO,OAAO,KAAK,MAAM,MAAM;AAC3C,aAAO,MAAM,2CAA2C,KAAK,IAAI,EAAE;AACnE,iBAAW,gBAAgB,KAAK,IAAI;AAAA,IACtC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM,OAAsB;AAC1B,QAAI,CAAC,KAAK,QAAQ;AAChB;AAAA,IACF;AACA,WAAO,IAAI,QAAc,CAAC,SAAS,WAAW;AAC5C,WAAK,OAAQ,MAAM,CAAC,QAAQ;AAC1B,YAAI,KAAK;AACP,iBAAO;AAAA,YACL,2CAA2C,KAAK,IAAI,KAAK,GAAG;AAAA,UAC9D;AACA,iBAAO,GAAG;AAAA,QACZ;AACA,cAAM,QAAQ,WAAW,gBAAgB,QAAQ,IAAI;AACrD,YAAI,QAAQ,IAAI;AACd,qBAAW,gBAAgB,OAAO,OAAO,CAAC;AAAA,QAC5C;AACA,eAAO;AAAA,UACL,uBAAuB,KAAK,IAAI;AAAA,QAClC;AACA,aAAK,OAAO;AACZ,aAAK,SAAS;AACd,gBAAQ;AAAA,MACV,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa,UAAU;AACrB,WAAO,QAAQ;AAAA,MACb,WAAW,gBAAgB,IAAI,CAAC,WAAW,OAAO,KAAK,CAAC;AAAA,IAC1D;AAAA,EACF;AACF;","names":[]}
|
|
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.
|
|
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.
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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: (
|
|
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 ('
|
|
244
|
-
const flowPath = `/${pathPrefix}${
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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}`);
|
package/tests/express_test.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 () => {
|