@flink-app/flink 1.0.0 → 2.0.0-alpha.49
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/CHANGELOG.md +12 -0
- package/cli/build.ts +8 -1
- package/cli/run.ts +8 -1
- package/dist/cli/build.js +8 -1
- package/dist/cli/run.js +8 -1
- package/dist/src/FlinkApp.d.ts +33 -0
- package/dist/src/FlinkApp.js +247 -27
- package/dist/src/FlinkContext.d.ts +21 -0
- package/dist/src/FlinkHttpHandler.d.ts +90 -1
- package/dist/src/TypeScriptCompiler.d.ts +42 -0
- package/dist/src/TypeScriptCompiler.js +366 -8
- package/dist/src/TypeScriptUtils.js +4 -0
- package/dist/src/ai/AgentRunner.d.ts +39 -0
- package/dist/src/ai/AgentRunner.js +625 -0
- package/dist/src/ai/FlinkAgent.d.ts +446 -0
- package/dist/src/ai/FlinkAgent.js +633 -0
- package/dist/src/ai/FlinkTool.d.ts +37 -0
- package/dist/src/ai/FlinkTool.js +2 -0
- package/dist/src/ai/LLMAdapter.d.ts +119 -0
- package/dist/src/ai/LLMAdapter.js +2 -0
- package/dist/src/ai/SubAgentExecutor.d.ts +36 -0
- package/dist/src/ai/SubAgentExecutor.js +220 -0
- package/dist/src/ai/ToolExecutor.d.ts +35 -0
- package/dist/src/ai/ToolExecutor.js +237 -0
- package/dist/src/ai/index.d.ts +5 -0
- package/dist/src/ai/index.js +21 -0
- package/dist/src/handlers/StreamWriterFactory.d.ts +20 -0
- package/dist/src/handlers/StreamWriterFactory.js +83 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +4 -0
- package/dist/src/utils.d.ts +30 -0
- package/dist/src/utils.js +52 -0
- package/package.json +14 -2
- package/readme.md +425 -0
- package/spec/AgentDuplicateDetection.spec.ts +112 -0
- package/spec/AgentRunner.spec.ts +527 -0
- package/spec/ConversationHooks.spec.ts +290 -0
- package/spec/FlinkAgent.spec.ts +310 -0
- package/spec/FlinkApp.onError.spec.ts +1 -2
- package/spec/StreamingIntegration.spec.ts +138 -0
- package/spec/SubAgentSupport.spec.ts +941 -0
- package/spec/ToolExecutor.spec.ts +360 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar.js +57 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCar2.js +59 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema2.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithArraySchema3.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithLiteralSchema2.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/GetCarWithSchemaInFile2.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler.js +53 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/ManuallyAddedHandler2.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchCar.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOnboardingSession.js +76 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchOrderWithComplexTypes.js +58 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchProductWithIntersection.js +59 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PatchUserWithUnion.js +59 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostCar.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogin.js +56 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PostLogout.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/handlers/PutCar.js +55 -0
- package/spec/mock-project/dist/spec/mock-project/src/index.js +83 -0
- package/spec/mock-project/dist/spec/mock-project/src/repos/CarRepo.js +26 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/Car.js +2 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/DefaultExportSchema.js +2 -0
- package/spec/mock-project/dist/spec/mock-project/src/schemas/FileWithTwoSchemas.js +2 -0
- package/spec/mock-project/dist/src/FlinkApp.js +1012 -0
- package/spec/mock-project/dist/src/FlinkContext.js +2 -0
- package/spec/mock-project/dist/src/FlinkErrors.js +143 -0
- package/spec/mock-project/dist/src/FlinkHttpHandler.js +47 -0
- package/spec/mock-project/dist/src/FlinkJob.js +2 -0
- package/spec/mock-project/dist/src/FlinkLog.js +26 -0
- package/spec/mock-project/dist/src/FlinkPlugin.js +2 -0
- package/spec/mock-project/dist/src/FlinkRepo.js +224 -0
- package/spec/mock-project/dist/src/FlinkResponse.js +2 -0
- package/spec/mock-project/dist/src/ai/AgentExecutor.js +279 -0
- package/spec/mock-project/dist/src/ai/AgentRunner.js +625 -0
- package/spec/mock-project/dist/src/ai/FlinkAgent.js +633 -0
- package/spec/mock-project/dist/src/ai/FlinkTool.js +2 -0
- package/spec/mock-project/dist/src/ai/LLMAdapter.js +2 -0
- package/spec/mock-project/dist/src/ai/SubAgentExecutor.js +220 -0
- package/spec/mock-project/dist/src/ai/ToolExecutor.js +237 -0
- package/spec/mock-project/dist/src/auth/FlinkAuthPlugin.js +2 -0
- package/spec/mock-project/dist/src/auth/FlinkAuthUser.js +2 -0
- package/spec/mock-project/dist/src/handlers/StreamWriterFactory.js +83 -0
- package/spec/mock-project/dist/src/index.js +17 -69
- package/spec/mock-project/dist/src/mock-data-generator.js +9 -0
- package/spec/mock-project/dist/src/utils.js +290 -0
- package/spec/mock-project/tsconfig.json +6 -1
- package/spec/testHelpers.ts +49 -0
- package/spec/utils.caseConversion.spec.ts +80 -0
- package/spec/utils.spec.ts +13 -13
- package/src/FlinkApp.ts +251 -7
- package/src/FlinkContext.ts +22 -0
- package/src/FlinkHttpHandler.ts +100 -2
- package/src/TypeScriptCompiler.ts +420 -9
- package/src/TypeScriptUtils.ts +5 -0
- package/src/ai/AgentRunner.ts +549 -0
- package/src/ai/FlinkAgent.ts +770 -0
- package/src/ai/FlinkTool.ts +40 -0
- package/src/ai/LLMAdapter.ts +96 -0
- package/src/ai/SubAgentExecutor.ts +199 -0
- package/src/ai/ToolExecutor.ts +193 -0
- package/src/ai/index.ts +5 -0
- package/src/handlers/StreamWriterFactory.ts +84 -0
- package/src/index.ts +4 -0
- package/src/utils.ts +52 -0
- package/tsconfig.json +6 -1
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { AgentExecuteContext, AgentExecuteInput, FlinkAgent, Message } from "../src/ai/FlinkAgent";
|
|
3
|
+
import { FlinkToolProps } from "../src/ai/FlinkTool";
|
|
4
|
+
import { LLMAdapter } from "../src/ai/LLMAdapter";
|
|
5
|
+
import { ToolExecutor } from "../src/ai/ToolExecutor";
|
|
6
|
+
import { FlinkContext } from "../src/FlinkContext";
|
|
7
|
+
import { createStreamingMock } from "./testHelpers";
|
|
8
|
+
|
|
9
|
+
describe("Conversation Hooks", () => {
|
|
10
|
+
let mockCtx: FlinkContext;
|
|
11
|
+
let mockLLMAdapter: LLMAdapter;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
mockCtx = {
|
|
15
|
+
repos: {},
|
|
16
|
+
plugins: {},
|
|
17
|
+
agents: {},
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
mockLLMAdapter = createStreamingMock([
|
|
21
|
+
{
|
|
22
|
+
textContent: "Test response",
|
|
23
|
+
toolCalls: [],
|
|
24
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
25
|
+
stopReason: "end_turn" as const,
|
|
26
|
+
},
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("beforeRun hook", () => {
|
|
31
|
+
it("should be called before execution with context", async () => {
|
|
32
|
+
const beforeRunSpy = jasmine.createSpy("beforeRun");
|
|
33
|
+
|
|
34
|
+
class TestAgent extends FlinkAgent<FlinkContext> {
|
|
35
|
+
description = "Test agent";
|
|
36
|
+
instructions = "Test instructions";
|
|
37
|
+
tools: string[] = [];
|
|
38
|
+
|
|
39
|
+
protected beforeRun = beforeRunSpy;
|
|
40
|
+
|
|
41
|
+
async query(message: string, conversationId?: string) {
|
|
42
|
+
const response = this.execute({ message, conversationId });
|
|
43
|
+
return await response.result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setContext(ctx: FlinkContext) {
|
|
47
|
+
(this as any).ctx = ctx;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const agent = new TestAgent();
|
|
52
|
+
agent.setContext(mockCtx);
|
|
53
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), {});
|
|
54
|
+
|
|
55
|
+
await agent.query("Hello", "conv-123");
|
|
56
|
+
|
|
57
|
+
expect(beforeRunSpy).toHaveBeenCalled();
|
|
58
|
+
const [input, context] = beforeRunSpy.calls.mostRecent().args;
|
|
59
|
+
expect(input.message).toBe("Hello");
|
|
60
|
+
expect(input.conversationId).toBe("conv-123");
|
|
61
|
+
expect(context.conversationId).toBe("conv-123");
|
|
62
|
+
expect(context.isSubAgent).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("should allow loading conversation history", async () => {
|
|
66
|
+
const mockHistory: Message[] = [
|
|
67
|
+
{ role: "user", content: "Previous question" },
|
|
68
|
+
{ role: "assistant", content: "Previous answer" },
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
class TestAgent extends FlinkAgent<FlinkContext> {
|
|
72
|
+
description = "Test agent";
|
|
73
|
+
instructions = "Test instructions";
|
|
74
|
+
tools: string[] = [];
|
|
75
|
+
|
|
76
|
+
protected async beforeRun(input: AgentExecuteInput, context: AgentExecuteContext) {
|
|
77
|
+
if (input.conversationId === "conv-123") {
|
|
78
|
+
input.history = mockHistory;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async query(message: string, conversationId?: string) {
|
|
83
|
+
const response = this.execute({ message, conversationId });
|
|
84
|
+
return await response.result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
setContext(ctx: FlinkContext) {
|
|
88
|
+
(this as any).ctx = ctx;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const agent = new TestAgent();
|
|
93
|
+
agent.setContext(mockCtx);
|
|
94
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), {});
|
|
95
|
+
|
|
96
|
+
await agent.query("New question", "conv-123");
|
|
97
|
+
|
|
98
|
+
// Verify LLM was called with history via stream
|
|
99
|
+
const llmCall = (mockLLMAdapter.stream as jasmine.Spy).calls.mostRecent();
|
|
100
|
+
const messages = llmCall.args[0].messages;
|
|
101
|
+
|
|
102
|
+
expect(messages.length).toBeGreaterThan(1);
|
|
103
|
+
expect(messages[0].content).toBe("Previous question");
|
|
104
|
+
expect(messages[1].content).toBe("Previous answer");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("onStep hook", () => {
|
|
109
|
+
it("should be called after each step with context", async () => {
|
|
110
|
+
const onStepSpy = jasmine.createSpy("onStep");
|
|
111
|
+
|
|
112
|
+
// Create tool that triggers multiple steps
|
|
113
|
+
const toolProps: FlinkToolProps = {
|
|
114
|
+
id: "test_tool",
|
|
115
|
+
description: "Test tool",
|
|
116
|
+
inputSchema: z.object({}),
|
|
117
|
+
};
|
|
118
|
+
const toolFn = jasmine.createSpy("toolFn").and.returnValue(Promise.resolve({ success: true, data: { result: "ok" } }));
|
|
119
|
+
const toolExecutor = new ToolExecutor(toolProps, toolFn as any, mockCtx);
|
|
120
|
+
const toolsMap = new Map([["test_tool", toolExecutor]]);
|
|
121
|
+
|
|
122
|
+
// Mock LLM to call tool, then finish
|
|
123
|
+
const toolMockAdapter = createStreamingMock([
|
|
124
|
+
{
|
|
125
|
+
textContent: undefined,
|
|
126
|
+
toolCalls: [{ id: "1", name: "test_tool", input: {} }],
|
|
127
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
128
|
+
stopReason: "tool_use" as const,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
textContent: "Done",
|
|
132
|
+
toolCalls: [],
|
|
133
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
134
|
+
stopReason: "end_turn" as const,
|
|
135
|
+
},
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
class TestAgent extends FlinkAgent<FlinkContext> {
|
|
139
|
+
description = "Test agent";
|
|
140
|
+
instructions = "Test instructions";
|
|
141
|
+
tools: string[] = ["test_tool"];
|
|
142
|
+
|
|
143
|
+
protected onStep = onStepSpy;
|
|
144
|
+
|
|
145
|
+
async query(message: string) {
|
|
146
|
+
const response = this.execute({ message });
|
|
147
|
+
return await response.result;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
setContext(ctx: FlinkContext) {
|
|
151
|
+
(this as any).ctx = ctx;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const agent = new TestAgent();
|
|
156
|
+
agent.setContext(mockCtx);
|
|
157
|
+
agent.__init(new Map([["default", toolMockAdapter]]), Object.fromEntries(toolsMap));
|
|
158
|
+
|
|
159
|
+
await agent.query("Test");
|
|
160
|
+
|
|
161
|
+
// Should be called twice (after each step)
|
|
162
|
+
expect(onStepSpy).toHaveBeenCalledTimes(2);
|
|
163
|
+
|
|
164
|
+
const firstCall = onStepSpy.calls.first().args[0];
|
|
165
|
+
expect(firstCall.step).toBe(1);
|
|
166
|
+
expect(firstCall.messages).toBeDefined();
|
|
167
|
+
expect(Array.isArray(firstCall.messages)).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("afterRun hook", () => {
|
|
172
|
+
it("should be called after execution with full context", async () => {
|
|
173
|
+
const afterRunSpy = jasmine.createSpy("afterRun");
|
|
174
|
+
|
|
175
|
+
class TestAgent extends FlinkAgent<FlinkContext> {
|
|
176
|
+
description = "Test agent";
|
|
177
|
+
instructions = "Test instructions";
|
|
178
|
+
tools: string[] = [];
|
|
179
|
+
|
|
180
|
+
protected afterRun = afterRunSpy;
|
|
181
|
+
|
|
182
|
+
async query(message: string, conversationId?: string) {
|
|
183
|
+
const response = this.execute({ message, conversationId });
|
|
184
|
+
return await response.result;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
setContext(ctx: FlinkContext) {
|
|
188
|
+
(this as any).ctx = ctx;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const agent = new TestAgent();
|
|
193
|
+
agent.setContext(mockCtx);
|
|
194
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), {});
|
|
195
|
+
|
|
196
|
+
await agent.query("Hello", "conv-456");
|
|
197
|
+
|
|
198
|
+
expect(afterRunSpy).toHaveBeenCalled();
|
|
199
|
+
const [result, context] = afterRunSpy.calls.mostRecent().args;
|
|
200
|
+
expect(result.message).toBe("Test response");
|
|
201
|
+
expect(context.conversationId).toBe("conv-456");
|
|
202
|
+
expect(context.messages).toBeDefined();
|
|
203
|
+
expect(context.result).toBe(result);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("should not be called for sub-agents when using inherit strategy", async () => {
|
|
207
|
+
const afterRunSpy = jasmine.createSpy("afterRun");
|
|
208
|
+
|
|
209
|
+
class TestAgent extends FlinkAgent<FlinkContext> {
|
|
210
|
+
description = "Test agent";
|
|
211
|
+
instructions = "Test instructions";
|
|
212
|
+
tools: string[] = [];
|
|
213
|
+
conversationStrategy = "inherit" as const;
|
|
214
|
+
|
|
215
|
+
protected afterRun = afterRunSpy;
|
|
216
|
+
|
|
217
|
+
async query(message: string) {
|
|
218
|
+
const response = this.execute({ message });
|
|
219
|
+
return await response.result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
setContext(ctx: FlinkContext) {
|
|
223
|
+
(this as any).ctx = ctx;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const agent = new TestAgent();
|
|
228
|
+
agent.setContext(mockCtx);
|
|
229
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), {});
|
|
230
|
+
|
|
231
|
+
// Execute with isSubAgentCall metadata using public run method
|
|
232
|
+
const response = agent.run({
|
|
233
|
+
message: "Test",
|
|
234
|
+
metadata: { isSubAgentCall: true, parentAgentId: "parent" },
|
|
235
|
+
});
|
|
236
|
+
await response.result;
|
|
237
|
+
|
|
238
|
+
// afterRun should still be called (agent decides whether to save based on isSubAgent flag)
|
|
239
|
+
expect(afterRunSpy).toHaveBeenCalled();
|
|
240
|
+
const context = afterRunSpy.calls.mostRecent().args[1];
|
|
241
|
+
expect(context.isSubAgent).toBe(true);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("metadata propagation", () => {
|
|
246
|
+
it("should propagate metadata through execution context", async () => {
|
|
247
|
+
const beforeRunSpy = jasmine.createSpy("beforeRun");
|
|
248
|
+
const afterRunSpy = jasmine.createSpy("afterRun");
|
|
249
|
+
|
|
250
|
+
class TestAgent extends FlinkAgent<FlinkContext> {
|
|
251
|
+
description = "Test agent";
|
|
252
|
+
instructions = "Test instructions";
|
|
253
|
+
tools: string[] = [];
|
|
254
|
+
|
|
255
|
+
protected beforeRun = beforeRunSpy;
|
|
256
|
+
protected afterRun = afterRunSpy;
|
|
257
|
+
|
|
258
|
+
async query(message: string) {
|
|
259
|
+
const response = this.execute({
|
|
260
|
+
message,
|
|
261
|
+
metadata: { customKey: "customValue", traceId: "trace-123" },
|
|
262
|
+
});
|
|
263
|
+
return await response.result;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
setContext(ctx: FlinkContext) {
|
|
267
|
+
(this as any).ctx = ctx;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const agent = new TestAgent();
|
|
272
|
+
agent.setContext(mockCtx);
|
|
273
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), {});
|
|
274
|
+
|
|
275
|
+
await agent.query("Test");
|
|
276
|
+
|
|
277
|
+
const beforeRunContext = beforeRunSpy.calls.mostRecent().args[1];
|
|
278
|
+
expect(beforeRunContext.metadata).toEqual({
|
|
279
|
+
customKey: "customValue",
|
|
280
|
+
traceId: "trace-123",
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const afterRunContext = afterRunSpy.calls.mostRecent().args[1];
|
|
284
|
+
expect(afterRunContext.metadata).toEqual({
|
|
285
|
+
customKey: "customValue",
|
|
286
|
+
traceId: "trace-123",
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
});
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { FlinkAgent } from "../src/ai/FlinkAgent";
|
|
3
|
+
import { FlinkToolProps } from "../src/ai/FlinkTool";
|
|
4
|
+
import { LLMAdapter } from "../src/ai/LLMAdapter";
|
|
5
|
+
import { ToolExecutor } from "../src/ai/ToolExecutor";
|
|
6
|
+
import { FlinkContext } from "../src/FlinkContext";
|
|
7
|
+
import { createStreamingMock } from "./testHelpers";
|
|
8
|
+
|
|
9
|
+
describe("FlinkAgent", () => {
|
|
10
|
+
let mockCtx: FlinkContext;
|
|
11
|
+
let mockLLMAdapter: LLMAdapter;
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
mockCtx = {
|
|
15
|
+
repos: {},
|
|
16
|
+
plugins: {},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Mock LLM Adapter
|
|
20
|
+
mockLLMAdapter = createStreamingMock([
|
|
21
|
+
{
|
|
22
|
+
textContent: "Test response",
|
|
23
|
+
toolCalls: [],
|
|
24
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
25
|
+
stopReason: "end_turn" as const,
|
|
26
|
+
},
|
|
27
|
+
]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("withUser binding", () => {
|
|
31
|
+
class TestAgent extends FlinkAgent<FlinkContext> {
|
|
32
|
+
description = "Test agent";
|
|
33
|
+
instructions = "Test instructions";
|
|
34
|
+
tools: string[] = [];
|
|
35
|
+
|
|
36
|
+
async query(message: string) {
|
|
37
|
+
const response = this.execute({ message });
|
|
38
|
+
return await response.result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
setContext(ctx: FlinkContext) {
|
|
42
|
+
(this as any).ctx = ctx;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
it("should bind user to agent instance", () => {
|
|
47
|
+
const agent = new TestAgent();
|
|
48
|
+
agent.setContext(mockCtx);
|
|
49
|
+
|
|
50
|
+
const user = { id: "123", permissions: ["admin"] };
|
|
51
|
+
const boundAgent = agent.withUser(user);
|
|
52
|
+
|
|
53
|
+
// Should return a new instance
|
|
54
|
+
expect(boundAgent).not.toBe(agent);
|
|
55
|
+
// Should have bound user
|
|
56
|
+
expect((boundAgent as any)._boundUser).toBe(user);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should use bound user for agent permission checks", async () => {
|
|
60
|
+
class PermissionedAgent extends TestAgent {
|
|
61
|
+
permissions = "admin";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const agent = new PermissionedAgent();
|
|
65
|
+
agent.setContext(mockCtx);
|
|
66
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), {});
|
|
67
|
+
|
|
68
|
+
const userWithoutPermission = { permissions: [] };
|
|
69
|
+
const userWithPermission = { permissions: ["admin"] };
|
|
70
|
+
|
|
71
|
+
// Should fail without permission
|
|
72
|
+
const boundAgentFail = agent.withUser(userWithoutPermission);
|
|
73
|
+
await expectAsync(boundAgentFail.query("test")).toBeRejected();
|
|
74
|
+
|
|
75
|
+
// Should succeed with permission
|
|
76
|
+
const boundAgentSuccess = agent.withUser(userWithPermission);
|
|
77
|
+
const result = await boundAgentSuccess.query("test");
|
|
78
|
+
expect(result.message).toBeDefined();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should pass bound user to tools", async () => {
|
|
82
|
+
const toolSpy = jasmine.createSpy("toolFn").and.returnValue(Promise.resolve({ success: true, data: { result: "ok" } }));
|
|
83
|
+
|
|
84
|
+
const toolProps: FlinkToolProps = {
|
|
85
|
+
id: "test_tool",
|
|
86
|
+
description: "Test tool",
|
|
87
|
+
inputSchema: z.object({}),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const toolExecutor = new ToolExecutor(toolProps, toolSpy as any, mockCtx);
|
|
91
|
+
const toolsMap = new Map([["test_tool", toolExecutor]]);
|
|
92
|
+
|
|
93
|
+
// Mock LLM to call tool
|
|
94
|
+
const toolMockAdapter = createStreamingMock([
|
|
95
|
+
{
|
|
96
|
+
textContent: undefined,
|
|
97
|
+
toolCalls: [{ id: "1", name: "test_tool", input: {} }],
|
|
98
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
99
|
+
stopReason: "tool_use" as const,
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
textContent: "Done",
|
|
103
|
+
toolCalls: [],
|
|
104
|
+
usage: { inputTokens: 10, outputTokens: 20 },
|
|
105
|
+
stopReason: "end_turn" as const,
|
|
106
|
+
},
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
class ToolAgent extends TestAgent {
|
|
110
|
+
tools: string[] = ["test_tool"];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const agent = new ToolAgent();
|
|
114
|
+
agent.setContext(mockCtx);
|
|
115
|
+
agent.__init(new Map([["default", toolMockAdapter]]), Object.fromEntries(toolsMap));
|
|
116
|
+
|
|
117
|
+
const user = { id: "123", permissions: ["admin"] };
|
|
118
|
+
const boundAgent = agent.withUser(user);
|
|
119
|
+
|
|
120
|
+
await boundAgent.query("test");
|
|
121
|
+
|
|
122
|
+
// Tool should have been called with the bound user
|
|
123
|
+
expect(toolSpy).toHaveBeenCalled();
|
|
124
|
+
const toolCall = (toolSpy as jasmine.Spy).calls.mostRecent();
|
|
125
|
+
expect(toolCall.args[0].user).toBe(user);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("Tool permission filtering", () => {
|
|
130
|
+
class TestAgent extends FlinkAgent<FlinkContext> {
|
|
131
|
+
description = "Test agent";
|
|
132
|
+
instructions = "Test instructions";
|
|
133
|
+
tools: string[] = ["public_tool", "admin_tool"];
|
|
134
|
+
|
|
135
|
+
async query(message: string) {
|
|
136
|
+
const response = this.execute({ message });
|
|
137
|
+
return await response.result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
setContext(ctx: FlinkContext) {
|
|
141
|
+
(this as any).ctx = ctx;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
it("should filter out tools user doesn't have permission for", async () => {
|
|
146
|
+
// Create public tool (no permissions)
|
|
147
|
+
const publicToolProps: FlinkToolProps = {
|
|
148
|
+
id: "public_tool",
|
|
149
|
+
description: "Public tool",
|
|
150
|
+
inputSchema: z.object({}),
|
|
151
|
+
};
|
|
152
|
+
const publicToolFn = async () => ({ success: true as const, data: {} });
|
|
153
|
+
const publicTool = new ToolExecutor(publicToolProps, publicToolFn, mockCtx);
|
|
154
|
+
|
|
155
|
+
// Create admin tool (requires admin permission)
|
|
156
|
+
const adminToolProps: FlinkToolProps = {
|
|
157
|
+
id: "admin_tool",
|
|
158
|
+
description: "Admin tool",
|
|
159
|
+
inputSchema: z.object({}),
|
|
160
|
+
permissions: "admin",
|
|
161
|
+
};
|
|
162
|
+
const adminToolFn = async () => ({ success: true as const, data: {} });
|
|
163
|
+
const adminTool = new ToolExecutor(adminToolProps, adminToolFn, mockCtx);
|
|
164
|
+
|
|
165
|
+
const toolsMap = new Map([
|
|
166
|
+
["public_tool", publicTool],
|
|
167
|
+
["admin_tool", adminTool],
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
const agent = new TestAgent();
|
|
171
|
+
agent.setContext(mockCtx);
|
|
172
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), Object.fromEntries(toolsMap));
|
|
173
|
+
|
|
174
|
+
// User without admin permission
|
|
175
|
+
const regularUser = { permissions: [] };
|
|
176
|
+
const boundAgent = agent.withUser(regularUser);
|
|
177
|
+
|
|
178
|
+
await boundAgent.query("test");
|
|
179
|
+
|
|
180
|
+
// LLM should have been called with only the public tool via stream
|
|
181
|
+
const llmCall = (mockLLMAdapter.stream as jasmine.Spy).calls.mostRecent();
|
|
182
|
+
const toolsProvided = llmCall.args[0].tools;
|
|
183
|
+
|
|
184
|
+
expect(toolsProvided.length).toBe(1);
|
|
185
|
+
expect(toolsProvided[0].name).toBe("public_tool");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should show all tools to admin users", async () => {
|
|
189
|
+
// Create public and admin tools (same as above)
|
|
190
|
+
const publicToolProps: FlinkToolProps = {
|
|
191
|
+
id: "public_tool",
|
|
192
|
+
description: "Public tool",
|
|
193
|
+
inputSchema: z.object({}),
|
|
194
|
+
};
|
|
195
|
+
const publicToolFn = async () => ({ success: true as const, data: {} });
|
|
196
|
+
const publicTool = new ToolExecutor(publicToolProps, publicToolFn, mockCtx);
|
|
197
|
+
|
|
198
|
+
const adminToolProps: FlinkToolProps = {
|
|
199
|
+
id: "admin_tool",
|
|
200
|
+
description: "Admin tool",
|
|
201
|
+
inputSchema: z.object({}),
|
|
202
|
+
permissions: "admin",
|
|
203
|
+
};
|
|
204
|
+
const adminToolFn = async () => ({ success: true as const, data: {} });
|
|
205
|
+
const adminTool = new ToolExecutor(adminToolProps, adminToolFn, mockCtx);
|
|
206
|
+
|
|
207
|
+
const toolsMap = new Map([
|
|
208
|
+
["public_tool", publicTool],
|
|
209
|
+
["admin_tool", adminTool],
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
const agent = new TestAgent();
|
|
213
|
+
agent.setContext(mockCtx);
|
|
214
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), Object.fromEntries(toolsMap));
|
|
215
|
+
|
|
216
|
+
// User with admin permission
|
|
217
|
+
const adminUser = { permissions: ["admin"] };
|
|
218
|
+
const boundAgent = agent.withUser(adminUser);
|
|
219
|
+
|
|
220
|
+
await boundAgent.query("test");
|
|
221
|
+
|
|
222
|
+
// LLM should have been called with both tools via stream
|
|
223
|
+
const llmCall = (mockLLMAdapter.stream as jasmine.Spy).calls.mostRecent();
|
|
224
|
+
const toolsProvided = llmCall.args[0].tools;
|
|
225
|
+
|
|
226
|
+
expect(toolsProvided.length).toBe(2);
|
|
227
|
+
expect(toolsProvided.map((t: any) => t.name).sort()).toEqual(["admin_tool", "public_tool"]);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
describe("Tool references (FlinkToolFile imports)", () => {
|
|
232
|
+
it("should support tool file references in addition to string IDs", () => {
|
|
233
|
+
// Create mock tool file (same structure as real tool files)
|
|
234
|
+
const mockToolFile = {
|
|
235
|
+
Tool: {
|
|
236
|
+
id: "mock-tool",
|
|
237
|
+
description: "Mock tool for testing",
|
|
238
|
+
inputSchema: z.object({ input: z.string() }),
|
|
239
|
+
},
|
|
240
|
+
default: async () => ({ success: true as const, data: "ok" }),
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Create tool executor from the mock file
|
|
244
|
+
const toolExecutor = new ToolExecutor(mockToolFile.Tool, mockToolFile.default as any, mockCtx);
|
|
245
|
+
|
|
246
|
+
// Set up tools in context (not as Map, but as plain object for _tools)
|
|
247
|
+
// Agent using tool reference instead of string ID
|
|
248
|
+
class TestAgentWithToolRef extends FlinkAgent<FlinkContext> {
|
|
249
|
+
description = "Test agent";
|
|
250
|
+
instructions = "Test instructions";
|
|
251
|
+
tools = [mockToolFile]; // Direct tool file reference!
|
|
252
|
+
|
|
253
|
+
async query(message: string) {
|
|
254
|
+
const response = this.execute({ message });
|
|
255
|
+
return await response.result;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const agent = new TestAgentWithToolRef();
|
|
260
|
+
(agent as any).ctx = mockCtx;
|
|
261
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), { "mock-tool": toolExecutor });
|
|
262
|
+
|
|
263
|
+
// Verify resolveTools() correctly extracts tool ID from FlinkToolFile
|
|
264
|
+
const resolvedTools = (agent as any).resolveTools();
|
|
265
|
+
expect(resolvedTools.has("mock-tool")).toBe(true);
|
|
266
|
+
expect(resolvedTools.get("mock-tool")).toBe(toolExecutor);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("should support mixed string IDs and tool file references", () => {
|
|
270
|
+
// Create two tools: one referenced by string, one by file reference
|
|
271
|
+
const mockToolFile = {
|
|
272
|
+
Tool: {
|
|
273
|
+
id: "tool-by-reference",
|
|
274
|
+
description: "Tool referenced by import",
|
|
275
|
+
inputSchema: z.object({}),
|
|
276
|
+
},
|
|
277
|
+
default: async () => ({ success: true as const, data: "ok" }),
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
const toolByRefExecutor = new ToolExecutor(mockToolFile.Tool, mockToolFile.default as any, mockCtx);
|
|
281
|
+
const toolByStringExecutor = new ToolExecutor(
|
|
282
|
+
{ id: "tool-by-string", description: "Tool by string", inputSchema: z.object({}) },
|
|
283
|
+
async () => ({ success: true as const, data: "ok" }),
|
|
284
|
+
mockCtx
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
class MixedToolsAgent extends FlinkAgent<FlinkContext> {
|
|
288
|
+
description = "Test agent";
|
|
289
|
+
instructions = "Test instructions";
|
|
290
|
+
tools = [
|
|
291
|
+
mockToolFile, // File reference
|
|
292
|
+
"tool-by-string", // String ID
|
|
293
|
+
];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const agent = new MixedToolsAgent();
|
|
297
|
+
(agent as any).ctx = mockCtx;
|
|
298
|
+
agent.__init(new Map([["default", mockLLMAdapter]]), {
|
|
299
|
+
"tool-by-reference": toolByRefExecutor,
|
|
300
|
+
"tool-by-string": toolByStringExecutor,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// Both tools should be resolved correctly
|
|
304
|
+
const resolvedTools = (agent as any).resolveTools();
|
|
305
|
+
expect(resolvedTools.size).toBe(2);
|
|
306
|
+
expect(resolvedTools.has("tool-by-reference")).toBe(true);
|
|
307
|
+
expect(resolvedTools.has("tool-by-string")).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
});
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { FlinkApp } from "../src/FlinkApp";
|
|
2
2
|
import { FlinkContext } from "../src/FlinkContext";
|
|
3
|
-
import { FlinkResponse } from "../src/FlinkResponse";
|
|
4
3
|
import { FlinkError } from "../src/FlinkErrors";
|
|
5
|
-
import { badRequest, internalServerError, notFound } from "../src/FlinkErrors";
|
|
6
4
|
import { HttpMethod } from "../src/FlinkHttpHandler";
|
|
5
|
+
import { FlinkResponse } from "../src/FlinkResponse";
|
|
7
6
|
|
|
8
7
|
interface TestContext extends FlinkContext {}
|
|
9
8
|
|