neoagent 2.4.4-beta.0 → 2.4.4-beta.11
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/.env.example +17 -0
- package/README.md +9 -3
- package/docs/capabilities.md +16 -7
- package/docs/configuration.md +1 -0
- package/docs/getting-started.md +6 -0
- package/docs/index.md +1 -0
- package/docs/security-boundaries.md +122 -0
- package/docs/supermemory-memory-review.md +852 -0
- package/flutter_app/lib/features/memory/views/retrieval_inspector_view.dart +128 -0
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +79 -15
- package/flutter_app/lib/main_app_shell.dart +22 -0
- package/flutter_app/lib/main_chat.dart +155 -8
- package/flutter_app/lib/main_controller.dart +74 -5
- package/flutter_app/lib/main_devices.dart +9 -3
- package/flutter_app/lib/main_models.dart +32 -0
- package/flutter_app/lib/main_operations.dart +13 -0
- package/flutter_app/lib/main_security.dart +967 -0
- package/flutter_app/lib/main_settings.dart +56 -0
- package/flutter_app/lib/src/backend_client.dart +60 -3
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +2 -0
- package/flutter_app/pubspec.lock +32 -0
- package/flutter_app/pubspec.yaml +1 -0
- package/lib/install_helpers.js +1 -0
- package/lib/manager.js +63 -1
- package/lib/schema_migrations.js +262 -0
- package/package.json +4 -2
- package/server/admin/admin.js +151 -0
- package/server/admin/index.html +55 -3
- package/server/db/database.js +3 -0
- package/server/http/routes.js +2 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +86 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +82243 -80308
- package/server/routes/account.js +2 -23
- package/server/routes/admin.js +18 -2
- package/server/routes/agents.js +5 -1
- package/server/routes/memory.js +41 -4
- package/server/routes/security.js +112 -0
- package/server/services/account/service_email_settings.js +167 -0
- package/server/services/ai/engine.js +269 -27
- package/server/services/ai/rate_limits.js +150 -0
- package/server/services/ai/systemPrompt.js +26 -3
- package/server/services/ai/tools.js +11 -8
- package/server/services/cli/shell_worker.js +135 -0
- package/server/services/cli/shell_worker_pool.js +125 -0
- package/server/services/integrations/google/gmail.js +7 -7
- package/server/services/manager.js +20 -1
- package/server/services/memory/consolidation.js +111 -0
- package/server/services/memory/embedding_index.js +175 -0
- package/server/services/memory/embeddings.js +22 -2
- package/server/services/memory/evaluation.js +187 -0
- package/server/services/memory/ingestion_chunking.js +191 -0
- package/server/services/memory/ingestion_documents.js +96 -26
- package/server/services/memory/intelligence.js +3 -1
- package/server/services/memory/manager.js +855 -43
- package/server/services/memory/policy.js +0 -40
- package/server/services/memory/retrieval_reasoning.js +191 -0
- package/server/services/runtime/manager.js +7 -0
- package/server/services/security/approval_gate_service.js +93 -0
- package/server/services/security/tool_categories.js +105 -0
- package/server/services/security/tool_policy_service.js +92 -0
- package/server/services/security/tool_security_hook.js +77 -0
- package/server/services/websocket.js +5 -1
|
@@ -141,12 +141,31 @@ async function getOpenAIEmbedding(text) {
|
|
|
141
141
|
* @returns {Float32Array|null}
|
|
142
142
|
*/
|
|
143
143
|
async function getEmbedding(text, provider) {
|
|
144
|
+
const result = await getEmbeddingWithMetadata(text, provider);
|
|
145
|
+
return result?.vector || null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function getEmbeddingWithMetadata(text, provider) {
|
|
144
149
|
if (!text || !text.trim()) return null;
|
|
145
150
|
if (provider === 'google' && process.env.GOOGLE_AI_KEY) {
|
|
146
151
|
const vec = await getGeminiEmbedding(text);
|
|
147
|
-
if (vec)
|
|
152
|
+
if (vec) {
|
|
153
|
+
return {
|
|
154
|
+
vector: vec,
|
|
155
|
+
provider: 'google',
|
|
156
|
+
model: GOOGLE_MODEL,
|
|
157
|
+
dimensions: vec.length,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
148
160
|
}
|
|
149
|
-
|
|
161
|
+
const vec = await getOpenAIEmbedding(text);
|
|
162
|
+
if (!vec) return null;
|
|
163
|
+
return {
|
|
164
|
+
vector: vec,
|
|
165
|
+
provider: 'openai',
|
|
166
|
+
model: OPENAI_MODEL,
|
|
167
|
+
dimensions: vec.length,
|
|
168
|
+
};
|
|
150
169
|
}
|
|
151
170
|
|
|
152
171
|
/**
|
|
@@ -203,6 +222,7 @@ function keywordSimilarity(query, text) {
|
|
|
203
222
|
|
|
204
223
|
module.exports = {
|
|
205
224
|
getEmbedding,
|
|
225
|
+
getEmbeddingWithMetadata,
|
|
206
226
|
cosineSimilarity,
|
|
207
227
|
serializeEmbedding,
|
|
208
228
|
deserializeEmbedding,
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { performance } = require('node:perf_hooks');
|
|
4
|
+
|
|
5
|
+
function mean(values) {
|
|
6
|
+
if (!values.length) return 0;
|
|
7
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function percentile(values, fraction) {
|
|
11
|
+
if (!values.length) return 0;
|
|
12
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
13
|
+
const index = Math.min(sorted.length - 1, Math.ceil(sorted.length * fraction) - 1);
|
|
14
|
+
return sorted[Math.max(0, index)];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function calculateRetrievalMetrics(results, relevantKeys, k) {
|
|
18
|
+
const returned = results.slice(0, k);
|
|
19
|
+
const relevance = returned.map((result) => (
|
|
20
|
+
relevantKeys.has(`memory:${result.id}`)
|
|
21
|
+
|| relevantKeys.has(`source:${result.sourceRef?.sourceId || ''}`)
|
|
22
|
+
? 1
|
|
23
|
+
: 0
|
|
24
|
+
));
|
|
25
|
+
const relevantRetrieved = relevance.reduce((sum, value) => sum + value, 0);
|
|
26
|
+
const totalRelevant = relevantKeys.size;
|
|
27
|
+
const precisionAtK = returned.length ? relevantRetrieved / returned.length : 0;
|
|
28
|
+
const recallAtK = totalRelevant ? relevantRetrieved / totalRelevant : 0;
|
|
29
|
+
const firstRelevant = relevance.indexOf(1);
|
|
30
|
+
const dcg = relevance.reduce(
|
|
31
|
+
(sum, value, index) => sum + value / Math.log2(index + 2),
|
|
32
|
+
0,
|
|
33
|
+
);
|
|
34
|
+
let idealDcg = 0;
|
|
35
|
+
for (let index = 0; index < Math.min(totalRelevant, returned.length); index += 1) {
|
|
36
|
+
idealDcg += 1 / Math.log2(index + 2);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
hitAtK: relevantRetrieved > 0 ? 1 : 0,
|
|
41
|
+
precisionAtK,
|
|
42
|
+
recallAtK,
|
|
43
|
+
f1AtK: precisionAtK + recallAtK
|
|
44
|
+
? (2 * precisionAtK * recallAtK) / (precisionAtK + recallAtK)
|
|
45
|
+
: 0,
|
|
46
|
+
mrr: firstRelevant >= 0 ? 1 / (firstRelevant + 1) : 0,
|
|
47
|
+
ndcg: idealDcg ? dcg / idealDcg : 0,
|
|
48
|
+
relevantRetrieved,
|
|
49
|
+
totalRelevant,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeQuestions(dataset) {
|
|
54
|
+
const questions = Array.isArray(dataset)
|
|
55
|
+
? dataset
|
|
56
|
+
: dataset?.questions || dataset?.queries;
|
|
57
|
+
if (!Array.isArray(questions) || !questions.length) {
|
|
58
|
+
throw new Error('Dataset must contain a non-empty questions or queries array.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return questions.map((question, index) => {
|
|
62
|
+
const query = String(question.query || question.question || '').trim();
|
|
63
|
+
const relevantMemoryIds = Array.isArray(question.relevantMemoryIds)
|
|
64
|
+
? question.relevantMemoryIds.map(String).filter(Boolean)
|
|
65
|
+
: [];
|
|
66
|
+
const relevantSourceIds = Array.isArray(question.relevantSourceIds)
|
|
67
|
+
? question.relevantSourceIds.map(String).filter(Boolean)
|
|
68
|
+
: Array.isArray(question.haystackSessionIds)
|
|
69
|
+
? question.haystackSessionIds.map(String).filter(Boolean)
|
|
70
|
+
: [];
|
|
71
|
+
if (!query) throw new Error(`Question ${index + 1} has no query.`);
|
|
72
|
+
if (!relevantMemoryIds.length && !relevantSourceIds.length) {
|
|
73
|
+
throw new Error(`Question ${index + 1} has no relevant memory or source IDs.`);
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
id: String(question.id || question.questionId || index + 1),
|
|
77
|
+
query,
|
|
78
|
+
category: String(question.category || question.questionType || 'uncategorized'),
|
|
79
|
+
relevantMemoryIds,
|
|
80
|
+
relevantSourceIds,
|
|
81
|
+
validAt: question.validAt || null,
|
|
82
|
+
knownAt: question.knownAt || null,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function aggregateEvaluations(evaluations, k) {
|
|
88
|
+
const metricNames = ['hitAtK', 'precisionAtK', 'recallAtK', 'f1AtK', 'mrr', 'ndcg'];
|
|
89
|
+
const retrieval = { k };
|
|
90
|
+
for (const metric of metricNames) {
|
|
91
|
+
retrieval[metric] = mean(evaluations.map((evaluation) => evaluation.metrics[metric]));
|
|
92
|
+
}
|
|
93
|
+
const latencies = evaluations.map((evaluation) => evaluation.latencyMs);
|
|
94
|
+
const contextTokens = evaluations.map((evaluation) => evaluation.contextTokensEstimate);
|
|
95
|
+
const candidateCounts = evaluations.map((evaluation) => evaluation.candidateCount);
|
|
96
|
+
return {
|
|
97
|
+
questions: evaluations.length,
|
|
98
|
+
retrieval,
|
|
99
|
+
latencyMs: {
|
|
100
|
+
mean: mean(latencies),
|
|
101
|
+
median: percentile(latencies, 0.5),
|
|
102
|
+
p95: percentile(latencies, 0.95),
|
|
103
|
+
p99: percentile(latencies, 0.99),
|
|
104
|
+
max: latencies.length ? Math.max(...latencies) : 0,
|
|
105
|
+
},
|
|
106
|
+
contextTokensEstimate: {
|
|
107
|
+
mean: mean(contextTokens),
|
|
108
|
+
p95: percentile(contextTokens, 0.95),
|
|
109
|
+
max: contextTokens.length ? Math.max(...contextTokens) : 0,
|
|
110
|
+
},
|
|
111
|
+
candidateCount: {
|
|
112
|
+
mean: mean(candidateCounts),
|
|
113
|
+
p95: percentile(candidateCounts, 0.95),
|
|
114
|
+
max: candidateCounts.length ? Math.max(...candidateCounts) : 0,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function runRetrievalBenchmark({
|
|
120
|
+
memoryManager,
|
|
121
|
+
userId,
|
|
122
|
+
agentId,
|
|
123
|
+
dataset,
|
|
124
|
+
k = 15,
|
|
125
|
+
}) {
|
|
126
|
+
const limit = Math.max(1, Math.min(Number(k) || 15, 50));
|
|
127
|
+
const questions = normalizeQuestions(dataset);
|
|
128
|
+
const evaluations = [];
|
|
129
|
+
|
|
130
|
+
for (const question of questions) {
|
|
131
|
+
const relevantKeys = new Set([
|
|
132
|
+
...question.relevantMemoryIds.map((id) => `memory:${id}`),
|
|
133
|
+
...question.relevantSourceIds.map((id) => `source:${id}`),
|
|
134
|
+
]);
|
|
135
|
+
const startedAt = performance.now();
|
|
136
|
+
const results = await memoryManager.recallMemory(userId, question.query, limit, {
|
|
137
|
+
agentId,
|
|
138
|
+
validAt: question.validAt,
|
|
139
|
+
knownAt: question.knownAt,
|
|
140
|
+
});
|
|
141
|
+
const latencyMs = performance.now() - startedAt;
|
|
142
|
+
const contextText = results
|
|
143
|
+
.map((result) => result.summary || result.content || '')
|
|
144
|
+
.join('\n');
|
|
145
|
+
|
|
146
|
+
evaluations.push({
|
|
147
|
+
id: question.id,
|
|
148
|
+
category: question.category,
|
|
149
|
+
query: question.query,
|
|
150
|
+
latencyMs,
|
|
151
|
+
contextTokensEstimate: Math.ceil(contextText.length / 4),
|
|
152
|
+
candidateCount: Math.max(
|
|
153
|
+
0,
|
|
154
|
+
...results.map((result) => Number(result.scoreBreakdown?.candidateCount) || 0),
|
|
155
|
+
),
|
|
156
|
+
retrievedMemoryIds: results.map((result) => result.id),
|
|
157
|
+
retrievedSourceIds: results
|
|
158
|
+
.map((result) => result.sourceRef?.sourceId)
|
|
159
|
+
.filter(Boolean),
|
|
160
|
+
metrics: calculateRetrievalMetrics(results, relevantKeys, limit),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const categories = {};
|
|
165
|
+
for (const category of new Set(evaluations.map((evaluation) => evaluation.category))) {
|
|
166
|
+
categories[category] = aggregateEvaluations(
|
|
167
|
+
evaluations.filter((evaluation) => evaluation.category === category),
|
|
168
|
+
limit,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
generatedAt: new Date().toISOString(),
|
|
174
|
+
dataset: String(dataset?.name || 'unnamed'),
|
|
175
|
+
userId,
|
|
176
|
+
agentId,
|
|
177
|
+
...aggregateEvaluations(evaluations, limit),
|
|
178
|
+
categories,
|
|
179
|
+
evaluations,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
module.exports = {
|
|
184
|
+
calculateRetrievalMetrics,
|
|
185
|
+
normalizeQuestions,
|
|
186
|
+
runRetrievalBenchmark,
|
|
187
|
+
};
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
|
|
5
|
+
const TARGET_CHARS = 1400;
|
|
6
|
+
const MAX_CHARS = 2200;
|
|
7
|
+
|
|
8
|
+
function contentHash(content) {
|
|
9
|
+
return crypto.createHash('sha256').update(content).digest('hex');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function messageBlocks(document) {
|
|
13
|
+
const messages = Array.isArray(document?.payload?.messages)
|
|
14
|
+
? document.payload.messages
|
|
15
|
+
: [];
|
|
16
|
+
return messages
|
|
17
|
+
.map((message) => {
|
|
18
|
+
const speaker = String(message.speaker || message.role || message.author || '').trim();
|
|
19
|
+
const timestamp = String(message.timestamp || message.createdAt || '').trim();
|
|
20
|
+
const content = String(message.content || message.text || message.body || '').trim();
|
|
21
|
+
if (!content) return null;
|
|
22
|
+
const prefix = [timestamp, speaker].filter(Boolean).join(' ');
|
|
23
|
+
return {
|
|
24
|
+
content,
|
|
25
|
+
rendered: prefix ? `${prefix}\n${content}` : content,
|
|
26
|
+
messageId: String(message.id || message.messageId || '').trim() || null,
|
|
27
|
+
};
|
|
28
|
+
})
|
|
29
|
+
.filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function rawStructuralBlocks(text) {
|
|
33
|
+
return String(text || '')
|
|
34
|
+
.replace(/\r\n?/g, '\n')
|
|
35
|
+
.split(/\n{2,}|(?=^#{1,6}\s)/gm)
|
|
36
|
+
.map((block) => block.trim())
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function rawEmailBlocks(text) {
|
|
41
|
+
return String(text || '')
|
|
42
|
+
.replace(/\r\n?/g, '\n')
|
|
43
|
+
.split(/\n(?=On\s.*wrote:|\s*>\s|-----Original Message-----|________________________________)/g)
|
|
44
|
+
.map((block) => block.trim())
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function rawCodeBlocks(text) {
|
|
49
|
+
return String(text || '')
|
|
50
|
+
.replace(/\r\n?/g, '\n')
|
|
51
|
+
.split(/\n(?=(?:export\s+)?(?:class|function|const|let|var|import)\s)/g)
|
|
52
|
+
.map((block) => block.trim())
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function findBlockPositions(originalContent, blocks) {
|
|
57
|
+
let searchFrom = 0;
|
|
58
|
+
return blocks.map((block) => {
|
|
59
|
+
const pos = originalContent.indexOf(block, searchFrom);
|
|
60
|
+
const charStart = pos >= 0 ? pos : searchFrom;
|
|
61
|
+
const charEnd = charStart + block.length;
|
|
62
|
+
searchFrom = charEnd;
|
|
63
|
+
return { content: block, charStart, charEnd };
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function splitOversizedWithPos({ content, charStart }) {
|
|
68
|
+
if (content.length <= MAX_CHARS) {
|
|
69
|
+
return [{ content, charStart, charEnd: charStart + content.length }];
|
|
70
|
+
}
|
|
71
|
+
const parts = [];
|
|
72
|
+
let offset = 0;
|
|
73
|
+
while (offset < content.length) {
|
|
74
|
+
const slice = content.slice(offset, offset + MAX_CHARS);
|
|
75
|
+
parts.push({
|
|
76
|
+
content: slice,
|
|
77
|
+
charStart: charStart + offset,
|
|
78
|
+
charEnd: charStart + offset + slice.length,
|
|
79
|
+
});
|
|
80
|
+
offset += MAX_CHARS;
|
|
81
|
+
}
|
|
82
|
+
return parts;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function packBlocksWithPositions(blocksWithPos) {
|
|
86
|
+
const chunks = [];
|
|
87
|
+
let group = [];
|
|
88
|
+
let groupLen = 0;
|
|
89
|
+
|
|
90
|
+
for (const bwp of blocksWithPos.flatMap(splitOversizedWithPos)) {
|
|
91
|
+
const addLen = (groupLen ? 2 : 0) + bwp.content.length;
|
|
92
|
+
if (group.length && groupLen + addLen > TARGET_CHARS) {
|
|
93
|
+
chunks.push(group);
|
|
94
|
+
group = [bwp];
|
|
95
|
+
groupLen = bwp.content.length;
|
|
96
|
+
} else {
|
|
97
|
+
group.push(bwp);
|
|
98
|
+
groupLen += addLen;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (group.length) chunks.push(group);
|
|
102
|
+
|
|
103
|
+
return chunks.map((g) => ({
|
|
104
|
+
content: g.map((b) => b.content).join('\n\n'),
|
|
105
|
+
charStart: g[0].charStart,
|
|
106
|
+
charEnd: g[g.length - 1].charEnd,
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function chunkDocument(document) {
|
|
111
|
+
const content = String(document?.content || '').trim();
|
|
112
|
+
if (!content) return [];
|
|
113
|
+
|
|
114
|
+
const messages = messageBlocks(document);
|
|
115
|
+
if (messages.length) {
|
|
116
|
+
let searchOffset = 0;
|
|
117
|
+
return messages.map((message, chunkIndex) => {
|
|
118
|
+
let charStart = content.indexOf(message.content, searchOffset);
|
|
119
|
+
if (charStart < 0) charStart = searchOffset;
|
|
120
|
+
const charEnd = Math.min(content.length, charStart + message.content.length);
|
|
121
|
+
searchOffset = charEnd;
|
|
122
|
+
return {
|
|
123
|
+
chunkIndex,
|
|
124
|
+
charStart,
|
|
125
|
+
charEnd,
|
|
126
|
+
content: message.rendered,
|
|
127
|
+
contentHash: contentHash(message.rendered),
|
|
128
|
+
metadata: {
|
|
129
|
+
boundary: 'message',
|
|
130
|
+
messageId: message.messageId,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const sourceType = String(document?.sourceType || document?.normalizedType || '').toLowerCase();
|
|
137
|
+
const title = String(document?.title || '').toLowerCase();
|
|
138
|
+
|
|
139
|
+
let rawBlocks;
|
|
140
|
+
let boundaryType;
|
|
141
|
+
|
|
142
|
+
if (sourceType.includes('email') || title.endsWith('.eml') || title.includes('message')) {
|
|
143
|
+
rawBlocks = rawEmailBlocks(content);
|
|
144
|
+
boundaryType = 'email_thread';
|
|
145
|
+
} else if (
|
|
146
|
+
sourceType.includes('code') ||
|
|
147
|
+
title.endsWith('.js') ||
|
|
148
|
+
title.endsWith('.ts') ||
|
|
149
|
+
title.endsWith('.py') ||
|
|
150
|
+
title.endsWith('.java')
|
|
151
|
+
) {
|
|
152
|
+
rawBlocks = rawCodeBlocks(content);
|
|
153
|
+
boundaryType = 'code_structure';
|
|
154
|
+
} else {
|
|
155
|
+
rawBlocks = rawStructuralBlocks(content);
|
|
156
|
+
boundaryType = 'structural';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const blocksWithPos = findBlockPositions(content, rawBlocks);
|
|
160
|
+
const packed = packBlocksWithPositions(blocksWithPos);
|
|
161
|
+
|
|
162
|
+
return packed.map((chunk, chunkIndex) => ({
|
|
163
|
+
chunkIndex,
|
|
164
|
+
charStart: chunk.charStart,
|
|
165
|
+
charEnd: chunk.charEnd,
|
|
166
|
+
content: chunk.content,
|
|
167
|
+
contentHash: contentHash(chunk.content),
|
|
168
|
+
metadata: { boundary: boundaryType },
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function overlapWindowChunks(chunks) {
|
|
173
|
+
if (chunks.length < 2) return [];
|
|
174
|
+
return chunks.slice(0, -1).map((chunk, i) => {
|
|
175
|
+
const next = chunks[i + 1];
|
|
176
|
+
const combined = `${chunk.content}\n\n${next.content}`;
|
|
177
|
+
return {
|
|
178
|
+
chunkIndex: -(i + 1),
|
|
179
|
+
charStart: chunk.charStart,
|
|
180
|
+
charEnd: next.charEnd,
|
|
181
|
+
content: combined.slice(0, MAX_CHARS * 2),
|
|
182
|
+
contentHash: contentHash(combined),
|
|
183
|
+
metadata: { boundary: 'overlap_window', baseChunkIndex: i },
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
module.exports = {
|
|
189
|
+
chunkDocument,
|
|
190
|
+
overlapWindowChunks,
|
|
191
|
+
};
|
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
parseJsonObject,
|
|
12
12
|
safeTrim,
|
|
13
13
|
} = require('./ingestion_support');
|
|
14
|
+
const { chunkDocument, overlapWindowChunks } = require('./ingestion_chunking');
|
|
14
15
|
|
|
15
16
|
async function ingestDocuments(service, userId, documents = [], options = {}) {
|
|
16
17
|
const agentId = resolveAgentId(userId, options.agentId || options.agent_id || null);
|
|
@@ -50,35 +51,104 @@ async function ingestDocuments(service, userId, documents = [], options = {}) {
|
|
|
50
51
|
{ agentId },
|
|
51
52
|
);
|
|
52
53
|
documentIds.push(documentId);
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
54
|
+
const retainedChunkIds = [];
|
|
55
|
+
const chunks = chunkDocument(document);
|
|
56
|
+
const savedChunkMemoryIds = [];
|
|
57
|
+
for (const chunk of chunks) {
|
|
58
|
+
const memoryId = await service.memoryManager.saveMemory(
|
|
59
|
+
userId,
|
|
60
|
+
`${document.title}\n${chunk.content}`,
|
|
61
|
+
SOURCE_MEMORY_CATEGORIES[document.normalizedType]
|
|
62
|
+
|| SOURCE_MEMORY_CATEGORIES[document.sourceType]
|
|
63
|
+
|| 'episodic',
|
|
64
|
+
document.salience,
|
|
65
|
+
{
|
|
66
|
+
agentId,
|
|
67
|
+
staleAfterDays: getFreshnessPolicy(document.sourceType).staleAfterDays,
|
|
68
|
+
sourceRef: {
|
|
69
|
+
sourceType: 'memory_ingestion',
|
|
70
|
+
sourceId: document.externalObjectId,
|
|
71
|
+
sourceLabel: document.title,
|
|
72
|
+
},
|
|
73
|
+
scope: {
|
|
74
|
+
scopeType: 'agent',
|
|
75
|
+
scopeId: agentId,
|
|
76
|
+
},
|
|
77
|
+
metadata: {
|
|
78
|
+
ingestionJobId: jobId,
|
|
79
|
+
ingestionDocumentId: documentId,
|
|
80
|
+
providerKey: document.providerKey || null,
|
|
81
|
+
connectionId: document.connectionId || null,
|
|
82
|
+
sourceType: document.sourceType,
|
|
83
|
+
trustLevel: 'external_source',
|
|
84
|
+
chunkIndex: chunk.chunkIndex,
|
|
85
|
+
sourceSpan: {
|
|
86
|
+
charStart: chunk.charStart,
|
|
87
|
+
charEnd: chunk.charEnd,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
67
90
|
},
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
91
|
+
);
|
|
92
|
+
if (!memoryId) continue;
|
|
93
|
+
memoryIds.push(memoryId);
|
|
94
|
+
savedChunkMemoryIds.push(memoryId);
|
|
95
|
+
retainedChunkIds.push(service.memoryManager.replaceSourceChunk(
|
|
96
|
+
documentId,
|
|
97
|
+
chunk,
|
|
98
|
+
memoryId,
|
|
99
|
+
{
|
|
100
|
+
userId,
|
|
101
|
+
agentId,
|
|
102
|
+
sourceTimestamp: document.sourceTimestamp,
|
|
103
|
+
metadata: {
|
|
104
|
+
ingestionJobId: jobId,
|
|
105
|
+
externalObjectId: document.externalObjectId,
|
|
106
|
+
},
|
|
71
107
|
},
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
108
|
+
));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const overlapChunks = overlapWindowChunks(chunks);
|
|
112
|
+
for (const window of overlapChunks) {
|
|
113
|
+
const windowMemoryId = await service.memoryManager.saveMemory(
|
|
114
|
+
userId,
|
|
115
|
+
`${document.title}\n${window.content}`,
|
|
116
|
+
SOURCE_MEMORY_CATEGORIES[document.normalizedType]
|
|
117
|
+
|| SOURCE_MEMORY_CATEGORIES[document.sourceType]
|
|
118
|
+
|| 'episodic',
|
|
119
|
+
document.salience,
|
|
120
|
+
{
|
|
121
|
+
agentId,
|
|
122
|
+
staleAfterDays: getFreshnessPolicy(document.sourceType).staleAfterDays,
|
|
123
|
+
sourceRef: {
|
|
124
|
+
sourceType: 'memory_ingestion',
|
|
125
|
+
sourceId: document.externalObjectId,
|
|
126
|
+
sourceLabel: document.title,
|
|
127
|
+
},
|
|
128
|
+
scope: {
|
|
129
|
+
scopeType: 'agent',
|
|
130
|
+
scopeId: agentId,
|
|
131
|
+
},
|
|
132
|
+
metadata: {
|
|
133
|
+
ingestionJobId: jobId,
|
|
134
|
+
ingestionDocumentId: documentId,
|
|
135
|
+
providerKey: document.providerKey || null,
|
|
136
|
+
connectionId: document.connectionId || null,
|
|
137
|
+
sourceType: document.sourceType,
|
|
138
|
+
trustLevel: 'external_source',
|
|
139
|
+
chunkIndex: window.chunkIndex,
|
|
140
|
+
sourceSpan: {
|
|
141
|
+
charStart: window.charStart,
|
|
142
|
+
charEnd: window.charEnd,
|
|
143
|
+
},
|
|
144
|
+
isOverlapWindow: true,
|
|
145
|
+
},
|
|
78
146
|
},
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
147
|
+
);
|
|
148
|
+
if (windowMemoryId) savedChunkMemoryIds.push(windowMemoryId);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
service.memoryManager.pruneSourceChunks(documentId, retainedChunkIds);
|
|
82
152
|
}
|
|
83
153
|
|
|
84
154
|
service.memoryManager.recordIngestionJob(userId, {
|
|
@@ -157,6 +157,7 @@ function scoreMemoryCandidate({
|
|
|
157
157
|
entityRank = -1,
|
|
158
158
|
baseScore = 0,
|
|
159
159
|
importance = 5,
|
|
160
|
+
confidence = 0.7,
|
|
160
161
|
accessCount = 0,
|
|
161
162
|
freshness = 1,
|
|
162
163
|
} = {}) {
|
|
@@ -166,8 +167,9 @@ function scoreMemoryCandidate({
|
|
|
166
167
|
rankFuse(entityRank, 0.95)
|
|
167
168
|
) * 20;
|
|
168
169
|
const quality = 0.15 + clamp(importance, 1, 10, 5) / 22;
|
|
170
|
+
const confidenceMultiplier = 0.65 + clamp(confidence, 0, 1, 0.7) * 0.35;
|
|
169
171
|
const usage = Math.min(0.08, Math.log1p(Math.max(0, Number(accessCount) || 0)) / 50);
|
|
170
|
-
return Math.max(baseScore, fused + quality + usage) * freshness;
|
|
172
|
+
return Math.max(baseScore, fused + quality + usage) * freshness * confidenceMultiplier;
|
|
171
173
|
}
|
|
172
174
|
|
|
173
175
|
module.exports = {
|