@worktango/ai-assistant 0.0.22 → 0.0.24
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/package.json +1 -1
- package/src/backend/express.test.ts +177 -10
- package/src/backend/express.ts +93 -12
- package/types.d.ts +35 -0
package/package.json
CHANGED
|
@@ -1,41 +1,208 @@
|
|
|
1
|
+
import http from "http";
|
|
2
|
+
|
|
1
3
|
import { Express } from "express";
|
|
2
4
|
|
|
3
5
|
import { setupAiAssistantRoutes } from "./express";
|
|
4
6
|
|
|
7
|
+
jest.mock("http", () => ({
|
|
8
|
+
request: jest.fn().mockImplementation((options, callback) => {
|
|
9
|
+
const mockProxyRes = {
|
|
10
|
+
statusCode: 200,
|
|
11
|
+
headers: { "content-type": "application/json" },
|
|
12
|
+
pipe: jest.fn(),
|
|
13
|
+
};
|
|
14
|
+
callback(mockProxyRes);
|
|
15
|
+
return {
|
|
16
|
+
on: jest.fn(),
|
|
17
|
+
end: jest.fn(),
|
|
18
|
+
};
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
|
|
5
22
|
describe("setupAiAssistantRoutes", () => {
|
|
6
23
|
let mockApp: jest.Mocked<Express>;
|
|
7
24
|
let mockPost: jest.Mock;
|
|
8
|
-
let
|
|
25
|
+
let mockGetBearerToken: jest.Mock;
|
|
26
|
+
let mockReq: any;
|
|
27
|
+
let mockRes: any;
|
|
28
|
+
let handlerPromise: Promise<void>;
|
|
29
|
+
|
|
30
|
+
const mockLogger = {
|
|
31
|
+
log: jest.fn(),
|
|
32
|
+
error: jest.fn(),
|
|
33
|
+
debug: jest.fn(),
|
|
34
|
+
};
|
|
9
35
|
|
|
10
36
|
beforeEach(() => {
|
|
11
|
-
|
|
37
|
+
mockReq = {
|
|
38
|
+
url: "http://localhost/ai-assistant",
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: { "content-type": "application/json" },
|
|
41
|
+
readable: false,
|
|
42
|
+
};
|
|
43
|
+
mockRes = {
|
|
44
|
+
statusCode: 200,
|
|
45
|
+
setHeader: jest.fn(),
|
|
46
|
+
status: jest.fn().mockReturnThis(),
|
|
47
|
+
send: jest.fn(),
|
|
48
|
+
};
|
|
12
49
|
mockPost = jest.fn().mockImplementation((path, handler) => {
|
|
13
|
-
|
|
14
|
-
handler({}, { send: mockSend });
|
|
50
|
+
handlerPromise = handler(mockReq, mockRes);
|
|
15
51
|
});
|
|
52
|
+
mockGetBearerToken = jest.fn().mockResolvedValue("mock-token");
|
|
16
53
|
|
|
17
54
|
mockApp = {
|
|
18
55
|
post: mockPost,
|
|
56
|
+
all: mockPost,
|
|
19
57
|
} as unknown as jest.Mocked<Express>;
|
|
58
|
+
|
|
59
|
+
(http.request as jest.Mock).mockClear();
|
|
20
60
|
});
|
|
21
61
|
|
|
22
|
-
it("sets up the default route correctly", () => {
|
|
23
|
-
setupAiAssistantRoutes(
|
|
62
|
+
it("sets up the default route correctly", async () => {
|
|
63
|
+
setupAiAssistantRoutes({
|
|
64
|
+
app: mockApp,
|
|
65
|
+
getBearerToken: mockGetBearerToken,
|
|
66
|
+
targetHost: "localhost",
|
|
67
|
+
logger: mockLogger,
|
|
68
|
+
});
|
|
24
69
|
|
|
25
70
|
expect(mockPost).toHaveBeenCalledWith(
|
|
26
71
|
"/ai-assistant",
|
|
27
72
|
expect.any(Function)
|
|
28
73
|
);
|
|
29
|
-
|
|
74
|
+
|
|
75
|
+
await handlerPromise;
|
|
76
|
+
|
|
77
|
+
expect(http.request).toHaveBeenCalledWith(
|
|
78
|
+
expect.objectContaining({
|
|
79
|
+
hostname: "localhost",
|
|
80
|
+
path: "/ai-assistant",
|
|
81
|
+
headers: expect.objectContaining({
|
|
82
|
+
Authorization: "Bearer mock-token",
|
|
83
|
+
}),
|
|
84
|
+
}),
|
|
85
|
+
expect.any(Function)
|
|
86
|
+
);
|
|
30
87
|
});
|
|
31
88
|
|
|
32
|
-
it("sets up a custom route when provided", () => {
|
|
33
|
-
|
|
89
|
+
it("sets up a custom route when provided", async () => {
|
|
90
|
+
mockReq.url = "http://localhost/custom-route";
|
|
91
|
+
|
|
92
|
+
setupAiAssistantRoutes({
|
|
93
|
+
app: mockApp,
|
|
94
|
+
getBearerToken: mockGetBearerToken,
|
|
95
|
+
targetHost: "localhost",
|
|
96
|
+
route: "/custom-route",
|
|
97
|
+
logger: mockLogger,
|
|
98
|
+
});
|
|
34
99
|
|
|
35
100
|
expect(mockPost).toHaveBeenCalledWith(
|
|
36
101
|
"/custom-route",
|
|
37
102
|
expect.any(Function)
|
|
38
103
|
);
|
|
39
|
-
|
|
104
|
+
|
|
105
|
+
await handlerPromise;
|
|
106
|
+
|
|
107
|
+
expect(http.request).toHaveBeenCalledWith(
|
|
108
|
+
expect.objectContaining({
|
|
109
|
+
hostname: "localhost",
|
|
110
|
+
path: "/custom-route",
|
|
111
|
+
headers: expect.objectContaining({
|
|
112
|
+
Authorization: "Bearer mock-token",
|
|
113
|
+
}),
|
|
114
|
+
}),
|
|
115
|
+
expect.any(Function)
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("handles errors when getting the bearer token", async () => {
|
|
120
|
+
mockGetBearerToken.mockRejectedValueOnce(
|
|
121
|
+
new Error("Error getting bearer token")
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
setupAiAssistantRoutes({
|
|
125
|
+
app: mockApp,
|
|
126
|
+
getBearerToken: mockGetBearerToken,
|
|
127
|
+
targetHost: "localhost",
|
|
128
|
+
logger: mockLogger,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await handlerPromise;
|
|
132
|
+
|
|
133
|
+
expect(mockRes.status).toHaveBeenCalledWith(500);
|
|
134
|
+
expect(mockRes.send).toHaveBeenCalledWith("Error getting bearer token");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("handles proxy errors correctly", async () => {
|
|
138
|
+
const mockError = new Error("Proxy error");
|
|
139
|
+
(http.request as jest.Mock).mockImplementationOnce(() => ({
|
|
140
|
+
on: jest.fn().mockImplementation((event, handler) => {
|
|
141
|
+
if (event === "error") {
|
|
142
|
+
handler(mockError);
|
|
143
|
+
}
|
|
144
|
+
}),
|
|
145
|
+
end: jest.fn(),
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
setupAiAssistantRoutes({
|
|
149
|
+
app: mockApp,
|
|
150
|
+
getBearerToken: mockGetBearerToken,
|
|
151
|
+
targetHost: "localhost",
|
|
152
|
+
logger: mockLogger,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await handlerPromise;
|
|
156
|
+
|
|
157
|
+
expect(mockRes.status).toHaveBeenCalledWith(500);
|
|
158
|
+
expect(mockRes.send).toHaveBeenCalledWith("Proxy Error: Proxy error");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("handles readable request bodies", async () => {
|
|
162
|
+
mockReq.readable = true;
|
|
163
|
+
mockReq.pipe = jest.fn();
|
|
164
|
+
|
|
165
|
+
setupAiAssistantRoutes({
|
|
166
|
+
app: mockApp,
|
|
167
|
+
getBearerToken: mockGetBearerToken,
|
|
168
|
+
targetHost: "localhost",
|
|
169
|
+
logger: mockLogger,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await handlerPromise;
|
|
173
|
+
|
|
174
|
+
expect(mockReq.pipe).toHaveBeenCalled();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("handles response headers and status code correctly", async () => {
|
|
178
|
+
const mockProxyRes = {
|
|
179
|
+
statusCode: undefined,
|
|
180
|
+
headers: {
|
|
181
|
+
"content-type": undefined,
|
|
182
|
+
"x-custom-header": "value",
|
|
183
|
+
},
|
|
184
|
+
pipe: jest.fn(),
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
(http.request as jest.Mock).mockImplementationOnce((options, callback) => {
|
|
188
|
+
callback(mockProxyRes);
|
|
189
|
+
return {
|
|
190
|
+
on: jest.fn(),
|
|
191
|
+
end: jest.fn(),
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
setupAiAssistantRoutes({
|
|
196
|
+
app: mockApp,
|
|
197
|
+
getBearerToken: mockGetBearerToken,
|
|
198
|
+
targetHost: "localhost",
|
|
199
|
+
logger: mockLogger,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
await handlerPromise;
|
|
203
|
+
|
|
204
|
+
expect(mockRes.statusCode).toBe(500);
|
|
205
|
+
expect(mockRes.setHeader).toHaveBeenCalledWith("x-custom-header", "value");
|
|
206
|
+
expect(mockRes.setHeader).toHaveBeenCalledWith("content-type", "");
|
|
40
207
|
});
|
|
41
208
|
});
|
package/src/backend/express.ts
CHANGED
|
@@ -1,17 +1,98 @@
|
|
|
1
|
-
import
|
|
1
|
+
import http from "http";
|
|
2
|
+
|
|
3
|
+
import { Express, RequestHandler } from "express";
|
|
4
|
+
|
|
5
|
+
import { coalesce } from "@kazoohr/helpers";
|
|
2
6
|
|
|
3
7
|
/**
|
|
4
8
|
* Sets up the AI assistant routes on the given Express app.
|
|
5
9
|
*/
|
|
6
|
-
export function setupAiAssistantRoutes(
|
|
7
|
-
app: Express
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
export function setupAiAssistantRoutes(args: {
|
|
11
|
+
app: Express;
|
|
12
|
+
/**
|
|
13
|
+
* A function that returns the bearer token for the AI assistant to use for
|
|
14
|
+
* back-channel request proxying. This should be a JWT token that can be
|
|
15
|
+
* decoded on the `kazoo-web` application to authenticate the request.
|
|
16
|
+
*/
|
|
17
|
+
getBearerToken: () => Promise<string>;
|
|
18
|
+
/**
|
|
19
|
+
* The host of the application to proxy requests to.
|
|
20
|
+
*/
|
|
21
|
+
targetHost: string;
|
|
22
|
+
/**
|
|
23
|
+
* The port of the application to proxy requests to. Defaults to 80.
|
|
24
|
+
*/
|
|
25
|
+
targetPort?: number;
|
|
26
|
+
/**
|
|
27
|
+
* The route path to the local service's AI assistant. Defaults to
|
|
28
|
+
* "/ai-assistant".
|
|
29
|
+
*/
|
|
30
|
+
route?: string;
|
|
31
|
+
/**
|
|
32
|
+
* The logger to use for logging.
|
|
33
|
+
*/
|
|
34
|
+
logger?: Pick<typeof console, "log" | "error" | "debug">;
|
|
35
|
+
/**
|
|
36
|
+
* Middleware functions to run before the proxy request.
|
|
37
|
+
*/
|
|
38
|
+
preflightMiddlewares?: RequestHandler[];
|
|
39
|
+
}) {
|
|
40
|
+
const logger = coalesce(args.logger, console);
|
|
41
|
+
const routePath = args.route ?? "/ai-assistant";
|
|
42
|
+
|
|
43
|
+
args.app.all(
|
|
44
|
+
routePath,
|
|
45
|
+
...coalesce(args.preflightMiddlewares, []),
|
|
46
|
+
async (req, res) => {
|
|
47
|
+
const bearerToken = await args.getBearerToken().catch((error) => {
|
|
48
|
+
logger.error("Error getting bearer token:", error);
|
|
49
|
+
res.status(500).send("Error getting bearer token");
|
|
50
|
+
return null;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
if (!bearerToken) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const parsedUrl = new URL(req.url);
|
|
58
|
+
|
|
59
|
+
const options = {
|
|
60
|
+
hostname: args.targetHost,
|
|
61
|
+
port: args.targetPort,
|
|
62
|
+
path: parsedUrl.pathname,
|
|
63
|
+
method: req.method,
|
|
64
|
+
headers: { ...req.headers },
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
options.headers["Authorization"] = `Bearer ${bearerToken}`;
|
|
68
|
+
delete options.headers.host;
|
|
69
|
+
|
|
70
|
+
logger.debug("Proxying request:", {
|
|
71
|
+
method: req.method,
|
|
72
|
+
headers: options.headers,
|
|
73
|
+
proxyOptions: options,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const proxyReq = http.request(options, (proxyRes) => {
|
|
77
|
+
res.statusCode = proxyRes.statusCode || 500;
|
|
78
|
+
|
|
79
|
+
Object.keys(proxyRes.headers).forEach((key) => {
|
|
80
|
+
res.setHeader(key, proxyRes.headers[key] || "");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
proxyRes.pipe(res);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
proxyReq.on("error", (error) => {
|
|
87
|
+
logger.error("Proxy request error:", error);
|
|
88
|
+
res.status(500).send("Proxy Error: " + error.message);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (req.readable) {
|
|
92
|
+
req.pipe(proxyReq);
|
|
93
|
+
} else {
|
|
94
|
+
proxyReq.end();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
);
|
|
17
98
|
}
|
package/types.d.ts
CHANGED
|
@@ -32,3 +32,38 @@ declare module "@worktango/ai-assistant/react" {
|
|
|
32
32
|
tools?: ToolDeclaration[];
|
|
33
33
|
}): JSX.Element;
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
declare module "@worktango/ai-assistant/express" {
|
|
37
|
+
export function setupAiAssistantRoutes(args: {
|
|
38
|
+
app: {
|
|
39
|
+
all: (path: string, ...args: any[]) => void;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* A function that returns the bearer token for the AI assistant to use for
|
|
43
|
+
* back-channel request proxying. This should be a JWT token that can be
|
|
44
|
+
* decoded on the `kazoo-web` application to authenticate the request.
|
|
45
|
+
*/
|
|
46
|
+
getBearerToken: () => Promise<string>;
|
|
47
|
+
/**
|
|
48
|
+
* The host of the application to proxy requests to.
|
|
49
|
+
*/
|
|
50
|
+
targetHost: string;
|
|
51
|
+
/**
|
|
52
|
+
* The port of the application to proxy requests to. Defaults to 80.
|
|
53
|
+
*/
|
|
54
|
+
targetPort?: number;
|
|
55
|
+
/**
|
|
56
|
+
* The route path to the local service's AI assistant. Defaults to
|
|
57
|
+
* "/ai-assistant".
|
|
58
|
+
*/
|
|
59
|
+
route?: string;
|
|
60
|
+
/**
|
|
61
|
+
* The logger to use for logging.
|
|
62
|
+
*/
|
|
63
|
+
logger?: Pick<typeof console, "log" | "error" | "debug">;
|
|
64
|
+
/**
|
|
65
|
+
* Middleware functions to run before the proxy request.
|
|
66
|
+
*/
|
|
67
|
+
preflightMiddlewares?: RequestHandler[];
|
|
68
|
+
}): void;
|
|
69
|
+
}
|