mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:20:43 +00:00
fix(hooks): respect live lancedb memory config
This commit is contained in:
@@ -384,6 +384,129 @@ describe("memory plugin e2e", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("uses live runtime config to skip auto-recall after registration", async () => {
|
||||
const embeddingsCreate = vi.fn(async () => ({
|
||||
data: [{ embedding: [0.1, 0.2, 0.3] }],
|
||||
}));
|
||||
const ensureGlobalUndiciEnvProxyDispatcher = vi.fn();
|
||||
const loadLanceDbModule = vi.fn(async () => ({
|
||||
connect: vi.fn(async () => ({
|
||||
tableNames: vi.fn(async () => ["memories"]),
|
||||
openTable: vi.fn(async () => ({
|
||||
vectorSearch: vi.fn(() => ({ limit: vi.fn(() => ({ toArray: vi.fn(async () => []) })) })),
|
||||
countRows: vi.fn(async () => 0),
|
||||
add: vi.fn(async () => undefined),
|
||||
delete: vi.fn(async () => undefined),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
let configFile: Record<string, unknown> = {
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-lancedb": {
|
||||
config: {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
autoCapture: false,
|
||||
autoRecall: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
vi.resetModules();
|
||||
vi.doMock("openclaw/plugin-sdk/runtime-env", () => ({
|
||||
ensureGlobalUndiciEnvProxyDispatcher,
|
||||
}));
|
||||
vi.doMock("openai", () => ({
|
||||
default: class MockOpenAI {
|
||||
embeddings = { create: embeddingsCreate };
|
||||
},
|
||||
}));
|
||||
vi.doMock("./lancedb-runtime.js", () => ({
|
||||
loadLanceDbModule,
|
||||
}));
|
||||
|
||||
try {
|
||||
const { default: dynamicMemoryPlugin } = await import("./index.js");
|
||||
const on = vi.fn();
|
||||
const mockApi = {
|
||||
id: "memory-lancedb",
|
||||
name: "Memory (LanceDB)",
|
||||
source: "test",
|
||||
config: {},
|
||||
pluginConfig: {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
autoCapture: false,
|
||||
autoRecall: true,
|
||||
},
|
||||
runtime: {
|
||||
config: {
|
||||
loadConfig: () => configFile,
|
||||
},
|
||||
},
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
registerTool: vi.fn(),
|
||||
registerCli: vi.fn(),
|
||||
registerService: vi.fn(),
|
||||
on,
|
||||
resolvePath: (p: string) => p,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
|
||||
configFile = {
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-lancedb": {
|
||||
config: {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
autoCapture: false,
|
||||
autoRecall: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const beforePromptBuild = on.mock.calls.find(
|
||||
([hookName]) => hookName === "before_prompt_build",
|
||||
)?.[1];
|
||||
expect(beforePromptBuild).toBeTypeOf("function");
|
||||
|
||||
const result = await beforePromptBuild?.(
|
||||
{ prompt: "what editor should i use?", messages: [] },
|
||||
{},
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(embeddingsCreate).not.toHaveBeenCalled();
|
||||
expect(loadLanceDbModule).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
vi.doUnmock("openclaw/plugin-sdk/runtime-env");
|
||||
vi.doUnmock("openai");
|
||||
vi.doUnmock("./lancedb-runtime.js");
|
||||
vi.resetModules();
|
||||
}
|
||||
});
|
||||
|
||||
test("runs auto-capture through the registered agent_end hook", async () => {
|
||||
const embeddingsCreate = vi.fn(async () => ({
|
||||
data: [{ embedding: [0.1, 0.2, 0.3] }],
|
||||
@@ -492,6 +615,131 @@ describe("memory plugin e2e", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("uses live runtime config to skip auto-capture after registration", async () => {
|
||||
const embeddingsCreate = vi.fn(async () => ({
|
||||
data: [{ embedding: [0.1, 0.2, 0.3] }],
|
||||
}));
|
||||
const ensureGlobalUndiciEnvProxyDispatcher = vi.fn();
|
||||
const add = vi.fn(async () => undefined);
|
||||
const loadLanceDbModule = vi.fn(async () => ({
|
||||
connect: vi.fn(async () => ({
|
||||
tableNames: vi.fn(async () => ["memories"]),
|
||||
openTable: vi.fn(async () => ({
|
||||
vectorSearch: vi.fn(() => ({ limit: vi.fn(() => ({ toArray: vi.fn(async () => []) })) })),
|
||||
countRows: vi.fn(async () => 0),
|
||||
add,
|
||||
delete: vi.fn(async () => undefined),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
let configFile: Record<string, unknown> = {
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-lancedb": {
|
||||
config: {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
autoCapture: true,
|
||||
autoRecall: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
vi.resetModules();
|
||||
vi.doMock("openclaw/plugin-sdk/runtime-env", () => ({
|
||||
ensureGlobalUndiciEnvProxyDispatcher,
|
||||
}));
|
||||
vi.doMock("openai", () => ({
|
||||
default: class MockOpenAI {
|
||||
embeddings = { create: embeddingsCreate };
|
||||
},
|
||||
}));
|
||||
vi.doMock("./lancedb-runtime.js", () => ({
|
||||
loadLanceDbModule,
|
||||
}));
|
||||
|
||||
try {
|
||||
const { default: dynamicMemoryPlugin } = await import("./index.js");
|
||||
const on = vi.fn();
|
||||
const mockApi = {
|
||||
id: "memory-lancedb",
|
||||
name: "Memory (LanceDB)",
|
||||
source: "test",
|
||||
config: {},
|
||||
pluginConfig: {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
autoCapture: true,
|
||||
autoRecall: false,
|
||||
},
|
||||
runtime: {
|
||||
config: {
|
||||
loadConfig: () => configFile,
|
||||
},
|
||||
},
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
registerTool: vi.fn(),
|
||||
registerCli: vi.fn(),
|
||||
registerService: vi.fn(),
|
||||
on,
|
||||
resolvePath: (p: string) => p,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
|
||||
configFile = {
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-lancedb": {
|
||||
config: {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
autoCapture: false,
|
||||
autoRecall: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const agentEnd = on.mock.calls.find(([hookName]) => hookName === "agent_end")?.[1];
|
||||
expect(agentEnd).toBeTypeOf("function");
|
||||
|
||||
await agentEnd?.(
|
||||
{
|
||||
success: true,
|
||||
messages: [{ role: "user", content: "I prefer Helix for editing code every day." }],
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
expect(embeddingsCreate).not.toHaveBeenCalled();
|
||||
expect(loadLanceDbModule).not.toHaveBeenCalled();
|
||||
expect(add).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
vi.doUnmock("openclaw/plugin-sdk/runtime-env");
|
||||
vi.doUnmock("openai");
|
||||
vi.doUnmock("./lancedb-runtime.js");
|
||||
vi.resetModules();
|
||||
}
|
||||
});
|
||||
|
||||
test("passes configured dimensions to OpenAI embeddings API", async () => {
|
||||
const embeddingsCreate = vi.fn(async () => ({
|
||||
data: [{ embedding: [0.1, 0.2, 0.3] }],
|
||||
|
||||
@@ -40,6 +40,12 @@ type MemorySearchResult = {
|
||||
score: number;
|
||||
};
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// LanceDB Provider
|
||||
// ============================================================================
|
||||
@@ -300,6 +306,33 @@ export default definePluginEntry({
|
||||
const vectorDim = dimensions ?? vectorDimsForModel(model);
|
||||
const db = new MemoryDB(resolvedDbPath, vectorDim, cfg.storageOptions);
|
||||
const embeddings = new Embeddings(apiKey, model, baseUrl, dimensions);
|
||||
const resolveCurrentHookConfig = () => {
|
||||
const runtimeConfig = api.runtime.config?.loadConfig?.();
|
||||
const runtimePlugins = asRecord(asRecord(runtimeConfig)?.plugins);
|
||||
const runtimeEntries = asRecord(runtimePlugins?.entries);
|
||||
const runtimePluginConfig = asRecord(runtimeEntries?.["memory-lancedb"])?.config;
|
||||
if (!runtimePluginConfig) {
|
||||
return cfg;
|
||||
}
|
||||
return memoryConfigSchema.parse({
|
||||
embedding: {
|
||||
apiKey: cfg.embedding.apiKey,
|
||||
model: cfg.embedding.model,
|
||||
...(cfg.embedding.baseUrl ? { baseUrl: cfg.embedding.baseUrl } : {}),
|
||||
...(typeof cfg.embedding.dimensions === "number"
|
||||
? { dimensions: cfg.embedding.dimensions }
|
||||
: {}),
|
||||
...asRecord(asRecord(runtimePluginConfig)?.embedding),
|
||||
},
|
||||
...(cfg.dreaming ? { dreaming: cfg.dreaming } : {}),
|
||||
dbPath: cfg.dbPath,
|
||||
autoCapture: cfg.autoCapture,
|
||||
autoRecall: cfg.autoRecall,
|
||||
captureMaxChars: cfg.captureMaxChars,
|
||||
...(cfg.storageOptions ? { storageOptions: cfg.storageOptions } : {}),
|
||||
...asRecord(runtimePluginConfig),
|
||||
});
|
||||
};
|
||||
|
||||
api.logger.info(`memory-lancedb: plugin registered (db: ${resolvedDbPath}, lazy init)`);
|
||||
|
||||
@@ -542,6 +575,10 @@ export default definePluginEntry({
|
||||
// Auto-recall: inject relevant memories during prompt build
|
||||
if (cfg.autoRecall) {
|
||||
api.on("before_prompt_build", async (event) => {
|
||||
const currentCfg = resolveCurrentHookConfig();
|
||||
if (!currentCfg.autoRecall) {
|
||||
return undefined;
|
||||
}
|
||||
if (!event.prompt || event.prompt.length < 5) {
|
||||
return undefined;
|
||||
}
|
||||
@@ -571,6 +608,10 @@ export default definePluginEntry({
|
||||
// Auto-capture: analyze and store important information after agent ends
|
||||
if (cfg.autoCapture) {
|
||||
api.on("agent_end", async (event) => {
|
||||
const currentCfg = resolveCurrentHookConfig();
|
||||
if (!currentCfg.autoCapture) {
|
||||
return;
|
||||
}
|
||||
if (!event.success || !event.messages || event.messages.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -618,7 +659,7 @@ export default definePluginEntry({
|
||||
|
||||
// Filter for capturable content
|
||||
const toCapture = texts.filter(
|
||||
(text) => text && shouldCapture(text, { maxChars: cfg.captureMaxChars }),
|
||||
(text) => text && shouldCapture(text, { maxChars: currentCfg.captureMaxChars }),
|
||||
);
|
||||
if (toCapture.length === 0) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user