om-memory-system 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +587 -0
- package/dist/adapters/opencode/auto-capture-host.d.ts +3 -0
- package/dist/adapters/opencode/auto-capture-host.js +29 -0
- package/dist/adapters/opencode/auto-capture-summary.d.ts +4 -0
- package/dist/adapters/opencode/auto-capture-summary.js +147 -0
- package/dist/adapters/opencode/conversation.d.ts +2 -0
- package/dist/adapters/opencode/conversation.js +50 -0
- package/dist/adapters/opencode/user-prompt.d.ts +9 -0
- package/dist/adapters/opencode/user-prompt.js +26 -0
- package/dist/adapters/pi/capture.d.ts +41 -0
- package/dist/adapters/pi/capture.js +91 -0
- package/dist/adapters/pi/conversation.d.ts +38 -0
- package/dist/adapters/pi/conversation.js +118 -0
- package/dist/adapters/pi/extension.d.ts +18 -0
- package/dist/adapters/pi/extension.js +234 -0
- package/dist/adapters/pi/import-command.d.ts +29 -0
- package/dist/adapters/pi/import-command.js +232 -0
- package/dist/adapters/pi/profile.d.ts +39 -0
- package/dist/adapters/pi/profile.js +148 -0
- package/dist/adapters/pi/provider.d.ts +50 -0
- package/dist/adapters/pi/provider.js +77 -0
- package/dist/config.d.ts +290 -0
- package/dist/config.js +742 -0
- package/dist/core/capture-context.d.ts +4 -0
- package/dist/core/capture-context.js +132 -0
- package/dist/core/capture.d.ts +15 -0
- package/dist/core/capture.js +68 -0
- package/dist/core/extraction.d.ts +80 -0
- package/dist/core/extraction.js +140 -0
- package/dist/core/host.d.ts +52 -0
- package/dist/core/host.js +1 -0
- package/dist/core/memory-operations.d.ts +27 -0
- package/dist/core/memory-operations.js +247 -0
- package/dist/core/retrieval.d.ts +12 -0
- package/dist/core/retrieval.js +72 -0
- package/dist/importer/discovery.d.ts +31 -0
- package/dist/importer/discovery.js +88 -0
- package/dist/importer/importer.d.ts +87 -0
- package/dist/importer/importer.js +260 -0
- package/dist/importer/ledger.d.ts +44 -0
- package/dist/importer/ledger.js +137 -0
- package/dist/importer/session-loader.d.ts +19 -0
- package/dist/importer/session-loader.js +12 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +601 -0
- package/dist/plugin.d.ts +9 -0
- package/dist/plugin.js +11 -0
- package/dist/services/ai/ai-provider-factory.d.ts +7 -0
- package/dist/services/ai/ai-provider-factory.js +41 -0
- package/dist/services/ai/api-key-placeholder.d.ts +1 -0
- package/dist/services/ai/api-key-placeholder.js +14 -0
- package/dist/services/ai/internal-capture-sessions.d.ts +6 -0
- package/dist/services/ai/internal-capture-sessions.js +33 -0
- package/dist/services/ai/opencode-diagnostics.d.ts +7 -0
- package/dist/services/ai/opencode-diagnostics.js +36 -0
- package/dist/services/ai/opencode-host-config.d.ts +10 -0
- package/dist/services/ai/opencode-host-config.js +51 -0
- package/dist/services/ai/opencode-provider-loader.d.ts +3 -0
- package/dist/services/ai/opencode-provider-loader.js +4 -0
- package/dist/services/ai/opencode-provider.d.ts +92 -0
- package/dist/services/ai/opencode-provider.js +419 -0
- package/dist/services/ai/opencode-sdk-client.d.ts +6 -0
- package/dist/services/ai/opencode-sdk-client.js +38 -0
- package/dist/services/ai/profile-llm-client.d.ts +2 -0
- package/dist/services/ai/profile-llm-client.js +27 -0
- package/dist/services/ai/provider-config.d.ts +17 -0
- package/dist/services/ai/provider-config.js +30 -0
- package/dist/services/ai/providers/anthropic-messages.d.ts +34 -0
- package/dist/services/ai/providers/anthropic-messages.js +220 -0
- package/dist/services/ai/providers/base-provider.d.ts +24 -0
- package/dist/services/ai/providers/base-provider.js +24 -0
- package/dist/services/ai/providers/google-gemini.d.ts +15 -0
- package/dist/services/ai/providers/google-gemini.js +228 -0
- package/dist/services/ai/providers/minimax.d.ts +35 -0
- package/dist/services/ai/providers/minimax.js +55 -0
- package/dist/services/ai/providers/openai-chat-completion.d.ts +22 -0
- package/dist/services/ai/providers/openai-chat-completion.js +387 -0
- package/dist/services/ai/providers/openai-responses.d.ts +13 -0
- package/dist/services/ai/providers/openai-responses.js +182 -0
- package/dist/services/ai/providers/orcarouter.d.ts +51 -0
- package/dist/services/ai/providers/orcarouter.js +66 -0
- package/dist/services/ai/session/ai-session-manager.d.ts +24 -0
- package/dist/services/ai/session/ai-session-manager.js +211 -0
- package/dist/services/ai/session/session-types.d.ts +42 -0
- package/dist/services/ai/session/session-types.js +1 -0
- package/dist/services/ai/tools/tool-schema.d.ts +40 -0
- package/dist/services/ai/tools/tool-schema.js +24 -0
- package/dist/services/ai/validators/user-profile-validator.d.ts +12 -0
- package/dist/services/ai/validators/user-profile-validator.js +114 -0
- package/dist/services/api-handlers.d.ts +186 -0
- package/dist/services/api-handlers.js +1284 -0
- package/dist/services/auth-token.d.ts +14 -0
- package/dist/services/auth-token.js +59 -0
- package/dist/services/auto-capture.d.ts +3 -0
- package/dist/services/auto-capture.js +127 -0
- package/dist/services/cleanup-service.d.ts +22 -0
- package/dist/services/cleanup-service.js +109 -0
- package/dist/services/client.d.ts +134 -0
- package/dist/services/client.js +327 -0
- package/dist/services/context.d.ts +10 -0
- package/dist/services/context.js +27 -0
- package/dist/services/cors.d.ts +12 -0
- package/dist/services/cors.js +52 -0
- package/dist/services/deduplication-service.d.ts +28 -0
- package/dist/services/deduplication-service.js +111 -0
- package/dist/services/embedding.d.ts +36 -0
- package/dist/services/embedding.js +200 -0
- package/dist/services/jsonc.d.ts +6 -0
- package/dist/services/jsonc.js +76 -0
- package/dist/services/language-detector.d.ts +2 -0
- package/dist/services/language-detector.js +33 -0
- package/dist/services/legacy-migration.d.ts +77 -0
- package/dist/services/legacy-migration.js +343 -0
- package/dist/services/logger.d.ts +1 -0
- package/dist/services/logger.js +119 -0
- package/dist/services/memory-portability-service.d.ts +37 -0
- package/dist/services/memory-portability-service.js +340 -0
- package/dist/services/memory-scope.d.ts +16 -0
- package/dist/services/memory-scope.js +43 -0
- package/dist/services/migration-service.d.ts +42 -0
- package/dist/services/migration-service.js +307 -0
- package/dist/services/onnxruntime-resolve.d.ts +24 -0
- package/dist/services/onnxruntime-resolve.js +174 -0
- package/dist/services/privacy.d.ts +26 -0
- package/dist/services/privacy.js +69 -0
- package/dist/services/request-timeouts.d.ts +9 -0
- package/dist/services/request-timeouts.js +11 -0
- package/dist/services/runtime-require.d.ts +17 -0
- package/dist/services/runtime-require.js +75 -0
- package/dist/services/secret-resolver.d.ts +1 -0
- package/dist/services/secret-resolver.js +65 -0
- package/dist/services/shard-inventory-service.d.ts +69 -0
- package/dist/services/shard-inventory-service.js +315 -0
- package/dist/services/shard-path-migration-service.d.ts +31 -0
- package/dist/services/shard-path-migration-service.js +367 -0
- package/dist/services/tag-prefix-migration.d.ts +60 -0
- package/dist/services/tag-prefix-migration.js +439 -0
- package/dist/services/tag-prefix-scan.d.ts +26 -0
- package/dist/services/tag-prefix-scan.js +91 -0
- package/dist/services/tags.d.ts +29 -0
- package/dist/services/tags.js +251 -0
- package/dist/services/turso/connection-manager.d.ts +15 -0
- package/dist/services/turso/connection-manager.js +163 -0
- package/dist/services/turso/cross-process-write-lock.d.ts +1 -0
- package/dist/services/turso/cross-process-write-lock.js +89 -0
- package/dist/services/turso/legacy-migrator.d.ts +1 -0
- package/dist/services/turso/legacy-migrator.js +540 -0
- package/dist/services/turso/lifecycle.d.ts +1 -0
- package/dist/services/turso/lifecycle.js +14 -0
- package/dist/services/turso/operation-lock.d.ts +2 -0
- package/dist/services/turso/operation-lock.js +61 -0
- package/dist/services/turso/ready.d.ts +2 -0
- package/dist/services/turso/ready.js +36 -0
- package/dist/services/turso/shard-manager.d.ts +38 -0
- package/dist/services/turso/shard-manager.js +496 -0
- package/dist/services/turso/sqlite-handle-release.d.ts +16 -0
- package/dist/services/turso/sqlite-handle-release.js +56 -0
- package/dist/services/turso/turso-db.d.ts +18 -0
- package/dist/services/turso/turso-db.js +53 -0
- package/dist/services/turso/types.d.ts +42 -0
- package/dist/services/turso/types.js +1 -0
- package/dist/services/turso/vector-search.d.ts +41 -0
- package/dist/services/turso/vector-search.js +384 -0
- package/dist/services/turso/vector-utils.d.ts +7 -0
- package/dist/services/turso/vector-utils.js +67 -0
- package/dist/services/user-memory-learning.d.ts +97 -0
- package/dist/services/user-memory-learning.js +673 -0
- package/dist/services/user-profile/ai-cleanup.d.ts +58 -0
- package/dist/services/user-profile/ai-cleanup.js +486 -0
- package/dist/services/user-profile/profile-context.d.ts +1 -0
- package/dist/services/user-profile/profile-context.js +105 -0
- package/dist/services/user-profile/profile-utils.d.ts +2 -0
- package/dist/services/user-profile/profile-utils.js +45 -0
- package/dist/services/user-profile/types.d.ts +92 -0
- package/dist/services/user-profile/types.js +1 -0
- package/dist/services/user-profile/user-profile-manager.d.ts +65 -0
- package/dist/services/user-profile/user-profile-manager.js +1660 -0
- package/dist/services/user-prompt/user-prompt-manager.d.ts +53 -0
- package/dist/services/user-prompt/user-prompt-manager.js +303 -0
- package/dist/services/web-api-auth.d.ts +3 -0
- package/dist/services/web-api-auth.js +25 -0
- package/dist/services/web-auth.d.ts +46 -0
- package/dist/services/web-auth.js +105 -0
- package/dist/services/web-server.d.ts +57 -0
- package/dist/services/web-server.js +659 -0
- package/dist/shared/api/index.d.ts +4 -0
- package/dist/shared/api/index.js +2 -0
- package/dist/shared/api/portability-schemas.d.ts +56 -0
- package/dist/shared/api/portability-schemas.js +39 -0
- package/dist/shared/api/schemas.d.ts +295 -0
- package/dist/shared/api/schemas.js +134 -0
- package/dist/shared/api/types.d.ts +28 -0
- package/dist/shared/api/types.js +1 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/context-limit.d.ts +10 -0
- package/dist/utils/context-limit.js +43 -0
- package/dist/utils/math.d.ts +7 -0
- package/dist/utils/math.js +48 -0
- package/dist/utils/profile.d.ts +2 -0
- package/dist/utils/profile.js +15 -0
- package/dist/v2/adapter.d.ts +5 -0
- package/dist/v2/adapter.js +218 -0
- package/dist/v2/legacy-client.d.ts +15 -0
- package/dist/v2/legacy-client.js +257 -0
- package/dist/v2/memory-bridge.d.ts +18 -0
- package/dist/v2/memory-bridge.js +29 -0
- package/dist/v2/plugin.d.ts +3 -0
- package/dist/v2/plugin.js +24 -0
- package/dist/web/assets/index-DPjEpD1h.css +2 -0
- package/dist/web/assets/index-do26c4JJ.js +122 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +14 -0
- package/dist/web/omms-icon.png +0 -0
- package/package.json +138 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import { getTags } from "./tags.js";
|
|
2
|
+
import { log } from "./logger.js";
|
|
3
|
+
import { CONFIG } from "../config.js";
|
|
4
|
+
import { userPromptManager } from "./user-prompt/user-prompt-manager.js";
|
|
5
|
+
import { userProfileManager } from "./user-profile/user-profile-manager.js";
|
|
6
|
+
import { sortProfileItems } from "../utils/profile.js";
|
|
7
|
+
import { loadOpencodeProvider } from "./ai/opencode-provider-loader.js";
|
|
8
|
+
let isLearningRunning = false;
|
|
9
|
+
export function shouldRunAutomaticProfileCleanup(previousPromptCount, addedPromptCount, interval = CONFIG.userProfileAutoCleanupInterval) {
|
|
10
|
+
if (!CONFIG.userProfileAutoCleanupEnabled || !Number.isInteger(interval) || interval <= 0) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
return (Math.floor(previousPromptCount / interval) <
|
|
14
|
+
Math.floor((previousPromptCount + addedPromptCount) / interval));
|
|
15
|
+
}
|
|
16
|
+
async function runAutomaticProfileCleanup(userId) {
|
|
17
|
+
try {
|
|
18
|
+
const profile = await userProfileManager.getActiveProfile(userId);
|
|
19
|
+
if (!profile)
|
|
20
|
+
return;
|
|
21
|
+
const profileData = JSON.parse(profile.profileData);
|
|
22
|
+
const itemCount = profileData.preferences.length + profileData.patterns.length + profileData.workflows.length;
|
|
23
|
+
if (itemCount < 2)
|
|
24
|
+
return;
|
|
25
|
+
const { aiCleanupProfile } = await import("./user-profile/ai-cleanup.js");
|
|
26
|
+
const result = await aiCleanupProfile(profileData);
|
|
27
|
+
if (result.diff.merged.length === 0 && result.diff.removed.length === 0)
|
|
28
|
+
return;
|
|
29
|
+
const updated = await userProfileManager.updateProfile(profile.id, result.cleaned, 0, `Automatic AI cleanup: ${result.diff.merged.length} merged, ${result.diff.removed.length} removed`);
|
|
30
|
+
log("user-profile-learning: automatic cleanup complete", {
|
|
31
|
+
userId,
|
|
32
|
+
updated,
|
|
33
|
+
merged: result.diff.merged.length,
|
|
34
|
+
removed: result.diff.removed.length,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
log("user-profile-learning: automatic cleanup failed", { userId, error: String(error) });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function performUserProfileLearning(ctx, directory) {
|
|
42
|
+
if (isLearningRunning)
|
|
43
|
+
return;
|
|
44
|
+
if (!CONFIG.autoCaptureProviderStatus || !CONFIG.autoCaptureProviderStatus.ready) {
|
|
45
|
+
log("user-profile-learning: skipped (provider not ready)", {
|
|
46
|
+
issues: CONFIG.autoCaptureProviderStatus?.issues ?? ["status undefined"],
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
isLearningRunning = true;
|
|
51
|
+
try {
|
|
52
|
+
const count = await userPromptManager.countUnanalyzedForUserLearning();
|
|
53
|
+
const threshold = CONFIG.userProfileAnalysisInterval;
|
|
54
|
+
log("user-profile-learning: check", { count, threshold });
|
|
55
|
+
if (count < threshold) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const prompts = await userPromptManager.getPromptsForUserLearning(threshold);
|
|
59
|
+
if (prompts.length === 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const tags = getTags(directory);
|
|
63
|
+
const userId = tags.user.userEmail || "unknown";
|
|
64
|
+
let existingProfile = await userProfileManager.getActiveProfile(userId);
|
|
65
|
+
const analysisStartTime = Date.now();
|
|
66
|
+
let validationPrompt;
|
|
67
|
+
let validationPrefKeys;
|
|
68
|
+
if (existingProfile && CONFIG.userProfileValidationEnabled) {
|
|
69
|
+
const profileData = JSON.parse(existingProfile.profileData);
|
|
70
|
+
const { data: decayed } = userProfileManager.decayInMemory(profileData);
|
|
71
|
+
for (const arr of [decayed.preferences, decayed.patterns, decayed.workflows]) {
|
|
72
|
+
for (const item of arr) {
|
|
73
|
+
if (item.pendingValidation && (item.lastSeen || 0) < analysisStartTime) {
|
|
74
|
+
item.pendingValidation = false;
|
|
75
|
+
item.alpha = (item.alpha || 1) + 1;
|
|
76
|
+
userProfileManager.syncConfidence(item);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
existingProfile = { ...existingProfile, profileData: JSON.stringify(decayed) };
|
|
81
|
+
const topPrefs = sortProfileItems(decayed.preferences, "confidence").slice(0, 5);
|
|
82
|
+
const topPats = sortProfileItems(decayed.patterns, "frequency").slice(0, 3);
|
|
83
|
+
const hasValidator = topPrefs.length >= 5;
|
|
84
|
+
if (hasValidator) {
|
|
85
|
+
const allValidated = [...topPrefs, ...topPats];
|
|
86
|
+
validationPrefKeys = allValidated.map((p) => `${p.category || "_"}||${(p.description || "").substring(0, 30)}`);
|
|
87
|
+
log("user-profile-learning: validation enabled", {
|
|
88
|
+
topPrefs: topPrefs.map((p) => `[${p.category}] ${(p.description || "").substring(0, 30)}`),
|
|
89
|
+
topPats: topPats.map((p) => `[${p.category}] ${(p.description || "").substring(0, 30)}`),
|
|
90
|
+
});
|
|
91
|
+
validationPrompt = `## Task 3: Validate Existing Profile Entries
|
|
92
|
+
|
|
93
|
+
CRITICAL: Complete Tasks 1-2 (new observations) FIRST. This task is separate — only check whether the entries below still match recent behavior. Do NOT let these descriptions influence your new observations.
|
|
94
|
+
|
|
95
|
+
${allValidated.map((p, i) => `${i}. [${p.category || "_"}] ${(p.description || "").substring(0, 30)} (conf: ${Math.round((p.confidence || 0) * 100) / 100})`).join("\n")}
|
|
96
|
+
|
|
97
|
+
For each entry above, judge whether recent prompts confirm or contradict it. Output:
|
|
98
|
+
{"validations": [{"index": 0, "verdict": "confirmed|contradicted|no_evidence|inaccurate|oversimplified", "reason": "one sentence"}]}
|
|
99
|
+
|
|
100
|
+
Rules:
|
|
101
|
+
- confirmed: recent prompts show clear evidence
|
|
102
|
+
- contradicted: recent prompts show the user has changed
|
|
103
|
+
- inaccurate: the description is directionally wrong (opposite behavior seen)
|
|
104
|
+
- oversimplified: the description is too vague, missing important nuance
|
|
105
|
+
- no_evidence: recent prompts don't address this topic
|
|
106
|
+
- Only mark contradicted if there is explicit evidence the user's behavior has changed`;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
log("user-profile-learning: validation skipped", {
|
|
110
|
+
prefCount: topPrefs.length,
|
|
111
|
+
reason: "needs ≥ 5 preferences",
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else if (existingProfile) {
|
|
116
|
+
const profileData = JSON.parse(existingProfile.profileData);
|
|
117
|
+
const { data: decayed } = userProfileManager.decayInMemory(profileData);
|
|
118
|
+
for (const arr of [decayed.preferences, decayed.patterns, decayed.workflows]) {
|
|
119
|
+
for (const item of arr) {
|
|
120
|
+
if (item.pendingValidation && (item.lastSeen || 0) < analysisStartTime) {
|
|
121
|
+
item.pendingValidation = false;
|
|
122
|
+
item.alpha = (item.alpha || 1) + 1;
|
|
123
|
+
userProfileManager.syncConfidence(item);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
existingProfile = { ...existingProfile, profileData: JSON.stringify(decayed) };
|
|
128
|
+
}
|
|
129
|
+
const context = buildUserAnalysisContext(prompts, existingProfile, validationPrompt);
|
|
130
|
+
const analysisResult = await analyzeUserProfile(context, existingProfile);
|
|
131
|
+
log("user-profile-learning: analyze done", { hasResult: !!analysisResult });
|
|
132
|
+
if (!analysisResult) {
|
|
133
|
+
await userPromptManager.markMultipleAsUserLearningCaptured(prompts.map((p) => p.id));
|
|
134
|
+
if (prompts.length >= 10 && existingProfile) {
|
|
135
|
+
buildLearningPaths(prompts, existingProfile.id).catch(() => { });
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const { raw: llmResult, merged: initialMerged } = analysisResult;
|
|
140
|
+
let cleanupPreviousPromptCount = 0;
|
|
141
|
+
if (existingProfile) {
|
|
142
|
+
let updatedProfileData = initialMerged;
|
|
143
|
+
const MAX_RETRIES = 2;
|
|
144
|
+
let retries = 0;
|
|
145
|
+
let success = false;
|
|
146
|
+
while (!success && retries <= MAX_RETRIES) {
|
|
147
|
+
if (retries > 0) {
|
|
148
|
+
existingProfile = await userProfileManager.getActiveProfile(userId);
|
|
149
|
+
if (!existingProfile)
|
|
150
|
+
break;
|
|
151
|
+
const retryProfileData = JSON.parse(existingProfile.profileData);
|
|
152
|
+
const { data: decayedRetry } = userProfileManager.decayInMemory(retryProfileData);
|
|
153
|
+
existingProfile = { ...existingProfile, profileData: JSON.stringify(decayedRetry) };
|
|
154
|
+
updatedProfileData = await userProfileManager.mergeProfileData(decayedRetry, llmResult, undefined, existingProfile.id);
|
|
155
|
+
log("user-profile-learning: retry merge", {
|
|
156
|
+
retry: retries,
|
|
157
|
+
profileId: existingProfile.id,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
cleanupPreviousPromptCount = existingProfile.totalPromptsAnalyzed;
|
|
161
|
+
let changeSummary = generateChangeSummary(JSON.parse(existingProfile.profileData), updatedProfileData);
|
|
162
|
+
const validationSummary = await applyValidations(updatedProfileData, llmResult, existingProfile.id, validationPrefKeys);
|
|
163
|
+
if (validationSummary) {
|
|
164
|
+
changeSummary = changeSummary + "; " + validationSummary;
|
|
165
|
+
}
|
|
166
|
+
success = await userProfileManager.updateProfile(existingProfile.id, updatedProfileData, prompts.length, changeSummary);
|
|
167
|
+
if (!success) {
|
|
168
|
+
log("User profile update conflict, retrying", {
|
|
169
|
+
profileId: existingProfile.id,
|
|
170
|
+
userId,
|
|
171
|
+
retry: retries,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
retries++;
|
|
175
|
+
}
|
|
176
|
+
if (!success) {
|
|
177
|
+
// Mark the batch anyway so the same prompts are not retried forever
|
|
178
|
+
// (token burn + repeated toasts on every subsequent idle).
|
|
179
|
+
log("User profile update conflict: exhausted retries", {
|
|
180
|
+
profileId: existingProfile?.id,
|
|
181
|
+
userId,
|
|
182
|
+
});
|
|
183
|
+
await userPromptManager.markMultipleAsUserLearningCaptured(prompts.map((p) => p.id));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
await userPromptManager.markMultipleAsUserLearningCaptured(prompts.map((p) => p.id));
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
await userProfileManager.createProfile(userId, tags.user.displayName || "Unknown", tags.user.userName || "unknown", tags.user.userEmail || "unknown", llmResult, prompts.length);
|
|
190
|
+
await userPromptManager.markMultipleAsUserLearningCaptured(prompts.map((p) => p.id));
|
|
191
|
+
}
|
|
192
|
+
if (shouldRunAutomaticProfileCleanup(cleanupPreviousPromptCount, prompts.length)) {
|
|
193
|
+
await runAutomaticProfileCleanup(userId);
|
|
194
|
+
}
|
|
195
|
+
if (CONFIG.showUserProfileToasts) {
|
|
196
|
+
await ctx.client?.tui
|
|
197
|
+
.showToast({
|
|
198
|
+
body: {
|
|
199
|
+
title: "User Profile Updated",
|
|
200
|
+
message: `Analyzed ${prompts.length} of ${count} pending prompts and updated your profile`,
|
|
201
|
+
variant: "success",
|
|
202
|
+
duration: 3000,
|
|
203
|
+
},
|
|
204
|
+
})
|
|
205
|
+
.catch(() => { });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
// Guard against corrupt stored profileData (JSON.parse throws) and any other
|
|
210
|
+
// fault: this runs fire-and-forget from the idle timer, so an uncaught rejection
|
|
211
|
+
// would surface as an unhandled promise rejection. The caller (src/index.ts idle
|
|
212
|
+
// timer) already wraps this call in its own try/catch, and issue #265 requires
|
|
213
|
+
// provider errors to propagate instead of being masked, so rethrow after logging.
|
|
214
|
+
log("user-profile-learning: aborted", { error: String(error) });
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
isLearningRunning = false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function generateChangeSummary(oldProfile, newProfile) {
|
|
222
|
+
const changes = [];
|
|
223
|
+
const prefDiff = newProfile.preferences.length - oldProfile.preferences.length;
|
|
224
|
+
if (prefDiff > 0)
|
|
225
|
+
changes.push(`+${prefDiff} preferences`);
|
|
226
|
+
const patternDiff = newProfile.patterns.length - oldProfile.patterns.length;
|
|
227
|
+
if (patternDiff > 0)
|
|
228
|
+
changes.push(`+${patternDiff} patterns`);
|
|
229
|
+
const workflowDiff = newProfile.workflows.length - oldProfile.workflows.length;
|
|
230
|
+
if (workflowDiff > 0)
|
|
231
|
+
changes.push(`+${workflowDiff} workflows`);
|
|
232
|
+
return changes.length > 0 ? changes.join(", ") : "Profile refinement";
|
|
233
|
+
}
|
|
234
|
+
function buildCategorySummary(profileData) {
|
|
235
|
+
const prefCats = [...new Set(profileData.preferences.map((p) => p.category))];
|
|
236
|
+
const patCats = [...new Set(profileData.patterns.map((p) => p.category))];
|
|
237
|
+
const catParts = [];
|
|
238
|
+
if (prefCats.length > 0) {
|
|
239
|
+
const catCounts = prefCats
|
|
240
|
+
.map((cat) => {
|
|
241
|
+
const cnt = profileData.preferences.filter((p) => p.category === cat).length;
|
|
242
|
+
return `${cat} (${cnt})`;
|
|
243
|
+
})
|
|
244
|
+
.join(", ");
|
|
245
|
+
catParts.push(`Preference categories: ${catCounts}`);
|
|
246
|
+
}
|
|
247
|
+
if (patCats.length > 0) {
|
|
248
|
+
const catCounts = patCats
|
|
249
|
+
.map((cat) => {
|
|
250
|
+
const cnt = profileData.patterns.filter((p) => p.category === cat).length;
|
|
251
|
+
return `${cat} (${cnt})`;
|
|
252
|
+
})
|
|
253
|
+
.join(", ");
|
|
254
|
+
catParts.push(`Pattern categories: ${catCounts}`);
|
|
255
|
+
}
|
|
256
|
+
const catSection = catParts.length > 0
|
|
257
|
+
? `## Existing Categories\nUse these exact category names when your observation fits:\n\n${catParts.join("\n")}\n`
|
|
258
|
+
: "";
|
|
259
|
+
const prefCount = profileData.preferences.length;
|
|
260
|
+
const patCount = profileData.patterns.length;
|
|
261
|
+
const wfCount = profileData.workflows.length;
|
|
262
|
+
const wfParts = [];
|
|
263
|
+
if (wfCount > 0) {
|
|
264
|
+
wfParts.push("## Existing Workflows\nFor reference — only report a workflow when recent prompts show a genuinely NEW sequence, NOT a minor variant of an existing one:");
|
|
265
|
+
profileData.workflows.forEach((wf, i) => {
|
|
266
|
+
const steps = wf.steps?.length
|
|
267
|
+
? ` (freq ${wf.frequency || 1}x: ${wf.steps.join(" → ")})`
|
|
268
|
+
: "";
|
|
269
|
+
wfParts.push(`${i + 1}. ${wf.description}${steps}`);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
const countSection = `## Profile Size\nPreferences: ${prefCount} | Patterns: ${patCount} | Workflows: ${wfCount}\n
|
|
273
|
+
New observations are matched via embedding cosine similarity — write descriptions in your own words; do not reuse existing wording.`;
|
|
274
|
+
return [catSection, ...wfParts, countSection].filter(Boolean).join("\n");
|
|
275
|
+
}
|
|
276
|
+
function buildUserAnalysisContext(prompts, existingProfile, validationPrompt) {
|
|
277
|
+
const base = `# User Profile Analysis
|
|
278
|
+
|
|
279
|
+
Analyze ${prompts.length} user prompts to ${existingProfile ? "update" : "create"} the user profile.
|
|
280
|
+
${existingProfile ? `The merge system will automatically connect your observations to existing profile entries — you only need to describe what you see in these recent prompts.` : `Create a new user profile from scratch based on the prompts below.`}
|
|
281
|
+
|
|
282
|
+
${existingProfile ? buildCategorySummary(JSON.parse(existingProfile.profileData)) : ""}
|
|
283
|
+
## Recent Prompts
|
|
284
|
+
|
|
285
|
+
${prompts.map((p, i) => `${i + 1}. ${p.content}`).join("\n\n")}
|
|
286
|
+
|
|
287
|
+
## Analysis Guidelines
|
|
288
|
+
|
|
289
|
+
Identify and ${existingProfile ? "report" : "create"}:
|
|
290
|
+
|
|
291
|
+
1. **Preferences**
|
|
292
|
+
- Code style, communication style, tool preferences
|
|
293
|
+
- Assign confidence 0.3-0.5 based on evidence strength in these recent prompts
|
|
294
|
+
- Include 1-3 example prompts as evidence
|
|
295
|
+
- **Revealed preferences**: when the user chooses one approach over alternatives (e.g. picks simpler solution, skips certain steps), capture the choice as a lower-confidence preference (0.3-0.5). What the user does NOT do is also a signal.
|
|
296
|
+
|
|
297
|
+
2. **Patterns**
|
|
298
|
+
- Recurring topics, problem domains, technical interests seen in these prompts
|
|
299
|
+
- Track frequency of occurrence
|
|
300
|
+
|
|
301
|
+
3. **Workflows**
|
|
302
|
+
- Distinct, named step sequences the user follows repeatedly
|
|
303
|
+
- Each workflow should represent a DIFFERENT activity (different purpose, different steps)
|
|
304
|
+
- Break down into 3-6 concrete, observable steps, NOT abstract phases
|
|
305
|
+
- Do NOT repeat the same workflow every cycle — only output when you observe a NEW recurring sequence
|
|
306
|
+
- Examples of distinct workflows: "debugging workflow", "code review workflow", "learning workflow", "refactoring workflow"
|
|
307
|
+
|
|
308
|
+
CRITICAL: Only output observations grounded in the RECENT PROMPTS above. Write descriptions in your own words — the system matches by embedding similarity, not exact wording. Do NOT output entries that lack evidence in recent prompts. Put the core semantics at the beginning of each description, keeping descriptions concise and specific (under 120 characters). Do NOT extract one-time debugging tasks, environment setup issues, or specific error investigations as preferences — these are transient events, not behavioral patterns.
|
|
309
|
+
|
|
310
|
+
## Few-Shot Examples
|
|
311
|
+
|
|
312
|
+
❌ Do NOT extract as preference:
|
|
313
|
+
- "User is debugging a NullPointerException in auth service" (one-time debugging task)
|
|
314
|
+
- "User installed Redis for the first time" (one-time setup event)
|
|
315
|
+
- "User ran npm audit fix" (routine maintenance, not a behavioral pattern)
|
|
316
|
+
|
|
317
|
+
✅ DO extract as preference:
|
|
318
|
+
- "User prefers functional programming style over OOP"
|
|
319
|
+
- "User consistently writes tests before implementation"
|
|
320
|
+
- "User asks for explanations before accepting code changes"
|
|
321
|
+
|
|
322
|
+
✅ DO extract as workflow (distinct, non-overlapping):
|
|
323
|
+
- Debugging workflow: "reproduce the error → check logs → grep source code → trace call chain → propose fix → verify fix"
|
|
324
|
+
- Code review workflow: "read the diff → check edge cases → verify consistency with existing patterns → report issues → suggest alternatives"
|
|
325
|
+
- Learning workflow: "ask for explanation → request examples → test understanding with a small task → apply to real problem"
|
|
326
|
+
|
|
327
|
+
❌ Do NOT extract as workflow:
|
|
328
|
+
- "User analyzes problems and verifies solutions" (too abstract — not a concrete step sequence)
|
|
329
|
+
- "User writes code and tests it" (too generic — covers everything)`;
|
|
330
|
+
const maxBytes = CONFIG.userProfileMaxContextBytes ?? 32768;
|
|
331
|
+
const truncate = (s) => s.length > maxBytes
|
|
332
|
+
? s.substring(0, maxBytes) + "\n[... context truncated to userProfileMaxContextBytes ...]"
|
|
333
|
+
: s;
|
|
334
|
+
if (validationPrompt) {
|
|
335
|
+
return truncate(base + "\n\n" + validationPrompt);
|
|
336
|
+
}
|
|
337
|
+
return truncate(base);
|
|
338
|
+
}
|
|
339
|
+
import { createUserProfileAnalysisSchema, USER_PROFILE_LLM_CONFIDENCE_MAX, } from "../core/extraction.js";
|
|
340
|
+
export { createUserProfileAnalysisSchema, USER_PROFILE_LLM_CONFIDENCE_MAX };
|
|
341
|
+
export function createUserProfileToolSchema(existingProfile) {
|
|
342
|
+
return {
|
|
343
|
+
type: "function",
|
|
344
|
+
function: {
|
|
345
|
+
name: "update_user_profile",
|
|
346
|
+
description: existingProfile
|
|
347
|
+
? "Update existing user profile with new insights"
|
|
348
|
+
: "Create new user profile",
|
|
349
|
+
parameters: {
|
|
350
|
+
type: "object",
|
|
351
|
+
properties: {
|
|
352
|
+
preferences: {
|
|
353
|
+
type: "array",
|
|
354
|
+
items: {
|
|
355
|
+
type: "object",
|
|
356
|
+
properties: {
|
|
357
|
+
category: { type: "string" },
|
|
358
|
+
description: { type: "string" },
|
|
359
|
+
confidence: {
|
|
360
|
+
type: "number",
|
|
361
|
+
minimum: 0,
|
|
362
|
+
maximum: USER_PROFILE_LLM_CONFIDENCE_MAX,
|
|
363
|
+
},
|
|
364
|
+
evidence: { type: "array", items: { type: "string" }, maxItems: 3 },
|
|
365
|
+
},
|
|
366
|
+
required: ["category", "description", "confidence", "evidence"],
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
patterns: {
|
|
370
|
+
type: "array",
|
|
371
|
+
items: {
|
|
372
|
+
type: "object",
|
|
373
|
+
properties: {
|
|
374
|
+
category: { type: "string" },
|
|
375
|
+
description: { type: "string" },
|
|
376
|
+
},
|
|
377
|
+
required: ["category", "description"],
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
workflows: {
|
|
381
|
+
type: "array",
|
|
382
|
+
items: {
|
|
383
|
+
type: "object",
|
|
384
|
+
properties: {
|
|
385
|
+
description: { type: "string" },
|
|
386
|
+
steps: { type: "array", items: { type: "string" } },
|
|
387
|
+
},
|
|
388
|
+
required: ["description", "steps"],
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
validations: {
|
|
392
|
+
type: "array",
|
|
393
|
+
items: {
|
|
394
|
+
type: "object",
|
|
395
|
+
properties: {
|
|
396
|
+
index: { type: "number" },
|
|
397
|
+
verdict: {
|
|
398
|
+
type: "string",
|
|
399
|
+
enum: [
|
|
400
|
+
"confirmed",
|
|
401
|
+
"contradicted",
|
|
402
|
+
"no_evidence",
|
|
403
|
+
"inaccurate",
|
|
404
|
+
"oversimplified",
|
|
405
|
+
],
|
|
406
|
+
},
|
|
407
|
+
reason: { type: "string" },
|
|
408
|
+
},
|
|
409
|
+
required: ["index", "verdict", "reason"],
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
required: ["preferences", "patterns", "workflows"],
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
async function applyValidations(profileData, llmResult, profileId, prefKeys) {
|
|
419
|
+
const validations = llmResult.validations;
|
|
420
|
+
if (!validations?.length || !prefKeys?.length)
|
|
421
|
+
return null;
|
|
422
|
+
const allItems = [...profileData.preferences, ...profileData.patterns];
|
|
423
|
+
const results = [];
|
|
424
|
+
let confirmed = 0;
|
|
425
|
+
let contradicted = 0;
|
|
426
|
+
let inaccurate = 0;
|
|
427
|
+
let oversimplified = 0;
|
|
428
|
+
for (const v of validations) {
|
|
429
|
+
const key = prefKeys[v.index];
|
|
430
|
+
if (!key)
|
|
431
|
+
continue;
|
|
432
|
+
const item = allItems.find((i) => `${i.category || "_"}||${(i.description || "").substring(0, 30)}` === key);
|
|
433
|
+
if (!item) {
|
|
434
|
+
log("user-profile-learning: validation match failed", { index: v.index, key });
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
if (v.verdict === "confirmed") {
|
|
438
|
+
item.alpha = (item.alpha || 1) + 0.5;
|
|
439
|
+
userProfileManager.syncConfidence(item);
|
|
440
|
+
confirmed++;
|
|
441
|
+
results.push(`confirmed [${v.index}] ${v.reason}`);
|
|
442
|
+
}
|
|
443
|
+
else if (v.verdict === "contradicted") {
|
|
444
|
+
const oldAlpha = item.alpha || 1;
|
|
445
|
+
item.alpha = oldAlpha * 0.75;
|
|
446
|
+
item.beta = (item.beta || 1) + oldAlpha * 0.25;
|
|
447
|
+
userProfileManager.syncConfidence(item);
|
|
448
|
+
contradicted++;
|
|
449
|
+
results.push(`contradicted [${v.index}] ${v.reason}`);
|
|
450
|
+
}
|
|
451
|
+
else if (v.verdict === "inaccurate") {
|
|
452
|
+
const oldAlpha = item.alpha || 1;
|
|
453
|
+
item.alpha = oldAlpha * 0.6;
|
|
454
|
+
item.beta = (item.beta || 1) + oldAlpha * 0.4;
|
|
455
|
+
userProfileManager.syncConfidence(item);
|
|
456
|
+
inaccurate++;
|
|
457
|
+
results.push(`inaccurate [${v.index}] ${v.reason}`);
|
|
458
|
+
}
|
|
459
|
+
else if (v.verdict === "oversimplified") {
|
|
460
|
+
const oldAlpha = item.alpha || 1;
|
|
461
|
+
item.alpha = oldAlpha * 0.85;
|
|
462
|
+
item.beta = (item.beta || 1) + oldAlpha * 0.15;
|
|
463
|
+
userProfileManager.syncConfidence(item);
|
|
464
|
+
oversimplified++;
|
|
465
|
+
results.push(`oversimplified [${v.index}] ${v.reason}`);
|
|
466
|
+
const evidence = item.evidence;
|
|
467
|
+
if (Array.isArray(evidence) && evidence.length >= 3) {
|
|
468
|
+
const itemType = profileData.preferences.includes(item) ? "preference" : "pattern";
|
|
469
|
+
// Await so the in-place description/centroid mutation completes before the
|
|
470
|
+
// caller serializes updatedProfileData — otherwise the evolved description is
|
|
471
|
+
// included or lost nondeterministically. Failures stay non-fatal.
|
|
472
|
+
try {
|
|
473
|
+
await userProfileManager.evolveAndUpdate(item, itemType, profileId);
|
|
474
|
+
}
|
|
475
|
+
catch {
|
|
476
|
+
// description evolution is best-effort
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
results.push(`no_evidence [${v.index}] ${v.reason}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
if (results.length > 0) {
|
|
485
|
+
log("user-profile-learning: validation results", { validated: results });
|
|
486
|
+
}
|
|
487
|
+
if (confirmed === 0 && contradicted === 0 && inaccurate === 0 && oversimplified === 0)
|
|
488
|
+
return null;
|
|
489
|
+
return `validated: ${confirmed} confirmed, ${contradicted} contradicted, ${inaccurate} inaccurate, ${oversimplified} oversimplified`;
|
|
490
|
+
}
|
|
491
|
+
async function analyzeUserProfile(context, existingProfile) {
|
|
492
|
+
log("user-profile-learning: analyze called", { hasProfile: !!existingProfile });
|
|
493
|
+
let opencodeProviderError;
|
|
494
|
+
if (CONFIG.opencodeProvider && CONFIG.opencodeModel) {
|
|
495
|
+
log("user-profile-learning: trying opencode provider");
|
|
496
|
+
try {
|
|
497
|
+
const { generateStructuredOutput } = await loadOpencodeProvider();
|
|
498
|
+
const { getOpenCodeClient } = await import("./ai/profile-llm-client.js");
|
|
499
|
+
log("user-profile-learning: opencode provider diag", {
|
|
500
|
+
provider: CONFIG.opencodeProvider,
|
|
501
|
+
model: CONFIG.opencodeModel,
|
|
502
|
+
});
|
|
503
|
+
const v2Client = await getOpenCodeClient();
|
|
504
|
+
const systemPrompt = `You are a user behavior analyst for a coding assistant.
|
|
505
|
+
|
|
506
|
+
Your task is to analyze user prompts and ${existingProfile ? "update" : "create"} a comprehensive user profile.
|
|
507
|
+
|
|
508
|
+
CRITICAL: Detect the language used by the user in their prompts. You MUST output all descriptions, categories, and text in the SAME language as the user's prompts.
|
|
509
|
+
|
|
510
|
+
CRITICAL: All JSON string values MUST escape double quotes with backslash. Do NOT use unescaped quotation marks inside string values.
|
|
511
|
+
|
|
512
|
+
Use the update_user_profile tool to save the ${existingProfile ? "updated" : "new"} profile.`;
|
|
513
|
+
const { z } = await import("zod");
|
|
514
|
+
const schema = createUserProfileAnalysisSchema(z);
|
|
515
|
+
log("user-profile-learning: calling LLM", { contextLen: context.length });
|
|
516
|
+
const result = await Promise.race([
|
|
517
|
+
generateStructuredOutput({
|
|
518
|
+
client: v2Client,
|
|
519
|
+
providerID: CONFIG.opencodeProvider,
|
|
520
|
+
modelID: CONFIG.opencodeModel,
|
|
521
|
+
systemPrompt,
|
|
522
|
+
userPrompt: context,
|
|
523
|
+
schema,
|
|
524
|
+
}),
|
|
525
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("user-profile-learning: timeout")), 120000)),
|
|
526
|
+
]);
|
|
527
|
+
log("user-profile-learning: LLM returned", {
|
|
528
|
+
prefCount: result.preferences?.length,
|
|
529
|
+
patCount: result.patterns?.length,
|
|
530
|
+
wfCount: result.workflows?.length,
|
|
531
|
+
});
|
|
532
|
+
const rawData = result;
|
|
533
|
+
if (existingProfile) {
|
|
534
|
+
const existingData = JSON.parse(existingProfile.profileData);
|
|
535
|
+
const merged = await userProfileManager.mergeProfileData(existingData, rawData, undefined, existingProfile.id);
|
|
536
|
+
return { raw: rawData, merged };
|
|
537
|
+
}
|
|
538
|
+
return { raw: rawData, merged: null };
|
|
539
|
+
}
|
|
540
|
+
catch (e) {
|
|
541
|
+
opencodeProviderError = e;
|
|
542
|
+
log("user-profile-learning: opencode provider failed, falling back to external API", {
|
|
543
|
+
error: String(e),
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (!CONFIG.memoryModel || !CONFIG.memoryApiUrl) {
|
|
548
|
+
if (opencodeProviderError) {
|
|
549
|
+
throw opencodeProviderError;
|
|
550
|
+
}
|
|
551
|
+
log("User Profile Config Check Failed:", {
|
|
552
|
+
memoryModel: CONFIG.memoryModel,
|
|
553
|
+
memoryApiUrl: CONFIG.memoryApiUrl,
|
|
554
|
+
});
|
|
555
|
+
throw new Error("External API not configured for user memory learning");
|
|
556
|
+
}
|
|
557
|
+
const { AIProviderFactory } = await import("./ai/ai-provider-factory.js");
|
|
558
|
+
const { buildMemoryProviderConfig } = await import("./ai/provider-config.js");
|
|
559
|
+
const providerConfig = buildMemoryProviderConfig(CONFIG);
|
|
560
|
+
const provider = AIProviderFactory.createProvider(CONFIG.memoryProvider, providerConfig);
|
|
561
|
+
const systemPrompt = `You are a user behavior analyst for a coding assistant.
|
|
562
|
+
|
|
563
|
+
Your task is to analyze user prompts and ${existingProfile ? "update" : "create"} a comprehensive user profile.
|
|
564
|
+
|
|
565
|
+
CRITICAL: Detect the language used by the user in their prompts. You MUST output all descriptions, categories, and text in the SAME language as the user's prompts.
|
|
566
|
+
|
|
567
|
+
CRITICAL: All JSON string values MUST escape double quotes with backslash. Do NOT use unescaped quotation marks inside string values.
|
|
568
|
+
|
|
569
|
+
Use the update_user_profile tool to save the ${existingProfile ? "updated" : "new"} profile.`;
|
|
570
|
+
const toolSchema = createUserProfileToolSchema(Boolean(existingProfile));
|
|
571
|
+
const result = await provider.executeToolCall(systemPrompt, context, toolSchema, `user-profile-${Date.now()}`);
|
|
572
|
+
if (!result.success || !result.data) {
|
|
573
|
+
throw new Error(result.error || "Failed to analyze user profile");
|
|
574
|
+
}
|
|
575
|
+
const rawData = result.data;
|
|
576
|
+
if (existingProfile) {
|
|
577
|
+
const existingData = JSON.parse(existingProfile.profileData);
|
|
578
|
+
const merged = await userProfileManager.mergeProfileData(existingData, rawData, undefined, existingProfile.id);
|
|
579
|
+
return { raw: rawData, merged };
|
|
580
|
+
}
|
|
581
|
+
return { raw: rawData, merged: null };
|
|
582
|
+
}
|
|
583
|
+
async function buildLearningPaths(prompts, profileId) {
|
|
584
|
+
const promptTexts = prompts.map((p, i) => `${i + 1}. ${p.content}`).join("\n");
|
|
585
|
+
const systemPrompt = "You are a learning path analyst. Identify causal chains across a user's prompts. Output valid JSON.";
|
|
586
|
+
const userPrompt = `Analyze these user prompts for causal learning chains:
|
|
587
|
+
|
|
588
|
+
${promptTexts}
|
|
589
|
+
|
|
590
|
+
Identify sequences where earlier prompts led to later ones (e.g. "learned X → applied X → refined X"). Return JSON:
|
|
591
|
+
{ "paths": [{ "topic": "string", "chain": ["step1", "step2", "step3"], "description": "one sentence summary" }] }
|
|
592
|
+
If no clear chains, return { "paths": [] }.`;
|
|
593
|
+
let result = null;
|
|
594
|
+
if (CONFIG.opencodeProvider && CONFIG.opencodeModel) {
|
|
595
|
+
try {
|
|
596
|
+
const { z } = await import("zod");
|
|
597
|
+
const { generateStructuredOutput } = await loadOpencodeProvider();
|
|
598
|
+
const { getOpenCodeClient } = await import("./ai/profile-llm-client.js");
|
|
599
|
+
let v2Client;
|
|
600
|
+
try {
|
|
601
|
+
v2Client = await getOpenCodeClient();
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
// provider not available, skip learning paths
|
|
605
|
+
}
|
|
606
|
+
if (v2Client) {
|
|
607
|
+
result = (await Promise.race([
|
|
608
|
+
generateStructuredOutput({
|
|
609
|
+
client: v2Client,
|
|
610
|
+
providerID: CONFIG.opencodeProvider,
|
|
611
|
+
modelID: CONFIG.opencodeModel,
|
|
612
|
+
systemPrompt,
|
|
613
|
+
userPrompt,
|
|
614
|
+
schema: z.object({
|
|
615
|
+
paths: z.array(z.object({
|
|
616
|
+
topic: z.string(),
|
|
617
|
+
chain: z.array(z.string()),
|
|
618
|
+
description: z.string(),
|
|
619
|
+
})),
|
|
620
|
+
}),
|
|
621
|
+
}),
|
|
622
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error("learning paths: opencode timeout")), 120000)),
|
|
623
|
+
]));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch (e) {
|
|
627
|
+
log("learning paths: native provider failed", { error: String(e) });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (!result && CONFIG.memoryModel && CONFIG.memoryApiUrl) {
|
|
631
|
+
try {
|
|
632
|
+
const response = await fetch(`${CONFIG.memoryApiUrl}/chat/completions`, {
|
|
633
|
+
method: "POST",
|
|
634
|
+
headers: {
|
|
635
|
+
"Content-Type": "application/json",
|
|
636
|
+
Authorization: `Bearer ${CONFIG.memoryApiKey || ""}`,
|
|
637
|
+
},
|
|
638
|
+
body: JSON.stringify({
|
|
639
|
+
model: CONFIG.memoryModel,
|
|
640
|
+
messages: [
|
|
641
|
+
{ role: "system", content: systemPrompt },
|
|
642
|
+
{ role: "user", content: userPrompt },
|
|
643
|
+
],
|
|
644
|
+
temperature: 0.3,
|
|
645
|
+
response_format: { type: "json_object" },
|
|
646
|
+
}),
|
|
647
|
+
signal: AbortSignal.timeout(60000),
|
|
648
|
+
});
|
|
649
|
+
if (response.ok) {
|
|
650
|
+
const data = (await response.json());
|
|
651
|
+
const content = data.choices?.[0]?.message?.content;
|
|
652
|
+
if (content)
|
|
653
|
+
result = JSON.parse(content);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
catch (e) {
|
|
657
|
+
log("learning paths: external API failed", { error: String(e) });
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (!result?.paths?.length)
|
|
661
|
+
return;
|
|
662
|
+
log("learning paths: detected", {
|
|
663
|
+
profileId,
|
|
664
|
+
pathCount: result.paths.length,
|
|
665
|
+
topics: result.paths.map((p) => p.topic).join(", "),
|
|
666
|
+
});
|
|
667
|
+
const profile = await userProfileManager.getProfileById(profileId);
|
|
668
|
+
if (!profile)
|
|
669
|
+
return;
|
|
670
|
+
const data = JSON.parse(profile.profileData);
|
|
671
|
+
data.learning_paths = result.paths;
|
|
672
|
+
await userProfileManager.updateProfile(profileId, data, 0, `Updated learning paths: ${result.paths.map((p) => p.topic).join(", ")}`);
|
|
673
|
+
}
|