@genkit-ai/express 1.0.0-rc.9 → 1.0.5
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 +25 -30
- package/lib/index.d.mts +11 -35
- package/lib/index.d.ts +11 -35
- package/lib/index.js +41 -51
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +45 -50
- package/lib/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/index.ts +60 -100
- package/tests/express_test.ts +47 -81
package/README.md
CHANGED
|
@@ -6,17 +6,14 @@ This plugin provides utilities for conveninetly exposing Genkit flows and action
|
|
|
6
6
|
import { expressHandler } from '@genkit-ai/express';
|
|
7
7
|
import express from 'express';
|
|
8
8
|
|
|
9
|
-
const simpleFlow = ai.defineFlow(
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return text;
|
|
18
|
-
}
|
|
19
|
-
);
|
|
9
|
+
const simpleFlow = ai.defineFlow('simpleFlow', async (input, { sendChunk }) => {
|
|
10
|
+
const { text } = await ai.generate({
|
|
11
|
+
model: gemini15Flash,
|
|
12
|
+
prompt: input,
|
|
13
|
+
onChunk: (c) => sendChunk(c.text),
|
|
14
|
+
});
|
|
15
|
+
return text;
|
|
16
|
+
});
|
|
20
17
|
|
|
21
18
|
const app = express();
|
|
22
19
|
app.use(express.json());
|
|
@@ -26,36 +23,34 @@ app.post('/simpleFlow', expressHandler(simpleFlow));
|
|
|
26
23
|
app.listen(8080);
|
|
27
24
|
```
|
|
28
25
|
|
|
29
|
-
You can also
|
|
26
|
+
You can also handle auth using context providers:
|
|
30
27
|
|
|
31
28
|
```ts
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
29
|
+
import { UserFacingError } from 'genkit';
|
|
30
|
+
import { ContextProvider, RequestData } from 'genkit/context';
|
|
31
|
+
|
|
32
|
+
const context: ContextProvider<Context> = (req: RequestData) => {
|
|
33
|
+
if (req.headers['authorization'] !== 'open sesame') {
|
|
34
|
+
throw new UserFacingError('PERMISSION_DENIED', 'not authorized');
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
auth: {
|
|
38
|
+
user: 'Ali Baba',
|
|
39
|
+
},
|
|
38
40
|
};
|
|
39
|
-
next();
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
app.post(
|
|
43
44
|
'/simpleFlow',
|
|
44
45
|
authMiddleware,
|
|
45
|
-
expressHandler(simpleFlow, {
|
|
46
|
-
authPolicy: ({ auth }) => {
|
|
47
|
-
if (auth.user !== 'Ali Baba') {
|
|
48
|
-
throw new Error('not authorized');
|
|
49
|
-
}
|
|
50
|
-
},
|
|
51
|
-
})
|
|
46
|
+
expressHandler(simpleFlow, { context })
|
|
52
47
|
);
|
|
53
48
|
```
|
|
54
49
|
|
|
55
|
-
Flows and actions exposed using the `expressHandler` function can be accessed using `genkit/client` library:
|
|
50
|
+
Flows and actions exposed using the `expressHandler` function can be accessed using `genkit/beta/client` library:
|
|
56
51
|
|
|
57
52
|
```ts
|
|
58
|
-
import { runFlow, streamFlow } from 'genkit/client';
|
|
53
|
+
import { runFlow, streamFlow } from 'genkit/beta/client';
|
|
59
54
|
|
|
60
55
|
const result = await runFlow({
|
|
61
56
|
url: `http://localhost:${port}/simpleFlow`,
|
|
@@ -80,10 +75,10 @@ const result = streamFlow({
|
|
|
80
75
|
url: `http://localhost:${port}/simpleFlow`,
|
|
81
76
|
input: 'say hello',
|
|
82
77
|
});
|
|
83
|
-
for await (const chunk of result.stream
|
|
78
|
+
for await (const chunk of result.stream) {
|
|
84
79
|
console.log(chunk);
|
|
85
80
|
}
|
|
86
|
-
console.log(await result.output
|
|
81
|
+
console.log(await result.output);
|
|
87
82
|
```
|
|
88
83
|
|
|
89
84
|
The sources for this package are in the main [Genkit](https://github.com/firebase/genkit) repo. Please file issues and pull requests against that repo.
|
package/lib/index.d.mts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import bodyParser from 'body-parser';
|
|
2
2
|
import { CorsOptions } from 'cors';
|
|
3
|
-
import express
|
|
4
|
-
import { z, Action, Flow } from 'genkit';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import { ActionContext, z, Action, Flow } from 'genkit';
|
|
5
|
+
import { ContextProvider } from 'genkit/context';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Copyright 2024 Google LLC
|
|
@@ -19,54 +20,29 @@ import { z, Action, Flow } from 'genkit';
|
|
|
19
20
|
* limitations under the License.
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
|
-
/**
|
|
23
|
-
* Auth policy context is an object passed to the auth policy providing details necessary for auth.
|
|
24
|
-
*/
|
|
25
|
-
interface AuthPolicyContext<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> {
|
|
26
|
-
action?: Action<I, O, S>;
|
|
27
|
-
input: z.infer<I>;
|
|
28
|
-
auth?: Record<string, any>;
|
|
29
|
-
request: RequestWithAuth;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Flow Auth policy. Consumes the authorization context of the flow and
|
|
33
|
-
* performs checks before the flow runs. If this throws, the flow will not
|
|
34
|
-
* be executed.
|
|
35
|
-
*/
|
|
36
|
-
interface AuthPolicy<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> {
|
|
37
|
-
(ctx: AuthPolicyContext<I, O, S>): void | Promise<void>;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* For express-based flows, req.auth should contain the value to bepassed into
|
|
41
|
-
* the flow context.
|
|
42
|
-
*/
|
|
43
|
-
interface RequestWithAuth extends express.Request {
|
|
44
|
-
auth?: Record<string, any>;
|
|
45
|
-
}
|
|
46
23
|
/**
|
|
47
24
|
* Exposes provided flow or an action as express handler.
|
|
48
25
|
*/
|
|
49
|
-
declare function expressHandler<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny>(action: Action<I, O, S>, opts?: {
|
|
50
|
-
|
|
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
|
+
contextProvider?: ContextProvider<C, I>;
|
|
51
28
|
}): express.RequestHandler;
|
|
52
29
|
/**
|
|
53
30
|
* A wrapper object containing a flow with its associated auth policy.
|
|
54
31
|
*/
|
|
55
|
-
type
|
|
32
|
+
type FlowWithContextProvider<C extends ActionContext = ActionContext, I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> = {
|
|
56
33
|
flow: Flow<I, O, S>;
|
|
57
|
-
|
|
58
|
-
authPolicy: AuthPolicy;
|
|
34
|
+
context: ContextProvider<C, I>;
|
|
59
35
|
};
|
|
60
36
|
/**
|
|
61
37
|
* Adds an auth policy to the flow.
|
|
62
38
|
*/
|
|
63
|
-
declare function
|
|
39
|
+
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>;
|
|
64
40
|
/**
|
|
65
41
|
* Options to configure the flow server.
|
|
66
42
|
*/
|
|
67
43
|
interface FlowServerOptions {
|
|
68
44
|
/** List of flows to expose via the flow server. */
|
|
69
|
-
flows: (Flow<any, any, any> |
|
|
45
|
+
flows: (Flow<any, any, any> | FlowWithContextProvider<any, any, any>)[];
|
|
70
46
|
/** Port to run the server on. Defaults to env.PORT or 3400. */
|
|
71
47
|
port?: number;
|
|
72
48
|
/** CORS options for the server. */
|
|
@@ -111,4 +87,4 @@ declare class FlowServer {
|
|
|
111
87
|
static stopAll(): Promise<void[]>;
|
|
112
88
|
}
|
|
113
89
|
|
|
114
|
-
export {
|
|
90
|
+
export { FlowServer, type FlowServerOptions, type FlowWithContextProvider, expressHandler, startFlowServer, withContextProvider };
|
package/lib/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import bodyParser from 'body-parser';
|
|
2
2
|
import { CorsOptions } from 'cors';
|
|
3
|
-
import express
|
|
4
|
-
import { z, Action, Flow } from 'genkit';
|
|
3
|
+
import express from 'express';
|
|
4
|
+
import { ActionContext, z, Action, Flow } from 'genkit';
|
|
5
|
+
import { ContextProvider } from 'genkit/context';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Copyright 2024 Google LLC
|
|
@@ -19,54 +20,29 @@ import { z, Action, Flow } from 'genkit';
|
|
|
19
20
|
* limitations under the License.
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
|
-
/**
|
|
23
|
-
* Auth policy context is an object passed to the auth policy providing details necessary for auth.
|
|
24
|
-
*/
|
|
25
|
-
interface AuthPolicyContext<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> {
|
|
26
|
-
action?: Action<I, O, S>;
|
|
27
|
-
input: z.infer<I>;
|
|
28
|
-
auth?: Record<string, any>;
|
|
29
|
-
request: RequestWithAuth;
|
|
30
|
-
}
|
|
31
|
-
/**
|
|
32
|
-
* Flow Auth policy. Consumes the authorization context of the flow and
|
|
33
|
-
* performs checks before the flow runs. If this throws, the flow will not
|
|
34
|
-
* be executed.
|
|
35
|
-
*/
|
|
36
|
-
interface AuthPolicy<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> {
|
|
37
|
-
(ctx: AuthPolicyContext<I, O, S>): void | Promise<void>;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* For express-based flows, req.auth should contain the value to bepassed into
|
|
41
|
-
* the flow context.
|
|
42
|
-
*/
|
|
43
|
-
interface RequestWithAuth extends express.Request {
|
|
44
|
-
auth?: Record<string, any>;
|
|
45
|
-
}
|
|
46
23
|
/**
|
|
47
24
|
* Exposes provided flow or an action as express handler.
|
|
48
25
|
*/
|
|
49
|
-
declare function expressHandler<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny>(action: Action<I, O, S>, opts?: {
|
|
50
|
-
|
|
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
|
+
contextProvider?: ContextProvider<C, I>;
|
|
51
28
|
}): express.RequestHandler;
|
|
52
29
|
/**
|
|
53
30
|
* A wrapper object containing a flow with its associated auth policy.
|
|
54
31
|
*/
|
|
55
|
-
type
|
|
32
|
+
type FlowWithContextProvider<C extends ActionContext = ActionContext, I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> = {
|
|
56
33
|
flow: Flow<I, O, S>;
|
|
57
|
-
|
|
58
|
-
authPolicy: AuthPolicy;
|
|
34
|
+
context: ContextProvider<C, I>;
|
|
59
35
|
};
|
|
60
36
|
/**
|
|
61
37
|
* Adds an auth policy to the flow.
|
|
62
38
|
*/
|
|
63
|
-
declare function
|
|
39
|
+
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>;
|
|
64
40
|
/**
|
|
65
41
|
* Options to configure the flow server.
|
|
66
42
|
*/
|
|
67
43
|
interface FlowServerOptions {
|
|
68
44
|
/** List of flows to expose via the flow server. */
|
|
69
|
-
flows: (Flow<any, any, any> |
|
|
45
|
+
flows: (Flow<any, any, any> | FlowWithContextProvider<any, any, any>)[];
|
|
70
46
|
/** Port to run the server on. Defaults to env.PORT or 3400. */
|
|
71
47
|
port?: number;
|
|
72
48
|
/** CORS options for the server. */
|
|
@@ -111,4 +87,4 @@ declare class FlowServer {
|
|
|
111
87
|
static stopAll(): Promise<void[]>;
|
|
112
88
|
}
|
|
113
89
|
|
|
114
|
-
export {
|
|
90
|
+
export { FlowServer, type FlowServerOptions, type FlowWithContextProvider, expressHandler, startFlowServer, withContextProvider };
|
package/lib/index.js
CHANGED
|
@@ -31,37 +31,38 @@ __export(src_exports, {
|
|
|
31
31
|
FlowServer: () => FlowServer,
|
|
32
32
|
expressHandler: () => expressHandler,
|
|
33
33
|
startFlowServer: () => startFlowServer,
|
|
34
|
-
|
|
34
|
+
withContextProvider: () => withContextProvider
|
|
35
35
|
});
|
|
36
36
|
module.exports = __toCommonJS(src_exports);
|
|
37
|
-
var
|
|
37
|
+
var import_body_parser = __toESM(require("body-parser"));
|
|
38
38
|
var import_cors = __toESM(require("cors"));
|
|
39
39
|
var import_express = __toESM(require("express"));
|
|
40
40
|
var import_genkit = require("genkit");
|
|
41
|
+
var import_context = require("genkit/context");
|
|
41
42
|
var import_logging = require("genkit/logging");
|
|
42
|
-
var import_utils = require("./utils");
|
|
43
43
|
const streamDelimiter = "\n\n";
|
|
44
44
|
function expressHandler(action, opts) {
|
|
45
45
|
return async (request, response) => {
|
|
46
46
|
const { stream } = request.query;
|
|
47
47
|
let input = request.body.data;
|
|
48
|
-
|
|
48
|
+
let context;
|
|
49
49
|
try {
|
|
50
|
-
await opts?.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
context = await opts?.contextProvider?.({
|
|
51
|
+
method: request.method,
|
|
52
|
+
headers: Object.fromEntries(
|
|
53
|
+
Object.entries(request.headers).map(([key, value]) => [
|
|
54
|
+
key.toLowerCase(),
|
|
55
|
+
Array.isArray(value) ? value.join(" ") : String(value)
|
|
56
|
+
])
|
|
57
|
+
),
|
|
58
|
+
input
|
|
59
|
+
}) || {};
|
|
56
60
|
} catch (e) {
|
|
57
|
-
import_logging.logger.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
};
|
|
64
|
-
response.status(403).send(respBody).end();
|
|
61
|
+
import_logging.logger.error(
|
|
62
|
+
`Auth policy failed with error: ${e.message}
|
|
63
|
+
${e.stack}`
|
|
64
|
+
);
|
|
65
|
+
response.status((0, import_context.getHttpStatus)(e)).json((0, import_context.getCallableJSON)(e)).end();
|
|
65
66
|
return;
|
|
66
67
|
}
|
|
67
68
|
if (request.get("Accept") === "text/event-stream" || stream === "true") {
|
|
@@ -80,7 +81,7 @@ function expressHandler(action, opts) {
|
|
|
80
81
|
onChunk,
|
|
81
82
|
() => action.run(input, {
|
|
82
83
|
onChunk,
|
|
83
|
-
context
|
|
84
|
+
context
|
|
84
85
|
})
|
|
85
86
|
);
|
|
86
87
|
response.write(
|
|
@@ -88,43 +89,37 @@ function expressHandler(action, opts) {
|
|
|
88
89
|
);
|
|
89
90
|
response.end();
|
|
90
91
|
} catch (e) {
|
|
91
|
-
import_logging.logger.error(
|
|
92
|
+
import_logging.logger.error(
|
|
93
|
+
`Streaming request failed with error: ${e.message}
|
|
94
|
+
${e.stack}`
|
|
95
|
+
);
|
|
92
96
|
response.write(
|
|
93
|
-
|
|
94
|
-
error: {
|
|
95
|
-
status: "INTERNAL",
|
|
96
|
-
message: (0, import_utils.getErrorMessage)(e),
|
|
97
|
-
details: (0, import_utils.getErrorStack)(e)
|
|
98
|
-
}
|
|
99
|
-
}) + streamDelimiter
|
|
97
|
+
`error: ${JSON.stringify({ error: (0, import_context.getCallableJSON)(e) })}${streamDelimiter}`
|
|
100
98
|
);
|
|
101
99
|
response.end();
|
|
102
100
|
}
|
|
103
101
|
} else {
|
|
104
102
|
try {
|
|
105
|
-
const result = await action.run(input, { context
|
|
103
|
+
const result = await action.run(input, { context });
|
|
106
104
|
response.setHeader("x-genkit-trace-id", result.telemetry.traceId);
|
|
107
105
|
response.setHeader("x-genkit-span-id", result.telemetry.spanId);
|
|
108
|
-
response.status(200).
|
|
106
|
+
response.status(200).json({
|
|
109
107
|
result: result.result
|
|
110
108
|
}).end();
|
|
111
109
|
} catch (e) {
|
|
112
|
-
|
|
113
|
-
error: {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
}).end();
|
|
110
|
+
import_logging.logger.error(
|
|
111
|
+
`Non-streaming request failed with error: ${e.message}
|
|
112
|
+
${e.stack}`
|
|
113
|
+
);
|
|
114
|
+
response.status((0, import_context.getHttpStatus)(e)).json((0, import_context.getCallableJSON)(e)).end();
|
|
119
115
|
}
|
|
120
116
|
}
|
|
121
117
|
};
|
|
122
118
|
}
|
|
123
|
-
function
|
|
119
|
+
function withContextProvider(flow, context) {
|
|
124
120
|
return {
|
|
125
121
|
flow,
|
|
126
|
-
|
|
127
|
-
authPolicy
|
|
122
|
+
context
|
|
128
123
|
};
|
|
129
124
|
}
|
|
130
125
|
function startFlowServer(options) {
|
|
@@ -151,27 +146,22 @@ class FlowServer {
|
|
|
151
146
|
*/
|
|
152
147
|
async start() {
|
|
153
148
|
const server = (0, import_express.default)();
|
|
154
|
-
server.use(
|
|
149
|
+
server.use(import_body_parser.default.json(this.options.jsonParserOptions));
|
|
155
150
|
server.use((0, import_cors.default)(this.options.cors));
|
|
156
151
|
import_logging.logger.debug("Running flow server with flow paths:");
|
|
157
152
|
const pathPrefix = this.options.pathPrefix ?? "";
|
|
158
153
|
this.options.flows?.forEach((flow) => {
|
|
159
|
-
if (flow
|
|
160
|
-
const
|
|
161
|
-
const flowPath = `/${pathPrefix}${flowWithPolicy.flow.__action.name}`;
|
|
154
|
+
if ("context" in flow) {
|
|
155
|
+
const flowPath = `/${pathPrefix}${flow.flow.__action.name}`;
|
|
162
156
|
import_logging.logger.debug(` - ${flowPath}`);
|
|
163
157
|
server.post(
|
|
164
158
|
flowPath,
|
|
165
|
-
|
|
166
|
-
expressHandler(flowWithPolicy.flow, {
|
|
167
|
-
authPolicy: flowWithPolicy.authPolicy
|
|
168
|
-
})
|
|
159
|
+
expressHandler(flow.flow, { contextProvider: flow.context })
|
|
169
160
|
);
|
|
170
161
|
} else {
|
|
171
|
-
const
|
|
172
|
-
const flowPath = `/${pathPrefix}${resolvedFlow.__action.name}`;
|
|
162
|
+
const flowPath = `/${pathPrefix}${flow.__action.name}`;
|
|
173
163
|
import_logging.logger.debug(` - ${flowPath}`);
|
|
174
|
-
server.post(flowPath, expressHandler(
|
|
164
|
+
server.post(flowPath, expressHandler(flow));
|
|
175
165
|
}
|
|
176
166
|
});
|
|
177
167
|
this.port = this.options?.port || (process.env.PORT ? parseInt(process.env.PORT) : 0) || 3400;
|
|
@@ -222,6 +212,6 @@ class FlowServer {
|
|
|
222
212
|
FlowServer,
|
|
223
213
|
expressHandler,
|
|
224
214
|
startFlowServer,
|
|
225
|
-
|
|
215
|
+
withContextProvider
|
|
226
216
|
});
|
|
227
217
|
//# 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 * as bodyParser from 'body-parser';\nimport cors, { CorsOptions } from 'cors';\nimport express, { RequestHandler } from 'express';\nimport { Action, Flow, runWithStreamingCallback, z } from 'genkit';\nimport { logger } from 'genkit/logging';\nimport { Server } from 'http';\nimport { getErrorMessage, getErrorStack } from './utils';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Auth policy context is an object passed to the auth policy providing details necessary for auth.\n */\nexport interface AuthPolicyContext<\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 input: z.infer<I>;\n auth?: Record<string, any>;\n request: RequestWithAuth;\n}\n\n/**\n * Flow Auth policy. Consumes the authorization context of the flow and\n * performs checks before the flow runs. If this throws, the flow will not\n * be executed.\n */\nexport interface AuthPolicy<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> {\n (ctx: AuthPolicyContext<I, O, S>): void | Promise<void>;\n}\n\n/**\n * For express-based flows, req.auth should contain the value to bepassed into\n * the flow context.\n */\nexport interface RequestWithAuth extends express.Request {\n auth?: Record<string, any>;\n}\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\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 authPolicy?: AuthPolicy<I, O, S>;\n }\n): express.RequestHandler {\n return async (\n request: RequestWithAuth,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n let input = request.body.data;\n const auth = request.auth;\n\n try {\n await opts?.authPolicy?.({\n action,\n auth,\n input,\n request,\n });\n } catch (e: any) {\n logger.debug(e);\n const respBody = {\n error: {\n status: 'PERMISSION_DENIED',\n message: e.message || 'Permission denied to resource',\n },\n };\n response.status(403).send(respBody).end();\n return;\n }\n\n if (request.get('Accept') === 'text/event-stream' || stream === 'true') {\n response.writeHead(200, {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n });\n try {\n const onChunk = (chunk: z.infer<S>) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n const result = await runWithStreamingCallback(\n action.__registry,\n onChunk,\n () =>\n action.run(input, {\n onChunk,\n context: { auth },\n })\n );\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n } catch (e) {\n logger.error(e);\n response.write(\n 'data: ' +\n JSON.stringify({\n error: {\n status: 'INTERNAL',\n message: getErrorMessage(e),\n details: getErrorStack(e),\n },\n }) +\n streamDelimiter\n );\n response.end();\n }\n } else {\n try {\n const result = await action.run(input, { context: { auth } });\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 .send({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n response\n .status(500)\n .send({\n error: {\n status: 'INTERNAL',\n message: getErrorMessage(e),\n details: getErrorStack(e),\n },\n })\n .end();\n }\n }\n };\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n */\nexport type FlowWithAuthPolicy<\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 authProvider: RequestHandler;\n authPolicy: AuthPolicy;\n};\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withAuth<\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 authProvider: RequestHandler,\n authPolicy: AuthPolicy\n): FlowWithAuthPolicy<I, O, S> {\n return {\n flow,\n authProvider,\n authPolicy,\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> | FlowWithAuthPolicy<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 ((flow as FlowWithAuthPolicy).authPolicy) {\n const flowWithPolicy = flow as FlowWithAuthPolicy;\n const flowPath = `/${pathPrefix}${flowWithPolicy.flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(\n flowPath,\n flowWithPolicy.authProvider,\n expressHandler(flowWithPolicy.flow, {\n authPolicy: flowWithPolicy.authPolicy,\n })\n );\n } else {\n const resolvedFlow = flow as Flow;\n const flowPath = `/${pathPrefix}${resolvedFlow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(resolvedFlow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":"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","names":["express","cors"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport bodyParser from 'body-parser';\nimport cors, { CorsOptions } from 'cors';\nimport express from 'express';\nimport {\n Action,\n ActionContext,\n Flow,\n runWithStreamingCallback,\n z,\n} from 'genkit';\nimport {\n ContextProvider,\n RequestData,\n getCallableJSON,\n getHttpStatus,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport { Server } from 'http';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n opts?: {\n contextProvider?: ContextProvider<C, I>;\n }\n): express.RequestHandler {\n return async (\n request: express.Request,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n let input = request.body.data as z.infer<I>;\n let context: Record<string, any>;\n\n try {\n context =\n (await opts?.contextProvider?.({\n method: request.method as RequestData['method'],\n headers: Object.fromEntries(\n Object.entries(request.headers).map(([key, value]) => [\n key.toLowerCase(),\n Array.isArray(value) ? value.join(' ') : String(value),\n ])\n ),\n input,\n })) || {};\n } catch (e: any) {\n logger.error(\n `Auth policy failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n return;\n }\n\n if (request.get('Accept') === 'text/event-stream' || stream === 'true') {\n response.writeHead(200, {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n });\n try {\n const onChunk = (chunk: z.infer<S>) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n const result = await runWithStreamingCallback(\n action.__registry,\n onChunk,\n () =>\n action.run(input, {\n onChunk,\n context,\n })\n );\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n } catch (e) {\n logger.error(\n `Streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.write(\n `error: ${JSON.stringify({ error: getCallableJSON(e) })}${streamDelimiter}`\n );\n response.end();\n }\n } else {\n try {\n const result = await action.run(input, { context });\n response.setHeader('x-genkit-trace-id', result.telemetry.traceId);\n response.setHeader('x-genkit-span-id', result.telemetry.spanId);\n // Responses for non-streaming flows are passed back with the flow result stored in a field called \"result.\"\n response\n .status(200)\n .json({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n logger.error(\n `Non-streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n }\n }\n };\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n */\nexport type FlowWithContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n context: ContextProvider<C, I>;\n};\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n context: ContextProvider<C, I>\n): FlowWithContextProvider<C, I, O, S> {\n return {\n flow,\n context,\n };\n}\n\n/**\n * Options to configure the flow server.\n */\nexport interface FlowServerOptions {\n /** List of flows to expose via the flow server. */\n flows: (Flow<any, any, any> | FlowWithContextProvider<any, any, any>)[];\n /** Port to run the server on. Defaults to env.PORT or 3400. */\n port?: number;\n /** CORS options for the server. */\n cors?: CorsOptions;\n /** HTTP method path prefix for the exposed flows. */\n pathPrefix?: string;\n /** JSON body parser options. */\n jsonParserOptions?: bodyParser.OptionsJson;\n}\n\n/**\n * Starts an express server with the provided flows and options.\n */\nexport function startFlowServer(options: FlowServerOptions): FlowServer {\n const server = new FlowServer(options);\n server.start();\n return server;\n}\n\n/**\n * Flow server exposes registered flows as HTTP endpoints.\n *\n * This is for use in production environments.\n *\n * @hidden\n */\nexport class FlowServer {\n /** List of all running servers needed to be cleaned up on process exit. */\n private static RUNNING_SERVERS: FlowServer[] = [];\n\n /** Options for the flow server configured by the developer. */\n private options: FlowServerOptions;\n /** Port the server is actually running on. This may differ from `options.port` if the original was occupied. Null is server is not running. */\n private port: number | null = null;\n /** Express server instance. Null if server is not running. */\n private server: Server | null = null;\n\n constructor(options: FlowServerOptions) {\n this.options = {\n ...options,\n };\n }\n\n /**\n * Starts the server and adds it to the list of running servers to clean up on exit.\n */\n async start() {\n const server = express();\n\n server.use(bodyParser.json(this.options.jsonParserOptions));\n server.use(cors(this.options.cors));\n\n logger.debug('Running flow server with flow paths:');\n const pathPrefix = this.options.pathPrefix ?? '';\n this.options.flows?.forEach((flow) => {\n if ('context' in flow) {\n const flowPath = `/${pathPrefix}${flow.flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(\n flowPath,\n expressHandler(flow.flow, { contextProvider: flow.context })\n );\n } else {\n const flowPath = `/${pathPrefix}${flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(flow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":"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","names":["express","bodyParser","cors"]}
|
package/lib/index.mjs
CHANGED
|
@@ -1,31 +1,37 @@
|
|
|
1
|
-
import
|
|
1
|
+
import bodyParser from "body-parser";
|
|
2
2
|
import cors from "cors";
|
|
3
3
|
import express from "express";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
runWithStreamingCallback
|
|
6
|
+
} from "genkit";
|
|
7
|
+
import {
|
|
8
|
+
getCallableJSON,
|
|
9
|
+
getHttpStatus
|
|
10
|
+
} from "genkit/context";
|
|
5
11
|
import { logger } from "genkit/logging";
|
|
6
|
-
import { getErrorMessage, getErrorStack } from "./utils";
|
|
7
12
|
const streamDelimiter = "\n\n";
|
|
8
13
|
function expressHandler(action, opts) {
|
|
9
14
|
return async (request, response) => {
|
|
10
15
|
const { stream } = request.query;
|
|
11
16
|
let input = request.body.data;
|
|
12
|
-
|
|
17
|
+
let context;
|
|
13
18
|
try {
|
|
14
|
-
await opts?.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
context = await opts?.contextProvider?.({
|
|
20
|
+
method: request.method,
|
|
21
|
+
headers: Object.fromEntries(
|
|
22
|
+
Object.entries(request.headers).map(([key, value]) => [
|
|
23
|
+
key.toLowerCase(),
|
|
24
|
+
Array.isArray(value) ? value.join(" ") : String(value)
|
|
25
|
+
])
|
|
26
|
+
),
|
|
27
|
+
input
|
|
28
|
+
}) || {};
|
|
20
29
|
} catch (e) {
|
|
21
|
-
logger.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
response.status(403).send(respBody).end();
|
|
30
|
+
logger.error(
|
|
31
|
+
`Auth policy failed with error: ${e.message}
|
|
32
|
+
${e.stack}`
|
|
33
|
+
);
|
|
34
|
+
response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();
|
|
29
35
|
return;
|
|
30
36
|
}
|
|
31
37
|
if (request.get("Accept") === "text/event-stream" || stream === "true") {
|
|
@@ -44,7 +50,7 @@ function expressHandler(action, opts) {
|
|
|
44
50
|
onChunk,
|
|
45
51
|
() => action.run(input, {
|
|
46
52
|
onChunk,
|
|
47
|
-
context
|
|
53
|
+
context
|
|
48
54
|
})
|
|
49
55
|
);
|
|
50
56
|
response.write(
|
|
@@ -52,43 +58,37 @@ function expressHandler(action, opts) {
|
|
|
52
58
|
);
|
|
53
59
|
response.end();
|
|
54
60
|
} catch (e) {
|
|
55
|
-
logger.error(
|
|
61
|
+
logger.error(
|
|
62
|
+
`Streaming request failed with error: ${e.message}
|
|
63
|
+
${e.stack}`
|
|
64
|
+
);
|
|
56
65
|
response.write(
|
|
57
|
-
|
|
58
|
-
error: {
|
|
59
|
-
status: "INTERNAL",
|
|
60
|
-
message: getErrorMessage(e),
|
|
61
|
-
details: getErrorStack(e)
|
|
62
|
-
}
|
|
63
|
-
}) + streamDelimiter
|
|
66
|
+
`error: ${JSON.stringify({ error: getCallableJSON(e) })}${streamDelimiter}`
|
|
64
67
|
);
|
|
65
68
|
response.end();
|
|
66
69
|
}
|
|
67
70
|
} else {
|
|
68
71
|
try {
|
|
69
|
-
const result = await action.run(input, { context
|
|
72
|
+
const result = await action.run(input, { context });
|
|
70
73
|
response.setHeader("x-genkit-trace-id", result.telemetry.traceId);
|
|
71
74
|
response.setHeader("x-genkit-span-id", result.telemetry.spanId);
|
|
72
|
-
response.status(200).
|
|
75
|
+
response.status(200).json({
|
|
73
76
|
result: result.result
|
|
74
77
|
}).end();
|
|
75
78
|
} catch (e) {
|
|
76
|
-
|
|
77
|
-
error: {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
}).end();
|
|
79
|
+
logger.error(
|
|
80
|
+
`Non-streaming request failed with error: ${e.message}
|
|
81
|
+
${e.stack}`
|
|
82
|
+
);
|
|
83
|
+
response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
};
|
|
86
87
|
}
|
|
87
|
-
function
|
|
88
|
+
function withContextProvider(flow, context) {
|
|
88
89
|
return {
|
|
89
90
|
flow,
|
|
90
|
-
|
|
91
|
-
authPolicy
|
|
91
|
+
context
|
|
92
92
|
};
|
|
93
93
|
}
|
|
94
94
|
function startFlowServer(options) {
|
|
@@ -120,22 +120,17 @@ class FlowServer {
|
|
|
120
120
|
logger.debug("Running flow server with flow paths:");
|
|
121
121
|
const pathPrefix = this.options.pathPrefix ?? "";
|
|
122
122
|
this.options.flows?.forEach((flow) => {
|
|
123
|
-
if (flow
|
|
124
|
-
const
|
|
125
|
-
const flowPath = `/${pathPrefix}${flowWithPolicy.flow.__action.name}`;
|
|
123
|
+
if ("context" in flow) {
|
|
124
|
+
const flowPath = `/${pathPrefix}${flow.flow.__action.name}`;
|
|
126
125
|
logger.debug(` - ${flowPath}`);
|
|
127
126
|
server.post(
|
|
128
127
|
flowPath,
|
|
129
|
-
|
|
130
|
-
expressHandler(flowWithPolicy.flow, {
|
|
131
|
-
authPolicy: flowWithPolicy.authPolicy
|
|
132
|
-
})
|
|
128
|
+
expressHandler(flow.flow, { contextProvider: flow.context })
|
|
133
129
|
);
|
|
134
130
|
} else {
|
|
135
|
-
const
|
|
136
|
-
const flowPath = `/${pathPrefix}${resolvedFlow.__action.name}`;
|
|
131
|
+
const flowPath = `/${pathPrefix}${flow.__action.name}`;
|
|
137
132
|
logger.debug(` - ${flowPath}`);
|
|
138
|
-
server.post(flowPath, expressHandler(
|
|
133
|
+
server.post(flowPath, expressHandler(flow));
|
|
139
134
|
}
|
|
140
135
|
});
|
|
141
136
|
this.port = this.options?.port || (process.env.PORT ? parseInt(process.env.PORT) : 0) || 3400;
|
|
@@ -185,6 +180,6 @@ export {
|
|
|
185
180
|
FlowServer,
|
|
186
181
|
expressHandler,
|
|
187
182
|
startFlowServer,
|
|
188
|
-
|
|
183
|
+
withContextProvider
|
|
189
184
|
};
|
|
190
185
|
//# 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 * as bodyParser from 'body-parser';\nimport cors, { CorsOptions } from 'cors';\nimport express, { RequestHandler } from 'express';\nimport { Action, Flow, runWithStreamingCallback, z } from 'genkit';\nimport { logger } from 'genkit/logging';\nimport { Server } from 'http';\nimport { getErrorMessage, getErrorStack } from './utils';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Auth policy context is an object passed to the auth policy providing details necessary for auth.\n */\nexport interface AuthPolicyContext<\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 input: z.infer<I>;\n auth?: Record<string, any>;\n request: RequestWithAuth;\n}\n\n/**\n * Flow Auth policy. Consumes the authorization context of the flow and\n * performs checks before the flow runs. If this throws, the flow will not\n * be executed.\n */\nexport interface AuthPolicy<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> {\n (ctx: AuthPolicyContext<I, O, S>): void | Promise<void>;\n}\n\n/**\n * For express-based flows, req.auth should contain the value to bepassed into\n * the flow context.\n */\nexport interface RequestWithAuth extends express.Request {\n auth?: Record<string, any>;\n}\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\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 authPolicy?: AuthPolicy<I, O, S>;\n }\n): express.RequestHandler {\n return async (\n request: RequestWithAuth,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n let input = request.body.data;\n const auth = request.auth;\n\n try {\n await opts?.authPolicy?.({\n action,\n auth,\n input,\n request,\n });\n } catch (e: any) {\n logger.debug(e);\n const respBody = {\n error: {\n status: 'PERMISSION_DENIED',\n message: e.message || 'Permission denied to resource',\n },\n };\n response.status(403).send(respBody).end();\n return;\n }\n\n if (request.get('Accept') === 'text/event-stream' || stream === 'true') {\n response.writeHead(200, {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n });\n try {\n const onChunk = (chunk: z.infer<S>) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n const result = await runWithStreamingCallback(\n action.__registry,\n onChunk,\n () =>\n action.run(input, {\n onChunk,\n context: { auth },\n })\n );\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n } catch (e) {\n logger.error(e);\n response.write(\n 'data: ' +\n JSON.stringify({\n error: {\n status: 'INTERNAL',\n message: getErrorMessage(e),\n details: getErrorStack(e),\n },\n }) +\n streamDelimiter\n );\n response.end();\n }\n } else {\n try {\n const result = await action.run(input, { context: { auth } });\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 .send({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n response\n .status(500)\n .send({\n error: {\n status: 'INTERNAL',\n message: getErrorMessage(e),\n details: getErrorStack(e),\n },\n })\n .end();\n }\n }\n };\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n */\nexport type FlowWithAuthPolicy<\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 authProvider: RequestHandler;\n authPolicy: AuthPolicy;\n};\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withAuth<\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 authProvider: RequestHandler,\n authPolicy: AuthPolicy\n): FlowWithAuthPolicy<I, O, S> {\n return {\n flow,\n authProvider,\n authPolicy,\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> | FlowWithAuthPolicy<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 ((flow as FlowWithAuthPolicy).authPolicy) {\n const flowWithPolicy = flow as FlowWithAuthPolicy;\n const flowPath = `/${pathPrefix}${flowWithPolicy.flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(\n flowPath,\n flowWithPolicy.authProvider,\n expressHandler(flowWithPolicy.flow, {\n authPolicy: flowWithPolicy.authPolicy,\n })\n );\n } else {\n const resolvedFlow = flow as Flow;\n const flowPath = `/${pathPrefix}${resolvedFlow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(resolvedFlow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["/**\n * Copyright 2024 Google LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport bodyParser from 'body-parser';\nimport cors, { CorsOptions } from 'cors';\nimport express from 'express';\nimport {\n Action,\n ActionContext,\n Flow,\n runWithStreamingCallback,\n z,\n} from 'genkit';\nimport {\n ContextProvider,\n RequestData,\n getCallableJSON,\n getHttpStatus,\n} from 'genkit/context';\nimport { logger } from 'genkit/logging';\nimport { Server } from 'http';\n\nconst streamDelimiter = '\\n\\n';\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function expressHandler<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n action: Action<I, O, S>,\n opts?: {\n contextProvider?: ContextProvider<C, I>;\n }\n): express.RequestHandler {\n return async (\n request: express.Request,\n response: express.Response\n ): Promise<void> => {\n const { stream } = request.query;\n let input = request.body.data as z.infer<I>;\n let context: Record<string, any>;\n\n try {\n context =\n (await opts?.contextProvider?.({\n method: request.method as RequestData['method'],\n headers: Object.fromEntries(\n Object.entries(request.headers).map(([key, value]) => [\n key.toLowerCase(),\n Array.isArray(value) ? value.join(' ') : String(value),\n ])\n ),\n input,\n })) || {};\n } catch (e: any) {\n logger.error(\n `Auth policy failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n return;\n }\n\n if (request.get('Accept') === 'text/event-stream' || stream === 'true') {\n response.writeHead(200, {\n 'Content-Type': 'text/plain',\n 'Transfer-Encoding': 'chunked',\n });\n try {\n const onChunk = (chunk: z.infer<S>) => {\n response.write(\n 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter\n );\n };\n const result = await runWithStreamingCallback(\n action.__registry,\n onChunk,\n () =>\n action.run(input, {\n onChunk,\n context,\n })\n );\n response.write(\n 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter\n );\n response.end();\n } catch (e) {\n logger.error(\n `Streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.write(\n `error: ${JSON.stringify({ error: getCallableJSON(e) })}${streamDelimiter}`\n );\n response.end();\n }\n } else {\n try {\n const result = await action.run(input, { context });\n response.setHeader('x-genkit-trace-id', result.telemetry.traceId);\n response.setHeader('x-genkit-span-id', result.telemetry.spanId);\n // Responses for non-streaming flows are passed back with the flow result stored in a field called \"result.\"\n response\n .status(200)\n .json({\n result: result.result,\n })\n .end();\n } catch (e) {\n // Errors for non-streaming flows are passed back as standard API errors.\n logger.error(\n `Non-streaming request failed with error: ${(e as Error).message}\\n${(e as Error).stack}`\n );\n response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();\n }\n }\n };\n}\n\n/**\n * A wrapper object containing a flow with its associated auth policy.\n */\nexport type FlowWithContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n> = {\n flow: Flow<I, O, S>;\n context: ContextProvider<C, I>;\n};\n\n/**\n * Adds an auth policy to the flow.\n */\nexport function withContextProvider<\n C extends ActionContext = ActionContext,\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n flow: Flow<I, O, S>,\n context: ContextProvider<C, I>\n): FlowWithContextProvider<C, I, O, S> {\n return {\n flow,\n context,\n };\n}\n\n/**\n * Options to configure the flow server.\n */\nexport interface FlowServerOptions {\n /** List of flows to expose via the flow server. */\n flows: (Flow<any, any, any> | FlowWithContextProvider<any, any, any>)[];\n /** Port to run the server on. Defaults to env.PORT or 3400. */\n port?: number;\n /** CORS options for the server. */\n cors?: CorsOptions;\n /** HTTP method path prefix for the exposed flows. */\n pathPrefix?: string;\n /** JSON body parser options. */\n jsonParserOptions?: bodyParser.OptionsJson;\n}\n\n/**\n * Starts an express server with the provided flows and options.\n */\nexport function startFlowServer(options: FlowServerOptions): FlowServer {\n const server = new FlowServer(options);\n server.start();\n return server;\n}\n\n/**\n * Flow server exposes registered flows as HTTP endpoints.\n *\n * This is for use in production environments.\n *\n * @hidden\n */\nexport class FlowServer {\n /** List of all running servers needed to be cleaned up on process exit. */\n private static RUNNING_SERVERS: FlowServer[] = [];\n\n /** Options for the flow server configured by the developer. */\n private options: FlowServerOptions;\n /** Port the server is actually running on. This may differ from `options.port` if the original was occupied. Null is server is not running. */\n private port: number | null = null;\n /** Express server instance. Null if server is not running. */\n private server: Server | null = null;\n\n constructor(options: FlowServerOptions) {\n this.options = {\n ...options,\n };\n }\n\n /**\n * Starts the server and adds it to the list of running servers to clean up on exit.\n */\n async start() {\n const server = express();\n\n server.use(bodyParser.json(this.options.jsonParserOptions));\n server.use(cors(this.options.cors));\n\n logger.debug('Running flow server with flow paths:');\n const pathPrefix = this.options.pathPrefix ?? '';\n this.options.flows?.forEach((flow) => {\n if ('context' in flow) {\n const flowPath = `/${pathPrefix}${flow.flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(\n flowPath,\n expressHandler(flow.flow, { contextProvider: flow.context })\n );\n } else {\n const flowPath = `/${pathPrefix}${flow.__action.name}`;\n logger.debug(` - ${flowPath}`);\n server.post(flowPath, expressHandler(flow));\n }\n });\n this.port =\n this.options?.port ||\n (process.env.PORT ? parseInt(process.env.PORT) : 0) ||\n 3400;\n this.server = server.listen(this.port, () => {\n logger.debug(`Flow server running on http://localhost:${this.port}`);\n FlowServer.RUNNING_SERVERS.push(this);\n });\n }\n\n /**\n * Stops the server and removes it from the list of running servers to clean up on exit.\n */\n async stop(): Promise<void> {\n if (!this.server) {\n return;\n }\n return new Promise<void>((resolve, reject) => {\n this.server!.close((err) => {\n if (err) {\n logger.error(\n `Error shutting down flow server on port ${this.port}: ${err}`\n );\n reject(err);\n }\n const index = FlowServer.RUNNING_SERVERS.indexOf(this);\n if (index > -1) {\n FlowServer.RUNNING_SERVERS.splice(index, 1);\n }\n logger.debug(\n `Flow server on port ${this.port} has successfully shut down.`\n );\n this.port = null;\n this.server = null;\n resolve();\n });\n });\n }\n\n /**\n * Stops all running servers.\n */\n static async stopAll() {\n return Promise.all(\n FlowServer.RUNNING_SERVERS.map((server) => server.stop())\n );\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"genai",
|
|
10
10
|
"generative-ai"
|
|
11
11
|
],
|
|
12
|
-
"version": "1.0.
|
|
12
|
+
"version": "1.0.5",
|
|
13
13
|
"type": "commonjs",
|
|
14
14
|
"repository": {
|
|
15
15
|
"type": "git",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {
|
|
26
26
|
"express": "^4.21.1",
|
|
27
|
-
"genkit": "1.0.
|
|
27
|
+
"@genkit-ai/core": "1.0.5",
|
|
28
|
+
"genkit": "^1.0.5"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
30
31
|
"get-port": "^5.1.0",
|
package/src/index.ts
CHANGED
|
@@ -14,88 +14,66 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import
|
|
17
|
+
import bodyParser from 'body-parser';
|
|
18
18
|
import cors, { CorsOptions } from 'cors';
|
|
19
|
-
import express
|
|
20
|
-
import {
|
|
19
|
+
import express from 'express';
|
|
20
|
+
import {
|
|
21
|
+
Action,
|
|
22
|
+
ActionContext,
|
|
23
|
+
Flow,
|
|
24
|
+
runWithStreamingCallback,
|
|
25
|
+
z,
|
|
26
|
+
} from 'genkit';
|
|
27
|
+
import {
|
|
28
|
+
ContextProvider,
|
|
29
|
+
RequestData,
|
|
30
|
+
getCallableJSON,
|
|
31
|
+
getHttpStatus,
|
|
32
|
+
} from 'genkit/context';
|
|
21
33
|
import { logger } from 'genkit/logging';
|
|
22
34
|
import { Server } from 'http';
|
|
23
|
-
import { getErrorMessage, getErrorStack } from './utils';
|
|
24
35
|
|
|
25
36
|
const streamDelimiter = '\n\n';
|
|
26
37
|
|
|
27
|
-
/**
|
|
28
|
-
* Auth policy context is an object passed to the auth policy providing details necessary for auth.
|
|
29
|
-
*/
|
|
30
|
-
export interface AuthPolicyContext<
|
|
31
|
-
I extends z.ZodTypeAny = z.ZodTypeAny,
|
|
32
|
-
O extends z.ZodTypeAny = z.ZodTypeAny,
|
|
33
|
-
S extends z.ZodTypeAny = z.ZodTypeAny,
|
|
34
|
-
> {
|
|
35
|
-
action?: Action<I, O, S>;
|
|
36
|
-
input: z.infer<I>;
|
|
37
|
-
auth?: Record<string, any>;
|
|
38
|
-
request: RequestWithAuth;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Flow Auth policy. Consumes the authorization context of the flow and
|
|
43
|
-
* performs checks before the flow runs. If this throws, the flow will not
|
|
44
|
-
* be executed.
|
|
45
|
-
*/
|
|
46
|
-
export interface AuthPolicy<
|
|
47
|
-
I extends z.ZodTypeAny = z.ZodTypeAny,
|
|
48
|
-
O extends z.ZodTypeAny = z.ZodTypeAny,
|
|
49
|
-
S extends z.ZodTypeAny = z.ZodTypeAny,
|
|
50
|
-
> {
|
|
51
|
-
(ctx: AuthPolicyContext<I, O, S>): void | Promise<void>;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* For express-based flows, req.auth should contain the value to bepassed into
|
|
56
|
-
* the flow context.
|
|
57
|
-
*/
|
|
58
|
-
export interface RequestWithAuth extends express.Request {
|
|
59
|
-
auth?: Record<string, any>;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
38
|
/**
|
|
63
39
|
* Exposes provided flow or an action as express handler.
|
|
64
40
|
*/
|
|
65
41
|
export function expressHandler<
|
|
42
|
+
C extends ActionContext = ActionContext,
|
|
66
43
|
I extends z.ZodTypeAny = z.ZodTypeAny,
|
|
67
44
|
O extends z.ZodTypeAny = z.ZodTypeAny,
|
|
68
45
|
S extends z.ZodTypeAny = z.ZodTypeAny,
|
|
69
46
|
>(
|
|
70
47
|
action: Action<I, O, S>,
|
|
71
48
|
opts?: {
|
|
72
|
-
|
|
49
|
+
contextProvider?: ContextProvider<C, I>;
|
|
73
50
|
}
|
|
74
51
|
): express.RequestHandler {
|
|
75
52
|
return async (
|
|
76
|
-
request:
|
|
53
|
+
request: express.Request,
|
|
77
54
|
response: express.Response
|
|
78
55
|
): Promise<void> => {
|
|
79
56
|
const { stream } = request.query;
|
|
80
|
-
let input = request.body.data
|
|
81
|
-
|
|
57
|
+
let input = request.body.data as z.infer<I>;
|
|
58
|
+
let context: Record<string, any>;
|
|
82
59
|
|
|
83
60
|
try {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
61
|
+
context =
|
|
62
|
+
(await opts?.contextProvider?.({
|
|
63
|
+
method: request.method as RequestData['method'],
|
|
64
|
+
headers: Object.fromEntries(
|
|
65
|
+
Object.entries(request.headers).map(([key, value]) => [
|
|
66
|
+
key.toLowerCase(),
|
|
67
|
+
Array.isArray(value) ? value.join(' ') : String(value),
|
|
68
|
+
])
|
|
69
|
+
),
|
|
70
|
+
input,
|
|
71
|
+
})) || {};
|
|
90
72
|
} catch (e: any) {
|
|
91
|
-
logger.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
message: e.message || 'Permission denied to resource',
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
response.status(403).send(respBody).end();
|
|
73
|
+
logger.error(
|
|
74
|
+
`Auth policy failed with error: ${(e as Error).message}\n${(e as Error).stack}`
|
|
75
|
+
);
|
|
76
|
+
response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();
|
|
99
77
|
return;
|
|
100
78
|
}
|
|
101
79
|
|
|
@@ -116,7 +94,7 @@ export function expressHandler<
|
|
|
116
94
|
() =>
|
|
117
95
|
action.run(input, {
|
|
118
96
|
onChunk,
|
|
119
|
-
context
|
|
97
|
+
context,
|
|
120
98
|
})
|
|
121
99
|
);
|
|
122
100
|
response.write(
|
|
@@ -124,44 +102,32 @@ export function expressHandler<
|
|
|
124
102
|
);
|
|
125
103
|
response.end();
|
|
126
104
|
} catch (e) {
|
|
127
|
-
logger.error(
|
|
105
|
+
logger.error(
|
|
106
|
+
`Streaming request failed with error: ${(e as Error).message}\n${(e as Error).stack}`
|
|
107
|
+
);
|
|
128
108
|
response.write(
|
|
129
|
-
|
|
130
|
-
JSON.stringify({
|
|
131
|
-
error: {
|
|
132
|
-
status: 'INTERNAL',
|
|
133
|
-
message: getErrorMessage(e),
|
|
134
|
-
details: getErrorStack(e),
|
|
135
|
-
},
|
|
136
|
-
}) +
|
|
137
|
-
streamDelimiter
|
|
109
|
+
`error: ${JSON.stringify({ error: getCallableJSON(e) })}${streamDelimiter}`
|
|
138
110
|
);
|
|
139
111
|
response.end();
|
|
140
112
|
}
|
|
141
113
|
} else {
|
|
142
114
|
try {
|
|
143
|
-
const result = await action.run(input, { context
|
|
115
|
+
const result = await action.run(input, { context });
|
|
144
116
|
response.setHeader('x-genkit-trace-id', result.telemetry.traceId);
|
|
145
117
|
response.setHeader('x-genkit-span-id', result.telemetry.spanId);
|
|
146
118
|
// Responses for non-streaming flows are passed back with the flow result stored in a field called "result."
|
|
147
119
|
response
|
|
148
120
|
.status(200)
|
|
149
|
-
.
|
|
121
|
+
.json({
|
|
150
122
|
result: result.result,
|
|
151
123
|
})
|
|
152
124
|
.end();
|
|
153
125
|
} catch (e) {
|
|
154
126
|
// Errors for non-streaming flows are passed back as standard API errors.
|
|
155
|
-
|
|
156
|
-
.
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
status: 'INTERNAL',
|
|
160
|
-
message: getErrorMessage(e),
|
|
161
|
-
details: getErrorStack(e),
|
|
162
|
-
},
|
|
163
|
-
})
|
|
164
|
-
.end();
|
|
127
|
+
logger.error(
|
|
128
|
+
`Non-streaming request failed with error: ${(e as Error).message}\n${(e as Error).stack}`
|
|
129
|
+
);
|
|
130
|
+
response.status(getHttpStatus(e)).json(getCallableJSON(e)).end();
|
|
165
131
|
}
|
|
166
132
|
}
|
|
167
133
|
};
|
|
@@ -170,32 +136,31 @@ export function expressHandler<
|
|
|
170
136
|
/**
|
|
171
137
|
* A wrapper object containing a flow with its associated auth policy.
|
|
172
138
|
*/
|
|
173
|
-
export type
|
|
139
|
+
export type FlowWithContextProvider<
|
|
140
|
+
C extends ActionContext = ActionContext,
|
|
174
141
|
I extends z.ZodTypeAny = z.ZodTypeAny,
|
|
175
142
|
O extends z.ZodTypeAny = z.ZodTypeAny,
|
|
176
143
|
S extends z.ZodTypeAny = z.ZodTypeAny,
|
|
177
144
|
> = {
|
|
178
145
|
flow: Flow<I, O, S>;
|
|
179
|
-
|
|
180
|
-
authPolicy: AuthPolicy;
|
|
146
|
+
context: ContextProvider<C, I>;
|
|
181
147
|
};
|
|
182
148
|
|
|
183
149
|
/**
|
|
184
150
|
* Adds an auth policy to the flow.
|
|
185
151
|
*/
|
|
186
|
-
export function
|
|
152
|
+
export function withContextProvider<
|
|
153
|
+
C extends ActionContext = ActionContext,
|
|
187
154
|
I extends z.ZodTypeAny = z.ZodTypeAny,
|
|
188
155
|
O extends z.ZodTypeAny = z.ZodTypeAny,
|
|
189
156
|
S extends z.ZodTypeAny = z.ZodTypeAny,
|
|
190
157
|
>(
|
|
191
158
|
flow: Flow<I, O, S>,
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
): FlowWithAuthPolicy<I, O, S> {
|
|
159
|
+
context: ContextProvider<C, I>
|
|
160
|
+
): FlowWithContextProvider<C, I, O, S> {
|
|
195
161
|
return {
|
|
196
162
|
flow,
|
|
197
|
-
|
|
198
|
-
authPolicy,
|
|
163
|
+
context,
|
|
199
164
|
};
|
|
200
165
|
}
|
|
201
166
|
|
|
@@ -204,7 +169,7 @@ export function withAuth<
|
|
|
204
169
|
*/
|
|
205
170
|
export interface FlowServerOptions {
|
|
206
171
|
/** List of flows to expose via the flow server. */
|
|
207
|
-
flows: (Flow<any, any, any> |
|
|
172
|
+
flows: (Flow<any, any, any> | FlowWithContextProvider<any, any, any>)[];
|
|
208
173
|
/** Port to run the server on. Defaults to env.PORT or 3400. */
|
|
209
174
|
port?: number;
|
|
210
175
|
/** CORS options for the server. */
|
|
@@ -260,22 +225,17 @@ export class FlowServer {
|
|
|
260
225
|
logger.debug('Running flow server with flow paths:');
|
|
261
226
|
const pathPrefix = this.options.pathPrefix ?? '';
|
|
262
227
|
this.options.flows?.forEach((flow) => {
|
|
263
|
-
if (
|
|
264
|
-
const
|
|
265
|
-
const flowPath = `/${pathPrefix}${flowWithPolicy.flow.__action.name}`;
|
|
228
|
+
if ('context' in flow) {
|
|
229
|
+
const flowPath = `/${pathPrefix}${flow.flow.__action.name}`;
|
|
266
230
|
logger.debug(` - ${flowPath}`);
|
|
267
231
|
server.post(
|
|
268
232
|
flowPath,
|
|
269
|
-
|
|
270
|
-
expressHandler(flowWithPolicy.flow, {
|
|
271
|
-
authPolicy: flowWithPolicy.authPolicy,
|
|
272
|
-
})
|
|
233
|
+
expressHandler(flow.flow, { contextProvider: flow.context })
|
|
273
234
|
);
|
|
274
235
|
} else {
|
|
275
|
-
const
|
|
276
|
-
const flowPath = `/${pathPrefix}${resolvedFlow.__action.name}`;
|
|
236
|
+
const flowPath = `/${pathPrefix}${flow.__action.name}`;
|
|
277
237
|
logger.debug(` - ${flowPath}`);
|
|
278
|
-
server.post(flowPath, expressHandler(
|
|
238
|
+
server.post(flowPath, expressHandler(flow));
|
|
279
239
|
}
|
|
280
240
|
});
|
|
281
241
|
this.port =
|
package/tests/express_test.ts
CHANGED
|
@@ -14,22 +14,50 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { RequestData } from '@genkit-ai/core';
|
|
17
18
|
import * as assert from 'assert';
|
|
18
19
|
import express from 'express';
|
|
19
|
-
import {
|
|
20
|
-
|
|
20
|
+
import {
|
|
21
|
+
GenerateResponseData,
|
|
22
|
+
Genkit,
|
|
23
|
+
UserFacingError,
|
|
24
|
+
genkit,
|
|
25
|
+
z,
|
|
26
|
+
} from 'genkit';
|
|
27
|
+
import { runFlow, streamFlow } from 'genkit/beta/client';
|
|
28
|
+
import { ContextProvider } from 'genkit/context';
|
|
21
29
|
import { GenerateResponseChunkData, ModelAction } from 'genkit/model';
|
|
22
30
|
import getPort from 'get-port';
|
|
23
31
|
import * as http from 'http';
|
|
24
32
|
import { afterEach, beforeEach, describe, it } from 'node:test';
|
|
25
33
|
import {
|
|
26
34
|
FlowServer,
|
|
27
|
-
RequestWithAuth,
|
|
28
35
|
expressHandler,
|
|
29
36
|
startFlowServer,
|
|
30
|
-
|
|
37
|
+
withContextProvider,
|
|
31
38
|
} from '../src/index.js';
|
|
32
39
|
|
|
40
|
+
interface Context {
|
|
41
|
+
auth: {
|
|
42
|
+
user: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const contextProvider: ContextProvider<Context> = (req: RequestData) => {
|
|
47
|
+
assert.ok(req.method, 'method must be set');
|
|
48
|
+
assert.ok(req.headers, 'headers must be set');
|
|
49
|
+
assert.ok(req.input, 'input must be set');
|
|
50
|
+
|
|
51
|
+
if (req.headers['authorization'] !== 'open sesame') {
|
|
52
|
+
throw new UserFacingError('PERMISSION_DENIED', 'not authorized');
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
auth: {
|
|
56
|
+
user: 'Ali Baba',
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
33
61
|
describe('expressHandler', async () => {
|
|
34
62
|
let server: http.Server;
|
|
35
63
|
let port;
|
|
@@ -82,7 +110,7 @@ describe('expressHandler', async () => {
|
|
|
82
110
|
inputSchema: z.object({ question: z.string() }),
|
|
83
111
|
},
|
|
84
112
|
async (input, { context }) => {
|
|
85
|
-
return `${input.question} - ${JSON.stringify(context
|
|
113
|
+
return `${input.question} - ${JSON.stringify(context!.auth)}`;
|
|
86
114
|
}
|
|
87
115
|
);
|
|
88
116
|
|
|
@@ -96,54 +124,13 @@ describe('expressHandler', async () => {
|
|
|
96
124
|
app.post('/streamingFlow', expressHandler(streamingFlow));
|
|
97
125
|
app.post(
|
|
98
126
|
'/flowWithAuth',
|
|
99
|
-
|
|
100
|
-
(req as RequestWithAuth).auth = {
|
|
101
|
-
user:
|
|
102
|
-
req.header('authorization') === 'open sesame'
|
|
103
|
-
? 'Ali Baba'
|
|
104
|
-
: '40 thieves',
|
|
105
|
-
};
|
|
106
|
-
next();
|
|
107
|
-
},
|
|
108
|
-
expressHandler(flowWithAuth, {
|
|
109
|
-
authPolicy: ({ auth, action, input, request }) => {
|
|
110
|
-
assert.ok(auth, 'auth must be set');
|
|
111
|
-
assert.ok(action, 'flow must be set');
|
|
112
|
-
assert.ok(input, 'input must be set');
|
|
113
|
-
assert.ok(request, 'request must be set');
|
|
114
|
-
|
|
115
|
-
if (auth.user !== 'Ali Baba') {
|
|
116
|
-
throw new Error('not authorized');
|
|
117
|
-
}
|
|
118
|
-
},
|
|
119
|
-
})
|
|
127
|
+
expressHandler(flowWithAuth, { contextProvider })
|
|
120
128
|
);
|
|
121
|
-
|
|
122
129
|
// Can also expose any action.
|
|
123
130
|
app.post('/echoModel', expressHandler(echoModel));
|
|
124
131
|
app.post(
|
|
125
132
|
'/echoModelWithAuth',
|
|
126
|
-
|
|
127
|
-
(req as RequestWithAuth).auth = {
|
|
128
|
-
user:
|
|
129
|
-
req.header('authorization') === 'open sesame'
|
|
130
|
-
? 'Ali Baba'
|
|
131
|
-
: '40 thieves',
|
|
132
|
-
};
|
|
133
|
-
next();
|
|
134
|
-
},
|
|
135
|
-
expressHandler(echoModel, {
|
|
136
|
-
authPolicy: ({ auth, action, input, request }) => {
|
|
137
|
-
assert.ok(auth, 'auth must be set');
|
|
138
|
-
assert.ok(action, 'flow must be set');
|
|
139
|
-
assert.ok(input, 'input must be set');
|
|
140
|
-
assert.ok(request, 'request must be set');
|
|
141
|
-
|
|
142
|
-
if (auth.user !== 'Ali Baba') {
|
|
143
|
-
throw new Error('not authorized');
|
|
144
|
-
}
|
|
145
|
-
},
|
|
146
|
-
})
|
|
133
|
+
expressHandler(echoModel, { contextProvider })
|
|
147
134
|
);
|
|
148
135
|
|
|
149
136
|
server = app.listen(port, () => {
|
|
@@ -213,7 +200,7 @@ describe('expressHandler', async () => {
|
|
|
213
200
|
question: 'hello',
|
|
214
201
|
},
|
|
215
202
|
headers: {
|
|
216
|
-
Authorization: '
|
|
203
|
+
Authorization: 'thief #24',
|
|
217
204
|
},
|
|
218
205
|
});
|
|
219
206
|
await assert.rejects(result, (err) => {
|
|
@@ -264,7 +251,7 @@ describe('expressHandler', async () => {
|
|
|
264
251
|
],
|
|
265
252
|
},
|
|
266
253
|
headers: {
|
|
267
|
-
Authorization: '
|
|
254
|
+
Authorization: 'thief #24',
|
|
268
255
|
},
|
|
269
256
|
});
|
|
270
257
|
await assert.rejects(result, (err) => {
|
|
@@ -283,7 +270,7 @@ describe('expressHandler', async () => {
|
|
|
283
270
|
});
|
|
284
271
|
|
|
285
272
|
const gotChunks: GenerateResponseChunkData[] = [];
|
|
286
|
-
for await (const chunk of result.stream
|
|
273
|
+
for await (const chunk of result.stream) {
|
|
287
274
|
gotChunks.push(chunk);
|
|
288
275
|
}
|
|
289
276
|
|
|
@@ -293,7 +280,7 @@ describe('expressHandler', async () => {
|
|
|
293
280
|
{ index: 0, role: 'model', content: [{ text: '1' }] },
|
|
294
281
|
]);
|
|
295
282
|
|
|
296
|
-
assert.strictEqual(await result.output
|
|
283
|
+
assert.strictEqual(await result.output, 'Echo: olleh');
|
|
297
284
|
});
|
|
298
285
|
|
|
299
286
|
it('stream a model', async () => {
|
|
@@ -310,11 +297,11 @@ describe('expressHandler', async () => {
|
|
|
310
297
|
});
|
|
311
298
|
|
|
312
299
|
const gotChunks: any[] = [];
|
|
313
|
-
for await (const chunk of result.stream
|
|
300
|
+
for await (const chunk of result.stream) {
|
|
314
301
|
gotChunks.push(chunk);
|
|
315
302
|
}
|
|
316
303
|
|
|
317
|
-
const output = await result.output
|
|
304
|
+
const output = await result.output;
|
|
318
305
|
assert.strictEqual(output.finishReason, 'stop');
|
|
319
306
|
assert.deepStrictEqual(output.message, {
|
|
320
307
|
role: 'model',
|
|
@@ -336,7 +323,7 @@ describe('startFlowServer', async () => {
|
|
|
336
323
|
|
|
337
324
|
beforeEach(async () => {
|
|
338
325
|
const ai = genkit({});
|
|
339
|
-
|
|
326
|
+
defineEchoModel(ai);
|
|
340
327
|
|
|
341
328
|
const voidInput = ai.defineFlow('voidInput', async () => {
|
|
342
329
|
return 'banana';
|
|
@@ -382,7 +369,7 @@ describe('startFlowServer', async () => {
|
|
|
382
369
|
inputSchema: z.object({ question: z.string() }),
|
|
383
370
|
},
|
|
384
371
|
async (input, { context }) => {
|
|
385
|
-
return `${input.question} - ${JSON.stringify(context
|
|
372
|
+
return `${input.question} - ${JSON.stringify(context!.auth)}`;
|
|
386
373
|
}
|
|
387
374
|
);
|
|
388
375
|
|
|
@@ -394,28 +381,7 @@ describe('startFlowServer', async () => {
|
|
|
394
381
|
stringInput,
|
|
395
382
|
objectInput,
|
|
396
383
|
streamingFlow,
|
|
397
|
-
|
|
398
|
-
flowWithAuth,
|
|
399
|
-
async (req, resp, next) => {
|
|
400
|
-
(req as RequestWithAuth).auth = {
|
|
401
|
-
user:
|
|
402
|
-
req.header('authorization') === 'open sesame'
|
|
403
|
-
? 'Ali Baba'
|
|
404
|
-
: '40 thieves',
|
|
405
|
-
};
|
|
406
|
-
return next();
|
|
407
|
-
},
|
|
408
|
-
({ auth, action, input, request }) => {
|
|
409
|
-
assert.ok(auth, 'auth must be set');
|
|
410
|
-
assert.ok(action, 'flow must be set');
|
|
411
|
-
assert.ok(input, 'input must be set');
|
|
412
|
-
assert.ok(request, 'request must be set');
|
|
413
|
-
|
|
414
|
-
if (auth.user !== 'Ali Baba') {
|
|
415
|
-
throw new Error('not authorized');
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
),
|
|
384
|
+
withContextProvider(flowWithAuth, contextProvider),
|
|
419
385
|
],
|
|
420
386
|
port,
|
|
421
387
|
});
|
|
@@ -483,7 +449,7 @@ describe('startFlowServer', async () => {
|
|
|
483
449
|
question: 'hello',
|
|
484
450
|
},
|
|
485
451
|
headers: {
|
|
486
|
-
Authorization: '
|
|
452
|
+
Authorization: 'thief #24',
|
|
487
453
|
},
|
|
488
454
|
});
|
|
489
455
|
await assert.rejects(result, (err) => {
|
|
@@ -502,7 +468,7 @@ describe('startFlowServer', async () => {
|
|
|
502
468
|
});
|
|
503
469
|
|
|
504
470
|
const gotChunks: GenerateResponseChunkData[] = [];
|
|
505
|
-
for await (const chunk of result.stream
|
|
471
|
+
for await (const chunk of result.stream) {
|
|
506
472
|
gotChunks.push(chunk);
|
|
507
473
|
}
|
|
508
474
|
|
|
@@ -512,7 +478,7 @@ describe('startFlowServer', async () => {
|
|
|
512
478
|
{ index: 0, role: 'model', content: [{ text: '1' }] },
|
|
513
479
|
]);
|
|
514
480
|
|
|
515
|
-
assert.strictEqual(await result.output
|
|
481
|
+
assert.strictEqual(await result.output, 'Echo: olleh');
|
|
516
482
|
});
|
|
517
483
|
});
|
|
518
484
|
});
|