@node-llm/orm 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -2
- package/README.md +94 -8
- package/bin/cli.js +40 -6
- package/dist/BaseChat.d.ts +3 -1
- package/dist/BaseChat.d.ts.map +1 -1
- package/dist/BaseChat.js +5 -0
- package/dist/adapters/prisma/AgentSession.d.ts +140 -0
- package/dist/adapters/prisma/AgentSession.d.ts.map +1 -0
- package/dist/adapters/prisma/AgentSession.js +284 -0
- package/dist/adapters/prisma/Chat.d.ts +3 -2
- package/dist/adapters/prisma/Chat.d.ts.map +1 -1
- package/dist/adapters/prisma/Chat.js +34 -4
- package/dist/adapters/prisma/index.d.ts +25 -2
- package/dist/adapters/prisma/index.d.ts.map +1 -1
- package/dist/adapters/prisma/index.js +25 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -1
- package/migrations/README.md +53 -0
- package/migrations/add_agent_session.sql +44 -0
- package/migrations/add_thinking_support.sql +34 -0
- package/package.json +6 -2
- package/schema.prisma +50 -33
- package/src/BaseChat.ts +8 -1
- package/src/adapters/prisma/AgentSession.ts +458 -0
- package/src/adapters/prisma/Chat.ts +55 -7
- package/src/adapters/prisma/index.ts +33 -2
- package/src/index.ts +21 -1
- package/test/AgentSession.test.ts +204 -0
- package/test/CodeWins.test.ts +116 -0
- package/test/Middleware.test.ts +137 -0
- package/test/Strictness.test.ts +117 -0
- package/test/docs/prisma-docs.test.ts +221 -0
- package/test/docs/readme-exports.test.ts +62 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -68,7 +68,8 @@ export class Chat extends BaseChat {
|
|
|
68
68
|
|
|
69
69
|
const coreChat = llmInstance.chat(model || undefined, {
|
|
70
70
|
messages: history,
|
|
71
|
-
...this.localOptions
|
|
71
|
+
...this.localOptions,
|
|
72
|
+
middlewares: this.customMiddlewares
|
|
72
73
|
}) as any;
|
|
73
74
|
|
|
74
75
|
// Register tools
|
|
@@ -333,17 +334,39 @@ export class Chat extends BaseChat {
|
|
|
333
334
|
}
|
|
334
335
|
}
|
|
335
336
|
|
|
337
|
+
/**
|
|
338
|
+
* Helper to find the correct table property in the prisma client.
|
|
339
|
+
* Prisma usually camelCases model names (e.g., AssistantChat -> assistantChat),
|
|
340
|
+
* but mapping can vary based on configuration.
|
|
341
|
+
*/
|
|
342
|
+
function getTable(prisma: any, tableName: string): any {
|
|
343
|
+
if (prisma[tableName]) return prisma[tableName];
|
|
344
|
+
|
|
345
|
+
// Try case-insensitive match if not found directly
|
|
346
|
+
const keys = Object.keys(prisma).filter((k) => !k.startsWith("$") && !k.startsWith("_"));
|
|
347
|
+
const match = keys.find((k) => k.toLowerCase() === tableName.toLowerCase());
|
|
348
|
+
|
|
349
|
+
if (match) return prisma[match];
|
|
350
|
+
|
|
351
|
+
// If still not found, search for the model name if it's different from the property name
|
|
352
|
+
// but for now, we'll just throw a clear error
|
|
353
|
+
throw new Error(
|
|
354
|
+
`[@node-llm/orm] Prisma table "${tableName}" not found. Available tables: ${keys.join(", ")}`
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
336
358
|
/**
|
|
337
359
|
* Convenience method to create a new chat session.
|
|
338
360
|
*/
|
|
339
361
|
export async function createChat<T = Record<string, any>>(
|
|
340
|
-
prisma:
|
|
362
|
+
prisma: any,
|
|
341
363
|
llm: NodeLLMCore,
|
|
342
364
|
options: ChatOptions & { tableNames?: TableNames } & T = {} as any
|
|
343
365
|
): Promise<Chat> {
|
|
344
366
|
const chatTable = options.tableNames?.chat || "llmChat";
|
|
345
367
|
|
|
346
368
|
// Extract known options so we don't double-pass them or pass them incorrectly
|
|
369
|
+
// runtime options should NOT be persisted to DB
|
|
347
370
|
const {
|
|
348
371
|
model,
|
|
349
372
|
provider,
|
|
@@ -352,16 +375,32 @@ export async function createChat<T = Record<string, any>>(
|
|
|
352
375
|
tableNames: _tableNames,
|
|
353
376
|
debug: _debug,
|
|
354
377
|
persistence: _persistence,
|
|
378
|
+
middlewares: _middlewares,
|
|
379
|
+
maxToolCalls: _maxToolCalls,
|
|
380
|
+
thinking: _thinking,
|
|
381
|
+
temperature: _temperature,
|
|
382
|
+
maxTokens: _maxTokens,
|
|
383
|
+
headers: _headers,
|
|
384
|
+
requestTimeout: _requestTimeout,
|
|
385
|
+
params: _params,
|
|
355
386
|
...extras
|
|
356
387
|
} = options;
|
|
357
388
|
|
|
358
|
-
|
|
389
|
+
if (options.debug) {
|
|
390
|
+
const keys = Object.keys(prisma).filter((k) => !k.startsWith("$") && !k.startsWith("_"));
|
|
391
|
+
console.log(
|
|
392
|
+
`[@node-llm/orm] createChat: table=${chatTable}, availableTables=${keys.join(", ")}`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const table = getTable(prisma, chatTable);
|
|
397
|
+
const record = await table.create({
|
|
359
398
|
data: {
|
|
360
399
|
model,
|
|
361
400
|
provider,
|
|
362
401
|
instructions,
|
|
363
402
|
metadata: metadata ?? null,
|
|
364
|
-
...extras
|
|
403
|
+
...(extras as any)
|
|
365
404
|
}
|
|
366
405
|
});
|
|
367
406
|
|
|
@@ -372,16 +411,25 @@ export async function createChat<T = Record<string, any>>(
|
|
|
372
411
|
* Convenience method to load an existing chat session.
|
|
373
412
|
*/
|
|
374
413
|
export async function loadChat(
|
|
375
|
-
prisma:
|
|
414
|
+
prisma: any,
|
|
376
415
|
llm: NodeLLMCore,
|
|
377
416
|
chatId: string,
|
|
378
|
-
options: ChatOptions & { tableNames?: TableNames } = {}
|
|
417
|
+
options: ChatOptions & { tableNames?: TableNames; debug?: boolean } = {}
|
|
379
418
|
): Promise<Chat | null> {
|
|
380
419
|
const chatTable = options.tableNames?.chat || "llmChat";
|
|
381
|
-
|
|
420
|
+
|
|
421
|
+
if (options.debug) {
|
|
422
|
+
const keys = Object.keys(prisma).filter((k) => !k.startsWith("$") && !k.startsWith("_"));
|
|
423
|
+
console.log(`[@node-llm/orm] loadChat: table=${chatTable}, availableTables=${keys.join(", ")}`);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const table = getTable(prisma, chatTable);
|
|
427
|
+
const record = await table.findUnique({
|
|
382
428
|
where: { id: chatId }
|
|
383
429
|
});
|
|
384
430
|
|
|
385
431
|
if (!record) return null;
|
|
432
|
+
|
|
433
|
+
// Reconstruct chat with options from DB or manual overrides if needed
|
|
386
434
|
return new Chat(prisma, llm, record, options, options.tableNames);
|
|
387
435
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Prisma adapter for NodeLLM ORM.
|
|
5
5
|
* Provides automatic persistence of chats, messages, tool calls, and API requests.
|
|
6
6
|
*
|
|
7
|
-
* @example
|
|
7
|
+
* @example Chat API (low-level)
|
|
8
8
|
* ```typescript
|
|
9
9
|
* import { PrismaClient } from '@prisma/client';
|
|
10
10
|
* import { createLLM } from '@node-llm/core';
|
|
@@ -21,7 +21,38 @@
|
|
|
21
21
|
* const response = await chat.ask('Hello!');
|
|
22
22
|
* console.log(response.content);
|
|
23
23
|
* ```
|
|
24
|
+
*
|
|
25
|
+
* @example AgentSession API (recommended for agents)
|
|
26
|
+
* ```typescript
|
|
27
|
+
* import { Agent } from '@node-llm/core';
|
|
28
|
+
* import { createAgentSession, loadAgentSession } from '@node-llm/orm/prisma';
|
|
29
|
+
*
|
|
30
|
+
* class SupportAgent extends Agent {
|
|
31
|
+
* static model = 'gpt-4.1';
|
|
32
|
+
* static instructions = 'You are a helpful support agent.';
|
|
33
|
+
* }
|
|
34
|
+
*
|
|
35
|
+
* // Create new session
|
|
36
|
+
* const session = await createAgentSession(prisma, llm, SupportAgent, {
|
|
37
|
+
* metadata: { userId: 'user_123' }
|
|
38
|
+
* });
|
|
39
|
+
* await session.ask('Hello!');
|
|
40
|
+
*
|
|
41
|
+
* // Resume later (Code Wins - model/tools from class, history from DB)
|
|
42
|
+
* const session = await loadAgentSession(prisma, llm, SupportAgent, sessionId);
|
|
43
|
+
* await session.ask('Continue our conversation');
|
|
44
|
+
* ```
|
|
24
45
|
*/
|
|
25
46
|
|
|
47
|
+
// Chat API
|
|
26
48
|
export { Chat, createChat, loadChat } from "./Chat.js";
|
|
27
|
-
export type { ChatRecord, MessageRecord, ChatOptions
|
|
49
|
+
export type { ChatRecord, MessageRecord, ChatOptions } from "./Chat.js";
|
|
50
|
+
|
|
51
|
+
// AgentSession API
|
|
52
|
+
export { AgentSession, createAgentSession, loadAgentSession } from "./AgentSession.js";
|
|
53
|
+
export type {
|
|
54
|
+
AgentSessionRecord,
|
|
55
|
+
CreateAgentSessionOptions,
|
|
56
|
+
LoadAgentSessionOptions,
|
|
57
|
+
TableNames // Export from AgentSession which includes agentSession key
|
|
58
|
+
} from "./AgentSession.js";
|
package/src/index.ts
CHANGED
|
@@ -23,13 +23,33 @@
|
|
|
23
23
|
* await chat.ask('Hello!');
|
|
24
24
|
* ```
|
|
25
25
|
*
|
|
26
|
+
* ## Agent Sessions (Recommended for Agents)
|
|
27
|
+
*
|
|
28
|
+
* ```typescript
|
|
29
|
+
* import { Agent } from '@node-llm/core';
|
|
30
|
+
* import { createAgentSession, loadAgentSession } from '@node-llm/orm/prisma';
|
|
31
|
+
*
|
|
32
|
+
* class SupportAgent extends Agent {
|
|
33
|
+
* static model = 'gpt-4.1';
|
|
34
|
+
* static instructions = 'You are a helpful support agent.';
|
|
35
|
+
* }
|
|
36
|
+
*
|
|
37
|
+
* // Create and persist
|
|
38
|
+
* const session = await createAgentSession(prisma, llm, SupportAgent);
|
|
39
|
+
* await session.ask('Hello!');
|
|
40
|
+
*
|
|
41
|
+
* // Resume later (Code Wins - model/tools from class, history from DB)
|
|
42
|
+
* const session = await loadAgentSession(prisma, llm, SupportAgent, sessionId);
|
|
43
|
+
* ```
|
|
44
|
+
*
|
|
26
45
|
* ## Adapters
|
|
27
46
|
*
|
|
28
47
|
* - `@node-llm/orm/prisma` - Prisma adapter (recommended)
|
|
29
48
|
*
|
|
30
49
|
* ## Schema
|
|
31
50
|
*
|
|
32
|
-
* The ORM tracks
|
|
51
|
+
* The ORM tracks five core entities:
|
|
52
|
+
* - **AgentSession** - Links Agent class to persistent Chat (v0.5.0+)
|
|
33
53
|
* - **Chat** - Session container (model, provider, instructions)
|
|
34
54
|
* - **Message** - User/Assistant conversation history
|
|
35
55
|
* - **ToolCall** - Tool executions (name, arguments, results)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { Agent, Tool, NodeLLM } from "@node-llm/core";
|
|
3
|
+
import { createAgentSession, loadAgentSession } from "../src/adapters/prisma/AgentSession";
|
|
4
|
+
|
|
5
|
+
// --- Mocks ---
|
|
6
|
+
|
|
7
|
+
// Mock Prisma Client
|
|
8
|
+
const mockPrisma = {
|
|
9
|
+
llmChat: {
|
|
10
|
+
create: vi.fn(),
|
|
11
|
+
findUnique: vi.fn()
|
|
12
|
+
},
|
|
13
|
+
llmAgentSession: {
|
|
14
|
+
create: vi.fn(),
|
|
15
|
+
findUnique: vi.fn()
|
|
16
|
+
},
|
|
17
|
+
llmMessage: {
|
|
18
|
+
create: vi.fn(),
|
|
19
|
+
findMany: vi.fn(),
|
|
20
|
+
update: vi.fn(),
|
|
21
|
+
delete: vi.fn()
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// Mock LLM
|
|
26
|
+
const createMockChat = () => {
|
|
27
|
+
const mockChat = {
|
|
28
|
+
withInstructions: vi.fn().mockReturnThis(),
|
|
29
|
+
withTools: vi.fn().mockReturnThis(),
|
|
30
|
+
withSchema: vi.fn().mockReturnThis(),
|
|
31
|
+
ask: vi.fn().mockResolvedValue({
|
|
32
|
+
content: "Response",
|
|
33
|
+
meta: {},
|
|
34
|
+
usage: { input_tokens: 10, output_tokens: 5 }
|
|
35
|
+
}),
|
|
36
|
+
stream: vi.fn(),
|
|
37
|
+
history: [],
|
|
38
|
+
totalUsage: { input_tokens: 0, output_tokens: 0 },
|
|
39
|
+
modelId: "mock-model",
|
|
40
|
+
// Hook methods required by Agent constructor
|
|
41
|
+
beforeRequest: vi.fn().mockReturnThis(),
|
|
42
|
+
onToolCallStart: vi.fn().mockReturnThis(),
|
|
43
|
+
onToolCallEnd: vi.fn().mockReturnThis(),
|
|
44
|
+
onToolCallError: vi.fn().mockReturnThis(),
|
|
45
|
+
onEndMessage: vi.fn().mockReturnThis()
|
|
46
|
+
};
|
|
47
|
+
return mockChat;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const mockLlm = {
|
|
51
|
+
chat: vi.fn().mockImplementation(() => createMockChat())
|
|
52
|
+
} as unknown as typeof NodeLLM;
|
|
53
|
+
|
|
54
|
+
// --- Test Classes ---
|
|
55
|
+
|
|
56
|
+
class TestAgent extends Agent {
|
|
57
|
+
static model = "gpt-4-test";
|
|
58
|
+
static instructions = "Test instructions";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
describe("AgentSession", () => {
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
vi.clearAllMocks();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("createAgentSession", () => {
|
|
67
|
+
it("creates a new session with correct metadata", async () => {
|
|
68
|
+
// Setup mocks
|
|
69
|
+
mockPrisma.llmChat.create.mockResolvedValue({ id: "chat-123" });
|
|
70
|
+
mockPrisma.llmAgentSession.create.mockResolvedValue({
|
|
71
|
+
id: "session-123",
|
|
72
|
+
chatId: "chat-123",
|
|
73
|
+
agentClass: "TestAgent",
|
|
74
|
+
metadata: { userId: "user-1" }
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const session = await createAgentSession(mockPrisma as any, mockLlm, TestAgent, {
|
|
78
|
+
metadata: { userId: "user-1" }
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Verify DB calls
|
|
82
|
+
expect(mockPrisma.llmChat.create).toHaveBeenCalledWith(
|
|
83
|
+
expect.objectContaining({
|
|
84
|
+
data: expect.objectContaining({
|
|
85
|
+
model: "gpt-4-test",
|
|
86
|
+
instructions: "Test instructions"
|
|
87
|
+
})
|
|
88
|
+
})
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(mockPrisma.llmAgentSession.create).toHaveBeenCalledWith(
|
|
92
|
+
expect.objectContaining({
|
|
93
|
+
data: expect.objectContaining({
|
|
94
|
+
agentClass: "TestAgent",
|
|
95
|
+
chatId: "chat-123",
|
|
96
|
+
metadata: { userId: "user-1" }
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// Verify Session Object
|
|
102
|
+
expect(session.id).toBe("session-123");
|
|
103
|
+
expect(session.metadata).toEqual({ userId: "user-1" });
|
|
104
|
+
expect(session.agentClass).toBe("TestAgent");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("loadAgentSession", () => {
|
|
109
|
+
it("loads an existing session and injects history", async () => {
|
|
110
|
+
// Setup Mocks
|
|
111
|
+
mockPrisma.llmAgentSession.findUnique.mockResolvedValue({
|
|
112
|
+
id: "session-123",
|
|
113
|
+
chatId: "chat-123",
|
|
114
|
+
agentClass: "TestAgent",
|
|
115
|
+
metadata: { userId: "user-1" }
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
mockPrisma.llmMessage.findMany.mockResolvedValue([
|
|
119
|
+
{ role: "user", content: "Hello" },
|
|
120
|
+
{ role: "assistant", content: "Hi there" }
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
const session = await loadAgentSession(mockPrisma as any, mockLlm, TestAgent, "session-123");
|
|
124
|
+
|
|
125
|
+
expect(session).not.toBeNull();
|
|
126
|
+
expect(session?.id).toBe("session-123");
|
|
127
|
+
|
|
128
|
+
// Verify history injection (implicit via mock setup, would check agent internals in real integration)
|
|
129
|
+
// Implementation detail: The Agent constructor is called with { messages: [...] }
|
|
130
|
+
// We can verify this by checking if the agent property exists and works
|
|
131
|
+
|
|
132
|
+
expect(mockPrisma.llmMessage.findMany).toHaveBeenCalledWith(
|
|
133
|
+
expect.objectContaining({
|
|
134
|
+
where: { chatId: "chat-123" },
|
|
135
|
+
orderBy: { createdAt: "asc" }
|
|
136
|
+
})
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("throws error on agent class mismatch", async () => {
|
|
141
|
+
mockPrisma.llmAgentSession.findUnique.mockResolvedValue({
|
|
142
|
+
id: "session-123",
|
|
143
|
+
agentClass: "OtherAgent", // Mismatch
|
|
144
|
+
chatId: "chat-123"
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await expect(
|
|
148
|
+
loadAgentSession(mockPrisma as any, mockLlm, TestAgent, "session-123")
|
|
149
|
+
).rejects.toThrow("Agent class mismatch");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("ask", () => {
|
|
154
|
+
it("persists user and assistant messages", async () => {
|
|
155
|
+
// Setup Session
|
|
156
|
+
const sessionRecord = {
|
|
157
|
+
id: "session-123",
|
|
158
|
+
chatId: "chat-123",
|
|
159
|
+
agentClass: "TestAgent",
|
|
160
|
+
metadata: {},
|
|
161
|
+
createdAt: new Date(),
|
|
162
|
+
updatedAt: new Date()
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Mock AgentSession manually to test .ask()
|
|
166
|
+
// But simpler to use createAgentSession mock return if we could,
|
|
167
|
+
// here we instantiate directly or via factory.
|
|
168
|
+
|
|
169
|
+
mockPrisma.llmChat.create.mockResolvedValue({ id: "chat-123" });
|
|
170
|
+
mockPrisma.llmAgentSession.create.mockResolvedValue(sessionRecord);
|
|
171
|
+
|
|
172
|
+
const session = await createAgentSession(mockPrisma as any, mockLlm, TestAgent);
|
|
173
|
+
|
|
174
|
+
// Mock message creation
|
|
175
|
+
mockPrisma.llmMessage.create
|
|
176
|
+
.mockResolvedValueOnce({ id: "msg-user" }) // User message
|
|
177
|
+
.mockResolvedValueOnce({ id: "msg-asst" }); // Assistant placeholder
|
|
178
|
+
|
|
179
|
+
mockPrisma.llmMessage.update.mockResolvedValue({
|
|
180
|
+
id: "msg-asst",
|
|
181
|
+
content: "Response",
|
|
182
|
+
role: "assistant"
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await session.ask("Hello");
|
|
186
|
+
|
|
187
|
+
// Verify persistence
|
|
188
|
+
expect(mockPrisma.llmMessage.create).toHaveBeenCalledTimes(2);
|
|
189
|
+
expect(mockPrisma.llmMessage.create).toHaveBeenNthCalledWith(
|
|
190
|
+
1,
|
|
191
|
+
expect.objectContaining({
|
|
192
|
+
data: expect.objectContaining({ role: "user", content: "Hello" })
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
expect(mockPrisma.llmMessage.update).toHaveBeenCalledWith(
|
|
197
|
+
expect.objectContaining({
|
|
198
|
+
where: { id: "msg-asst" },
|
|
199
|
+
data: expect.objectContaining({ content: "Response" })
|
|
200
|
+
})
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { PrismaClient } from "@prisma/client";
|
|
3
|
+
import { NodeLLMCore, Agent } from "@node-llm/core";
|
|
4
|
+
import { createAgentSession, loadAgentSession } from "../src/adapters/prisma/AgentSession.js";
|
|
5
|
+
|
|
6
|
+
// Mock Prisma
|
|
7
|
+
const mockChatTable = {
|
|
8
|
+
create: vi.fn(),
|
|
9
|
+
findUnique: vi.fn(),
|
|
10
|
+
update: vi.fn()
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const mockSessionTable = {
|
|
14
|
+
create: vi.fn(),
|
|
15
|
+
findUnique: vi.fn()
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const mockMessageTable = {
|
|
19
|
+
create: vi.fn(),
|
|
20
|
+
findMany: vi.fn(),
|
|
21
|
+
delete: vi.fn(),
|
|
22
|
+
update: vi.fn()
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const prisma = {
|
|
26
|
+
llmChat: mockChatTable,
|
|
27
|
+
llmAgentSession: mockSessionTable,
|
|
28
|
+
llmMessage: mockMessageTable
|
|
29
|
+
} as unknown as PrismaClient;
|
|
30
|
+
|
|
31
|
+
// Mock Agent & LLM
|
|
32
|
+
class TestAgent extends Agent {
|
|
33
|
+
static override model = "agent-model";
|
|
34
|
+
static override instructions = "agent-instructions";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const mockChat = {
|
|
38
|
+
withInstructions: vi.fn().mockReturnThis(),
|
|
39
|
+
withTools: vi.fn().mockReturnThis(),
|
|
40
|
+
withSchema: vi.fn().mockReturnThis(),
|
|
41
|
+
beforeRequest: vi.fn().mockReturnThis(),
|
|
42
|
+
onToolCallStart: vi.fn().mockReturnThis(),
|
|
43
|
+
onToolCallEnd: vi.fn().mockReturnThis(),
|
|
44
|
+
onToolCallError: vi.fn().mockReturnThis(),
|
|
45
|
+
onEndMessage: vi.fn().mockReturnThis(),
|
|
46
|
+
ask: vi.fn(),
|
|
47
|
+
messages: [],
|
|
48
|
+
modelId: "agent-model"
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const llm = {
|
|
52
|
+
chat: vi.fn(() => mockChat)
|
|
53
|
+
} as unknown as NodeLLMCore;
|
|
54
|
+
|
|
55
|
+
describe("AgentSession - Code Wins Sovereignty", () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
vi.clearAllMocks();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("should prioritize Agent class instructions over DB instructions when loading a session", async () => {
|
|
61
|
+
const sessionId = "sess_123";
|
|
62
|
+
const chatId = "chat_456";
|
|
63
|
+
|
|
64
|
+
// 1. Mock DB returning DIFFERENT instructions than the class
|
|
65
|
+
mockSessionTable.findUnique.mockResolvedValue({
|
|
66
|
+
id: sessionId,
|
|
67
|
+
chatId: chatId,
|
|
68
|
+
agentClass: "TestAgent"
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
mockChatTable.findUnique.mockResolvedValue({
|
|
72
|
+
id: chatId,
|
|
73
|
+
model: "db-model", // DB says db-model
|
|
74
|
+
instructions: "db-instructions" // DB says db-instructions
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
mockMessageTable.findMany.mockResolvedValue([]);
|
|
78
|
+
|
|
79
|
+
// 2. Load the session
|
|
80
|
+
const session = await loadAgentSession(prisma, llm, TestAgent, sessionId);
|
|
81
|
+
|
|
82
|
+
expect(session).toBeDefined();
|
|
83
|
+
|
|
84
|
+
// 3. Verify Agent was instantiated with the correct LLM and history (empty here)
|
|
85
|
+
// The Agent constructor calls llm.chat(model, options)
|
|
86
|
+
expect(llm.chat).toHaveBeenCalledWith("agent-model", expect.any(Object));
|
|
87
|
+
|
|
88
|
+
// 4. Verify instructions applied to chat came from TestAgent.instructions
|
|
89
|
+
expect(mockChat.withInstructions).toHaveBeenCalledWith("agent-instructions");
|
|
90
|
+
expect(mockChat.withInstructions).not.toHaveBeenCalledWith("db-instructions");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("should prioritize Agent class model over DB model when creating a session", async () => {
|
|
94
|
+
mockChatTable.create.mockResolvedValue({ id: "chat_789" });
|
|
95
|
+
mockSessionTable.create.mockResolvedValue({
|
|
96
|
+
id: "sess_789",
|
|
97
|
+
chatId: "chat_789",
|
|
98
|
+
agentClass: "TestAgent"
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// 1. Create a session
|
|
102
|
+
await createAgentSession(prisma, llm, TestAgent);
|
|
103
|
+
|
|
104
|
+
// 2. Verify chat record was created with Agent class properties
|
|
105
|
+
expect(mockChatTable.create).toHaveBeenCalledWith({
|
|
106
|
+
data: expect.objectContaining({
|
|
107
|
+
model: "agent-model",
|
|
108
|
+
instructions: "agent-instructions"
|
|
109
|
+
})
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// 3. Verify the live agent instance also uses these
|
|
113
|
+
expect(llm.chat).toHaveBeenCalledWith("agent-model", expect.any(Object));
|
|
114
|
+
expect(mockChat.withInstructions).toHaveBeenCalledWith("agent-instructions");
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
2
|
+
import { createChat } from "../src/adapters/prisma/index.js";
|
|
3
|
+
import { Middleware } from "@node-llm/core";
|
|
4
|
+
|
|
5
|
+
// Mock Prisma Client
|
|
6
|
+
const createMockPrisma = () => {
|
|
7
|
+
const messages: any[] = [];
|
|
8
|
+
const chats: any[] = [];
|
|
9
|
+
|
|
10
|
+
const tables: any = {
|
|
11
|
+
chat: {
|
|
12
|
+
create: vi.fn(async ({ data }) => {
|
|
13
|
+
const chat = { id: "chat-123", ...data, createdAt: new Date(), updatedAt: new Date() };
|
|
14
|
+
chats.push(chat);
|
|
15
|
+
return chat;
|
|
16
|
+
})
|
|
17
|
+
},
|
|
18
|
+
message: {
|
|
19
|
+
create: vi.fn(async ({ data }) => {
|
|
20
|
+
const message = { id: `msg-${messages.length}`, ...data, createdAt: new Date() };
|
|
21
|
+
messages.push(message);
|
|
22
|
+
return message;
|
|
23
|
+
}),
|
|
24
|
+
findMany: vi.fn(async ({ where }) => {
|
|
25
|
+
let filtered = messages.filter((m) => m.chatId === where.chatId);
|
|
26
|
+
if (where.id?.notIn) {
|
|
27
|
+
filtered = filtered.filter((m) => !where.id.notIn.includes(m.id));
|
|
28
|
+
}
|
|
29
|
+
return filtered;
|
|
30
|
+
}),
|
|
31
|
+
update: vi.fn(async ({ where, data }) => {
|
|
32
|
+
const message = messages.find((m) => m.id === where.id);
|
|
33
|
+
if (message) {
|
|
34
|
+
Object.assign(message, data);
|
|
35
|
+
}
|
|
36
|
+
return message;
|
|
37
|
+
}),
|
|
38
|
+
delete: vi.fn()
|
|
39
|
+
},
|
|
40
|
+
toolCall: {
|
|
41
|
+
create: vi.fn(),
|
|
42
|
+
update: vi.fn()
|
|
43
|
+
},
|
|
44
|
+
request: {
|
|
45
|
+
create: vi.fn()
|
|
46
|
+
},
|
|
47
|
+
_messages: messages
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return new Proxy(tables, {
|
|
51
|
+
get(target, prop) {
|
|
52
|
+
if (prop in target) return target[prop];
|
|
53
|
+
if (typeof prop === "string") {
|
|
54
|
+
if (prop.toLowerCase().includes("chat")) return target.chat;
|
|
55
|
+
if (prop.toLowerCase().includes("message")) return target.message;
|
|
56
|
+
if (prop.toLowerCase().includes("toolcall")) return target.toolCall;
|
|
57
|
+
if (prop.toLowerCase().includes("request")) return target.request;
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Mock NodeLLM with middleware support
|
|
65
|
+
const createMockLLM = () => {
|
|
66
|
+
let capturedMiddlewares: Middleware[] = [];
|
|
67
|
+
|
|
68
|
+
const mockChat: any = {
|
|
69
|
+
system: vi.fn(() => mockChat),
|
|
70
|
+
withTools: vi.fn(() => mockChat),
|
|
71
|
+
onToolCallStart: vi.fn(() => mockChat),
|
|
72
|
+
onToolCallEnd: vi.fn(() => mockChat),
|
|
73
|
+
afterResponse: vi.fn(() => mockChat),
|
|
74
|
+
onNewMessage: vi.fn(() => mockChat),
|
|
75
|
+
onEndMessage: vi.fn(() => mockChat),
|
|
76
|
+
beforeRequest: vi.fn(() => mockChat),
|
|
77
|
+
ask: vi.fn(async () => {
|
|
78
|
+
return {
|
|
79
|
+
content: "Test response",
|
|
80
|
+
meta: { model: "gpt-4o-mini", provider: "openai" },
|
|
81
|
+
usage: { input_tokens: 10, output_tokens: 5, total_tokens: 15 },
|
|
82
|
+
model: "gpt-4o-mini",
|
|
83
|
+
provider: "openai"
|
|
84
|
+
};
|
|
85
|
+
})
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const llm: any = {
|
|
89
|
+
chat: vi.fn((model, options) => {
|
|
90
|
+
if (options?.middlewares) {
|
|
91
|
+
capturedMiddlewares = options.middlewares;
|
|
92
|
+
}
|
|
93
|
+
return mockChat;
|
|
94
|
+
}),
|
|
95
|
+
withProvider: vi.fn(() => llm),
|
|
96
|
+
getMiddlewares: () => capturedMiddlewares
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return llm;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
describe("ORM Middleware Integration", () => {
|
|
103
|
+
let mockPrisma: any;
|
|
104
|
+
let mockLLM: any;
|
|
105
|
+
|
|
106
|
+
beforeEach(() => {
|
|
107
|
+
mockPrisma = createMockPrisma();
|
|
108
|
+
mockLLM = createMockLLM();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("should pass middlewares to core Chat instance", async () => {
|
|
112
|
+
const middleware: Middleware = {
|
|
113
|
+
name: "TestMiddleware",
|
|
114
|
+
onRequest: vi.fn()
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const chat = await createChat(mockPrisma, mockLLM, {
|
|
118
|
+
model: "gpt-4o-mini",
|
|
119
|
+
middlewares: [middleware]
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Middlewares are passed when ask() is called
|
|
123
|
+
await chat.ask("Hello");
|
|
124
|
+
|
|
125
|
+
// Verify chat was created with middlewares
|
|
126
|
+
expect(mockLLM.chat).toHaveBeenCalledWith(
|
|
127
|
+
"gpt-4o-mini",
|
|
128
|
+
expect.objectContaining({
|
|
129
|
+
middlewares: [middleware]
|
|
130
|
+
})
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
const captured = mockLLM.getMiddlewares();
|
|
134
|
+
expect(captured).toHaveLength(1);
|
|
135
|
+
expect(captured[0].name).toBe("TestMiddleware");
|
|
136
|
+
});
|
|
137
|
+
});
|