@lobehub/lobehub 2.0.0-next.320 → 2.0.0-next.321
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/.codex/skills/vercel-react-best-practices/AGENTS.md +2410 -0
- package/.codex/skills/vercel-react-best-practices/SKILL.md +125 -0
- package/.codex/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/.codex/skills/vercel-react-best-practices/rules/advanced-use-latest.md +49 -0
- package/.codex/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/.codex/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
- package/.codex/skills/vercel-react-best-practices/rules/async-dependencies.md +36 -0
- package/.codex/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/.codex/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/.codex/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/.codex/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/.codex/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/.codex/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/.codex/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/.codex/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/.codex/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/.codex/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/.codex/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/.codex/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +57 -0
- package/.codex/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/.codex/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/.codex/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/.codex/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/.codex/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/.codex/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/.codex/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/.codex/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/.codex/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/.codex/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/.codex/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/.codex/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/.codex/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/.codex/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/.codex/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/.codex/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/.codex/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/.codex/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/.codex/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/.codex/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/.codex/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/.codex/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/.codex/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/.codex/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/.codex/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/.codex/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/.codex/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/.codex/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/.codex/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/.codex/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/.cursor/skills/vercel-react-best-practices/AGENTS.md +2410 -0
- package/.cursor/skills/vercel-react-best-practices/SKILL.md +125 -0
- package/.cursor/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/.cursor/skills/vercel-react-best-practices/rules/advanced-use-latest.md +49 -0
- package/.cursor/skills/vercel-react-best-practices/rules/async-api-routes.md +38 -0
- package/.cursor/skills/vercel-react-best-practices/rules/async-defer-await.md +80 -0
- package/.cursor/skills/vercel-react-best-practices/rules/async-dependencies.md +36 -0
- package/.cursor/skills/vercel-react-best-practices/rules/async-parallel.md +28 -0
- package/.cursor/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/.cursor/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/.cursor/skills/vercel-react-best-practices/rules/bundle-conditional.md +31 -0
- package/.cursor/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/.cursor/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/.cursor/skills/vercel-react-best-practices/rules/bundle-preload.md +50 -0
- package/.cursor/skills/vercel-react-best-practices/rules/client-event-listeners.md +74 -0
- package/.cursor/skills/vercel-react-best-practices/rules/client-localstorage-schema.md +71 -0
- package/.cursor/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md +48 -0
- package/.cursor/skills/vercel-react-best-practices/rules/client-swr-dedup.md +56 -0
- package/.cursor/skills/vercel-react-best-practices/rules/js-batch-dom-css.md +57 -0
- package/.cursor/skills/vercel-react-best-practices/rules/js-cache-function-results.md +80 -0
- package/.cursor/skills/vercel-react-best-practices/rules/js-cache-property-access.md +28 -0
- package/.cursor/skills/vercel-react-best-practices/rules/js-cache-storage.md +70 -0
- package/.cursor/skills/vercel-react-best-practices/rules/js-combine-iterations.md +32 -0
- package/.cursor/skills/vercel-react-best-practices/rules/js-early-exit.md +50 -0
- package/.cursor/skills/vercel-react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/.cursor/skills/vercel-react-best-practices/rules/js-index-maps.md +37 -0
- package/.cursor/skills/vercel-react-best-practices/rules/js-length-check-first.md +49 -0
- package/.cursor/skills/vercel-react-best-practices/rules/js-min-max-loop.md +82 -0
- package/.cursor/skills/vercel-react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/.cursor/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/.cursor/skills/vercel-react-best-practices/rules/rendering-activity.md +26 -0
- package/.cursor/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/.cursor/skills/vercel-react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/.cursor/skills/vercel-react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/.cursor/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/.cursor/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/.cursor/skills/vercel-react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/.cursor/skills/vercel-react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/.cursor/skills/vercel-react-best-practices/rules/rerender-dependencies.md +45 -0
- package/.cursor/skills/vercel-react-best-practices/rules/rerender-derived-state.md +29 -0
- package/.cursor/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/.cursor/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/.cursor/skills/vercel-react-best-practices/rules/rerender-memo.md +44 -0
- package/.cursor/skills/vercel-react-best-practices/rules/rerender-transitions.md +40 -0
- package/.cursor/skills/vercel-react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/.cursor/skills/vercel-react-best-practices/rules/server-cache-lru.md +41 -0
- package/.cursor/skills/vercel-react-best-practices/rules/server-cache-react.md +76 -0
- package/.cursor/skills/vercel-react-best-practices/rules/server-parallel-fetching.md +83 -0
- package/.cursor/skills/vercel-react-best-practices/rules/server-serialization.md +38 -0
- package/CHANGELOG.md +25 -0
- package/changelog/v1.json +5 -0
- package/package.json +1 -1
- package/src/layout/GlobalProvider/FaviconProvider.tsx +45 -21
- package/src/server/globalConfig/parseMemoryExtractionConfig.ts +43 -4
- package/src/server/services/memory/userMemory/__tests__/extract.payload.test.ts +101 -0
- package/src/server/services/memory/userMemory/__tests__/extract.runtime.test.ts +111 -0
- package/src/server/services/memory/userMemory/extract.ts +155 -17
|
@@ -32,12 +32,18 @@ export type MemoryLayerExtractorConfig = MemoryLayerExtractorPublicConfig &
|
|
|
32
32
|
|
|
33
33
|
export interface MemoryExtractionPrivateConfig {
|
|
34
34
|
agentGateKeeper: MemoryAgentConfig;
|
|
35
|
+
agentGateKeeperPreferredModels?: string[];
|
|
36
|
+
agentGateKeeperPreferredProviders?: string[];
|
|
35
37
|
agentLayerExtractor: MemoryLayerExtractorConfig;
|
|
38
|
+
agentLayerExtractorPreferredModels?: string[];
|
|
39
|
+
agentLayerExtractorPreferredProviders?: string[];
|
|
36
40
|
concurrency?: number;
|
|
37
41
|
embedding: MemoryAgentConfig;
|
|
42
|
+
embeddingPreferredModels?: string[];
|
|
43
|
+
embeddingPreferredProviders?: string[];
|
|
38
44
|
featureFlags: {
|
|
39
45
|
enableBenchmarkLoCoMo: boolean;
|
|
40
|
-
}
|
|
46
|
+
};
|
|
41
47
|
observabilityS3?: {
|
|
42
48
|
accessKeyId?: string;
|
|
43
49
|
bucketName?: string;
|
|
@@ -157,6 +163,12 @@ const sanitizeAgent = (agent?: MemoryAgentConfig): MemoryAgentPublicConfig | und
|
|
|
157
163
|
return sanitized as MemoryAgentPublicConfig;
|
|
158
164
|
};
|
|
159
165
|
|
|
166
|
+
const parsePreferredList = (value?: string) =>
|
|
167
|
+
value
|
|
168
|
+
?.split(',')
|
|
169
|
+
.map((item) => item.trim().toLowerCase())
|
|
170
|
+
.filter(Boolean);
|
|
171
|
+
|
|
160
172
|
export const parseMemoryExtractionConfig = (): MemoryExtractionPrivateConfig => {
|
|
161
173
|
const agentGateKeeper = parseGateKeeperAgent();
|
|
162
174
|
const agentLayerExtractor = parseLayerExtractorAgent(agentGateKeeper.model);
|
|
@@ -167,8 +179,8 @@ export const parseMemoryExtractionConfig = (): MemoryExtractionPrivateConfig =>
|
|
|
167
179
|
);
|
|
168
180
|
const extractorObservabilityS3 = parseExtractorAgentObservabilityS3();
|
|
169
181
|
const featureFlags = {
|
|
170
|
-
enableBenchmarkLoCoMo: process.env.MEMORY_USER_MEMORY_FEATURE_FLAG_BENCHMARK_LOCOMO === 'true'
|
|
171
|
-
}
|
|
182
|
+
enableBenchmarkLoCoMo: process.env.MEMORY_USER_MEMORY_FEATURE_FLAG_BENCHMARK_LOCOMO === 'true',
|
|
183
|
+
};
|
|
172
184
|
const concurrencyRaw = process.env.MEMORY_USER_MEMORY_CONCURRENCY;
|
|
173
185
|
const concurrency =
|
|
174
186
|
concurrencyRaw !== undefined
|
|
@@ -191,7 +203,9 @@ export const parseMemoryExtractionConfig = (): MemoryExtractionPrivateConfig =>
|
|
|
191
203
|
return acc;
|
|
192
204
|
}, {});
|
|
193
205
|
|
|
194
|
-
const upstashWorkflowExtraHeaders = process.env.MEMORY_USER_MEMORY_WORKFLOW_EXTRA_HEADERS?.split(
|
|
206
|
+
const upstashWorkflowExtraHeaders = process.env.MEMORY_USER_MEMORY_WORKFLOW_EXTRA_HEADERS?.split(
|
|
207
|
+
',',
|
|
208
|
+
)
|
|
195
209
|
.filter(Boolean)
|
|
196
210
|
.reduce<Record<string, string>>((acc, pair) => {
|
|
197
211
|
const [key, value] = pair.split('=').map((s) => s.trim());
|
|
@@ -201,11 +215,36 @@ export const parseMemoryExtractionConfig = (): MemoryExtractionPrivateConfig =>
|
|
|
201
215
|
return acc;
|
|
202
216
|
}, {});
|
|
203
217
|
|
|
218
|
+
const agentGateKeeperPreferredProviders = parsePreferredList(
|
|
219
|
+
process.env.MEMORY_USER_MEMORY_GATEKEEPER_PREFERRED_PROVIDERS,
|
|
220
|
+
);
|
|
221
|
+
const agentGateKeeperPreferredModels = parsePreferredList(
|
|
222
|
+
process.env.MEMORY_USER_MEMORY_GATEKEEPER_PREFERRED_MODELS,
|
|
223
|
+
);
|
|
224
|
+
const embeddingPreferredProviders = parsePreferredList(
|
|
225
|
+
process.env.MEMORY_USER_MEMORY_EMBEDDING_PREFERRED_PROVIDERS,
|
|
226
|
+
);
|
|
227
|
+
const embeddingPreferredModels = parsePreferredList(
|
|
228
|
+
process.env.MEMORY_USER_MEMORY_EMBEDDING_PREFERRED_MODELS,
|
|
229
|
+
);
|
|
230
|
+
const agentLayerExtractorPreferredProviders = parsePreferredList(
|
|
231
|
+
process.env.MEMORY_USER_MEMORY_LAYER_EXTRACTOR_PREFERRED_PROVIDERS,
|
|
232
|
+
);
|
|
233
|
+
const agentLayerExtractorPreferredModels = parsePreferredList(
|
|
234
|
+
process.env.MEMORY_USER_MEMORY_LAYER_EXTRACTOR_PREFERRED_MODELS,
|
|
235
|
+
);
|
|
236
|
+
|
|
204
237
|
return {
|
|
205
238
|
agentGateKeeper,
|
|
239
|
+
agentGateKeeperPreferredModels,
|
|
240
|
+
agentGateKeeperPreferredProviders,
|
|
206
241
|
agentLayerExtractor,
|
|
242
|
+
agentLayerExtractorPreferredModels,
|
|
243
|
+
agentLayerExtractorPreferredProviders,
|
|
207
244
|
concurrency,
|
|
208
245
|
embedding,
|
|
246
|
+
embeddingPreferredModels,
|
|
247
|
+
embeddingPreferredProviders,
|
|
209
248
|
featureFlags,
|
|
210
249
|
observabilityS3: extractorObservabilityS3,
|
|
211
250
|
upstashWorkflowExtraHeaders,
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { LayersEnum, MemorySourceType } from '@/types/userMemory';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
type MemoryExtractionNormalizedPayload,
|
|
7
|
+
type MemoryExtractionPayloadInput,
|
|
8
|
+
buildWorkflowPayloadInput,
|
|
9
|
+
normalizeMemoryExtractionPayload,
|
|
10
|
+
} from '../extract';
|
|
11
|
+
|
|
12
|
+
describe('normalizeMemoryExtractionPayload', () => {
|
|
13
|
+
it('normalizes sources, layers, ids, and dates with fallback baseUrl', () => {
|
|
14
|
+
const fromDate = new Date('2024-01-01T00:00:00Z');
|
|
15
|
+
const toDate = new Date('2024-02-01T00:00:00Z');
|
|
16
|
+
|
|
17
|
+
const payload: MemoryExtractionPayloadInput = {
|
|
18
|
+
forceAll: true,
|
|
19
|
+
forceTopics: true,
|
|
20
|
+
fromDate,
|
|
21
|
+
identityCursor: 3,
|
|
22
|
+
layers: [LayersEnum.Context, LayersEnum.Identity, LayersEnum.Context],
|
|
23
|
+
mode: 'direct',
|
|
24
|
+
sourceIds: ['source-1', 'source-1', ''],
|
|
25
|
+
sources: ['chatTopics', 'benchmark_locomo', 'unknown'],
|
|
26
|
+
toDate,
|
|
27
|
+
topicIds: ['topic-1', 'topic-1', ''],
|
|
28
|
+
userId: 'user-a',
|
|
29
|
+
userIds: ['user-a', 'user-b', ''],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const normalized = normalizeMemoryExtractionPayload(payload, 'https://api.example.com');
|
|
33
|
+
|
|
34
|
+
expect(normalized.baseUrl).toBe('https://api.example.com');
|
|
35
|
+
expect(normalized.forceAll).toBe(true);
|
|
36
|
+
expect(normalized.forceTopics).toBe(true);
|
|
37
|
+
expect(normalized.from).toEqual(fromDate);
|
|
38
|
+
expect(normalized.to).toEqual(toDate);
|
|
39
|
+
expect(normalized.identityCursor).toBe(3);
|
|
40
|
+
expect(normalized.layers).toEqual([LayersEnum.Context, LayersEnum.Identity]);
|
|
41
|
+
expect(normalized.sources).toEqual([
|
|
42
|
+
MemorySourceType.ChatTopic,
|
|
43
|
+
MemorySourceType.BenchmarkLocomo,
|
|
44
|
+
]);
|
|
45
|
+
expect(normalized.sourceIds).toEqual(['source-1']);
|
|
46
|
+
expect(normalized.topicIds).toEqual(['topic-1']);
|
|
47
|
+
expect(normalized.userId).toBe('user-a');
|
|
48
|
+
expect(normalized.userIds).toEqual(['user-a', 'user-b']);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('throws when baseUrl is missing in both payload and fallback', () => {
|
|
52
|
+
const payload: MemoryExtractionPayloadInput = {
|
|
53
|
+
forceAll: false,
|
|
54
|
+
forceTopics: false,
|
|
55
|
+
userIds: [],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
expect(() => normalizeMemoryExtractionPayload(payload)).toThrow('Missing baseUrl');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('buildWorkflowPayloadInput', () => {
|
|
63
|
+
const baseNormalized: MemoryExtractionNormalizedPayload = {
|
|
64
|
+
baseUrl: 'https://api.example.com',
|
|
65
|
+
forceAll: false,
|
|
66
|
+
forceTopics: false,
|
|
67
|
+
from: undefined,
|
|
68
|
+
identityCursor: 0,
|
|
69
|
+
layers: [],
|
|
70
|
+
mode: 'workflow',
|
|
71
|
+
sourceIds: [],
|
|
72
|
+
sources: [MemorySourceType.ChatTopic],
|
|
73
|
+
to: undefined,
|
|
74
|
+
topicCursor: undefined,
|
|
75
|
+
topicIds: [],
|
|
76
|
+
userCursor: undefined,
|
|
77
|
+
userId: undefined,
|
|
78
|
+
userIds: ['user-x', 'user-y'],
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
it('falls back to the first user id when userId is missing', () => {
|
|
82
|
+
const payload = buildWorkflowPayloadInput(baseNormalized);
|
|
83
|
+
|
|
84
|
+
expect(payload.userId).toBe('user-x');
|
|
85
|
+
expect(payload.userIds).toEqual(['user-x', 'user-y']);
|
|
86
|
+
expect(payload.baseUrl).toBe('https://api.example.com');
|
|
87
|
+
expect(payload.mode).toBe('workflow');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('preserves explicit userId when provided', () => {
|
|
91
|
+
const normalized: MemoryExtractionNormalizedPayload = {
|
|
92
|
+
...baseNormalized,
|
|
93
|
+
userId: 'user-z',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const payload = buildWorkflowPayloadInput(normalized);
|
|
97
|
+
|
|
98
|
+
expect(payload.userId).toBe('user-z');
|
|
99
|
+
expect(payload.userIds).toEqual(['user-x', 'user-y']);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { AiProviderRuntimeState } from '@lobechat/types';
|
|
2
|
+
import type { EnabledAiModel } from 'model-bank';
|
|
3
|
+
import { describe, expect, it } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import type { MemoryExtractionPrivateConfig } from '@/server/globalConfig/parseMemoryExtractionConfig';
|
|
6
|
+
|
|
7
|
+
import { MemoryExtractionExecutor } from '../extract';
|
|
8
|
+
|
|
9
|
+
const createRuntimeState = (models: EnabledAiModel[], keyVaults: Record<string, any>) =>
|
|
10
|
+
({
|
|
11
|
+
enabledAiModels: models,
|
|
12
|
+
enabledAiProviders: [],
|
|
13
|
+
enabledChatAiProviders: [],
|
|
14
|
+
enabledImageAiProviders: [],
|
|
15
|
+
runtimeConfig: Object.fromEntries(
|
|
16
|
+
Object.entries(keyVaults).map(([providerId, vault]) => [
|
|
17
|
+
providerId,
|
|
18
|
+
{ config: {}, keyVaults: vault, settings: {} },
|
|
19
|
+
]),
|
|
20
|
+
),
|
|
21
|
+
}) as AiProviderRuntimeState;
|
|
22
|
+
|
|
23
|
+
const createExecutor = (privateOverrides?: Partial<MemoryExtractionPrivateConfig>) => {
|
|
24
|
+
const basePrivateConfig: MemoryExtractionPrivateConfig = {
|
|
25
|
+
agentGateKeeper: { model: 'gate-2', provider: 'provider-b' },
|
|
26
|
+
agentLayerExtractor: {
|
|
27
|
+
contextLimit: 2048,
|
|
28
|
+
layers: {
|
|
29
|
+
context: 'layer-ctx',
|
|
30
|
+
experience: 'layer-exp',
|
|
31
|
+
identity: 'layer-id',
|
|
32
|
+
preference: 'layer-pref',
|
|
33
|
+
},
|
|
34
|
+
model: 'layer-1',
|
|
35
|
+
provider: 'provider-l',
|
|
36
|
+
},
|
|
37
|
+
concurrency: 1,
|
|
38
|
+
embedding: { model: 'embed-1', provider: 'provider-e' },
|
|
39
|
+
featureFlags: { enableBenchmarkLoCoMo: false },
|
|
40
|
+
observabilityS3: { enabled: false },
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const serverConfig = {
|
|
44
|
+
aiProvider: {},
|
|
45
|
+
memory: {},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
49
|
+
// @ts-ignore accessing private constructor for testing
|
|
50
|
+
return new MemoryExtractionExecutor(serverConfig as any, {
|
|
51
|
+
...basePrivateConfig,
|
|
52
|
+
...privateOverrides,
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
describe('MemoryExtractionExecutor.resolveRuntimeKeyVaults', () => {
|
|
57
|
+
it('prefers configured providers/models for gatekeeper, embedding, and layer extractors', () => {
|
|
58
|
+
const executor = createExecutor({
|
|
59
|
+
embeddingPreferredProviders: ['provider-e'],
|
|
60
|
+
agentGateKeeperPreferredModels: ['gate-1'],
|
|
61
|
+
agentGateKeeperPreferredProviders: ['provider-a', 'provider-b'],
|
|
62
|
+
agentLayerExtractorPreferredProviders: ['provider-l'],
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const runtimeState = createRuntimeState(
|
|
66
|
+
[
|
|
67
|
+
{ abilities: {}, id: 'gate-1', providerId: 'provider-a', type: 'chat' },
|
|
68
|
+
{ abilities: {}, id: 'gate-2', providerId: 'provider-b', type: 'chat' },
|
|
69
|
+
{ abilities: {}, id: 'embed-1', providerId: 'provider-e', type: 'embedding' },
|
|
70
|
+
{ abilities: {}, id: 'layer-ctx', providerId: 'provider-l', type: 'chat' },
|
|
71
|
+
{ abilities: {}, id: 'layer-exp', providerId: 'provider-l', type: 'chat' },
|
|
72
|
+
{ abilities: {}, id: 'layer-id', providerId: 'provider-l', type: 'chat' },
|
|
73
|
+
{ abilities: {}, id: 'layer-pref', providerId: 'provider-l', type: 'chat' },
|
|
74
|
+
],
|
|
75
|
+
{
|
|
76
|
+
'provider-a': { apiKey: 'a-key' },
|
|
77
|
+
'provider-b': { apiKey: 'b-key' },
|
|
78
|
+
'provider-e': { apiKey: 'e-key' },
|
|
79
|
+
'provider-l': { apiKey: 'l-key' },
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const keyVaults = (executor as any).resolveRuntimeKeyVaults(runtimeState);
|
|
84
|
+
|
|
85
|
+
expect(keyVaults).toMatchObject({
|
|
86
|
+
'provider-a': { apiKey: 'a-key' }, // gatekeeper picked preferred provider/model
|
|
87
|
+
'provider-e': { apiKey: 'e-key' }, // embedding honored preferred provider
|
|
88
|
+
'provider-l': { apiKey: 'l-key' }, // layer extractor models resolved
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('throws when no provider can satisfy an embedding model', () => {
|
|
93
|
+
const executor = createExecutor();
|
|
94
|
+
|
|
95
|
+
const runtimeState = createRuntimeState(
|
|
96
|
+
[
|
|
97
|
+
{ abilities: {}, id: 'gate-2', providerId: 'provider-b', type: 'chat' },
|
|
98
|
+
{ abilities: {}, id: 'layer-ctx', providerId: 'provider-l', type: 'chat' },
|
|
99
|
+
{ abilities: {}, id: 'layer-exp', providerId: 'provider-l', type: 'chat' },
|
|
100
|
+
{ abilities: {}, id: 'layer-id', providerId: 'provider-l', type: 'chat' },
|
|
101
|
+
{ abilities: {}, id: 'layer-pref', providerId: 'provider-l', type: 'chat' },
|
|
102
|
+
],
|
|
103
|
+
{
|
|
104
|
+
'provider-b': { apiKey: 'b-key' },
|
|
105
|
+
'provider-l': { apiKey: 'l-key' },
|
|
106
|
+
},
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
expect(() => (executor as any).resolveRuntimeKeyVaults(runtimeState)).toThrow(/embedding/i);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
} from '@lobechat/observability-otel/modules/memory-user-memory';
|
|
39
39
|
import { attributesCommon } from '@lobechat/observability-otel/node';
|
|
40
40
|
import type {
|
|
41
|
+
AiProviderRuntimeState,
|
|
41
42
|
IdentityMemoryDetail,
|
|
42
43
|
MemoryExtractionAgentCallTrace,
|
|
43
44
|
MemoryExtractionTraceError,
|
|
@@ -55,6 +56,7 @@ import type { ListUsersForMemoryExtractorCursor } from '@/database/models/user';
|
|
|
55
56
|
import { UserModel } from '@/database/models/user';
|
|
56
57
|
import { UserMemoryModel } from '@/database/models/userMemory';
|
|
57
58
|
import { UserMemorySourceBenchmarkLoCoMoModel } from '@/database/models/userMemory/sources/benchmarkLoCoMo';
|
|
59
|
+
import { AiInfraRepos } from '@/database/repositories/aiInfra';
|
|
58
60
|
import { getServerDB } from '@/database/server';
|
|
59
61
|
import { getServerGlobalConfig } from '@/server/globalConfig';
|
|
60
62
|
import {
|
|
@@ -64,7 +66,7 @@ import {
|
|
|
64
66
|
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
|
|
65
67
|
import { S3 } from '@/server/modules/S3';
|
|
66
68
|
import type { GlobalMemoryLayer } from '@/types/serverConfig';
|
|
67
|
-
import type {
|
|
69
|
+
import type { ProviderConfig } from '@/types/user/settings';
|
|
68
70
|
import {
|
|
69
71
|
LayersEnum,
|
|
70
72
|
MemorySourceType,
|
|
@@ -212,6 +214,11 @@ export interface TopicBatchWorkflowPayload extends MemoryExtractionPayloadInput
|
|
|
212
214
|
userId: string;
|
|
213
215
|
}
|
|
214
216
|
|
|
217
|
+
type ProviderKeyVaultMap = Record<
|
|
218
|
+
string,
|
|
219
|
+
AiProviderRuntimeState['runtimeConfig'][string]['keyVaults'] | undefined
|
|
220
|
+
>;
|
|
221
|
+
|
|
215
222
|
export const buildWorkflowPayloadInput = (
|
|
216
223
|
payload: MemoryExtractionNormalizedPayload,
|
|
217
224
|
): MemoryExtractionPayloadInput => ({
|
|
@@ -232,11 +239,9 @@ export const buildWorkflowPayloadInput = (
|
|
|
232
239
|
userIds: payload.userIds,
|
|
233
240
|
});
|
|
234
241
|
|
|
235
|
-
const normalizeProvider = (provider: string) => provider.toLowerCase()
|
|
236
|
-
|
|
237
|
-
const extractCredentialsFromVault = (provider: string, keyVaults?: UserKeyVaults) => {
|
|
238
|
-
const vault = keyVaults?.[normalizeProvider(provider)];
|
|
242
|
+
const normalizeProvider = (provider: string) => provider.toLowerCase();
|
|
239
243
|
|
|
244
|
+
const extractCredentialsFromVault = (vault?: Record<string, unknown>) => {
|
|
240
245
|
if (!vault || typeof vault !== 'object') return {};
|
|
241
246
|
|
|
242
247
|
const apiKey = 'apiKey' in vault && typeof vault.apiKey === 'string' ? vault.apiKey : undefined;
|
|
@@ -275,11 +280,10 @@ const maskSecret = (value?: string) => {
|
|
|
275
280
|
return `${value.slice(0, 6)}***${value.slice(-4)}`;
|
|
276
281
|
};
|
|
277
282
|
|
|
278
|
-
const resolveRuntimeAgentConfig = (agent: MemoryAgentConfig, keyVaults?:
|
|
283
|
+
const resolveRuntimeAgentConfig = (agent: MemoryAgentConfig, keyVaults?: ProviderKeyVaultMap) => {
|
|
279
284
|
const provider = agent.provider || 'openai';
|
|
280
285
|
const { apiKey: userApiKey, baseURL: userBaseURL } = extractCredentialsFromVault(
|
|
281
|
-
provider,
|
|
282
|
-
keyVaults,
|
|
286
|
+
keyVaults?.[normalizeProvider(provider)],
|
|
283
287
|
);
|
|
284
288
|
|
|
285
289
|
// Only use the user baseURL if we are also using their API key; otherwise fall back entirely
|
|
@@ -309,7 +313,7 @@ const debugRuntimeInit = (
|
|
|
309
313
|
});
|
|
310
314
|
};
|
|
311
315
|
|
|
312
|
-
const initRuntimeForAgent = async (agent: MemoryAgentConfig, keyVaults?:
|
|
316
|
+
const initRuntimeForAgent = async (agent: MemoryAgentConfig, keyVaults?: ProviderKeyVaultMap) => {
|
|
313
317
|
const resolved = resolveRuntimeAgentConfig(agent, keyVaults);
|
|
314
318
|
debugRuntimeInit(agent, resolved);
|
|
315
319
|
|
|
@@ -366,6 +370,13 @@ type MemoryExtractionConfig = ReturnType<typeof parseMemoryExtractionConfig>;
|
|
|
366
370
|
type ServerConfig = Awaited<ReturnType<typeof getServerGlobalConfig>>;
|
|
367
371
|
|
|
368
372
|
export class MemoryExtractionExecutor {
|
|
373
|
+
private readonly aiProviderConfig: Record<string, ProviderConfig>;
|
|
374
|
+
private readonly embeddingPreferredModels?: string[];
|
|
375
|
+
private readonly embeddingPreferredProviders?: string[];
|
|
376
|
+
private readonly gatekeeperPreferredModels?: string[];
|
|
377
|
+
private readonly gatekeeperPreferredProviders?: string[];
|
|
378
|
+
private readonly layerPreferredModels?: string[];
|
|
379
|
+
private readonly layerPreferredProviders?: string[];
|
|
369
380
|
private readonly privateConfig: MemoryExtractionConfig;
|
|
370
381
|
private readonly modelConfig: {
|
|
371
382
|
embeddingsModel: string;
|
|
@@ -380,6 +391,13 @@ export class MemoryExtractionExecutor {
|
|
|
380
391
|
|
|
381
392
|
private constructor(serverConfig: ServerConfig, privateConfig: MemoryExtractionConfig) {
|
|
382
393
|
this.privateConfig = privateConfig;
|
|
394
|
+
this.aiProviderConfig = (serverConfig.aiProvider || {}) as Record<string, ProviderConfig>;
|
|
395
|
+
this.embeddingPreferredProviders = privateConfig.embeddingPreferredProviders;
|
|
396
|
+
this.embeddingPreferredModels = privateConfig.embeddingPreferredModels;
|
|
397
|
+
this.gatekeeperPreferredProviders = privateConfig.agentGateKeeperPreferredProviders;
|
|
398
|
+
this.gatekeeperPreferredModels = privateConfig.agentGateKeeperPreferredModels;
|
|
399
|
+
this.layerPreferredProviders = privateConfig.agentLayerExtractorPreferredProviders;
|
|
400
|
+
this.layerPreferredModels = privateConfig.agentLayerExtractorPreferredModels;
|
|
383
401
|
|
|
384
402
|
const publicMemoryConfig = serverConfig.memory?.userMemory;
|
|
385
403
|
|
|
@@ -1014,8 +1032,11 @@ export class MemoryExtractionExecutor {
|
|
|
1014
1032
|
};
|
|
1015
1033
|
|
|
1016
1034
|
const userModel = new UserModel(db, job.userId);
|
|
1017
|
-
const userState = await
|
|
1018
|
-
|
|
1035
|
+
const [userState, aiProviderRuntimeState] = await Promise.all([
|
|
1036
|
+
userModel.getUserState(KeyVaultsGateKeeper.getUserKeyVaults),
|
|
1037
|
+
this.getAiProviderRuntimeState(job.userId),
|
|
1038
|
+
]);
|
|
1039
|
+
const keyVaults = this.resolveRuntimeKeyVaults(aiProviderRuntimeState);
|
|
1019
1040
|
const language = userState.settings?.general?.responseLanguage;
|
|
1020
1041
|
|
|
1021
1042
|
const runtimes = await this.getRuntime(job.userId, keyVaults);
|
|
@@ -1625,7 +1646,112 @@ export class MemoryExtractionExecutor {
|
|
|
1625
1646
|
};
|
|
1626
1647
|
}
|
|
1627
1648
|
|
|
1628
|
-
private async
|
|
1649
|
+
private async getAiProviderRuntimeState(userId: string): Promise<AiProviderRuntimeState> {
|
|
1650
|
+
const db = await this.db;
|
|
1651
|
+
const aiInfraRepos = new AiInfraRepos(db, userId, this.aiProviderConfig);
|
|
1652
|
+
|
|
1653
|
+
return aiInfraRepos.getAiProviderRuntimeState(KeyVaultsGateKeeper.getUserKeyVaults);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
private resolveRuntimeKeyVaults(runtimeState: AiProviderRuntimeState): ProviderKeyVaultMap {
|
|
1657
|
+
const normalizedRuntimeConfig = Object.fromEntries(
|
|
1658
|
+
Object.entries(runtimeState.runtimeConfig || {}).map(([providerId, config]) => [
|
|
1659
|
+
normalizeProvider(providerId),
|
|
1660
|
+
config,
|
|
1661
|
+
]),
|
|
1662
|
+
);
|
|
1663
|
+
const providerModels = runtimeState.enabledAiModels.reduce<Record<string, Set<string>>>(
|
|
1664
|
+
(acc, model) => {
|
|
1665
|
+
const providerId = normalizeProvider(model.providerId);
|
|
1666
|
+
acc[providerId] = acc[providerId] || new Set<string>();
|
|
1667
|
+
acc[providerId].add(model.id);
|
|
1668
|
+
return acc;
|
|
1669
|
+
},
|
|
1670
|
+
{},
|
|
1671
|
+
);
|
|
1672
|
+
|
|
1673
|
+
const resolveProviderForModel = (
|
|
1674
|
+
modelId: string,
|
|
1675
|
+
fallbackProvider?: string,
|
|
1676
|
+
preferredProviders?: string[],
|
|
1677
|
+
preferredModels?: string[],
|
|
1678
|
+
label?: string,
|
|
1679
|
+
) => {
|
|
1680
|
+
const providerOrder = Array.from(
|
|
1681
|
+
new Set(
|
|
1682
|
+
[
|
|
1683
|
+
...(preferredProviders?.map(normalizeProvider) || []),
|
|
1684
|
+
fallbackProvider ? normalizeProvider(fallbackProvider) : undefined,
|
|
1685
|
+
...Object.keys(providerModels),
|
|
1686
|
+
].filter(Boolean) as string[],
|
|
1687
|
+
),
|
|
1688
|
+
);
|
|
1689
|
+
|
|
1690
|
+
const candidateModels = preferredModels && preferredModels.length > 0 ? preferredModels : [];
|
|
1691
|
+
|
|
1692
|
+
for (const providerId of providerOrder) {
|
|
1693
|
+
const models = providerModels[providerId];
|
|
1694
|
+
if (!models) continue;
|
|
1695
|
+
|
|
1696
|
+
if (models.has(modelId)) return providerId;
|
|
1697
|
+
|
|
1698
|
+
const preferredMatch = candidateModels.find((preferredModel) => models.has(preferredModel));
|
|
1699
|
+
if (preferredMatch) return providerId;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
throw new Error(
|
|
1703
|
+
`Unable to resolve provider for ${label || 'model'} "${modelId}". Check preferred providers/models configuration.`,
|
|
1704
|
+
);
|
|
1705
|
+
};
|
|
1706
|
+
|
|
1707
|
+
const keyVaults: ProviderKeyVaultMap = {};
|
|
1708
|
+
|
|
1709
|
+
const gatekeeperProvider = resolveProviderForModel(
|
|
1710
|
+
this.modelConfig.gateModel,
|
|
1711
|
+
this.privateConfig.agentGateKeeper.provider,
|
|
1712
|
+
this.gatekeeperPreferredProviders,
|
|
1713
|
+
this.gatekeeperPreferredModels,
|
|
1714
|
+
'gatekeeper',
|
|
1715
|
+
);
|
|
1716
|
+
const gatekeeperRuntime = normalizedRuntimeConfig[gatekeeperProvider];
|
|
1717
|
+
if (gatekeeperRuntime?.keyVaults) {
|
|
1718
|
+
keyVaults[gatekeeperProvider] = gatekeeperRuntime.keyVaults;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
const embeddingProvider = resolveProviderForModel(
|
|
1722
|
+
this.modelConfig.embeddingsModel,
|
|
1723
|
+
this.privateConfig.embedding.provider,
|
|
1724
|
+
this.embeddingPreferredProviders,
|
|
1725
|
+
this.embeddingPreferredModels,
|
|
1726
|
+
'embedding',
|
|
1727
|
+
);
|
|
1728
|
+
const embeddingRuntime = normalizedRuntimeConfig[embeddingProvider];
|
|
1729
|
+
if (embeddingRuntime?.keyVaults) {
|
|
1730
|
+
keyVaults[embeddingProvider] = embeddingRuntime.keyVaults;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
Object.values(this.modelConfig.layerModels).forEach((model) => {
|
|
1734
|
+
if (!model) return;
|
|
1735
|
+
const providerId = resolveProviderForModel(
|
|
1736
|
+
model,
|
|
1737
|
+
this.privateConfig.agentLayerExtractor.provider,
|
|
1738
|
+
this.layerPreferredProviders,
|
|
1739
|
+
this.layerPreferredModels,
|
|
1740
|
+
'layer extractor',
|
|
1741
|
+
);
|
|
1742
|
+
const runtime = normalizedRuntimeConfig[providerId];
|
|
1743
|
+
if (runtime?.keyVaults) {
|
|
1744
|
+
keyVaults[providerId] = runtime.keyVaults;
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
return keyVaults;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
private async getRuntime(
|
|
1752
|
+
userId: string,
|
|
1753
|
+
keyVaults?: ProviderKeyVaultMap,
|
|
1754
|
+
): Promise<RuntimeBundle> {
|
|
1629
1755
|
// TODO: implement a better cache eviction strategy
|
|
1630
1756
|
// TODO: make cache size configurable
|
|
1631
1757
|
if (this.runtimeCache.keys.length > 200) {
|
|
@@ -1673,8 +1799,11 @@ export class MemoryExtractionExecutor {
|
|
|
1673
1799
|
try {
|
|
1674
1800
|
const db = await this.db;
|
|
1675
1801
|
const userModel = new UserModel(db, params.userId);
|
|
1676
|
-
const userState = await
|
|
1677
|
-
|
|
1802
|
+
const [userState, aiProviderRuntimeState] = await Promise.all([
|
|
1803
|
+
userModel.getUserState(KeyVaultsGateKeeper.getUserKeyVaults),
|
|
1804
|
+
this.getAiProviderRuntimeState(params.userId),
|
|
1805
|
+
]);
|
|
1806
|
+
const keyVaults = this.resolveRuntimeKeyVaults(aiProviderRuntimeState);
|
|
1678
1807
|
const language = params.language || userState.settings?.general?.responseLanguage;
|
|
1679
1808
|
|
|
1680
1809
|
const runtimes = await this.getRuntime(params.userId, keyVaults);
|
|
@@ -1849,7 +1978,10 @@ export class MemoryExtractionWorkflowService {
|
|
|
1849
1978
|
return this.client;
|
|
1850
1979
|
}
|
|
1851
1980
|
|
|
1852
|
-
static triggerProcessUsers(
|
|
1981
|
+
static triggerProcessUsers(
|
|
1982
|
+
payload: MemoryExtractionPayloadInput,
|
|
1983
|
+
options?: { extraHeaders?: Record<string, string> },
|
|
1984
|
+
) {
|
|
1853
1985
|
if (!payload.baseUrl) {
|
|
1854
1986
|
throw new Error('Missing baseUrl for workflow trigger');
|
|
1855
1987
|
}
|
|
@@ -1858,7 +1990,10 @@ export class MemoryExtractionWorkflowService {
|
|
|
1858
1990
|
return this.getClient().trigger({ body: payload, headers: options?.extraHeaders, url });
|
|
1859
1991
|
}
|
|
1860
1992
|
|
|
1861
|
-
static triggerProcessUserTopics(
|
|
1993
|
+
static triggerProcessUserTopics(
|
|
1994
|
+
payload: UserTopicWorkflowPayload,
|
|
1995
|
+
options?: { extraHeaders?: Record<string, string> },
|
|
1996
|
+
) {
|
|
1862
1997
|
if (!payload.baseUrl) {
|
|
1863
1998
|
throw new Error('Missing baseUrl for workflow trigger');
|
|
1864
1999
|
}
|
|
@@ -1867,7 +2002,10 @@ export class MemoryExtractionWorkflowService {
|
|
|
1867
2002
|
return this.getClient().trigger({ body: payload, headers: options?.extraHeaders, url });
|
|
1868
2003
|
}
|
|
1869
2004
|
|
|
1870
|
-
static triggerProcessTopics(
|
|
2005
|
+
static triggerProcessTopics(
|
|
2006
|
+
payload: MemoryExtractionPayloadInput,
|
|
2007
|
+
options?: { extraHeaders?: Record<string, string> },
|
|
2008
|
+
) {
|
|
1871
2009
|
if (!payload.baseUrl) {
|
|
1872
2010
|
throw new Error('Missing baseUrl for workflow trigger');
|
|
1873
2011
|
}
|