@lobehub/lobehub 2.0.0-next.320 → 2.0.0-next.322
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 +50 -0
- package/changelog/v1.json +10 -0
- package/package.json +1 -1
- package/src/app/[variants]/(main)/agent/profile/index.tsx +15 -2
- package/src/features/PageEditor/PageEditor.tsx +20 -8
- 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 +121 -0
- package/src/server/services/memory/userMemory/extract.ts +164 -17
- package/src/utils/styles.ts +10 -0
|
@@ -15,6 +15,7 @@ import { builtinAgentSelectors } from '@/store/agent/selectors';
|
|
|
15
15
|
import { useDocumentStore } from '@/store/document';
|
|
16
16
|
import { editorSelectors } from '@/store/document/slices/editor';
|
|
17
17
|
import { usePageStore } from '@/store/page';
|
|
18
|
+
import { StyleSheet } from '@/utils/styles';
|
|
18
19
|
|
|
19
20
|
import Copilot from './Copilot';
|
|
20
21
|
import EditorCanvas from './EditorCanvas';
|
|
@@ -25,6 +26,22 @@ import PageTitle from './PageTitle';
|
|
|
25
26
|
import TitleSection from './TitleSection';
|
|
26
27
|
import { usePageEditorStore } from './store';
|
|
27
28
|
|
|
29
|
+
const styles = StyleSheet.create({
|
|
30
|
+
contentWrapper: {
|
|
31
|
+
display: 'flex',
|
|
32
|
+
overflowY: 'auto',
|
|
33
|
+
position: 'relative',
|
|
34
|
+
},
|
|
35
|
+
editorContainer: {
|
|
36
|
+
minWidth: 0,
|
|
37
|
+
position: 'relative',
|
|
38
|
+
},
|
|
39
|
+
editorContent: {
|
|
40
|
+
overflowY: 'auto',
|
|
41
|
+
position: 'relative',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
28
45
|
interface PageEditorProps {
|
|
29
46
|
emoji?: string;
|
|
30
47
|
knowledgeBaseId?: string;
|
|
@@ -77,16 +94,11 @@ const PageEditorCanvas = memo(() => {
|
|
|
77
94
|
style={{ backgroundColor: cssVar.colorBgContainer }}
|
|
78
95
|
width={'100%'}
|
|
79
96
|
>
|
|
80
|
-
<Flexbox flex={1} height={'100%'} style={
|
|
97
|
+
<Flexbox flex={1} height={'100%'} style={styles.editorContainer}>
|
|
81
98
|
<Header />
|
|
82
|
-
<Flexbox
|
|
83
|
-
height={'100%'}
|
|
84
|
-
horizontal
|
|
85
|
-
style={{ display: 'flex', overflowY: 'auto', position: 'relative' }}
|
|
86
|
-
width={'100%'}
|
|
87
|
-
>
|
|
99
|
+
<Flexbox height={'100%'} horizontal style={styles.contentWrapper} width={'100%'}>
|
|
88
100
|
<WideScreenContainer onClick={() => editor?.focus()} wrapperStyle={{ cursor: 'text' }}>
|
|
89
|
-
<Flexbox flex={1} style={
|
|
101
|
+
<Flexbox flex={1} style={styles.editorContent}>
|
|
90
102
|
<TitleSection />
|
|
91
103
|
<EditorCanvas />
|
|
92
104
|
</Flexbox>
|
|
@@ -1,22 +1,34 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { type ReactNode, createContext, memo, useCallback, useContext, useState } from 'react';
|
|
3
|
+
import { type ReactNode, createContext, memo, useCallback, useContext, useMemo, useState } from 'react';
|
|
4
4
|
|
|
5
5
|
export type FaviconState = 'default' | 'done' | 'error' | 'progress';
|
|
6
6
|
|
|
7
|
-
interface
|
|
7
|
+
interface FaviconStateContextValue {
|
|
8
8
|
currentState: FaviconState;
|
|
9
9
|
isDevMode: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface FaviconSettersContextValue {
|
|
10
13
|
setFavicon: (state: FaviconState) => void;
|
|
11
14
|
setIsDevMode: (isDev: boolean) => void;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
const
|
|
17
|
+
const FaviconStateContext = createContext<FaviconStateContextValue | null>(null);
|
|
18
|
+
const FaviconSettersContext = createContext<FaviconSettersContextValue | null>(null);
|
|
19
|
+
|
|
20
|
+
export const useFaviconState = () => {
|
|
21
|
+
const context = useContext(FaviconStateContext);
|
|
22
|
+
if (!context) {
|
|
23
|
+
throw new Error('useFaviconState must be used within FaviconProvider');
|
|
24
|
+
}
|
|
25
|
+
return context;
|
|
26
|
+
};
|
|
15
27
|
|
|
16
|
-
export const
|
|
17
|
-
const context = useContext(
|
|
28
|
+
export const useFaviconSetters = () => {
|
|
29
|
+
const context = useContext(FaviconSettersContext);
|
|
18
30
|
if (!context) {
|
|
19
|
-
throw new Error('
|
|
31
|
+
throw new Error('useFaviconSetters must be used within FaviconProvider');
|
|
20
32
|
}
|
|
21
33
|
return context;
|
|
22
34
|
};
|
|
@@ -66,26 +78,38 @@ export const FaviconProvider = memo<{ children: ReactNode }>(({ children }) => {
|
|
|
66
78
|
const [currentState, setCurrentState] = useState<FaviconState>('default');
|
|
67
79
|
const [isDevMode, setIsDevModeState] = useState<boolean>(defaultIsDev);
|
|
68
80
|
|
|
69
|
-
const setFavicon = useCallback(
|
|
70
|
-
(state
|
|
71
|
-
|
|
72
|
-
updateFaviconDOM(state,
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
const setFavicon = useCallback((state: FaviconState) => {
|
|
82
|
+
setCurrentState(state);
|
|
83
|
+
setIsDevModeState((isDev) => {
|
|
84
|
+
updateFaviconDOM(state, isDev);
|
|
85
|
+
return isDev;
|
|
86
|
+
});
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
const setIsDevMode = useCallback((isDev: boolean) => {
|
|
90
|
+
setIsDevModeState(isDev);
|
|
91
|
+
setCurrentState((state) => {
|
|
92
|
+
updateFaviconDOM(state, isDev);
|
|
93
|
+
return state;
|
|
94
|
+
});
|
|
95
|
+
}, []);
|
|
96
|
+
|
|
97
|
+
const stateValue = useMemo(
|
|
98
|
+
() => ({ currentState, isDevMode }),
|
|
99
|
+
[currentState, isDevMode],
|
|
75
100
|
);
|
|
76
101
|
|
|
77
|
-
const
|
|
78
|
-
(
|
|
79
|
-
|
|
80
|
-
updateFaviconDOM(currentState, isDev);
|
|
81
|
-
},
|
|
82
|
-
[currentState],
|
|
102
|
+
const settersValue = useMemo(
|
|
103
|
+
() => ({ setFavicon, setIsDevMode }),
|
|
104
|
+
[setFavicon, setIsDevMode],
|
|
83
105
|
);
|
|
84
106
|
|
|
85
107
|
return (
|
|
86
|
-
<
|
|
87
|
-
{
|
|
88
|
-
|
|
108
|
+
<FaviconStateContext.Provider value={stateValue}>
|
|
109
|
+
<FaviconSettersContext.Provider value={settersValue}>
|
|
110
|
+
{children}
|
|
111
|
+
</FaviconSettersContext.Provider>
|
|
112
|
+
</FaviconStateContext.Provider>
|
|
89
113
|
);
|
|
90
114
|
});
|
|
91
115
|
|
|
@@ -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,121 @@
|
|
|
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('warns and falls back to server provider when no enabled provider satisfies embedding model', () => {
|
|
93
|
+
const executor = createExecutor();
|
|
94
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
95
|
+
|
|
96
|
+
const runtimeState = createRuntimeState(
|
|
97
|
+
[
|
|
98
|
+
{ abilities: {}, id: 'gate-2', providerId: 'provider-b', type: 'chat' },
|
|
99
|
+
{ abilities: {}, id: 'layer-ctx', providerId: 'provider-l', type: 'chat' },
|
|
100
|
+
{ abilities: {}, id: 'layer-exp', providerId: 'provider-l', type: 'chat' },
|
|
101
|
+
{ abilities: {}, id: 'layer-id', providerId: 'provider-l', type: 'chat' },
|
|
102
|
+
{ abilities: {}, id: 'layer-pref', providerId: 'provider-l', type: 'chat' },
|
|
103
|
+
],
|
|
104
|
+
{
|
|
105
|
+
'provider-b': { apiKey: 'b-key' },
|
|
106
|
+
'provider-l': { apiKey: 'l-key' },
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const keyVaults = (executor as any).resolveRuntimeKeyVaults(runtimeState);
|
|
111
|
+
|
|
112
|
+
expect(keyVaults).toMatchObject({
|
|
113
|
+
'provider-b': { apiKey: 'b-key' },
|
|
114
|
+
'provider-l': { apiKey: 'l-key' },
|
|
115
|
+
});
|
|
116
|
+
expect(keyVaults).not.toHaveProperty('provider-e');
|
|
117
|
+
expect(warnSpy).toHaveBeenCalled();
|
|
118
|
+
|
|
119
|
+
warnSpy.mockRestore();
|
|
120
|
+
});
|
|
121
|
+
});
|