@genkit-ai/express 1.0.0-rc.1

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 ADDED
@@ -0,0 +1,93 @@
1
+ # Genkit Express Plugin
2
+
3
+ This plugin provides utilities for conveninetly exposing Genkit flows and actions via Express HTTP server as REST APIs.
4
+
5
+ ```ts
6
+ import { handler } from '@genkit-ai/express';
7
+ import express from 'express';
8
+
9
+ const simpleFlow = ai.defineFlow(
10
+ 'simpleFlow',
11
+ async (input, streamingCallback) => {
12
+ const { text } = await ai.generate({
13
+ model: gemini15Flash,
14
+ prompt: input,
15
+ streamingCallback,
16
+ });
17
+ return text;
18
+ }
19
+ );
20
+
21
+ const app = express();
22
+ app.use(express.json());
23
+
24
+ app.post('/simpleFlow', handler(simpleFlow));
25
+
26
+ app.listen(8080);
27
+ ```
28
+
29
+ You can also set auth policies:
30
+
31
+ ```ts
32
+ // middleware for handling auth headers.
33
+ const authMiddleware = async (req, resp, next) => {
34
+ // parse auth headers and convert to auth object.
35
+ (req as RequestWithAuth).auth = {
36
+ user:
37
+ req.header('authorization') === 'open sesame' ? 'Ali Baba' : '40 thieves',
38
+ };
39
+ next();
40
+ };
41
+
42
+ app.post(
43
+ '/simpleFlow',
44
+ authMiddleware,
45
+ handler(simpleFlow, {
46
+ authPolicy: ({ auth }) => {
47
+ if (auth.user !== 'Ali Baba') {
48
+ throw new Error('not authorized');
49
+ }
50
+ },
51
+ })
52
+ );
53
+ ```
54
+
55
+ Flows and actions exposed using the `handler` function can be accessed using `genkit/client` library:
56
+
57
+ ```ts
58
+ import { runFlow, streamFlow } from 'genkit/client';
59
+
60
+ const result = await runFlow({
61
+ url: `http://localhost:${port}/simpleFlow`,
62
+ input: 'say hello',
63
+ });
64
+
65
+ console.log(result); // hello
66
+
67
+ // set auth headers (when using auth policies)
68
+ const result = await runFlow({
69
+ url: `http://localhost:${port}/simpleFlow`,
70
+ headers: {
71
+ Authorization: 'open sesame',
72
+ },
73
+ input: 'say hello',
74
+ });
75
+
76
+ console.log(result); // hello
77
+
78
+ // and streamed
79
+ const result = streamFlow({
80
+ url: `http://localhost:${port}/simpleFlow`,
81
+ input: 'say hello',
82
+ });
83
+ for await (const chunk of result.stream()) {
84
+ console.log(chunk);
85
+ }
86
+ console.log(await result.output());
87
+ ```
88
+
89
+ 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.
90
+
91
+ Usage information and reference details can be found in [Genkit documentation](https://firebase.google.com/docs/genkit).
92
+
93
+ License: Apache 2.0
@@ -0,0 +1,25 @@
1
+ var __async = (__this, __arguments, generator) => {
2
+ return new Promise((resolve, reject) => {
3
+ var fulfilled = (value) => {
4
+ try {
5
+ step(generator.next(value));
6
+ } catch (e) {
7
+ reject(e);
8
+ }
9
+ };
10
+ var rejected = (value) => {
11
+ try {
12
+ step(generator.throw(value));
13
+ } catch (e) {
14
+ reject(e);
15
+ }
16
+ };
17
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
18
+ step((generator = generator.apply(__this, __arguments)).next());
19
+ });
20
+ };
21
+
22
+ export {
23
+ __async
24
+ };
25
+ //# sourceMappingURL=chunk-IQXHJV5O.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,52 @@
1
+ import express from 'express';
2
+ import { z, Flow, Action, CallableFlow } from 'genkit';
3
+
4
+ /**
5
+ * Copyright 2024 Google LLC
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+
20
+ /**
21
+ * Auth policy context is an object passed to the auth policy providing details necessary for auth.
22
+ */
23
+ interface AuthPolicyContext<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> {
24
+ flow?: Flow<I, O, S>;
25
+ action?: Action<I, O, S>;
26
+ input: z.infer<I>;
27
+ auth: any | undefined;
28
+ request: RequestWithAuth;
29
+ }
30
+ /**
31
+ * Flow Auth policy. Consumes the authorization context of the flow and
32
+ * performs checks before the flow runs. If this throws, the flow will not
33
+ * be executed.
34
+ */
35
+ interface AuthPolicy<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> {
36
+ (ctx: AuthPolicyContext<I, O, S>): void | Promise<void>;
37
+ }
38
+ /**
39
+ * For express-based flows, req.auth should contain the value to bepassed into
40
+ * the flow context.
41
+ */
42
+ interface RequestWithAuth extends express.Request {
43
+ auth?: unknown;
44
+ }
45
+ /**
46
+ * Exposes provided flow or an action as express handler.
47
+ */
48
+ declare function handler<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny>(f: CallableFlow<I, O, S> | Flow<I, O, S> | Action<I, O, S>, opts?: {
49
+ authPolicy?: AuthPolicy<I, O, S>;
50
+ }): express.RequestHandler;
51
+
52
+ export { type AuthPolicy, type AuthPolicyContext, type RequestWithAuth, handler };
package/lib/index.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ import express from 'express';
2
+ import { z, Flow, Action, CallableFlow } from 'genkit';
3
+
4
+ /**
5
+ * Copyright 2024 Google LLC
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the "License");
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+
20
+ /**
21
+ * Auth policy context is an object passed to the auth policy providing details necessary for auth.
22
+ */
23
+ interface AuthPolicyContext<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> {
24
+ flow?: Flow<I, O, S>;
25
+ action?: Action<I, O, S>;
26
+ input: z.infer<I>;
27
+ auth: any | undefined;
28
+ request: RequestWithAuth;
29
+ }
30
+ /**
31
+ * Flow Auth policy. Consumes the authorization context of the flow and
32
+ * performs checks before the flow runs. If this throws, the flow will not
33
+ * be executed.
34
+ */
35
+ interface AuthPolicy<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny> {
36
+ (ctx: AuthPolicyContext<I, O, S>): void | Promise<void>;
37
+ }
38
+ /**
39
+ * For express-based flows, req.auth should contain the value to bepassed into
40
+ * the flow context.
41
+ */
42
+ interface RequestWithAuth extends express.Request {
43
+ auth?: unknown;
44
+ }
45
+ /**
46
+ * Exposes provided flow or an action as express handler.
47
+ */
48
+ declare function handler<I extends z.ZodTypeAny = z.ZodTypeAny, O extends z.ZodTypeAny = z.ZodTypeAny, S extends z.ZodTypeAny = z.ZodTypeAny>(f: CallableFlow<I, O, S> | Flow<I, O, S> | Action<I, O, S>, opts?: {
49
+ authPolicy?: AuthPolicy<I, O, S>;
50
+ }): express.RequestHandler;
51
+
52
+ export { type AuthPolicy, type AuthPolicyContext, type RequestWithAuth, handler };
package/lib/index.js ADDED
@@ -0,0 +1,136 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var __async = (__this, __arguments, generator) => {
20
+ return new Promise((resolve, reject) => {
21
+ var fulfilled = (value) => {
22
+ try {
23
+ step(generator.next(value));
24
+ } catch (e) {
25
+ reject(e);
26
+ }
27
+ };
28
+ var rejected = (value) => {
29
+ try {
30
+ step(generator.throw(value));
31
+ } catch (e) {
32
+ reject(e);
33
+ }
34
+ };
35
+ var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
36
+ step((generator = generator.apply(__this, __arguments)).next());
37
+ });
38
+ };
39
+ var src_exports = {};
40
+ __export(src_exports, {
41
+ handler: () => handler
42
+ });
43
+ module.exports = __toCommonJS(src_exports);
44
+ var import_genkit = require("genkit");
45
+ var import_logging = require("genkit/logging");
46
+ var import_utils = require("./utils");
47
+ const streamDelimiter = "\n\n";
48
+ function handler(f, opts) {
49
+ const flow = f.invoke ? f : f.flow ? f.flow : void 0;
50
+ const action = flow ? flow.action : f;
51
+ const registry = flow ? flow.registry : action.__registry;
52
+ return (request, response) => __async(this, null, function* () {
53
+ var _a;
54
+ const { stream } = request.query;
55
+ let input = request.body.data;
56
+ const auth = request.auth;
57
+ try {
58
+ yield (_a = opts == null ? void 0 : opts.authPolicy) == null ? void 0 : _a.call(opts, {
59
+ flow,
60
+ action,
61
+ auth,
62
+ input,
63
+ request
64
+ });
65
+ } catch (e) {
66
+ import_logging.logger.debug(e);
67
+ const respBody = {
68
+ error: {
69
+ status: "PERMISSION_DENIED",
70
+ message: e.message || "Permission denied to resource"
71
+ }
72
+ };
73
+ response.status(403).send(respBody).end();
74
+ return;
75
+ }
76
+ if (request.get("Accept") === "text/event-stream" || stream === "true") {
77
+ response.writeHead(200, {
78
+ "Content-Type": "text/plain",
79
+ "Transfer-Encoding": "chunked"
80
+ });
81
+ try {
82
+ const onChunk = (chunk) => {
83
+ response.write(
84
+ "data: " + JSON.stringify({ message: chunk }) + streamDelimiter
85
+ );
86
+ };
87
+ const result = yield (0, import_genkit.runWithStreamingCallback)(
88
+ registry,
89
+ onChunk,
90
+ () => action.run(input, {
91
+ onChunk,
92
+ context: auth
93
+ })
94
+ );
95
+ response.write(
96
+ "data: " + JSON.stringify({ result: result.result }) + streamDelimiter
97
+ );
98
+ response.end();
99
+ } catch (e) {
100
+ import_logging.logger.error(e);
101
+ response.write(
102
+ "data: " + JSON.stringify({
103
+ error: {
104
+ status: "INTERNAL",
105
+ message: (0, import_utils.getErrorMessage)(e),
106
+ details: (0, import_utils.getErrorStack)(e)
107
+ }
108
+ }) + streamDelimiter
109
+ );
110
+ response.end();
111
+ }
112
+ } else {
113
+ try {
114
+ const result = yield action.run(input, { context: auth });
115
+ response.setHeader("x-genkit-trace-id", result.telemetry.traceId);
116
+ response.setHeader("x-genkit-span-id", result.telemetry.spanId);
117
+ response.status(200).send({
118
+ result: result.result
119
+ }).end();
120
+ } catch (e) {
121
+ response.status(500).send({
122
+ error: {
123
+ status: "INTERNAL",
124
+ message: (0, import_utils.getErrorMessage)(e),
125
+ details: (0, import_utils.getErrorStack)(e)
126
+ }
127
+ }).end();
128
+ }
129
+ }
130
+ });
131
+ }
132
+ // Annotate the CommonJS export names for ESM import in node:
133
+ 0 && (module.exports = {
134
+ handler
135
+ });
136
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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 express from 'express';\nimport {\n Action,\n CallableFlow,\n Flow,\n runWithStreamingCallback,\n z,\n} from 'genkit';\nimport { logger } from 'genkit/logging';\nimport { Registry } from 'genkit/registry';\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 flow?: Flow<I, O, S>;\n action?: Action<I, O, S>;\n input: z.infer<I>;\n auth: any | undefined;\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?: unknown;\n}\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function handler<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n f: CallableFlow<I, O, S> | Flow<I, O, S> | Action<I, O, S>,\n opts?: {\n authPolicy?: AuthPolicy<I, O, S>;\n }\n): express.RequestHandler {\n const flow: Flow<I, O, S> | undefined = (f as Flow<I, O, S>).invoke\n ? (f as Flow<I, O, S>)\n : (f as CallableFlow<I, O, S>).flow\n ? (f as CallableFlow<I, O, S>).flow\n : undefined;\n const action: Action<I, O, S> = flow ? flow.action : (f as Action<I, O, S>);\n const registry: Registry = flow ? flow.registry : action.__registry;\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 flow,\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(registry, onChunk, () =>\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"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAiBA,oBAMO;AACP,qBAAuB;AAEvB,mBAA+C;AAE/C,MAAM,kBAAkB;AAyCjB,SAAS,QAKd,GACA,MAGwB;AACxB,QAAM,OAAmC,EAAoB,SACxD,IACA,EAA4B,OAC1B,EAA4B,OAC7B;AACN,QAAM,SAA0B,OAAO,KAAK,SAAU;AACtD,QAAM,WAAqB,OAAO,KAAK,WAAW,OAAO;AACzD,SAAO,CACL,SACA,aACkB;AAzFtB;AA0FI,UAAM,EAAE,OAAO,IAAI,QAAQ;AAC3B,QAAI,QAAQ,QAAQ,KAAK;AACzB,UAAM,OAAO,QAAQ;AAErB,QAAI;AACF,aAAM,kCAAM,eAAN,8BAAmB;AAAA,QACvB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF,SAAS,GAAQ;AACf,4BAAO,MAAM,CAAC;AACd,YAAM,WAAW;AAAA,QACf,OAAO;AAAA,UACL,QAAQ;AAAA,UACR,SAAS,EAAE,WAAW;AAAA,QACxB;AAAA,MACF;AACA,eAAS,OAAO,GAAG,EAAE,KAAK,QAAQ,EAAE,IAAI;AACxC;AAAA,IACF;AAEA,QAAI,QAAQ,IAAI,QAAQ,MAAM,uBAAuB,WAAW,QAAQ;AACtE,eAAS,UAAU,KAAK;AAAA,QACtB,gBAAgB;AAAA,QAChB,qBAAqB;AAAA,MACvB,CAAC;AACD,UAAI;AACF,cAAM,UAAU,CAAC,UAAsB;AACrC,mBAAS;AAAA,YACP,WAAW,KAAK,UAAU,EAAE,SAAS,MAAM,CAAC,IAAI;AAAA,UAClD;AAAA,QACF;AACA,cAAM,SAAS,UAAM;AAAA,UAAyB;AAAA,UAAU;AAAA,UAAS,MAC/D,OAAO,IAAI,OAAO;AAAA,YAChB;AAAA,YACA,SAAS;AAAA,UACX,CAAC;AAAA,QACH;AACA,iBAAS;AAAA,UACP,WAAW,KAAK,UAAU,EAAE,QAAQ,OAAO,OAAO,CAAC,IAAI;AAAA,QACzD;AACA,iBAAS,IAAI;AAAA,MACf,SAAS,GAAG;AACV,8BAAO,MAAM,CAAC;AACd,iBAAS;AAAA,UACP,WACE,KAAK,UAAU;AAAA,YACb,OAAO;AAAA,cACL,QAAQ;AAAA,cACR,aAAS,8BAAgB,CAAC;AAAA,cAC1B,aAAS,4BAAc,CAAC;AAAA,YAC1B;AAAA,UACF,CAAC,IACD;AAAA,QACJ;AACA,iBAAS,IAAI;AAAA,MACf;AAAA,IACF,OAAO;AACL,UAAI;AACF,cAAM,SAAS,MAAM,OAAO,IAAI,OAAO,EAAE,SAAS,KAAK,CAAC;AACxD,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,iBACG,OAAO,GAAG,EACV,KAAK;AAAA,UACJ,OAAO;AAAA,YACL,QAAQ;AAAA,YACR,aAAS,8BAAgB,CAAC;AAAA,YAC1B,aAAS,4BAAc,CAAC;AAAA,UAC1B;AAAA,QACF,CAAC,EACA,IAAI;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
package/lib/index.mjs ADDED
@@ -0,0 +1,97 @@
1
+ import {
2
+ __async
3
+ } from "./chunk-IQXHJV5O.mjs";
4
+ import {
5
+ runWithStreamingCallback
6
+ } from "genkit";
7
+ import { logger } from "genkit/logging";
8
+ import { getErrorMessage, getErrorStack } from "./utils";
9
+ const streamDelimiter = "\n\n";
10
+ function handler(f, opts) {
11
+ const flow = f.invoke ? f : f.flow ? f.flow : void 0;
12
+ const action = flow ? flow.action : f;
13
+ const registry = flow ? flow.registry : action.__registry;
14
+ return (request, response) => __async(this, null, function* () {
15
+ var _a;
16
+ const { stream } = request.query;
17
+ let input = request.body.data;
18
+ const auth = request.auth;
19
+ try {
20
+ yield (_a = opts == null ? void 0 : opts.authPolicy) == null ? void 0 : _a.call(opts, {
21
+ flow,
22
+ action,
23
+ auth,
24
+ input,
25
+ request
26
+ });
27
+ } catch (e) {
28
+ logger.debug(e);
29
+ const respBody = {
30
+ error: {
31
+ status: "PERMISSION_DENIED",
32
+ message: e.message || "Permission denied to resource"
33
+ }
34
+ };
35
+ response.status(403).send(respBody).end();
36
+ return;
37
+ }
38
+ if (request.get("Accept") === "text/event-stream" || stream === "true") {
39
+ response.writeHead(200, {
40
+ "Content-Type": "text/plain",
41
+ "Transfer-Encoding": "chunked"
42
+ });
43
+ try {
44
+ const onChunk = (chunk) => {
45
+ response.write(
46
+ "data: " + JSON.stringify({ message: chunk }) + streamDelimiter
47
+ );
48
+ };
49
+ const result = yield runWithStreamingCallback(
50
+ registry,
51
+ onChunk,
52
+ () => action.run(input, {
53
+ onChunk,
54
+ context: auth
55
+ })
56
+ );
57
+ response.write(
58
+ "data: " + JSON.stringify({ result: result.result }) + streamDelimiter
59
+ );
60
+ response.end();
61
+ } catch (e) {
62
+ logger.error(e);
63
+ response.write(
64
+ "data: " + JSON.stringify({
65
+ error: {
66
+ status: "INTERNAL",
67
+ message: getErrorMessage(e),
68
+ details: getErrorStack(e)
69
+ }
70
+ }) + streamDelimiter
71
+ );
72
+ response.end();
73
+ }
74
+ } else {
75
+ try {
76
+ const result = yield action.run(input, { context: auth });
77
+ response.setHeader("x-genkit-trace-id", result.telemetry.traceId);
78
+ response.setHeader("x-genkit-span-id", result.telemetry.spanId);
79
+ response.status(200).send({
80
+ result: result.result
81
+ }).end();
82
+ } catch (e) {
83
+ response.status(500).send({
84
+ error: {
85
+ status: "INTERNAL",
86
+ message: getErrorMessage(e),
87
+ details: getErrorStack(e)
88
+ }
89
+ }).end();
90
+ }
91
+ }
92
+ });
93
+ }
94
+ export {
95
+ handler
96
+ };
97
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +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 express from 'express';\nimport {\n Action,\n CallableFlow,\n Flow,\n runWithStreamingCallback,\n z,\n} from 'genkit';\nimport { logger } from 'genkit/logging';\nimport { Registry } from 'genkit/registry';\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 flow?: Flow<I, O, S>;\n action?: Action<I, O, S>;\n input: z.infer<I>;\n auth: any | undefined;\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?: unknown;\n}\n\n/**\n * Exposes provided flow or an action as express handler.\n */\nexport function handler<\n I extends z.ZodTypeAny = z.ZodTypeAny,\n O extends z.ZodTypeAny = z.ZodTypeAny,\n S extends z.ZodTypeAny = z.ZodTypeAny,\n>(\n f: CallableFlow<I, O, S> | Flow<I, O, S> | Action<I, O, S>,\n opts?: {\n authPolicy?: AuthPolicy<I, O, S>;\n }\n): express.RequestHandler {\n const flow: Flow<I, O, S> | undefined = (f as Flow<I, O, S>).invoke\n ? (f as Flow<I, O, S>)\n : (f as CallableFlow<I, O, S>).flow\n ? (f as CallableFlow<I, O, S>).flow\n : undefined;\n const action: Action<I, O, S> = flow ? flow.action : (f as Action<I, O, S>);\n const registry: Registry = flow ? flow.registry : action.__registry;\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 flow,\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(registry, onChunk, () =>\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"],"mappings":";;;AAiBA;AAAA,EAIE;AAAA,OAEK;AACP,SAAS,cAAc;AAEvB,SAAS,iBAAiB,qBAAqB;AAE/C,MAAM,kBAAkB;AAyCjB,SAAS,QAKd,GACA,MAGwB;AACxB,QAAM,OAAmC,EAAoB,SACxD,IACA,EAA4B,OAC1B,EAA4B,OAC7B;AACN,QAAM,SAA0B,OAAO,KAAK,SAAU;AACtD,QAAM,WAAqB,OAAO,KAAK,WAAW,OAAO;AACzD,SAAO,CACL,SACA,aACkB;AAzFtB;AA0FI,UAAM,EAAE,OAAO,IAAI,QAAQ;AAC3B,QAAI,QAAQ,QAAQ,KAAK;AACzB,UAAM,OAAO,QAAQ;AAErB,QAAI;AACF,aAAM,kCAAM,eAAN,8BAAmB;AAAA,QACvB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF;AAAA,IACF,SAAS,GAAQ;AACf,aAAO,MAAM,CAAC;AACd,YAAM,WAAW;AAAA,QACf,OAAO;AAAA,UACL,QAAQ;AAAA,UACR,SAAS,EAAE,WAAW;AAAA,QACxB;AAAA,MACF;AACA,eAAS,OAAO,GAAG,EAAE,KAAK,QAAQ,EAAE,IAAI;AACxC;AAAA,IACF;AAEA,QAAI,QAAQ,IAAI,QAAQ,MAAM,uBAAuB,WAAW,QAAQ;AACtE,eAAS,UAAU,KAAK;AAAA,QACtB,gBAAgB;AAAA,QAChB,qBAAqB;AAAA,MACvB,CAAC;AACD,UAAI;AACF,cAAM,UAAU,CAAC,UAAsB;AACrC,mBAAS;AAAA,YACP,WAAW,KAAK,UAAU,EAAE,SAAS,MAAM,CAAC,IAAI;AAAA,UAClD;AAAA,QACF;AACA,cAAM,SAAS,MAAM;AAAA,UAAyB;AAAA,UAAU;AAAA,UAAS,MAC/D,OAAO,IAAI,OAAO;AAAA,YAChB;AAAA,YACA,SAAS;AAAA,UACX,CAAC;AAAA,QACH;AACA,iBAAS;AAAA,UACP,WAAW,KAAK,UAAU,EAAE,QAAQ,OAAO,OAAO,CAAC,IAAI;AAAA,QACzD;AACA,iBAAS,IAAI;AAAA,MACf,SAAS,GAAG;AACV,eAAO,MAAM,CAAC;AACd,iBAAS;AAAA,UACP,WACE,KAAK,UAAU;AAAA,YACb,OAAO;AAAA,cACL,QAAQ;AAAA,cACR,SAAS,gBAAgB,CAAC;AAAA,cAC1B,SAAS,cAAc,CAAC;AAAA,YAC1B;AAAA,UACF,CAAC,IACD;AAAA,QACJ;AACA,iBAAS,IAAI;AAAA,MACf;AAAA,IACF,OAAO;AACL,UAAI;AACF,cAAM,SAAS,MAAM,OAAO,IAAI,OAAO,EAAE,SAAS,KAAK,CAAC;AACxD,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,iBACG,OAAO,GAAG,EACV,KAAK;AAAA,UACJ,OAAO;AAAA,YACL,QAAQ;AAAA,YACR,SAAS,gBAAgB,CAAC;AAAA,YAC1B,SAAS,cAAc,CAAC;AAAA,UAC1B;AAAA,QACF,CAAC,EACA,IAAI;AAAA,MACT;AAAA,IACF;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Copyright 2024 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /**
17
+ * Extracts error message from the given error object, or if input is not an error then just turn the error into a string.
18
+ */
19
+ declare function getErrorMessage(e: any): string;
20
+ /**
21
+ * Extracts stack trace from the given error object, or if input is not an error then returns undefined.
22
+ */
23
+ declare function getErrorStack(e: any): string | undefined;
24
+
25
+ export { getErrorMessage, getErrorStack };
package/lib/utils.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Copyright 2024 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+ /**
17
+ * Extracts error message from the given error object, or if input is not an error then just turn the error into a string.
18
+ */
19
+ declare function getErrorMessage(e: any): string;
20
+ /**
21
+ * Extracts stack trace from the given error object, or if input is not an error then returns undefined.
22
+ */
23
+ declare function getErrorStack(e: any): string | undefined;
24
+
25
+ export { getErrorMessage, getErrorStack };
package/lib/utils.js ADDED
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+ var utils_exports = {};
20
+ __export(utils_exports, {
21
+ getErrorMessage: () => getErrorMessage,
22
+ getErrorStack: () => getErrorStack
23
+ });
24
+ module.exports = __toCommonJS(utils_exports);
25
+ function getErrorMessage(e) {
26
+ if (e instanceof Error) {
27
+ return e.message;
28
+ }
29
+ return `${e}`;
30
+ }
31
+ function getErrorStack(e) {
32
+ if (e instanceof Error) {
33
+ return e.stack;
34
+ }
35
+ return void 0;
36
+ }
37
+ // Annotate the CommonJS export names for ESM import in node:
38
+ 0 && (module.exports = {
39
+ getErrorMessage,
40
+ getErrorStack
41
+ });
42
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils.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\n/**\n * Extracts error message from the given error object, or if input is not an error then just turn the error into a string.\n */\nexport function getErrorMessage(e: any): string {\n if (e instanceof Error) {\n return e.message;\n }\n return `${e}`;\n}\n\n/**\n * Extracts stack trace from the given error object, or if input is not an error then returns undefined.\n */\nexport function getErrorStack(e: any): string | undefined {\n if (e instanceof Error) {\n return e.stack;\n }\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmBO,SAAS,gBAAgB,GAAgB;AAC9C,MAAI,aAAa,OAAO;AACtB,WAAO,EAAE;AAAA,EACX;AACA,SAAO,GAAG,CAAC;AACb;AAKO,SAAS,cAAc,GAA4B;AACxD,MAAI,aAAa,OAAO;AACtB,WAAO,EAAE;AAAA,EACX;AACA,SAAO;AACT;","names":[]}
package/lib/utils.mjs ADDED
@@ -0,0 +1,18 @@
1
+ import "./chunk-IQXHJV5O.mjs";
2
+ function getErrorMessage(e) {
3
+ if (e instanceof Error) {
4
+ return e.message;
5
+ }
6
+ return `${e}`;
7
+ }
8
+ function getErrorStack(e) {
9
+ if (e instanceof Error) {
10
+ return e.stack;
11
+ }
12
+ return void 0;
13
+ }
14
+ export {
15
+ getErrorMessage,
16
+ getErrorStack
17
+ };
18
+ //# sourceMappingURL=utils.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils.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\n/**\n * Extracts error message from the given error object, or if input is not an error then just turn the error into a string.\n */\nexport function getErrorMessage(e: any): string {\n if (e instanceof Error) {\n return e.message;\n }\n return `${e}`;\n}\n\n/**\n * Extracts stack trace from the given error object, or if input is not an error then returns undefined.\n */\nexport function getErrorStack(e: any): string | undefined {\n if (e instanceof Error) {\n return e.stack;\n }\n return undefined;\n}\n"],"mappings":";AAmBO,SAAS,gBAAgB,GAAgB;AAC9C,MAAI,aAAa,OAAO;AACtB,WAAO,EAAE;AAAA,EACX;AACA,SAAO,GAAG,CAAC;AACb;AAKO,SAAS,cAAc,GAA4B;AACxD,MAAI,aAAa,OAAO;AACtB,WAAO,EAAE;AAAA,EACX;AACA,SAAO;AACT;","names":[]}
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@genkit-ai/express",
3
+ "description": "Genkit AI framework plugin for Express server",
4
+ "keywords": [
5
+ "genkit",
6
+ "genkit-plugin",
7
+ "langchain",
8
+ "ai",
9
+ "genai",
10
+ "generative-ai"
11
+ ],
12
+ "version": "1.0.0-rc.1",
13
+ "type": "commonjs",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/firebase/genkit.git",
17
+ "directory": "js/plugins/express"
18
+ },
19
+ "author": "genkit",
20
+ "license": "Apache-2.0",
21
+ "dependencies": {},
22
+ "peerDependencies": {
23
+ "express": "^4.21.1",
24
+ "genkit": "1.0.0-rc.1"
25
+ },
26
+ "devDependencies": {
27
+ "get-port": "^5.1.0",
28
+ "@types/express": "^4.17.21",
29
+ "@types/node": "^20.11.16",
30
+ "npm-run-all": "^4.1.5",
31
+ "rimraf": "^6.0.1",
32
+ "tsup": "^8.3.5",
33
+ "tsx": "^4.19.2",
34
+ "typescript": "^4.9.0"
35
+ },
36
+ "types": "./lib/index.d.ts",
37
+ "exports": {
38
+ ".": {
39
+ "require": "./lib/index.js",
40
+ "default": "./lib/index.js",
41
+ "import": "./lib/index.mjs",
42
+ "types": "./lib/index.d.ts"
43
+ }
44
+ },
45
+ "scripts": {
46
+ "check": "tsc",
47
+ "compile": "tsup-node",
48
+ "build:clean": "rimraf ./lib",
49
+ "build": "npm-run-all build:clean check compile",
50
+ "build:watch": "tsup-node --watch",
51
+ "test": "node --import tsx --test tests/*_test.ts",
52
+ "test:watch": "node --import tsx --watch --test tests/*_test.ts"
53
+ }
54
+ }
package/src/index.ts ADDED
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Copyright 2024 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import express from 'express';
18
+ import {
19
+ Action,
20
+ CallableFlow,
21
+ Flow,
22
+ runWithStreamingCallback,
23
+ z,
24
+ } from 'genkit';
25
+ import { logger } from 'genkit/logging';
26
+ import { Registry } from 'genkit/registry';
27
+ import { getErrorMessage, getErrorStack } from './utils';
28
+
29
+ const streamDelimiter = '\n\n';
30
+
31
+ /**
32
+ * Auth policy context is an object passed to the auth policy providing details necessary for auth.
33
+ */
34
+ export interface AuthPolicyContext<
35
+ I extends z.ZodTypeAny = z.ZodTypeAny,
36
+ O extends z.ZodTypeAny = z.ZodTypeAny,
37
+ S extends z.ZodTypeAny = z.ZodTypeAny,
38
+ > {
39
+ flow?: Flow<I, O, S>;
40
+ action?: Action<I, O, S>;
41
+ input: z.infer<I>;
42
+ auth: any | undefined;
43
+ request: RequestWithAuth;
44
+ }
45
+
46
+ /**
47
+ * Flow Auth policy. Consumes the authorization context of the flow and
48
+ * performs checks before the flow runs. If this throws, the flow will not
49
+ * be executed.
50
+ */
51
+ export interface AuthPolicy<
52
+ I extends z.ZodTypeAny = z.ZodTypeAny,
53
+ O extends z.ZodTypeAny = z.ZodTypeAny,
54
+ S extends z.ZodTypeAny = z.ZodTypeAny,
55
+ > {
56
+ (ctx: AuthPolicyContext<I, O, S>): void | Promise<void>;
57
+ }
58
+
59
+ /**
60
+ * For express-based flows, req.auth should contain the value to bepassed into
61
+ * the flow context.
62
+ */
63
+ export interface RequestWithAuth extends express.Request {
64
+ auth?: unknown;
65
+ }
66
+
67
+ /**
68
+ * Exposes provided flow or an action as express handler.
69
+ */
70
+ export function handler<
71
+ I extends z.ZodTypeAny = z.ZodTypeAny,
72
+ O extends z.ZodTypeAny = z.ZodTypeAny,
73
+ S extends z.ZodTypeAny = z.ZodTypeAny,
74
+ >(
75
+ f: CallableFlow<I, O, S> | Flow<I, O, S> | Action<I, O, S>,
76
+ opts?: {
77
+ authPolicy?: AuthPolicy<I, O, S>;
78
+ }
79
+ ): express.RequestHandler {
80
+ const flow: Flow<I, O, S> | undefined = (f as Flow<I, O, S>).invoke
81
+ ? (f as Flow<I, O, S>)
82
+ : (f as CallableFlow<I, O, S>).flow
83
+ ? (f as CallableFlow<I, O, S>).flow
84
+ : undefined;
85
+ const action: Action<I, O, S> = flow ? flow.action : (f as Action<I, O, S>);
86
+ const registry: Registry = flow ? flow.registry : action.__registry;
87
+ return async (
88
+ request: RequestWithAuth,
89
+ response: express.Response
90
+ ): Promise<void> => {
91
+ const { stream } = request.query;
92
+ let input = request.body.data;
93
+ const auth = request.auth;
94
+
95
+ try {
96
+ await opts?.authPolicy?.({
97
+ flow,
98
+ action,
99
+ auth,
100
+ input,
101
+ request,
102
+ });
103
+ } catch (e: any) {
104
+ logger.debug(e);
105
+ const respBody = {
106
+ error: {
107
+ status: 'PERMISSION_DENIED',
108
+ message: e.message || 'Permission denied to resource',
109
+ },
110
+ };
111
+ response.status(403).send(respBody).end();
112
+ return;
113
+ }
114
+
115
+ if (request.get('Accept') === 'text/event-stream' || stream === 'true') {
116
+ response.writeHead(200, {
117
+ 'Content-Type': 'text/plain',
118
+ 'Transfer-Encoding': 'chunked',
119
+ });
120
+ try {
121
+ const onChunk = (chunk: z.infer<S>) => {
122
+ response.write(
123
+ 'data: ' + JSON.stringify({ message: chunk }) + streamDelimiter
124
+ );
125
+ };
126
+ const result = await runWithStreamingCallback(registry, onChunk, () =>
127
+ action.run(input, {
128
+ onChunk,
129
+ context: auth,
130
+ })
131
+ );
132
+ response.write(
133
+ 'data: ' + JSON.stringify({ result: result.result }) + streamDelimiter
134
+ );
135
+ response.end();
136
+ } catch (e) {
137
+ logger.error(e);
138
+ response.write(
139
+ 'data: ' +
140
+ JSON.stringify({
141
+ error: {
142
+ status: 'INTERNAL',
143
+ message: getErrorMessage(e),
144
+ details: getErrorStack(e),
145
+ },
146
+ }) +
147
+ streamDelimiter
148
+ );
149
+ response.end();
150
+ }
151
+ } else {
152
+ try {
153
+ const result = await action.run(input, { context: auth });
154
+ response.setHeader('x-genkit-trace-id', result.telemetry.traceId);
155
+ response.setHeader('x-genkit-span-id', result.telemetry.spanId);
156
+ // Responses for non-streaming flows are passed back with the flow result stored in a field called "result."
157
+ response
158
+ .status(200)
159
+ .send({
160
+ result: result.result,
161
+ })
162
+ .end();
163
+ } catch (e) {
164
+ // Errors for non-streaming flows are passed back as standard API errors.
165
+ response
166
+ .status(500)
167
+ .send({
168
+ error: {
169
+ status: 'INTERNAL',
170
+ message: getErrorMessage(e),
171
+ details: getErrorStack(e),
172
+ },
173
+ })
174
+ .end();
175
+ }
176
+ }
177
+ };
178
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Copyright 2024 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ /**
18
+ * Extracts error message from the given error object, or if input is not an error then just turn the error into a string.
19
+ */
20
+ export function getErrorMessage(e: any): string {
21
+ if (e instanceof Error) {
22
+ return e.message;
23
+ }
24
+ return `${e}`;
25
+ }
26
+
27
+ /**
28
+ * Extracts stack trace from the given error object, or if input is not an error then returns undefined.
29
+ */
30
+ export function getErrorStack(e: any): string | undefined {
31
+ if (e instanceof Error) {
32
+ return e.stack;
33
+ }
34
+ return undefined;
35
+ }
@@ -0,0 +1,376 @@
1
+ /**
2
+ * Copyright 2024 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import express from 'express';
18
+ import { GenerateResponseData, Genkit, genkit, z } from 'genkit';
19
+ import { runFlow, streamFlow } from 'genkit/client';
20
+ import { GenerateResponseChunkData, ModelAction } from 'genkit/model';
21
+ import getPort from 'get-port';
22
+ import * as http from 'http';
23
+ import assert from 'node:assert';
24
+ import { afterEach, beforeEach, describe, it } from 'node:test';
25
+ import { RequestWithAuth, handler } from '../src/index.js';
26
+
27
+ describe('telemetry', async () => {
28
+ let server: http.Server;
29
+ let port;
30
+
31
+ beforeEach(async () => {
32
+ const ai = genkit({});
33
+ const echoModel = defineEchoModel(ai);
34
+
35
+ const voidInput = ai.defineFlow('voidInput', async () => {
36
+ return 'banana';
37
+ });
38
+
39
+ const stringInput = ai.defineFlow('stringInput', async (input) => {
40
+ const { text } = await ai.generate({
41
+ model: 'echoModel',
42
+ prompt: input,
43
+ });
44
+ return text;
45
+ });
46
+
47
+ const objectInput = ai.defineFlow(
48
+ { name: 'objectInput', inputSchema: z.object({ question: z.string() }) },
49
+ async (input) => {
50
+ const { text } = await ai.generate({
51
+ model: 'echoModel',
52
+ prompt: input.question,
53
+ });
54
+ return text;
55
+ }
56
+ );
57
+
58
+ const streamingFlow = ai.defineFlow(
59
+ {
60
+ name: 'streamingFlow',
61
+ inputSchema: z.object({ question: z.string() }),
62
+ },
63
+ async (input, sendChunk) => {
64
+ const { text } = await ai.generate({
65
+ model: 'echoModel',
66
+ prompt: input.question,
67
+ onChunk: sendChunk,
68
+ });
69
+ return text;
70
+ }
71
+ );
72
+
73
+ const flowWithContext = ai.defineFlow(
74
+ {
75
+ name: 'flowWithContext',
76
+ inputSchema: z.object({ question: z.string() }),
77
+ },
78
+ async (input, { context }) => {
79
+ return `${input.question} - ${JSON.stringify(context)}`;
80
+ }
81
+ );
82
+
83
+ const app = express();
84
+ app.use(express.json());
85
+ port = await getPort();
86
+
87
+ app.post('/voidInput', handler(voidInput));
88
+ app.post('/stringInput', handler(stringInput));
89
+ app.post('/objectInput', handler(objectInput));
90
+ app.post('/streamingFlow', handler(streamingFlow));
91
+ app.post(
92
+ '/flowWithAuth',
93
+ async (req, resp, next) => {
94
+ (req as RequestWithAuth).auth = {
95
+ user:
96
+ req.header('authorization') === 'open sesame'
97
+ ? 'Ali Baba'
98
+ : '40 thieves',
99
+ };
100
+ next();
101
+ },
102
+ handler(flowWithContext, {
103
+ authPolicy: ({ auth, action, input, request }) => {
104
+ assert.ok(auth, 'auth must be set');
105
+ assert.ok(action, 'flow must be set');
106
+ assert.ok(input, 'input must be set');
107
+ assert.ok(request, 'request must be set');
108
+
109
+ if (auth.user !== 'Ali Baba') {
110
+ throw new Error('not authorized');
111
+ }
112
+ },
113
+ })
114
+ );
115
+
116
+ // Can also expose any action.
117
+ app.post('/echoModel', handler(echoModel));
118
+ app.post(
119
+ '/echoModelWithAuth',
120
+ async (req, resp, next) => {
121
+ (req as RequestWithAuth).auth = {
122
+ user:
123
+ req.header('authorization') === 'open sesame'
124
+ ? 'Ali Baba'
125
+ : '40 thieves',
126
+ };
127
+ next();
128
+ },
129
+ handler(echoModel, {
130
+ authPolicy: ({ auth, action, input, request }) => {
131
+ assert.ok(auth, 'auth must be set');
132
+ assert.ok(action, 'flow must be set');
133
+ assert.ok(input, 'input must be set');
134
+ assert.ok(request, 'request must be set');
135
+
136
+ if (auth.user !== 'Ali Baba') {
137
+ throw new Error('not authorized');
138
+ }
139
+ },
140
+ })
141
+ );
142
+
143
+ server = app.listen(port, () => {
144
+ console.log(`Example app listening on port ${port}`);
145
+ });
146
+ });
147
+
148
+ afterEach(() => {
149
+ server.close();
150
+ });
151
+
152
+ describe('runFlow', () => {
153
+ it('should call a void input flow', async () => {
154
+ const result = await runFlow({
155
+ url: `http://localhost:${port}/voidInput`,
156
+ });
157
+ assert.strictEqual(result, 'banana');
158
+ });
159
+
160
+ it('should run a flow with string input', async () => {
161
+ const result = await runFlow({
162
+ url: `http://localhost:${port}/stringInput`,
163
+ input: 'hello',
164
+ });
165
+ assert.strictEqual(result, 'Echo: hello');
166
+ });
167
+
168
+ it('should run a flow with object input', async () => {
169
+ const result = await runFlow({
170
+ url: `http://localhost:${port}/objectInput`,
171
+ input: {
172
+ question: 'olleh',
173
+ },
174
+ });
175
+ assert.strictEqual(result, 'Echo: olleh');
176
+ });
177
+
178
+ it('should fail a bad input', async () => {
179
+ const result = runFlow({
180
+ url: `http://localhost:${port}/objectInput`,
181
+ input: {
182
+ badField: 'hello',
183
+ },
184
+ });
185
+ await assert.rejects(result, (err: Error) => {
186
+ return err.message.includes('INVALID_ARGUMENT');
187
+ });
188
+ });
189
+
190
+ it('should call a flow with auth', async () => {
191
+ const result = await runFlow<string>({
192
+ url: `http://localhost:${port}/flowWithAuth`,
193
+ input: {
194
+ question: 'hello',
195
+ },
196
+ headers: {
197
+ Authorization: 'open sesame',
198
+ },
199
+ });
200
+ assert.strictEqual(result, 'hello - {"user":"Ali Baba"}');
201
+ });
202
+
203
+ it('should fail a flow with auth', async () => {
204
+ const result = runFlow({
205
+ url: `http://localhost:${port}/flowWithAuth`,
206
+ input: {
207
+ question: 'hello',
208
+ },
209
+ headers: {
210
+ Authorization: 'thieve #24',
211
+ },
212
+ });
213
+ await assert.rejects(result, (err) => {
214
+ return (err as Error).message.includes('not authorized');
215
+ });
216
+ });
217
+
218
+ it('should call a model', async () => {
219
+ const result = await runFlow({
220
+ url: `http://localhost:${port}/echoModel`,
221
+ input: {
222
+ messages: [{ role: 'user', content: [{ text: 'hello' }] }],
223
+ },
224
+ });
225
+ assert.strictEqual(result.finishReason, 'stop');
226
+ assert.deepStrictEqual(result.message, {
227
+ role: 'model',
228
+ content: [{ text: 'Echo: hello' }],
229
+ });
230
+ });
231
+
232
+ it('should call a model with auth', async () => {
233
+ const result = await runFlow<GenerateResponseData>({
234
+ url: `http://localhost:${port}/echoModelWithAuth`,
235
+ input: {
236
+ messages: [{ role: 'user', content: [{ text: 'hello' }] }],
237
+ },
238
+ headers: {
239
+ Authorization: 'open sesame',
240
+ },
241
+ });
242
+ assert.strictEqual(result.finishReason, 'stop');
243
+ assert.deepStrictEqual(result.message, {
244
+ role: 'model',
245
+ content: [{ text: 'Echo: hello' }],
246
+ });
247
+ });
248
+
249
+ it('should fail a flow with auth', async () => {
250
+ const result = runFlow({
251
+ url: `http://localhost:${port}/echoModelWithAuth`,
252
+ input: {
253
+ messages: [
254
+ {
255
+ role: 'user',
256
+ content: [{ text: 'hello' }],
257
+ },
258
+ ],
259
+ },
260
+ headers: {
261
+ Authorization: 'thieve #24',
262
+ },
263
+ });
264
+ await assert.rejects(result, (err) => {
265
+ return (err as Error).message.includes('not authorized');
266
+ });
267
+ });
268
+ });
269
+
270
+ describe('streamFlow', () => {
271
+ it('stream a flow', async () => {
272
+ const result = streamFlow<string, GenerateResponseChunkData>({
273
+ url: `http://localhost:${port}/streamingFlow`,
274
+ input: {
275
+ question: 'olleh',
276
+ },
277
+ });
278
+
279
+ const gotChunks: GenerateResponseChunkData[] = [];
280
+ for await (const chunk of result.stream()) {
281
+ gotChunks.push(chunk);
282
+ }
283
+
284
+ assert.deepStrictEqual(gotChunks, [
285
+ { content: [{ text: '3' }] },
286
+ { content: [{ text: '2' }] },
287
+ { content: [{ text: '1' }] },
288
+ ]);
289
+
290
+ assert.strictEqual(await result.output(), 'Echo: olleh');
291
+ });
292
+
293
+ it('stream a model', async () => {
294
+ const result = streamFlow({
295
+ url: `http://localhost:${port}/echoModel`,
296
+ input: {
297
+ messages: [
298
+ {
299
+ role: 'user',
300
+ content: [{ text: 'olleh' }],
301
+ },
302
+ ],
303
+ },
304
+ });
305
+
306
+ const gotChunks: any[] = [];
307
+ for await (const chunk of result.stream()) {
308
+ gotChunks.push(chunk);
309
+ }
310
+
311
+ const output = await result.output();
312
+ assert.strictEqual(output.finishReason, 'stop');
313
+ assert.deepStrictEqual(output.message, {
314
+ role: 'model',
315
+ content: [{ text: 'Echo: olleh' }],
316
+ });
317
+
318
+ assert.deepStrictEqual(gotChunks, [
319
+ { content: [{ text: '3' }] },
320
+ { content: [{ text: '2' }] },
321
+ { content: [{ text: '1' }] },
322
+ ]);
323
+ });
324
+ });
325
+ });
326
+
327
+ export function defineEchoModel(ai: Genkit): ModelAction {
328
+ return ai.defineModel(
329
+ {
330
+ name: 'echoModel',
331
+ },
332
+ async (request, streamingCallback) => {
333
+ streamingCallback?.({
334
+ content: [
335
+ {
336
+ text: '3',
337
+ },
338
+ ],
339
+ });
340
+ streamingCallback?.({
341
+ content: [
342
+ {
343
+ text: '2',
344
+ },
345
+ ],
346
+ });
347
+ streamingCallback?.({
348
+ content: [
349
+ {
350
+ text: '1',
351
+ },
352
+ ],
353
+ });
354
+ return {
355
+ message: {
356
+ role: 'model',
357
+ content: [
358
+ {
359
+ text:
360
+ 'Echo: ' +
361
+ request.messages
362
+ .map(
363
+ (m) =>
364
+ (m.role === 'user' || m.role === 'model'
365
+ ? ''
366
+ : `${m.role}: `) + m.content.map((c) => c.text).join()
367
+ )
368
+ .join(),
369
+ },
370
+ ],
371
+ },
372
+ finishReason: 'stop',
373
+ };
374
+ }
375
+ );
376
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "include": ["src"]
4
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Copyright 2024 Google LLC
3
+ *
4
+ * Licensed under the Apache License, Version 2.0 (the "License");
5
+ * you may not use this file except in compliance with the License.
6
+ * You may obtain a copy of the License at
7
+ *
8
+ * http://www.apache.org/licenses/LICENSE-2.0
9
+ *
10
+ * Unless required by applicable law or agreed to in writing, software
11
+ * distributed under the License is distributed on an "AS IS" BASIS,
12
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ * See the License for the specific language governing permissions and
14
+ * limitations under the License.
15
+ */
16
+
17
+ import { defineConfig, Options } from 'tsup';
18
+ import { defaultOptions } from '../../tsup.common';
19
+
20
+ export default defineConfig({
21
+ ...(defaultOptions as Options),
22
+ });
package/typedoc.json ADDED
@@ -0,0 +1,3 @@
1
+ {
2
+ "entryPoints": ["src/index.ts"]
3
+ }