zeitlich 0.2.26 → 0.2.27
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/dist/{activities-LVQdLF6I.d.ts → activities-DE3_q9yq.d.ts} +5 -2
- package/dist/{activities-BEJRyDVU.d.cts → activities-p8PDlRIK.d.cts} +5 -2
- package/dist/adapters/thread/anthropic/index.cjs +13 -6
- package/dist/adapters/thread/anthropic/index.cjs.map +1 -1
- package/dist/adapters/thread/anthropic/index.d.cts +8 -5
- package/dist/adapters/thread/anthropic/index.d.ts +8 -5
- package/dist/adapters/thread/anthropic/index.js +13 -6
- package/dist/adapters/thread/anthropic/index.js.map +1 -1
- package/dist/adapters/thread/anthropic/workflow.d.cts +2 -2
- package/dist/adapters/thread/anthropic/workflow.d.ts +2 -2
- package/dist/adapters/thread/google-genai/index.cjs +11 -6
- package/dist/adapters/thread/google-genai/index.cjs.map +1 -1
- package/dist/adapters/thread/google-genai/index.d.cts +7 -4
- package/dist/adapters/thread/google-genai/index.d.ts +7 -4
- package/dist/adapters/thread/google-genai/index.js +11 -6
- package/dist/adapters/thread/google-genai/index.js.map +1 -1
- package/dist/adapters/thread/google-genai/workflow.d.cts +2 -2
- package/dist/adapters/thread/google-genai/workflow.d.ts +2 -2
- package/dist/adapters/thread/langchain/index.cjs +32 -5
- package/dist/adapters/thread/langchain/index.cjs.map +1 -1
- package/dist/adapters/thread/langchain/index.d.cts +20 -7
- package/dist/adapters/thread/langchain/index.d.ts +20 -7
- package/dist/adapters/thread/langchain/index.js +32 -6
- package/dist/adapters/thread/langchain/index.js.map +1 -1
- package/dist/adapters/thread/langchain/workflow.d.cts +2 -2
- package/dist/adapters/thread/langchain/workflow.d.ts +2 -2
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/{thread-manager-DOnQzImf.d.cts → thread-manager-Bh9x847n.d.ts} +5 -3
- package/dist/{thread-manager-CH9krS3h.d.ts → thread-manager-BlHua5_v.d.cts} +4 -2
- package/dist/{thread-manager-Czhpxbt6.d.ts → thread-manager-Bz8txKKj.d.cts} +5 -3
- package/dist/{thread-manager-b4DML-qu.d.cts → thread-manager-dzaJHQEA.d.ts} +4 -2
- package/dist/{types-BDRDbm3h.d.cts → types-BfIQABzu.d.cts} +13 -1
- package/dist/{types-WNSeZbWa.d.ts → types-CIkYBoF8.d.ts} +13 -1
- package/package.json +1 -1
- package/src/adapters/thread/anthropic/activities.ts +3 -0
- package/src/adapters/thread/anthropic/model-invoker.ts +7 -2
- package/src/adapters/thread/anthropic/thread-manager.test.ts +137 -0
- package/src/adapters/thread/anthropic/thread-manager.ts +13 -2
- package/src/adapters/thread/google-genai/activities.ts +3 -1
- package/src/adapters/thread/google-genai/model-invoker.ts +7 -3
- package/src/adapters/thread/google-genai/thread-manager.test.ts +159 -0
- package/src/adapters/thread/google-genai/thread-manager.ts +13 -2
- package/src/adapters/thread/langchain/activities.ts +3 -1
- package/src/adapters/thread/langchain/hooks.ts +37 -0
- package/src/adapters/thread/langchain/index.ts +3 -0
- package/src/adapters/thread/langchain/model-invoker.ts +7 -4
- package/src/adapters/thread/langchain/thread-manager.test.ts +144 -0
- package/src/adapters/thread/langchain/thread-manager.ts +12 -1
- package/src/lib/thread/index.ts +1 -0
- package/src/lib/thread/types.ts +13 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { StoredMessage } from "./thread-manager";
|
|
3
|
+
import { createAnthropicThreadManager } from "./thread-manager";
|
|
4
|
+
|
|
5
|
+
function createMockRedis(stored: StoredMessage[]) {
|
|
6
|
+
return {
|
|
7
|
+
exists: vi.fn().mockResolvedValue(1),
|
|
8
|
+
lrange: vi.fn().mockResolvedValue(stored.map((m) => JSON.stringify(m))),
|
|
9
|
+
del: vi.fn().mockResolvedValue(1),
|
|
10
|
+
set: vi.fn().mockResolvedValue("OK"),
|
|
11
|
+
rpush: vi.fn().mockResolvedValue(1),
|
|
12
|
+
expire: vi.fn().mockResolvedValue(1),
|
|
13
|
+
eval: vi.fn().mockResolvedValue(1),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const systemMsg: StoredMessage = {
|
|
18
|
+
id: "sys-1",
|
|
19
|
+
message: { role: "user", content: "You are helpful." },
|
|
20
|
+
isSystem: true,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const userMsg: StoredMessage = {
|
|
24
|
+
id: "msg-1",
|
|
25
|
+
message: { role: "user", content: [{ type: "text", text: "Hello" }] },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const assistantMsg: StoredMessage = {
|
|
29
|
+
id: "msg-2",
|
|
30
|
+
message: { role: "assistant", content: [{ type: "text", text: "Hi there!" }] },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe("Anthropic thread manager hooks", () => {
|
|
34
|
+
describe("onPrepareMessage", () => {
|
|
35
|
+
it("transforms stored messages before system extraction and merge", async () => {
|
|
36
|
+
const hook = vi.fn((msg: StoredMessage) => {
|
|
37
|
+
if (msg.isSystem) return msg;
|
|
38
|
+
const firstBlock = (msg.message.content as Array<{ text: string }>)[0];
|
|
39
|
+
return {
|
|
40
|
+
...msg,
|
|
41
|
+
message: {
|
|
42
|
+
...msg.message,
|
|
43
|
+
content: [{ type: "text" as const, text: `[modified] ${firstBlock?.text}` }],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const redis = createMockRedis([systemMsg, userMsg, assistantMsg]);
|
|
49
|
+
const tm = createAnthropicThreadManager({
|
|
50
|
+
redis: redis as never,
|
|
51
|
+
threadId: "t1",
|
|
52
|
+
hooks: { onPrepareMessage: hook },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const { messages, system } = await tm.prepareForInvocation();
|
|
56
|
+
|
|
57
|
+
expect(hook).toHaveBeenCalledTimes(3);
|
|
58
|
+
expect(hook).toHaveBeenCalledWith(systemMsg, 0, [systemMsg, userMsg, assistantMsg]);
|
|
59
|
+
expect(system).toBe("You are helpful.");
|
|
60
|
+
expect(messages[0]?.content).toEqual([{ type: "text", text: "[modified] Hello" }]);
|
|
61
|
+
expect(messages[1]?.content).toEqual([{ type: "text", text: "[modified] Hi there!" }]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("is not called when not configured", async () => {
|
|
65
|
+
const redis = createMockRedis([userMsg]);
|
|
66
|
+
const tm = createAnthropicThreadManager({
|
|
67
|
+
redis: redis as never,
|
|
68
|
+
threadId: "t1",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const { messages } = await tm.prepareForInvocation();
|
|
72
|
+
expect(messages).toHaveLength(1);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("onPreparedMessage", () => {
|
|
77
|
+
it("transforms SDK-native messages after merge", async () => {
|
|
78
|
+
const hook = vi.fn((msg) => ({
|
|
79
|
+
...msg,
|
|
80
|
+
content: [{ type: "text" as const, text: "[post] done" }],
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
const redis = createMockRedis([userMsg, assistantMsg]);
|
|
84
|
+
const tm = createAnthropicThreadManager({
|
|
85
|
+
redis: redis as never,
|
|
86
|
+
threadId: "t1",
|
|
87
|
+
hooks: { onPreparedMessage: hook },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const { messages } = await tm.prepareForInvocation();
|
|
91
|
+
|
|
92
|
+
expect(hook).toHaveBeenCalledTimes(2);
|
|
93
|
+
expect(messages[0]?.content).toEqual([{ type: "text", text: "[post] done" }]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("receives the full prepared messages array", async () => {
|
|
97
|
+
const hook = vi.fn((msg) => msg);
|
|
98
|
+
|
|
99
|
+
const redis = createMockRedis([userMsg, assistantMsg]);
|
|
100
|
+
const tm = createAnthropicThreadManager({
|
|
101
|
+
redis: redis as never,
|
|
102
|
+
threadId: "t1",
|
|
103
|
+
hooks: { onPreparedMessage: hook },
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
await tm.prepareForInvocation();
|
|
107
|
+
|
|
108
|
+
const args = hook.mock.calls[0] as unknown as [unknown, number, unknown[]];
|
|
109
|
+
expect(args[2]).toHaveLength(2);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("both hooks combined", () => {
|
|
114
|
+
it("runs onPrepareMessage before onPreparedMessage", async () => {
|
|
115
|
+
const order: string[] = [];
|
|
116
|
+
|
|
117
|
+
const redis = createMockRedis([userMsg]);
|
|
118
|
+
const tm = createAnthropicThreadManager({
|
|
119
|
+
redis: redis as never,
|
|
120
|
+
threadId: "t1",
|
|
121
|
+
hooks: {
|
|
122
|
+
onPrepareMessage: (msg) => {
|
|
123
|
+
order.push("pre");
|
|
124
|
+
return msg;
|
|
125
|
+
},
|
|
126
|
+
onPreparedMessage: (msg) => {
|
|
127
|
+
order.push("post");
|
|
128
|
+
return msg;
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
await tm.prepareForInvocation();
|
|
134
|
+
expect(order).toEqual(["pre", "post"]);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
createThreadManager,
|
|
6
6
|
type ProviderThreadManager,
|
|
7
7
|
type ThreadManagerConfig,
|
|
8
|
+
type ThreadManagerHooks,
|
|
8
9
|
} from "../../../lib/thread";
|
|
9
10
|
|
|
10
11
|
/** SDK-native content type for Anthropic human messages */
|
|
@@ -20,11 +21,14 @@ export interface StoredMessage {
|
|
|
20
21
|
isSystem?: boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
export type AnthropicThreadManagerHooks = ThreadManagerHooks<StoredMessage, Anthropic.Messages.MessageParam>;
|
|
25
|
+
|
|
23
26
|
export interface AnthropicThreadManagerConfig {
|
|
24
27
|
redis: Redis;
|
|
25
28
|
threadId: string;
|
|
26
29
|
/** Thread key, defaults to 'messages' */
|
|
27
30
|
key?: string;
|
|
31
|
+
hooks?: AnthropicThreadManagerHooks;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
/** Prepared payload ready to send to the Anthropic API */
|
|
@@ -165,11 +169,15 @@ export function createAnthropicThreadManager(
|
|
|
165
169
|
|
|
166
170
|
async prepareForInvocation(): Promise<AnthropicInvocationPayload> {
|
|
167
171
|
const stored = await base.load();
|
|
172
|
+
const { onPrepareMessage, onPreparedMessage } = config.hooks ?? {};
|
|
173
|
+
const mapped = onPrepareMessage
|
|
174
|
+
? stored.map((msg, i) => onPrepareMessage(msg, i, stored))
|
|
175
|
+
: stored;
|
|
168
176
|
|
|
169
177
|
let system: string | undefined;
|
|
170
178
|
const conversationMessages: Anthropic.Messages.MessageParam[] = [];
|
|
171
179
|
|
|
172
|
-
for (const item of
|
|
180
|
+
for (const item of mapped) {
|
|
173
181
|
if (item.isSystem) {
|
|
174
182
|
system =
|
|
175
183
|
typeof item.message.content === "string"
|
|
@@ -180,8 +188,11 @@ export function createAnthropicThreadManager(
|
|
|
180
188
|
}
|
|
181
189
|
}
|
|
182
190
|
|
|
191
|
+
const messages = mergeConsecutiveMessages(conversationMessages);
|
|
183
192
|
return {
|
|
184
|
-
messages:
|
|
193
|
+
messages: onPreparedMessage
|
|
194
|
+
? messages.map((msg, i) => onPreparedMessage(msg, i, messages))
|
|
195
|
+
: messages,
|
|
185
196
|
...(system ? { system } : {}),
|
|
186
197
|
};
|
|
187
198
|
},
|
|
@@ -15,6 +15,7 @@ import type { ModelInvoker } from "../../../lib/model";
|
|
|
15
15
|
import {
|
|
16
16
|
createGoogleGenAIThreadManager,
|
|
17
17
|
type GoogleGenAIContent,
|
|
18
|
+
type GoogleGenAIThreadManagerHooks,
|
|
18
19
|
} from "./thread-manager";
|
|
19
20
|
import { createGoogleGenAIModelInvoker } from "./model-invoker";
|
|
20
21
|
|
|
@@ -31,6 +32,7 @@ export interface GoogleGenAIAdapterConfig {
|
|
|
31
32
|
client?: GoogleGenAI;
|
|
32
33
|
/** Default model name (e.g. 'gemini-2.5-flash'). If omitted, use `createModelInvoker()` */
|
|
33
34
|
model?: string;
|
|
35
|
+
hooks?: GoogleGenAIThreadManagerHooks;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
/**
|
|
@@ -222,7 +224,7 @@ export function createGoogleGenAIAdapter(
|
|
|
222
224
|
model: string,
|
|
223
225
|
client: GoogleGenAI
|
|
224
226
|
): ModelInvoker<Content> =>
|
|
225
|
-
createGoogleGenAIModelInvoker({ redis, client, model });
|
|
227
|
+
createGoogleGenAIModelInvoker({ redis, client, model, hooks: config.hooks });
|
|
226
228
|
|
|
227
229
|
const invoker: ModelInvoker<Content> =
|
|
228
230
|
config.model && config.client
|
|
@@ -2,13 +2,14 @@ import type Redis from "ioredis";
|
|
|
2
2
|
import type { GoogleGenAI, Content, FunctionDeclaration } from "@google/genai";
|
|
3
3
|
import type { SerializableToolDefinition } from "../../../lib/types";
|
|
4
4
|
import type { AgentResponse, ModelInvokerConfig } from "../../../lib/model";
|
|
5
|
-
import { createGoogleGenAIThreadManager } from "./thread-manager";
|
|
5
|
+
import { createGoogleGenAIThreadManager, type GoogleGenAIThreadManagerHooks } from "./thread-manager";
|
|
6
6
|
import { v4 as uuidv4 } from "uuid";
|
|
7
7
|
|
|
8
8
|
export interface GoogleGenAIModelInvokerConfig {
|
|
9
9
|
redis: Redis;
|
|
10
10
|
client: GoogleGenAI;
|
|
11
11
|
model: string;
|
|
12
|
+
hooks?: GoogleGenAIThreadManagerHooks;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
function toFunctionDeclarations(
|
|
@@ -49,13 +50,14 @@ export function createGoogleGenAIModelInvoker({
|
|
|
49
50
|
redis,
|
|
50
51
|
client,
|
|
51
52
|
model,
|
|
53
|
+
hooks,
|
|
52
54
|
}: GoogleGenAIModelInvokerConfig) {
|
|
53
55
|
return async function invokeGoogleGenAIModel(
|
|
54
56
|
config: ModelInvokerConfig,
|
|
55
57
|
): Promise<AgentResponse<Content>> {
|
|
56
58
|
const { threadId, threadKey, state } = config;
|
|
57
59
|
|
|
58
|
-
const thread = createGoogleGenAIThreadManager({ redis, threadId, key: threadKey });
|
|
60
|
+
const thread = createGoogleGenAIThreadManager({ redis, threadId, key: threadKey, hooks });
|
|
59
61
|
const { contents, systemInstruction } =
|
|
60
62
|
await thread.prepareForInvocation();
|
|
61
63
|
|
|
@@ -104,13 +106,15 @@ export async function invokeGoogleGenAIModel({
|
|
|
104
106
|
redis,
|
|
105
107
|
client,
|
|
106
108
|
model,
|
|
109
|
+
hooks,
|
|
107
110
|
config,
|
|
108
111
|
}: {
|
|
109
112
|
redis: Redis;
|
|
110
113
|
client: GoogleGenAI;
|
|
111
114
|
model: string;
|
|
115
|
+
hooks?: GoogleGenAIThreadManagerHooks;
|
|
112
116
|
config: ModelInvokerConfig;
|
|
113
117
|
}): Promise<AgentResponse<Content>> {
|
|
114
|
-
const invoker = createGoogleGenAIModelInvoker({ redis, client, model });
|
|
118
|
+
const invoker = createGoogleGenAIModelInvoker({ redis, client, model, hooks });
|
|
115
119
|
return invoker(config);
|
|
116
120
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { Content } from "@google/genai";
|
|
3
|
+
import type { StoredContent } from "./thread-manager";
|
|
4
|
+
import { createGoogleGenAIThreadManager } from "./thread-manager";
|
|
5
|
+
|
|
6
|
+
function createMockRedis(stored: StoredContent[]) {
|
|
7
|
+
return {
|
|
8
|
+
exists: vi.fn().mockResolvedValue(1),
|
|
9
|
+
lrange: vi.fn().mockResolvedValue(stored.map((m) => JSON.stringify(m))),
|
|
10
|
+
del: vi.fn().mockResolvedValue(1),
|
|
11
|
+
set: vi.fn().mockResolvedValue("OK"),
|
|
12
|
+
rpush: vi.fn().mockResolvedValue(1),
|
|
13
|
+
expire: vi.fn().mockResolvedValue(1),
|
|
14
|
+
eval: vi.fn().mockResolvedValue(1),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const systemContent: StoredContent = {
|
|
19
|
+
id: "sys-1",
|
|
20
|
+
content: { role: "system", parts: [{ text: "You are helpful." }] },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const userContent: StoredContent = {
|
|
24
|
+
id: "msg-1",
|
|
25
|
+
content: { role: "user", parts: [{ text: "Hello" }] },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const modelContent: StoredContent = {
|
|
29
|
+
id: "msg-2",
|
|
30
|
+
content: { role: "model", parts: [{ text: "Hi there!" }] },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe("Google GenAI thread manager hooks", () => {
|
|
34
|
+
describe("onPrepareMessage", () => {
|
|
35
|
+
it("transforms stored messages before system extraction and merge", async () => {
|
|
36
|
+
const hook = vi.fn((msg: StoredContent) => {
|
|
37
|
+
if (msg.content.role === "system") return msg;
|
|
38
|
+
return {
|
|
39
|
+
...msg,
|
|
40
|
+
content: {
|
|
41
|
+
...msg.content,
|
|
42
|
+
parts: [{ text: `[modified] ${msg.content.parts?.[0]?.text ?? ""}` }],
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const redis = createMockRedis([systemContent, userContent, modelContent]);
|
|
48
|
+
const tm = createGoogleGenAIThreadManager({
|
|
49
|
+
redis: redis as never,
|
|
50
|
+
threadId: "t1",
|
|
51
|
+
hooks: { onPrepareMessage: hook },
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const { contents, systemInstruction } = await tm.prepareForInvocation();
|
|
55
|
+
|
|
56
|
+
expect(hook).toHaveBeenCalledTimes(3);
|
|
57
|
+
expect(hook).toHaveBeenCalledWith(systemContent, 0, [systemContent, userContent, modelContent]);
|
|
58
|
+
expect(systemInstruction).toBe("You are helpful.");
|
|
59
|
+
expect(contents[0]?.parts?.[0]?.text).toBe("[modified] Hello");
|
|
60
|
+
expect(contents[1]?.parts?.[0]?.text).toBe("[modified] Hi there!");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("is not called when not configured", async () => {
|
|
64
|
+
const redis = createMockRedis([userContent]);
|
|
65
|
+
const tm = createGoogleGenAIThreadManager({
|
|
66
|
+
redis: redis as never,
|
|
67
|
+
threadId: "t1",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const { contents } = await tm.prepareForInvocation();
|
|
71
|
+
expect(contents).toHaveLength(1);
|
|
72
|
+
expect(contents[0]?.parts?.[0]?.text).toBe("Hello");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("onPreparedMessage", () => {
|
|
77
|
+
it("transforms SDK-native Content after merge", async () => {
|
|
78
|
+
const hook = vi.fn((msg: Content) => ({
|
|
79
|
+
...msg,
|
|
80
|
+
parts: [{ text: `[post] ${msg.parts?.[0]?.text ?? ""}` }],
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
const redis = createMockRedis([userContent, modelContent]);
|
|
84
|
+
const tm = createGoogleGenAIThreadManager({
|
|
85
|
+
redis: redis as never,
|
|
86
|
+
threadId: "t1",
|
|
87
|
+
hooks: { onPreparedMessage: hook },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const { contents } = await tm.prepareForInvocation();
|
|
91
|
+
|
|
92
|
+
expect(hook).toHaveBeenCalledTimes(2);
|
|
93
|
+
expect(contents[0]?.parts?.[0]?.text).toBe("[post] Hello");
|
|
94
|
+
expect(contents[1]?.parts?.[0]?.text).toBe("[post] Hi there!");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("receives the full prepared contents array", async () => {
|
|
98
|
+
const hook = vi.fn((msg: Content) => msg);
|
|
99
|
+
|
|
100
|
+
const redis = createMockRedis([userContent, modelContent]);
|
|
101
|
+
const tm = createGoogleGenAIThreadManager({
|
|
102
|
+
redis: redis as never,
|
|
103
|
+
threadId: "t1",
|
|
104
|
+
hooks: { onPreparedMessage: hook },
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await tm.prepareForInvocation();
|
|
108
|
+
|
|
109
|
+
const args = hook.mock.calls[0] as unknown as [Content, number, Content[]];
|
|
110
|
+
expect(args[2]).toHaveLength(2);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("both hooks combined", () => {
|
|
115
|
+
it("runs onPrepareMessage before onPreparedMessage", async () => {
|
|
116
|
+
const order: string[] = [];
|
|
117
|
+
|
|
118
|
+
const redis = createMockRedis([userContent]);
|
|
119
|
+
const tm = createGoogleGenAIThreadManager({
|
|
120
|
+
redis: redis as never,
|
|
121
|
+
threadId: "t1",
|
|
122
|
+
hooks: {
|
|
123
|
+
onPrepareMessage: (msg) => {
|
|
124
|
+
order.push("pre");
|
|
125
|
+
return msg;
|
|
126
|
+
},
|
|
127
|
+
onPreparedMessage: (msg) => {
|
|
128
|
+
order.push("post");
|
|
129
|
+
return msg;
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await tm.prepareForInvocation();
|
|
135
|
+
expect(order).toEqual(["pre", "post"]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("onPreparedMessage sees results of onPrepareMessage", async () => {
|
|
139
|
+
const redis = createMockRedis([userContent]);
|
|
140
|
+
const tm = createGoogleGenAIThreadManager({
|
|
141
|
+
redis: redis as never,
|
|
142
|
+
threadId: "t1",
|
|
143
|
+
hooks: {
|
|
144
|
+
onPrepareMessage: (msg) => ({
|
|
145
|
+
...msg,
|
|
146
|
+
content: { ...msg.content, parts: [{ text: "replaced" }] },
|
|
147
|
+
}),
|
|
148
|
+
onPreparedMessage: (msg) => {
|
|
149
|
+
expect(msg.parts?.[0]?.text).toBe("replaced");
|
|
150
|
+
return msg;
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const { contents } = await tm.prepareForInvocation();
|
|
156
|
+
expect(contents[0]?.parts?.[0]?.text).toBe("replaced");
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createThreadManager,
|
|
5
5
|
type ProviderThreadManager,
|
|
6
6
|
type ThreadManagerConfig,
|
|
7
|
+
type ThreadManagerHooks,
|
|
7
8
|
} from "../../../lib/thread";
|
|
8
9
|
import type { GoogleGenAIToolResponse } from "./activities";
|
|
9
10
|
|
|
@@ -16,11 +17,14 @@ export interface StoredContent {
|
|
|
16
17
|
content: Content;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
export type GoogleGenAIThreadManagerHooks = ThreadManagerHooks<StoredContent, Content>;
|
|
21
|
+
|
|
19
22
|
export interface GoogleGenAIThreadManagerConfig {
|
|
20
23
|
redis: Redis;
|
|
21
24
|
threadId: string;
|
|
22
25
|
/** Thread key, defaults to 'messages' */
|
|
23
26
|
key?: string;
|
|
27
|
+
hooks?: GoogleGenAIThreadManagerHooks;
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
/** Prepared payload ready to send to the Google GenAI API */
|
|
@@ -141,11 +145,15 @@ export function createGoogleGenAIThreadManager(
|
|
|
141
145
|
|
|
142
146
|
async prepareForInvocation(): Promise<GoogleGenAIInvocationPayload> {
|
|
143
147
|
const stored = await base.load();
|
|
148
|
+
const { onPrepareMessage, onPreparedMessage } = config.hooks ?? {};
|
|
149
|
+
const mapped = onPrepareMessage
|
|
150
|
+
? stored.map((msg, i) => onPrepareMessage(msg, i, stored))
|
|
151
|
+
: stored;
|
|
144
152
|
|
|
145
153
|
let systemInstruction: string | undefined;
|
|
146
154
|
const conversationContents: Content[] = [];
|
|
147
155
|
|
|
148
|
-
for (const item of
|
|
156
|
+
for (const item of mapped) {
|
|
149
157
|
if (item.content.role === "system") {
|
|
150
158
|
systemInstruction = item.content.parts?.[0]?.text;
|
|
151
159
|
} else {
|
|
@@ -153,8 +161,11 @@ export function createGoogleGenAIThreadManager(
|
|
|
153
161
|
}
|
|
154
162
|
}
|
|
155
163
|
|
|
164
|
+
const contents = mergeConsecutiveContents(conversationContents);
|
|
156
165
|
return {
|
|
157
|
-
contents:
|
|
166
|
+
contents: onPreparedMessage
|
|
167
|
+
? contents.map((msg, i) => onPreparedMessage(msg, i, contents))
|
|
168
|
+
: contents,
|
|
158
169
|
...(systemInstruction ? { systemInstruction } : {}),
|
|
159
170
|
};
|
|
160
171
|
},
|
|
@@ -17,6 +17,7 @@ import type { BaseChatModel } from "@langchain/core/language_models/chat_models"
|
|
|
17
17
|
import {
|
|
18
18
|
createLangChainThreadManager,
|
|
19
19
|
type LangChainContent,
|
|
20
|
+
type LangChainThreadManagerHooks,
|
|
20
21
|
} from "./thread-manager";
|
|
21
22
|
import { createLangChainModelInvoker } from "./model-invoker";
|
|
22
23
|
|
|
@@ -30,6 +31,7 @@ export interface LangChainAdapterConfig {
|
|
|
30
31
|
/** Optional default model — if omitted, use `createModelInvoker()` to create invokers later */
|
|
31
32
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
32
33
|
model?: BaseChatModel<any>;
|
|
34
|
+
hooks?: LangChainThreadManagerHooks;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/**
|
|
@@ -177,7 +179,7 @@ export function createLangChainAdapter(
|
|
|
177
179
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
178
180
|
model: BaseChatModel<any>,
|
|
179
181
|
): ModelInvoker<StoredMessage> =>
|
|
180
|
-
createLangChainModelInvoker({ redis, model });
|
|
182
|
+
createLangChainModelInvoker({ redis, model, hooks: config.hooks });
|
|
181
183
|
|
|
182
184
|
const invoker: ModelInvoker<StoredMessage> = config.model
|
|
183
185
|
? makeInvoker(config.model)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { BaseMessage, MessageContent } from "@langchain/core/messages";
|
|
2
|
+
|
|
3
|
+
type ContentBlock = MessageContent extends (infer U)[] | string ? U : never;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates an `onPreparedMessage` hook that appends a cache-point content
|
|
7
|
+
* block to the last message in the thread.
|
|
8
|
+
*
|
|
9
|
+
* Skips appending if the last message already contains a block with the
|
|
10
|
+
* same `type`.
|
|
11
|
+
*/
|
|
12
|
+
export function appendCachePoint(
|
|
13
|
+
block: ContentBlock,
|
|
14
|
+
): (message: BaseMessage, index: number, messages: readonly BaseMessage[]) => BaseMessage {
|
|
15
|
+
return (message, index, messages) => {
|
|
16
|
+
if (index !== messages.length - 1) {
|
|
17
|
+
return message;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { content } = message;
|
|
21
|
+
|
|
22
|
+
if (Array.isArray(content)) {
|
|
23
|
+
if (content.some((b) => b.type === block.type)) {
|
|
24
|
+
return message;
|
|
25
|
+
}
|
|
26
|
+
message.content = [...content, block];
|
|
27
|
+
return message;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (typeof content === "string") {
|
|
31
|
+
message.content = [{ type: "text", text: content }, block] satisfies MessageContent;
|
|
32
|
+
return message;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return message;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -3,12 +3,13 @@ import type { AgentResponse, ModelInvokerConfig } from "../../../lib/model";
|
|
|
3
3
|
import type { StoredMessage } from "@langchain/core/messages";
|
|
4
4
|
import { v4 as uuidv4 } from "uuid";
|
|
5
5
|
import type { BaseChatModel } from "@langchain/core/language_models/chat_models";
|
|
6
|
-
import { createLangChainThreadManager } from "./thread-manager";
|
|
6
|
+
import { createLangChainThreadManager, type LangChainThreadManagerHooks } from "./thread-manager";
|
|
7
7
|
|
|
8
8
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
9
|
export interface LangChainModelInvokerConfig<TModel extends BaseChatModel<any> = BaseChatModel<any>> {
|
|
10
10
|
redis: Redis;
|
|
11
11
|
model: TModel;
|
|
12
|
+
hooks?: LangChainThreadManagerHooks;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -32,14 +33,14 @@ export interface LangChainModelInvokerConfig<TModel extends BaseChatModel<any> =
|
|
|
32
33
|
*/
|
|
33
34
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
34
35
|
export function createLangChainModelInvoker<TModel extends BaseChatModel<any> = BaseChatModel<any>>(
|
|
35
|
-
{ redis, model }: LangChainModelInvokerConfig<TModel>,
|
|
36
|
+
{ redis, model, hooks }: LangChainModelInvokerConfig<TModel>,
|
|
36
37
|
) {
|
|
37
38
|
return async function invokeLangChainModel(
|
|
38
39
|
config: ModelInvokerConfig,
|
|
39
40
|
): Promise<AgentResponse<StoredMessage>> {
|
|
40
41
|
const { threadId, threadKey, agentName, state, metadata } = config;
|
|
41
42
|
|
|
42
|
-
const thread = createLangChainThreadManager({ redis, threadId, key: threadKey });
|
|
43
|
+
const thread = createLangChainThreadManager({ redis, threadId, key: threadKey, hooks });
|
|
43
44
|
const runId = uuidv4();
|
|
44
45
|
|
|
45
46
|
const { messages } = await thread.prepareForInvocation();
|
|
@@ -86,12 +87,14 @@ export function createLangChainModelInvoker<TModel extends BaseChatModel<any> =
|
|
|
86
87
|
export async function invokeLangChainModel<TModel extends BaseChatModel<any> = BaseChatModel<any>>({
|
|
87
88
|
redis,
|
|
88
89
|
model,
|
|
90
|
+
hooks,
|
|
89
91
|
config,
|
|
90
92
|
}: {
|
|
91
93
|
redis: Redis;
|
|
92
94
|
config: ModelInvokerConfig;
|
|
93
95
|
model: TModel;
|
|
96
|
+
hooks?: LangChainThreadManagerHooks;
|
|
94
97
|
}): Promise<AgentResponse<StoredMessage>> {
|
|
95
|
-
const invoker = createLangChainModelInvoker({ redis, model });
|
|
98
|
+
const invoker = createLangChainModelInvoker({ redis, model, hooks });
|
|
96
99
|
return invoker(config);
|
|
97
100
|
}
|