@ordinatio/entities 1.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 +328 -0
- package/dist/chunk-QXBSEBJW.mjs +750 -0
- package/dist/index.mjs +2265 -0
- package/dist/observer-YUTEGG4S.mjs +10 -0
- package/package.json +54 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2265 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AGENTKNOW_ERRORS,
|
|
3
|
+
ContactAlreadyConvertedError,
|
|
4
|
+
ContactExistsError,
|
|
5
|
+
ContactNotFoundError,
|
|
6
|
+
KNOWLEDGE_ERRORS,
|
|
7
|
+
agentKnowledgeError,
|
|
8
|
+
checkConstraints,
|
|
9
|
+
evaluateConstraint,
|
|
10
|
+
fireObservers,
|
|
11
|
+
knowledgeError
|
|
12
|
+
} from "./chunk-QXBSEBJW.mjs";
|
|
13
|
+
|
|
14
|
+
// src/knowledge/field-definitions.ts
|
|
15
|
+
import { Prisma } from "@prisma/client";
|
|
16
|
+
async function getFieldDefinitions(db, entityType, status) {
|
|
17
|
+
try {
|
|
18
|
+
const fields = await db.entityFieldDefinition.findMany({
|
|
19
|
+
where: {
|
|
20
|
+
...entityType ? { entityType } : {},
|
|
21
|
+
...status ? { status } : { status: { not: "dismissed" } },
|
|
22
|
+
isActive: true
|
|
23
|
+
},
|
|
24
|
+
orderBy: [{ category: "asc" }, { sortOrder: "asc" }, { label: "asc" }]
|
|
25
|
+
});
|
|
26
|
+
const grouped = {};
|
|
27
|
+
for (const field of fields) {
|
|
28
|
+
if (!grouped[field.category]) grouped[field.category] = [];
|
|
29
|
+
grouped[field.category].push(field);
|
|
30
|
+
}
|
|
31
|
+
return { fields, grouped };
|
|
32
|
+
} catch (error) {
|
|
33
|
+
const err = knowledgeError("KNOWLEDGE_300", { entityType, status, error: String(error) });
|
|
34
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async function createFieldDefinition(db, input, callbacks) {
|
|
39
|
+
try {
|
|
40
|
+
const field = await db.entityFieldDefinition.create({
|
|
41
|
+
data: {
|
|
42
|
+
entityType: input.entityType,
|
|
43
|
+
key: input.key,
|
|
44
|
+
label: input.label,
|
|
45
|
+
dataType: input.dataType,
|
|
46
|
+
category: input.category,
|
|
47
|
+
enumOptions: input.enumOptions ?? Prisma.JsonNull,
|
|
48
|
+
extractionHint: input.extractionHint ?? null,
|
|
49
|
+
sortOrder: input.sortOrder ?? 0,
|
|
50
|
+
status: "approved"
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
await callbacks?.logActivity?.(
|
|
55
|
+
"KNOWLEDGE_FIELD_CREATED",
|
|
56
|
+
`Knowledge field "${input.label}" created for ${input.entityType}`
|
|
57
|
+
);
|
|
58
|
+
} catch {
|
|
59
|
+
}
|
|
60
|
+
return field;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
const err = knowledgeError("KNOWLEDGE_301", { input, error: String(error) });
|
|
63
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function updateFieldDefinition(db, id, input, callbacks) {
|
|
68
|
+
try {
|
|
69
|
+
const existing = await db.entityFieldDefinition.findUnique({ where: { id } });
|
|
70
|
+
if (!existing) return null;
|
|
71
|
+
const field = await db.entityFieldDefinition.update({
|
|
72
|
+
where: { id },
|
|
73
|
+
data: {
|
|
74
|
+
...input.label !== void 0 ? { label: input.label } : {},
|
|
75
|
+
...input.category !== void 0 ? { category: input.category } : {},
|
|
76
|
+
...input.enumOptions !== void 0 ? { enumOptions: input.enumOptions ?? Prisma.JsonNull } : {},
|
|
77
|
+
...input.extractionHint !== void 0 ? { extractionHint: input.extractionHint } : {},
|
|
78
|
+
...input.status !== void 0 ? { status: input.status } : {},
|
|
79
|
+
...input.mergedIntoId !== void 0 ? { mergedIntoId: input.mergedIntoId } : {},
|
|
80
|
+
...input.sortOrder !== void 0 ? { sortOrder: input.sortOrder } : {},
|
|
81
|
+
...input.isActive !== void 0 ? { isActive: input.isActive } : {}
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
if (input.status && input.status !== existing.status) {
|
|
85
|
+
const actionMap = {
|
|
86
|
+
approved: "KNOWLEDGE_FIELD_APPROVED",
|
|
87
|
+
dismissed: "KNOWLEDGE_FIELD_DISMISSED"
|
|
88
|
+
};
|
|
89
|
+
const action = actionMap[input.status];
|
|
90
|
+
if (action) {
|
|
91
|
+
try {
|
|
92
|
+
await callbacks?.logActivity?.(action, `Knowledge field "${existing.label}" ${input.status}`);
|
|
93
|
+
} catch {
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return field;
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const err = knowledgeError("KNOWLEDGE_302", { id, input, error: String(error) });
|
|
100
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async function getFieldDefinitionById(db, id) {
|
|
105
|
+
return db.entityFieldDefinition.findUnique({ where: { id } });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/knowledge/ledger.ts
|
|
109
|
+
async function getEntityFields(db, entityType, entityId) {
|
|
110
|
+
try {
|
|
111
|
+
const entries = await db.knowledgeLedgerEntry.findMany({
|
|
112
|
+
where: {
|
|
113
|
+
entityType,
|
|
114
|
+
entityId,
|
|
115
|
+
supersededAt: null
|
|
116
|
+
},
|
|
117
|
+
include: {
|
|
118
|
+
field: true
|
|
119
|
+
},
|
|
120
|
+
orderBy: { createdAt: "desc" }
|
|
121
|
+
});
|
|
122
|
+
const grouped = {};
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
const cat = entry.field.category;
|
|
125
|
+
if (!grouped[cat]) grouped[cat] = [];
|
|
126
|
+
grouped[cat].push({
|
|
127
|
+
fieldId: entry.fieldId,
|
|
128
|
+
key: entry.field.key,
|
|
129
|
+
label: entry.field.label,
|
|
130
|
+
dataType: entry.field.dataType,
|
|
131
|
+
category: cat,
|
|
132
|
+
value: entry.value,
|
|
133
|
+
confidence: Number(entry.confidence),
|
|
134
|
+
source: entry.source,
|
|
135
|
+
updatedAt: entry.createdAt
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return { entries, grouped };
|
|
139
|
+
} catch (error) {
|
|
140
|
+
const err = knowledgeError("KNOWLEDGE_310", { entityType, entityId, error: String(error) });
|
|
141
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
142
|
+
throw error;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function setEntityFields(db, entityType, entityId, fields, source, sourceId, confidence = 1, setBy, callbacks) {
|
|
146
|
+
try {
|
|
147
|
+
const fieldKeys = Object.keys(fields);
|
|
148
|
+
const fieldDefs = await db.entityFieldDefinition.findMany({
|
|
149
|
+
where: {
|
|
150
|
+
entityType,
|
|
151
|
+
key: { in: fieldKeys },
|
|
152
|
+
isActive: true,
|
|
153
|
+
status: "approved"
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
const defMap = new Map(fieldDefs.map((d) => [d.key, d]));
|
|
157
|
+
const results = [];
|
|
158
|
+
const skipped = [];
|
|
159
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
160
|
+
const def = defMap.get(key);
|
|
161
|
+
if (!def) {
|
|
162
|
+
skipped.push(key);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
await db.knowledgeLedgerEntry.updateMany({
|
|
166
|
+
where: {
|
|
167
|
+
fieldId: def.id,
|
|
168
|
+
entityType,
|
|
169
|
+
entityId,
|
|
170
|
+
supersededAt: null
|
|
171
|
+
},
|
|
172
|
+
data: { supersededAt: /* @__PURE__ */ new Date() }
|
|
173
|
+
});
|
|
174
|
+
const entry = await db.knowledgeLedgerEntry.create({
|
|
175
|
+
data: {
|
|
176
|
+
fieldId: def.id,
|
|
177
|
+
entityType,
|
|
178
|
+
entityId,
|
|
179
|
+
value,
|
|
180
|
+
confidence,
|
|
181
|
+
source,
|
|
182
|
+
sourceId: sourceId ?? null,
|
|
183
|
+
setBy: setBy ?? null
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
results.push({ key, entryId: entry.id });
|
|
187
|
+
}
|
|
188
|
+
if (skipped.length > 0) {
|
|
189
|
+
const err = knowledgeError("KNOWLEDGE_313", { entityType, entityId, skippedKeys: skipped });
|
|
190
|
+
console.warn(`[${err.ref}] ${err.description}`, skipped);
|
|
191
|
+
}
|
|
192
|
+
if (results.length > 0) {
|
|
193
|
+
try {
|
|
194
|
+
await callbacks?.logActivity?.(
|
|
195
|
+
"KNOWLEDGE_VALUE_SET",
|
|
196
|
+
`${results.length} knowledge field(s) set on ${entityType} ${entityId}`,
|
|
197
|
+
entityType === "client" ? { clientId: entityId } : void 0
|
|
198
|
+
);
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const { fireObservers: fireObservers2 } = await import("./observer-YUTEGG4S.mjs");
|
|
203
|
+
await fireObservers2(db, entityType, entityId, results.map((r) => r.key), callbacks);
|
|
204
|
+
} catch {
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return { written: results, skipped };
|
|
208
|
+
} catch (error) {
|
|
209
|
+
const err = knowledgeError("KNOWLEDGE_311", { entityType, entityId, error: String(error) });
|
|
210
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
async function getFieldHistory(db, entityType, entityId, fieldId, limit = 50) {
|
|
215
|
+
try {
|
|
216
|
+
const entries = await db.knowledgeLedgerEntry.findMany({
|
|
217
|
+
where: {
|
|
218
|
+
entityType,
|
|
219
|
+
entityId,
|
|
220
|
+
...fieldId ? { fieldId } : {}
|
|
221
|
+
},
|
|
222
|
+
include: { field: true },
|
|
223
|
+
orderBy: { createdAt: "desc" },
|
|
224
|
+
take: limit
|
|
225
|
+
});
|
|
226
|
+
return entries;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
const err = knowledgeError("KNOWLEDGE_312", { entityType, entityId, fieldId, error: String(error) });
|
|
229
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
230
|
+
throw error;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/knowledge/search.ts
|
|
235
|
+
async function searchByFields(db, entityType, filters, limit = 50) {
|
|
236
|
+
try {
|
|
237
|
+
const filterEntries = Object.entries(filters);
|
|
238
|
+
let matchingEntityIds = null;
|
|
239
|
+
for (const [key, value] of filterEntries) {
|
|
240
|
+
const fieldDef = await db.entityFieldDefinition.findFirst({
|
|
241
|
+
where: { entityType, key, isActive: true }
|
|
242
|
+
});
|
|
243
|
+
if (!fieldDef) continue;
|
|
244
|
+
const entries = await db.knowledgeLedgerEntry.findMany({
|
|
245
|
+
where: {
|
|
246
|
+
fieldId: fieldDef.id,
|
|
247
|
+
entityType,
|
|
248
|
+
supersededAt: null,
|
|
249
|
+
value: { equals: value }
|
|
250
|
+
},
|
|
251
|
+
select: { entityId: true },
|
|
252
|
+
distinct: ["entityId"]
|
|
253
|
+
});
|
|
254
|
+
const ids = entries.map((e) => e.entityId);
|
|
255
|
+
if (matchingEntityIds === null) {
|
|
256
|
+
matchingEntityIds = ids;
|
|
257
|
+
} else {
|
|
258
|
+
const idSet = new Set(ids);
|
|
259
|
+
matchingEntityIds = matchingEntityIds.filter((id) => idSet.has(id));
|
|
260
|
+
}
|
|
261
|
+
if (matchingEntityIds.length === 0) break;
|
|
262
|
+
}
|
|
263
|
+
const entityIds = (matchingEntityIds ?? []).slice(0, limit);
|
|
264
|
+
return { entityIds, total: matchingEntityIds?.length ?? 0 };
|
|
265
|
+
} catch (error) {
|
|
266
|
+
const err = knowledgeError("KNOWLEDGE_305", { entityType, filters, error: String(error) });
|
|
267
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
268
|
+
throw error;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
async function logSearchQuery(db, input) {
|
|
272
|
+
try {
|
|
273
|
+
const now = /* @__PURE__ */ new Date();
|
|
274
|
+
await db.searchQueryLog.create({
|
|
275
|
+
data: {
|
|
276
|
+
query: input.query,
|
|
277
|
+
source: input.source,
|
|
278
|
+
userId: input.userId ?? null,
|
|
279
|
+
entityType: input.entityType ?? null,
|
|
280
|
+
resultCount: input.resultCount ?? null,
|
|
281
|
+
hourOfDay: now.getHours(),
|
|
282
|
+
dayOfWeek: now.getDay(),
|
|
283
|
+
monthOfYear: now.getMonth() + 1,
|
|
284
|
+
weekOfYear: getWeekOfYear(now)
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
} catch (error) {
|
|
288
|
+
const err = knowledgeError("KNOWLEDGE_320", { input, error: String(error) });
|
|
289
|
+
console.warn(`[${err.ref}] ${err.description}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function getWeekOfYear(date) {
|
|
293
|
+
const start = new Date(date.getFullYear(), 0, 1);
|
|
294
|
+
const diff = date.getTime() - start.getTime();
|
|
295
|
+
const oneWeek = 7 * 24 * 60 * 60 * 1e3;
|
|
296
|
+
return Math.ceil(diff / oneWeek + 1);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/knowledge/time-travel.ts
|
|
300
|
+
async function getKnowledgeAt(db, entityType, entityId, timestamp) {
|
|
301
|
+
try {
|
|
302
|
+
const entries = await db.knowledgeLedgerEntry.findMany({
|
|
303
|
+
where: {
|
|
304
|
+
entityType,
|
|
305
|
+
entityId,
|
|
306
|
+
createdAt: { lte: timestamp },
|
|
307
|
+
OR: [
|
|
308
|
+
{ supersededAt: null },
|
|
309
|
+
{ supersededAt: { gt: timestamp } }
|
|
310
|
+
]
|
|
311
|
+
},
|
|
312
|
+
include: { field: true },
|
|
313
|
+
orderBy: { createdAt: "desc" }
|
|
314
|
+
});
|
|
315
|
+
const fields = {};
|
|
316
|
+
for (const entry of entries) {
|
|
317
|
+
const key = entry.field.key;
|
|
318
|
+
if (!fields[key]) {
|
|
319
|
+
fields[key] = {
|
|
320
|
+
value: entry.value,
|
|
321
|
+
confidence: Number(entry.confidence),
|
|
322
|
+
source: entry.source,
|
|
323
|
+
setAt: entry.createdAt
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return { fields };
|
|
328
|
+
} catch (error) {
|
|
329
|
+
const err = knowledgeError("KNOWLEDGE_340", { entityType, entityId, timestamp: timestamp.toISOString(), error: String(error) });
|
|
330
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
331
|
+
throw error;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function getFieldValueAt(db, entityType, entityId, fieldKey, timestamp) {
|
|
335
|
+
try {
|
|
336
|
+
const fieldDef = await db.entityFieldDefinition.findFirst({
|
|
337
|
+
where: { entityType, key: fieldKey }
|
|
338
|
+
});
|
|
339
|
+
if (!fieldDef) return null;
|
|
340
|
+
const entry = await db.knowledgeLedgerEntry.findFirst({
|
|
341
|
+
where: {
|
|
342
|
+
fieldId: fieldDef.id,
|
|
343
|
+
entityType,
|
|
344
|
+
entityId,
|
|
345
|
+
createdAt: { lte: timestamp },
|
|
346
|
+
OR: [
|
|
347
|
+
{ supersededAt: null },
|
|
348
|
+
{ supersededAt: { gt: timestamp } }
|
|
349
|
+
]
|
|
350
|
+
},
|
|
351
|
+
orderBy: { createdAt: "desc" }
|
|
352
|
+
});
|
|
353
|
+
if (!entry) return null;
|
|
354
|
+
return {
|
|
355
|
+
value: entry.value,
|
|
356
|
+
confidence: Number(entry.confidence),
|
|
357
|
+
source: entry.source,
|
|
358
|
+
setAt: entry.createdAt
|
|
359
|
+
};
|
|
360
|
+
} catch (error) {
|
|
361
|
+
const err = knowledgeError("KNOWLEDGE_340", { entityType, entityId, fieldKey, timestamp: timestamp.toISOString(), error: String(error) });
|
|
362
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async function getKnowledgeTimeline(db, entityType, entityId, from, to, limit = 100) {
|
|
367
|
+
if (from > to) {
|
|
368
|
+
const err = knowledgeError("KNOWLEDGE_341", { entityType, entityId, from: from.toISOString(), to: to.toISOString() });
|
|
369
|
+
console.error(`[${err.ref}] ${err.description}`);
|
|
370
|
+
throw new Error(err.description);
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
const entries = await db.knowledgeLedgerEntry.findMany({
|
|
374
|
+
where: {
|
|
375
|
+
entityType,
|
|
376
|
+
entityId,
|
|
377
|
+
createdAt: { gte: from, lte: to }
|
|
378
|
+
},
|
|
379
|
+
include: { field: true },
|
|
380
|
+
orderBy: { createdAt: "asc" },
|
|
381
|
+
take: limit
|
|
382
|
+
});
|
|
383
|
+
return entries.map((entry) => ({
|
|
384
|
+
fieldKey: entry.field.key,
|
|
385
|
+
value: entry.value,
|
|
386
|
+
confidence: Number(entry.confidence),
|
|
387
|
+
source: entry.source,
|
|
388
|
+
createdAt: entry.createdAt,
|
|
389
|
+
supersededAt: entry.supersededAt
|
|
390
|
+
}));
|
|
391
|
+
} catch (error) {
|
|
392
|
+
const err = knowledgeError("KNOWLEDGE_340", { entityType, entityId, error: String(error) });
|
|
393
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// src/knowledge/scoring.ts
|
|
399
|
+
var SOURCE_RELIABILITY = {
|
|
400
|
+
manual: 1,
|
|
401
|
+
note: 0.9,
|
|
402
|
+
email: 0.7,
|
|
403
|
+
agent: 0.6,
|
|
404
|
+
"ai-batch": 0.5,
|
|
405
|
+
predicted: 0.3
|
|
406
|
+
};
|
|
407
|
+
function computeRecencyFactor(createdAt, now, maxAgeDays = 365) {
|
|
408
|
+
const current = now ?? /* @__PURE__ */ new Date();
|
|
409
|
+
const ageDays = (current.getTime() - createdAt.getTime()) / (1e3 * 60 * 60 * 24);
|
|
410
|
+
if (ageDays <= 0) return 1;
|
|
411
|
+
return Math.exp(-ageDays / maxAgeDays * 3);
|
|
412
|
+
}
|
|
413
|
+
function computeTruthScore(assertions, now) {
|
|
414
|
+
if (assertions.length === 0) return 0;
|
|
415
|
+
let numerator = 0;
|
|
416
|
+
let denominator = 0;
|
|
417
|
+
for (const a of assertions) {
|
|
418
|
+
const reliability = SOURCE_RELIABILITY[a.source] ?? 0.5;
|
|
419
|
+
const recency = computeRecencyFactor(a.createdAt, now);
|
|
420
|
+
const weight = a.confidence * reliability;
|
|
421
|
+
numerator += weight * recency;
|
|
422
|
+
denominator += weight;
|
|
423
|
+
}
|
|
424
|
+
if (denominator === 0) return 0;
|
|
425
|
+
return numerator / denominator;
|
|
426
|
+
}
|
|
427
|
+
function computeComplexityScore(fieldsUsed, fieldsAvailable, categoriesUsed, maxCategories) {
|
|
428
|
+
if (fieldsAvailable === 0 || maxCategories === 0) return 0;
|
|
429
|
+
const categoryRatio = Math.min(categoriesUsed / maxCategories, 1);
|
|
430
|
+
const fieldRatio = Math.min(fieldsUsed / fieldsAvailable, 1);
|
|
431
|
+
return 0.6 * categoryRatio + 0.4 * fieldRatio;
|
|
432
|
+
}
|
|
433
|
+
async function computeEntityTruthScores(db, entityType, entityId) {
|
|
434
|
+
try {
|
|
435
|
+
const allEntries = await db.knowledgeLedgerEntry.findMany({
|
|
436
|
+
where: { entityType, entityId },
|
|
437
|
+
include: { field: true },
|
|
438
|
+
orderBy: { createdAt: "desc" }
|
|
439
|
+
});
|
|
440
|
+
const byField = /* @__PURE__ */ new Map();
|
|
441
|
+
const currentByField = /* @__PURE__ */ new Map();
|
|
442
|
+
for (const entry of allEntries) {
|
|
443
|
+
const key = entry.field.key;
|
|
444
|
+
if (!byField.has(key)) byField.set(key, []);
|
|
445
|
+
byField.get(key).push({
|
|
446
|
+
confidence: Number(entry.confidence),
|
|
447
|
+
source: entry.source,
|
|
448
|
+
createdAt: entry.createdAt,
|
|
449
|
+
supersededAt: entry.supersededAt
|
|
450
|
+
});
|
|
451
|
+
if (!entry.supersededAt && !currentByField.has(key)) {
|
|
452
|
+
currentByField.set(key, {
|
|
453
|
+
confidence: Number(entry.confidence),
|
|
454
|
+
source: entry.source
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
const result = {};
|
|
459
|
+
for (const [key, assertions] of byField) {
|
|
460
|
+
const current = currentByField.get(key);
|
|
461
|
+
if (!current) continue;
|
|
462
|
+
result[key] = {
|
|
463
|
+
truthScore: computeTruthScore(assertions),
|
|
464
|
+
confidence: current.confidence,
|
|
465
|
+
source: current.source,
|
|
466
|
+
assertions: assertions.length
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
return result;
|
|
470
|
+
} catch (error) {
|
|
471
|
+
const err = knowledgeError("KNOWLEDGE_348", { entityType, entityId, error: String(error) });
|
|
472
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// src/knowledge/decay.ts
|
|
478
|
+
function computeDecayedConfidence(originalConfidence, createdAt, halfLifeDays, now) {
|
|
479
|
+
if (halfLifeDays <= 0) return originalConfidence;
|
|
480
|
+
const current = now ?? /* @__PURE__ */ new Date();
|
|
481
|
+
const ageDays = (current.getTime() - createdAt.getTime()) / (1e3 * 60 * 60 * 24);
|
|
482
|
+
if (ageDays <= 0) return originalConfidence;
|
|
483
|
+
return originalConfidence * Math.pow(0.5, ageDays / halfLifeDays);
|
|
484
|
+
}
|
|
485
|
+
function isStale(originalConfidence, createdAt, halfLifeDays, threshold = 0.3, now) {
|
|
486
|
+
const decayed = computeDecayedConfidence(originalConfidence, createdAt, halfLifeDays, now);
|
|
487
|
+
return decayed < threshold;
|
|
488
|
+
}
|
|
489
|
+
async function getStaleFields(db, entityType, entityId, threshold = 0.3) {
|
|
490
|
+
try {
|
|
491
|
+
const entries = await db.knowledgeLedgerEntry.findMany({
|
|
492
|
+
where: {
|
|
493
|
+
entityType,
|
|
494
|
+
entityId,
|
|
495
|
+
supersededAt: null
|
|
496
|
+
},
|
|
497
|
+
include: { field: true }
|
|
498
|
+
});
|
|
499
|
+
const now = /* @__PURE__ */ new Date();
|
|
500
|
+
const staleFields = [];
|
|
501
|
+
for (const entry of entries) {
|
|
502
|
+
const halfLife = entry.field.halfLifeDays;
|
|
503
|
+
if (halfLife == null) continue;
|
|
504
|
+
const originalConfidence = Number(entry.confidence);
|
|
505
|
+
const decayed = computeDecayedConfidence(originalConfidence, entry.createdAt, halfLife, now);
|
|
506
|
+
if (decayed < threshold) {
|
|
507
|
+
const ageDays = (now.getTime() - entry.createdAt.getTime()) / (1e3 * 60 * 60 * 24);
|
|
508
|
+
staleFields.push({
|
|
509
|
+
fieldKey: entry.field.key,
|
|
510
|
+
label: entry.field.label,
|
|
511
|
+
originalConfidence,
|
|
512
|
+
decayedConfidence: decayed,
|
|
513
|
+
halfLifeDays: halfLife,
|
|
514
|
+
lastUpdated: entry.createdAt,
|
|
515
|
+
staleSinceDays: Math.floor(ageDays)
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return staleFields;
|
|
520
|
+
} catch (error) {
|
|
521
|
+
const err = knowledgeError("KNOWLEDGE_342", { entityType, entityId, error: String(error) });
|
|
522
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function formatStalenessWarnings(staleFields) {
|
|
527
|
+
return staleFields.map((f) => {
|
|
528
|
+
const pct = Math.round(f.decayedConfidence * 100);
|
|
529
|
+
return `"${f.label}" confidence has decayed to ${pct}% (${f.staleSinceDays} days since last update)`;
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/knowledge/branching.ts
|
|
534
|
+
function shouldBranch(newConfidence, existingConfidence, threshold = 0.7) {
|
|
535
|
+
if (newConfidence >= threshold) return "proceed";
|
|
536
|
+
if (existingConfidence != null && existingConfidence > newConfidence) return "validate";
|
|
537
|
+
return "proceed";
|
|
538
|
+
}
|
|
539
|
+
async function setEntityFieldsWithBranching(db, entityType, entityId, fields, source, confidence, options) {
|
|
540
|
+
const threshold = options?.branchingThreshold ?? 0.7;
|
|
541
|
+
const written = [];
|
|
542
|
+
const skipped = [];
|
|
543
|
+
const deferred = [];
|
|
544
|
+
const validationRequested = [];
|
|
545
|
+
try {
|
|
546
|
+
const fieldKeys = Object.keys(fields);
|
|
547
|
+
const fieldDefs = await db.entityFieldDefinition.findMany({
|
|
548
|
+
where: {
|
|
549
|
+
entityType,
|
|
550
|
+
key: { in: fieldKeys },
|
|
551
|
+
isActive: true,
|
|
552
|
+
status: "approved"
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
const defMap = new Map(fieldDefs.map((d) => [d.key, d]));
|
|
556
|
+
const existingEntries = await db.knowledgeLedgerEntry.findMany({
|
|
557
|
+
where: {
|
|
558
|
+
entityType,
|
|
559
|
+
entityId,
|
|
560
|
+
fieldId: { in: fieldDefs.map((d) => d.id) },
|
|
561
|
+
supersededAt: null
|
|
562
|
+
},
|
|
563
|
+
include: { field: true }
|
|
564
|
+
});
|
|
565
|
+
const existingByKey = new Map(
|
|
566
|
+
existingEntries.map((e) => [e.field.key, { confidence: Number(e.confidence), value: e.value }])
|
|
567
|
+
);
|
|
568
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
569
|
+
const def = defMap.get(key);
|
|
570
|
+
if (!def) {
|
|
571
|
+
skipped.push(key);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const existing = existingByKey.get(key);
|
|
575
|
+
const decision = shouldBranch(confidence, existing?.confidence, threshold);
|
|
576
|
+
if (decision === "validate") {
|
|
577
|
+
if (options?.validationCallbacks?.requestValidation) {
|
|
578
|
+
try {
|
|
579
|
+
const verdict = await options.validationCallbacks.requestValidation(
|
|
580
|
+
entityType,
|
|
581
|
+
entityId,
|
|
582
|
+
key,
|
|
583
|
+
value,
|
|
584
|
+
confidence,
|
|
585
|
+
existing?.value,
|
|
586
|
+
existing?.confidence
|
|
587
|
+
);
|
|
588
|
+
if (verdict === "reject") {
|
|
589
|
+
skipped.push(key);
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
if (verdict === "defer") {
|
|
593
|
+
deferred.push({ key, reason: "Validation deferred by reviewer" });
|
|
594
|
+
validationRequested.push({ key, value, confidence });
|
|
595
|
+
continue;
|
|
596
|
+
}
|
|
597
|
+
} catch (error) {
|
|
598
|
+
const err = knowledgeError("KNOWLEDGE_344", { entityType, entityId, key, error: String(error) });
|
|
599
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
600
|
+
deferred.push({ key, reason: "Validation callback failed" });
|
|
601
|
+
validationRequested.push({ key, value, confidence });
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
} else {
|
|
605
|
+
deferred.push({ key, reason: "Low confidence, no validator available" });
|
|
606
|
+
validationRequested.push({ key, value, confidence });
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
await db.knowledgeLedgerEntry.updateMany({
|
|
611
|
+
where: {
|
|
612
|
+
fieldId: def.id,
|
|
613
|
+
entityType,
|
|
614
|
+
entityId,
|
|
615
|
+
supersededAt: null
|
|
616
|
+
},
|
|
617
|
+
data: { supersededAt: /* @__PURE__ */ new Date() }
|
|
618
|
+
});
|
|
619
|
+
const entry = await db.knowledgeLedgerEntry.create({
|
|
620
|
+
data: {
|
|
621
|
+
fieldId: def.id,
|
|
622
|
+
entityType,
|
|
623
|
+
entityId,
|
|
624
|
+
value,
|
|
625
|
+
confidence,
|
|
626
|
+
source,
|
|
627
|
+
sourceId: options?.sourceId ?? null,
|
|
628
|
+
setBy: options?.setBy ?? null
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
written.push({ key, entryId: entry.id });
|
|
632
|
+
}
|
|
633
|
+
if (written.length > 0) {
|
|
634
|
+
try {
|
|
635
|
+
await options?.callbacks?.logActivity?.(
|
|
636
|
+
"KNOWLEDGE_VALUE_SET",
|
|
637
|
+
`${written.length} knowledge field(s) set on ${entityType} ${entityId}`,
|
|
638
|
+
entityType === "client" ? { clientId: entityId } : void 0
|
|
639
|
+
);
|
|
640
|
+
} catch {
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return { written, skipped, deferred, validationRequested };
|
|
644
|
+
} catch (error) {
|
|
645
|
+
const err = knowledgeError("KNOWLEDGE_343", { entityType, entityId, error: String(error) });
|
|
646
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
647
|
+
throw error;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// src/knowledge/shadow-graph.ts
|
|
652
|
+
function computeRelationshipStrength(sharedFields, totalFieldsOnSource) {
|
|
653
|
+
if (sharedFields.length === 0 || totalFieldsOnSource === 0) return 0;
|
|
654
|
+
const avgConfProduct = sharedFields.reduce(
|
|
655
|
+
(sum, f) => sum + f.sourceConfidence * f.targetConfidence,
|
|
656
|
+
0
|
|
657
|
+
) / sharedFields.length;
|
|
658
|
+
const coverage = Math.min(sharedFields.length / totalFieldsOnSource, 1);
|
|
659
|
+
return avgConfProduct * coverage;
|
|
660
|
+
}
|
|
661
|
+
async function findRelatedEntities(db, entityType, entityId, options) {
|
|
662
|
+
try {
|
|
663
|
+
const minShared = options?.minSharedFields ?? 1;
|
|
664
|
+
const minStrength = options?.minStrength ?? 0;
|
|
665
|
+
const limit = options?.limit ?? 20;
|
|
666
|
+
const sourceEntries = await db.knowledgeLedgerEntry.findMany({
|
|
667
|
+
where: { entityType, entityId, supersededAt: null },
|
|
668
|
+
include: { field: true }
|
|
669
|
+
});
|
|
670
|
+
if (sourceEntries.length === 0) return [];
|
|
671
|
+
const relationships = /* @__PURE__ */ new Map();
|
|
672
|
+
for (const sourceEntry of sourceEntries) {
|
|
673
|
+
if (sourceEntry.value === null || sourceEntry.value === void 0) continue;
|
|
674
|
+
const matchingEntries = await db.knowledgeLedgerEntry.findMany({
|
|
675
|
+
where: {
|
|
676
|
+
fieldId: sourceEntry.fieldId,
|
|
677
|
+
value: { equals: sourceEntry.value },
|
|
678
|
+
supersededAt: null,
|
|
679
|
+
NOT: {
|
|
680
|
+
AND: [
|
|
681
|
+
{ entityType },
|
|
682
|
+
{ entityId }
|
|
683
|
+
]
|
|
684
|
+
},
|
|
685
|
+
...options?.targetEntityType ? { entityType: options.targetEntityType } : {}
|
|
686
|
+
},
|
|
687
|
+
include: { field: true },
|
|
688
|
+
take: 100
|
|
689
|
+
// Unbounded query guard
|
|
690
|
+
});
|
|
691
|
+
for (const match of matchingEntries) {
|
|
692
|
+
const targetKey = `${match.entityType}:${match.entityId}`;
|
|
693
|
+
if (!relationships.has(targetKey)) {
|
|
694
|
+
relationships.set(targetKey, {
|
|
695
|
+
sourceEntityType: entityType,
|
|
696
|
+
sourceEntityId: entityId,
|
|
697
|
+
targetEntityType: match.entityType,
|
|
698
|
+
targetEntityId: match.entityId,
|
|
699
|
+
sharedFields: [],
|
|
700
|
+
strength: 0
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
relationships.get(targetKey).sharedFields.push({
|
|
704
|
+
fieldKey: sourceEntry.field.key,
|
|
705
|
+
value: sourceEntry.value,
|
|
706
|
+
sourceConfidence: Number(sourceEntry.confidence),
|
|
707
|
+
targetConfidence: Number(match.confidence)
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
const results = [];
|
|
712
|
+
for (const rel of relationships.values()) {
|
|
713
|
+
if (rel.sharedFields.length < minShared) continue;
|
|
714
|
+
rel.strength = computeRelationshipStrength(
|
|
715
|
+
rel.sharedFields,
|
|
716
|
+
sourceEntries.length
|
|
717
|
+
);
|
|
718
|
+
if (rel.strength < minStrength) continue;
|
|
719
|
+
results.push(rel);
|
|
720
|
+
}
|
|
721
|
+
results.sort((a, b) => b.strength - a.strength);
|
|
722
|
+
return results.slice(0, limit);
|
|
723
|
+
} catch (error) {
|
|
724
|
+
const err = knowledgeError("KNOWLEDGE_345", { entityType, entityId, error: String(error) });
|
|
725
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
726
|
+
throw error;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
async function getEntityRelationships(db, entityType, entityId, limit = 20) {
|
|
730
|
+
return findRelatedEntities(db, entityType, entityId, { limit });
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
// src/knowledge/reflection.ts
|
|
734
|
+
var DEFAULT_CONFLICT_RULES = [
|
|
735
|
+
{
|
|
736
|
+
type: "value_contradiction",
|
|
737
|
+
description: "Budget range contradicts premium preference",
|
|
738
|
+
fieldA: "budget_range",
|
|
739
|
+
fieldB: "quality_preference",
|
|
740
|
+
check: (budgetVal, qualityVal) => {
|
|
741
|
+
const budget = String(budgetVal).toLowerCase();
|
|
742
|
+
const quality = String(qualityVal).toLowerCase();
|
|
743
|
+
const isLowBudget = budget.includes("budget") || budget.includes("economy") || budget.includes("under");
|
|
744
|
+
const isPremium = quality.includes("premium") || quality.includes("luxury") || quality.includes("high-end");
|
|
745
|
+
return isLowBudget && isPremium;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
];
|
|
749
|
+
function evaluateConflictRule(rule, fields) {
|
|
750
|
+
const fieldA = fields[rule.fieldA];
|
|
751
|
+
if (!fieldA) return null;
|
|
752
|
+
const fieldB = rule.fieldB ? fields[rule.fieldB] : void 0;
|
|
753
|
+
if (rule.fieldB && !fieldB) return null;
|
|
754
|
+
const isConflict = rule.check(fieldA.value, fieldB?.value);
|
|
755
|
+
if (!isConflict) return null;
|
|
756
|
+
const minTruth = Math.min(fieldA.truthScore, fieldB?.truthScore ?? fieldA.truthScore);
|
|
757
|
+
const severity = minTruth > 0.7 ? "high" : minTruth > 0.4 ? "medium" : "low";
|
|
758
|
+
return {
|
|
759
|
+
rule,
|
|
760
|
+
entityType: "",
|
|
761
|
+
entityId: "",
|
|
762
|
+
fieldA: { key: rule.fieldA, ...fieldA },
|
|
763
|
+
fieldB: fieldB ? { key: rule.fieldB, ...fieldB } : void 0,
|
|
764
|
+
severity,
|
|
765
|
+
suggestion: rule.fieldB ? `Review "${rule.fieldA}" and "${rule.fieldB}" \u2014 ${rule.description}` : `Review "${rule.fieldA}" \u2014 ${rule.description}`
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
async function detectConflicts(db, entityType, entityId, rules) {
|
|
769
|
+
try {
|
|
770
|
+
const activeRules = rules ?? DEFAULT_CONFLICT_RULES;
|
|
771
|
+
const allEntries = await db.knowledgeLedgerEntry.findMany({
|
|
772
|
+
where: { entityType, entityId },
|
|
773
|
+
include: { field: true },
|
|
774
|
+
orderBy: { createdAt: "desc" }
|
|
775
|
+
});
|
|
776
|
+
const entriesByField = /* @__PURE__ */ new Map();
|
|
777
|
+
const currentByField = /* @__PURE__ */ new Map();
|
|
778
|
+
for (const entry of allEntries) {
|
|
779
|
+
const key = entry.field.key;
|
|
780
|
+
if (!entriesByField.has(key)) entriesByField.set(key, []);
|
|
781
|
+
entriesByField.get(key).push({
|
|
782
|
+
confidence: Number(entry.confidence),
|
|
783
|
+
source: entry.source,
|
|
784
|
+
createdAt: entry.createdAt,
|
|
785
|
+
supersededAt: entry.supersededAt
|
|
786
|
+
});
|
|
787
|
+
if (!entry.supersededAt && !currentByField.has(key)) {
|
|
788
|
+
currentByField.set(key, { value: entry.value, confidence: Number(entry.confidence) });
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
const fields = {};
|
|
792
|
+
for (const [key, current] of currentByField) {
|
|
793
|
+
const assertions = entriesByField.get(key) ?? [];
|
|
794
|
+
fields[key] = {
|
|
795
|
+
value: current.value,
|
|
796
|
+
confidence: current.confidence,
|
|
797
|
+
truthScore: computeTruthScore(assertions)
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
const conflicts = [];
|
|
801
|
+
for (const rule of activeRules) {
|
|
802
|
+
const conflict = evaluateConflictRule(rule, fields);
|
|
803
|
+
if (conflict) {
|
|
804
|
+
conflict.entityType = entityType;
|
|
805
|
+
conflict.entityId = entityId;
|
|
806
|
+
conflicts.push(conflict);
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return conflicts;
|
|
810
|
+
} catch (error) {
|
|
811
|
+
const err = knowledgeError("KNOWLEDGE_346", { entityType, entityId, error: String(error) });
|
|
812
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
813
|
+
throw error;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
async function scanForConflicts(db, entityType, rules, limit = 100) {
|
|
817
|
+
try {
|
|
818
|
+
const distinctEntries = await db.knowledgeLedgerEntry.findMany({
|
|
819
|
+
where: { entityType, supersededAt: null },
|
|
820
|
+
select: { entityId: true },
|
|
821
|
+
distinct: ["entityId"],
|
|
822
|
+
take: limit
|
|
823
|
+
});
|
|
824
|
+
const results = [];
|
|
825
|
+
for (const { entityId } of distinctEntries) {
|
|
826
|
+
const conflicts = await detectConflicts(db, entityType, entityId, rules);
|
|
827
|
+
if (conflicts.length > 0) {
|
|
828
|
+
results.push({ entityId, conflicts });
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return results;
|
|
832
|
+
} catch (error) {
|
|
833
|
+
const err = knowledgeError("KNOWLEDGE_347", { entityType, error: String(error) });
|
|
834
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
835
|
+
throw error;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/knowledge/co-occurrence.ts
|
|
840
|
+
async function buildCoOccurrenceMap(db, entityType, fieldKeyA, fieldKeyB, minOccurrences = 3) {
|
|
841
|
+
try {
|
|
842
|
+
const fieldDefs = await db.entityFieldDefinition.findMany({
|
|
843
|
+
where: { entityType, key: { in: [fieldKeyA, fieldKeyB] }, isActive: true }
|
|
844
|
+
});
|
|
845
|
+
const defA = fieldDefs.find((d) => d.key === fieldKeyA);
|
|
846
|
+
const defB = fieldDefs.find((d) => d.key === fieldKeyB);
|
|
847
|
+
if (!defA || !defB) return /* @__PURE__ */ new Map();
|
|
848
|
+
const entriesA = await db.knowledgeLedgerEntry.findMany({
|
|
849
|
+
where: { entityType, fieldId: defA.id, supersededAt: null },
|
|
850
|
+
take: 1e3
|
|
851
|
+
});
|
|
852
|
+
const entriesB = await db.knowledgeLedgerEntry.findMany({
|
|
853
|
+
where: { entityType, fieldId: defB.id, supersededAt: null },
|
|
854
|
+
take: 1e3
|
|
855
|
+
});
|
|
856
|
+
const bByEntity = /* @__PURE__ */ new Map();
|
|
857
|
+
for (const entry of entriesB) {
|
|
858
|
+
bByEntity.set(entry.entityId, entry.value);
|
|
859
|
+
}
|
|
860
|
+
const coOccurrence = /* @__PURE__ */ new Map();
|
|
861
|
+
const totalPerA = /* @__PURE__ */ new Map();
|
|
862
|
+
for (const entryA of entriesA) {
|
|
863
|
+
const valA = String(entryA.value);
|
|
864
|
+
const valB = bByEntity.get(entryA.entityId);
|
|
865
|
+
if (valB === void 0) continue;
|
|
866
|
+
const strB = String(valB);
|
|
867
|
+
totalPerA.set(valA, (totalPerA.get(valA) ?? 0) + 1);
|
|
868
|
+
if (!coOccurrence.has(valA)) coOccurrence.set(valA, /* @__PURE__ */ new Map());
|
|
869
|
+
const inner = coOccurrence.get(valA);
|
|
870
|
+
inner.set(strB, (inner.get(strB) ?? 0) + 1);
|
|
871
|
+
}
|
|
872
|
+
const result = /* @__PURE__ */ new Map();
|
|
873
|
+
for (const [valA, innerMap] of coOccurrence) {
|
|
874
|
+
const total = totalPerA.get(valA) ?? 0;
|
|
875
|
+
if (total < minOccurrences) continue;
|
|
876
|
+
const rateMap = /* @__PURE__ */ new Map();
|
|
877
|
+
for (const [valB, count] of innerMap) {
|
|
878
|
+
rateMap.set(valB, { count, total, rate: count / total });
|
|
879
|
+
}
|
|
880
|
+
result.set(valA, rateMap);
|
|
881
|
+
}
|
|
882
|
+
return result;
|
|
883
|
+
} catch (error) {
|
|
884
|
+
const err = knowledgeError("KNOWLEDGE_355", { entityType, fieldKeyA, fieldKeyB, error: String(error) });
|
|
885
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
886
|
+
throw error;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// src/knowledge/ghost-fields.ts
|
|
891
|
+
async function predictGhostFields(db, entityType, entityId, options) {
|
|
892
|
+
try {
|
|
893
|
+
const minConfidence = options?.minConfidence ?? 0.3;
|
|
894
|
+
const maxPredictions = options?.maxPredictions ?? 10;
|
|
895
|
+
const currentEntries = await db.knowledgeLedgerEntry.findMany({
|
|
896
|
+
where: { entityType, entityId, supersededAt: null },
|
|
897
|
+
include: { field: true }
|
|
898
|
+
});
|
|
899
|
+
if (currentEntries.length === 0) return [];
|
|
900
|
+
const currentKeys = new Set(currentEntries.map((e) => e.field.key));
|
|
901
|
+
const allFields = await db.entityFieldDefinition.findMany({
|
|
902
|
+
where: { entityType, isActive: true, status: "approved" }
|
|
903
|
+
});
|
|
904
|
+
const missingFields = allFields.filter((f) => !currentKeys.has(f.key));
|
|
905
|
+
if (missingFields.length === 0) return [];
|
|
906
|
+
const predictions = [];
|
|
907
|
+
for (const missingField of missingFields) {
|
|
908
|
+
if (predictions.length >= maxPredictions) break;
|
|
909
|
+
for (const currentEntry of currentEntries) {
|
|
910
|
+
const coMap = await buildCoOccurrenceMap(
|
|
911
|
+
db,
|
|
912
|
+
entityType,
|
|
913
|
+
currentEntry.field.key,
|
|
914
|
+
missingField.key,
|
|
915
|
+
3
|
|
916
|
+
);
|
|
917
|
+
const currentValStr = String(currentEntry.value);
|
|
918
|
+
const rates = coMap.get(currentValStr);
|
|
919
|
+
if (!rates) continue;
|
|
920
|
+
let bestValue;
|
|
921
|
+
let bestRate = 0;
|
|
922
|
+
for (const [val, data] of rates) {
|
|
923
|
+
if (data.rate > bestRate) {
|
|
924
|
+
bestRate = data.rate;
|
|
925
|
+
bestValue = val;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
if (bestValue && bestRate >= minConfidence) {
|
|
929
|
+
const existing = predictions.find((p) => p.fieldKey === missingField.key);
|
|
930
|
+
if (existing) {
|
|
931
|
+
if (bestRate > existing.confidence) {
|
|
932
|
+
existing.predictedValue = bestValue;
|
|
933
|
+
existing.confidence = bestRate;
|
|
934
|
+
existing.basedOn = [{
|
|
935
|
+
fieldKey: currentEntry.field.key,
|
|
936
|
+
value: currentEntry.value,
|
|
937
|
+
coOccurrenceRate: bestRate
|
|
938
|
+
}];
|
|
939
|
+
}
|
|
940
|
+
} else {
|
|
941
|
+
predictions.push({
|
|
942
|
+
fieldKey: missingField.key,
|
|
943
|
+
label: missingField.label,
|
|
944
|
+
predictedValue: bestValue,
|
|
945
|
+
confidence: bestRate,
|
|
946
|
+
basedOn: [{
|
|
947
|
+
fieldKey: currentEntry.field.key,
|
|
948
|
+
value: currentEntry.value,
|
|
949
|
+
coOccurrenceRate: bestRate
|
|
950
|
+
}],
|
|
951
|
+
source: "predicted"
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
return predictions;
|
|
958
|
+
} catch (error) {
|
|
959
|
+
const err = knowledgeError("KNOWLEDGE_355", { entityType, entityId, error: String(error) });
|
|
960
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
961
|
+
throw error;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
async function writeGhostFields(db, entityType, entityId, predictions, callbacks) {
|
|
965
|
+
try {
|
|
966
|
+
let written = 0;
|
|
967
|
+
let skipped = 0;
|
|
968
|
+
for (const prediction of predictions) {
|
|
969
|
+
const fieldDef = await db.entityFieldDefinition.findFirst({
|
|
970
|
+
where: { entityType, key: prediction.fieldKey, isActive: true }
|
|
971
|
+
});
|
|
972
|
+
if (!fieldDef) {
|
|
973
|
+
skipped++;
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
const existing = await db.knowledgeLedgerEntry.findFirst({
|
|
977
|
+
where: {
|
|
978
|
+
fieldId: fieldDef.id,
|
|
979
|
+
entityType,
|
|
980
|
+
entityId,
|
|
981
|
+
supersededAt: null,
|
|
982
|
+
NOT: { source: "predicted" }
|
|
983
|
+
}
|
|
984
|
+
});
|
|
985
|
+
if (existing) {
|
|
986
|
+
skipped++;
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
await db.knowledgeLedgerEntry.updateMany({
|
|
990
|
+
where: {
|
|
991
|
+
fieldId: fieldDef.id,
|
|
992
|
+
entityType,
|
|
993
|
+
entityId,
|
|
994
|
+
supersededAt: null
|
|
995
|
+
},
|
|
996
|
+
data: { supersededAt: /* @__PURE__ */ new Date() }
|
|
997
|
+
});
|
|
998
|
+
await db.knowledgeLedgerEntry.create({
|
|
999
|
+
data: {
|
|
1000
|
+
fieldId: fieldDef.id,
|
|
1001
|
+
entityType,
|
|
1002
|
+
entityId,
|
|
1003
|
+
value: prediction.predictedValue,
|
|
1004
|
+
confidence: prediction.confidence,
|
|
1005
|
+
source: "predicted",
|
|
1006
|
+
sourceId: null,
|
|
1007
|
+
setBy: null
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
written++;
|
|
1011
|
+
}
|
|
1012
|
+
if (written > 0) {
|
|
1013
|
+
try {
|
|
1014
|
+
await callbacks?.logActivity?.(
|
|
1015
|
+
"KNOWLEDGE_GHOST_WRITTEN",
|
|
1016
|
+
`${written} predicted field(s) written for ${entityType} ${entityId}`
|
|
1017
|
+
);
|
|
1018
|
+
} catch {
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
return { written, skipped };
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
const err = knowledgeError("KNOWLEDGE_356", { entityType, entityId, error: String(error) });
|
|
1024
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
1025
|
+
throw error;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// src/knowledge/health.ts
|
|
1030
|
+
function computeOverallScore(completeness, freshness, conflictRate, truthAverage) {
|
|
1031
|
+
return 0.4 * completeness + 0.3 * freshness + 0.2 * (1 - conflictRate) + 0.1 * truthAverage;
|
|
1032
|
+
}
|
|
1033
|
+
async function computeEntityHealth(db, entityType, entityId, conflictRules) {
|
|
1034
|
+
try {
|
|
1035
|
+
const fieldDefs = await db.entityFieldDefinition.findMany({
|
|
1036
|
+
where: { entityType, isActive: true, status: "approved" }
|
|
1037
|
+
});
|
|
1038
|
+
const totalFieldCount = fieldDefs.length;
|
|
1039
|
+
const currentEntries = await db.knowledgeLedgerEntry.findMany({
|
|
1040
|
+
where: { entityType, entityId, supersededAt: null },
|
|
1041
|
+
include: { field: true }
|
|
1042
|
+
});
|
|
1043
|
+
const filledFieldCount = currentEntries.length;
|
|
1044
|
+
const completeness = totalFieldCount > 0 ? Math.min(filledFieldCount / totalFieldCount, 1) : 0;
|
|
1045
|
+
const now = /* @__PURE__ */ new Date();
|
|
1046
|
+
let freshnessSum = 0;
|
|
1047
|
+
let staleFieldCount = 0;
|
|
1048
|
+
for (const entry of currentEntries) {
|
|
1049
|
+
const halfLife = entry.field.halfLifeDays;
|
|
1050
|
+
if (halfLife != null) {
|
|
1051
|
+
const decayed = computeDecayedConfidence(Number(entry.confidence), entry.createdAt, halfLife, now);
|
|
1052
|
+
freshnessSum += decayed / Number(entry.confidence || 1);
|
|
1053
|
+
if (decayed < 0.3) staleFieldCount++;
|
|
1054
|
+
} else {
|
|
1055
|
+
freshnessSum += computeRecencyFactor(entry.createdAt, now);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
const freshness = currentEntries.length > 0 ? freshnessSum / currentEntries.length : 0;
|
|
1059
|
+
const allEntries = await db.knowledgeLedgerEntry.findMany({
|
|
1060
|
+
where: { entityType, entityId },
|
|
1061
|
+
include: { field: true },
|
|
1062
|
+
orderBy: { createdAt: "desc" }
|
|
1063
|
+
});
|
|
1064
|
+
const entriesByField = /* @__PURE__ */ new Map();
|
|
1065
|
+
for (const entry of allEntries) {
|
|
1066
|
+
const key = entry.field.key;
|
|
1067
|
+
if (!entriesByField.has(key)) entriesByField.set(key, []);
|
|
1068
|
+
entriesByField.get(key).push({
|
|
1069
|
+
confidence: Number(entry.confidence),
|
|
1070
|
+
source: entry.source,
|
|
1071
|
+
createdAt: entry.createdAt,
|
|
1072
|
+
supersededAt: entry.supersededAt
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
let truthSum = 0;
|
|
1076
|
+
let truthCount = 0;
|
|
1077
|
+
for (const assertions of entriesByField.values()) {
|
|
1078
|
+
const score = computeTruthScore(assertions, now);
|
|
1079
|
+
truthSum += score;
|
|
1080
|
+
truthCount++;
|
|
1081
|
+
}
|
|
1082
|
+
const truthAverage = truthCount > 0 ? truthSum / truthCount : 0;
|
|
1083
|
+
const conflicts = await detectConflicts(db, entityType, entityId, conflictRules);
|
|
1084
|
+
const conflictCount = conflicts.length;
|
|
1085
|
+
const conflictRate = filledFieldCount > 0 ? Math.min(conflictCount / filledFieldCount, 1) : 0;
|
|
1086
|
+
const overallScore = computeOverallScore(completeness, freshness, conflictRate, truthAverage);
|
|
1087
|
+
const warnings = [];
|
|
1088
|
+
if (completeness < 0.3) warnings.push("Entity has very few fields filled");
|
|
1089
|
+
if (freshness < 0.3) warnings.push("Many fields are stale \u2014 data may be outdated");
|
|
1090
|
+
if (conflictCount > 0) warnings.push(`${conflictCount} contradiction(s) detected`);
|
|
1091
|
+
if (staleFieldCount > 0) warnings.push(`${staleFieldCount} field(s) have decayed below threshold`);
|
|
1092
|
+
return {
|
|
1093
|
+
entityType,
|
|
1094
|
+
entityId,
|
|
1095
|
+
completeness,
|
|
1096
|
+
freshness,
|
|
1097
|
+
conflictRate,
|
|
1098
|
+
truthAverage,
|
|
1099
|
+
overallScore,
|
|
1100
|
+
staleFieldCount,
|
|
1101
|
+
conflictCount,
|
|
1102
|
+
filledFieldCount,
|
|
1103
|
+
totalFieldCount,
|
|
1104
|
+
warnings
|
|
1105
|
+
};
|
|
1106
|
+
} catch (error) {
|
|
1107
|
+
const err = knowledgeError("KNOWLEDGE_360", { entityType, entityId, error: String(error) });
|
|
1108
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
1109
|
+
throw error;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
async function getEntityTypeHealth(db, entityType, options) {
|
|
1113
|
+
try {
|
|
1114
|
+
const limit = options?.limit ?? 200;
|
|
1115
|
+
const worstN = options?.worstN ?? 5;
|
|
1116
|
+
const bestN = options?.bestN ?? 5;
|
|
1117
|
+
const distinctEntries = await db.knowledgeLedgerEntry.findMany({
|
|
1118
|
+
where: { entityType, supersededAt: null },
|
|
1119
|
+
select: { entityId: true },
|
|
1120
|
+
distinct: ["entityId"],
|
|
1121
|
+
take: limit
|
|
1122
|
+
});
|
|
1123
|
+
const entityCount = distinctEntries.length;
|
|
1124
|
+
if (entityCount === 0) {
|
|
1125
|
+
return {
|
|
1126
|
+
entityType,
|
|
1127
|
+
entityCount: 0,
|
|
1128
|
+
avgCompleteness: 0,
|
|
1129
|
+
avgFreshness: 0,
|
|
1130
|
+
avgConflictRate: 0,
|
|
1131
|
+
avgOverallScore: 0,
|
|
1132
|
+
worstEntities: [],
|
|
1133
|
+
bestEntities: []
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
const reports = [];
|
|
1137
|
+
for (const { entityId } of distinctEntries) {
|
|
1138
|
+
const report = await computeEntityHealth(db, entityType, entityId);
|
|
1139
|
+
reports.push(report);
|
|
1140
|
+
}
|
|
1141
|
+
const avgCompleteness = reports.reduce((s, r) => s + r.completeness, 0) / entityCount;
|
|
1142
|
+
const avgFreshness = reports.reduce((s, r) => s + r.freshness, 0) / entityCount;
|
|
1143
|
+
const avgConflictRate = reports.reduce((s, r) => s + r.conflictRate, 0) / entityCount;
|
|
1144
|
+
const avgOverallScore = reports.reduce((s, r) => s + r.overallScore, 0) / entityCount;
|
|
1145
|
+
const sorted = [...reports].sort((a, b) => a.overallScore - b.overallScore);
|
|
1146
|
+
return {
|
|
1147
|
+
entityType,
|
|
1148
|
+
entityCount,
|
|
1149
|
+
avgCompleteness,
|
|
1150
|
+
avgFreshness,
|
|
1151
|
+
avgConflictRate,
|
|
1152
|
+
avgOverallScore,
|
|
1153
|
+
worstEntities: sorted.slice(0, worstN).map((r) => ({
|
|
1154
|
+
entityId: r.entityId,
|
|
1155
|
+
overallScore: r.overallScore
|
|
1156
|
+
})),
|
|
1157
|
+
bestEntities: sorted.slice(-bestN).reverse().map((r) => ({
|
|
1158
|
+
entityId: r.entityId,
|
|
1159
|
+
overallScore: r.overallScore
|
|
1160
|
+
}))
|
|
1161
|
+
};
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
const err = knowledgeError("KNOWLEDGE_361", { entityType, error: String(error) });
|
|
1164
|
+
console.error(`[${err.ref}] ${err.description}`, error);
|
|
1165
|
+
throw error;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// src/agent/knowledge.ts
|
|
1170
|
+
import { Prisma as Prisma2 } from "@prisma/client";
|
|
1171
|
+
var seedChecked = false;
|
|
1172
|
+
async function queryKnowledge(db, input, seedProvider) {
|
|
1173
|
+
if (!seedChecked) {
|
|
1174
|
+
await ensureKnowledgeDefaults(db, seedProvider);
|
|
1175
|
+
seedChecked = true;
|
|
1176
|
+
}
|
|
1177
|
+
const { entity, field, search, category, limit } = input;
|
|
1178
|
+
const where = {
|
|
1179
|
+
entity,
|
|
1180
|
+
isActive: true
|
|
1181
|
+
};
|
|
1182
|
+
if (field) where.field = field;
|
|
1183
|
+
if (category) where.category = category;
|
|
1184
|
+
if (search) {
|
|
1185
|
+
const searchLower = search.toLowerCase();
|
|
1186
|
+
const terms = searchLower.split(/\s+/).filter(Boolean);
|
|
1187
|
+
const entries = await db.agentKnowledge.findMany({
|
|
1188
|
+
where: {
|
|
1189
|
+
...where,
|
|
1190
|
+
OR: [
|
|
1191
|
+
{ value: { contains: searchLower, mode: "insensitive" } },
|
|
1192
|
+
{ label: { contains: searchLower, mode: "insensitive" } }
|
|
1193
|
+
]
|
|
1194
|
+
},
|
|
1195
|
+
orderBy: [{ sortOrder: "asc" }, { label: "asc" }],
|
|
1196
|
+
take: limit
|
|
1197
|
+
});
|
|
1198
|
+
const aliasEntries = await db.agentKnowledge.findMany({
|
|
1199
|
+
where: {
|
|
1200
|
+
...where,
|
|
1201
|
+
id: { notIn: entries.map((e) => e.id) }
|
|
1202
|
+
},
|
|
1203
|
+
orderBy: [{ sortOrder: "asc" }, { label: "asc" }]
|
|
1204
|
+
});
|
|
1205
|
+
const aliasMatches = aliasEntries.filter(
|
|
1206
|
+
(entry) => entry.aliases.some(
|
|
1207
|
+
(alias) => terms.every((term) => alias.toLowerCase().includes(term))
|
|
1208
|
+
)
|
|
1209
|
+
);
|
|
1210
|
+
return [...entries, ...aliasMatches].slice(0, limit);
|
|
1211
|
+
}
|
|
1212
|
+
return db.agentKnowledge.findMany({
|
|
1213
|
+
where,
|
|
1214
|
+
orderBy: [{ sortOrder: "asc" }, { label: "asc" }],
|
|
1215
|
+
take: limit
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
async function createKnowledgeEntry(db, input) {
|
|
1219
|
+
const existing = await db.agentKnowledge.findUnique({
|
|
1220
|
+
where: {
|
|
1221
|
+
entity_field_value: {
|
|
1222
|
+
entity: input.entity,
|
|
1223
|
+
field: input.field,
|
|
1224
|
+
value: input.value
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
if (existing) {
|
|
1229
|
+
throw agentKnowledgeError("AGENTKNOW_403", {
|
|
1230
|
+
entity: input.entity,
|
|
1231
|
+
field: input.field,
|
|
1232
|
+
value: input.value
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
return db.agentKnowledge.create({
|
|
1236
|
+
data: {
|
|
1237
|
+
entity: input.entity,
|
|
1238
|
+
field: input.field,
|
|
1239
|
+
value: input.value,
|
|
1240
|
+
label: input.label,
|
|
1241
|
+
aliases: input.aliases,
|
|
1242
|
+
category: input.category ?? null,
|
|
1243
|
+
metadata: input.metadata ?? Prisma2.JsonNull,
|
|
1244
|
+
sortOrder: input.sortOrder,
|
|
1245
|
+
source: "manual"
|
|
1246
|
+
}
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
async function updateKnowledgeEntry(db, id, input) {
|
|
1250
|
+
const existing = await db.agentKnowledge.findUnique({ where: { id } });
|
|
1251
|
+
if (!existing) {
|
|
1252
|
+
throw agentKnowledgeError("AGENTKNOW_404", { id });
|
|
1253
|
+
}
|
|
1254
|
+
return db.agentKnowledge.update({
|
|
1255
|
+
where: { id },
|
|
1256
|
+
data: {
|
|
1257
|
+
...input.label !== void 0 && { label: input.label },
|
|
1258
|
+
...input.aliases !== void 0 && { aliases: input.aliases },
|
|
1259
|
+
...input.category !== void 0 && { category: input.category },
|
|
1260
|
+
...input.metadata !== void 0 && { metadata: input.metadata },
|
|
1261
|
+
...input.sortOrder !== void 0 && { sortOrder: input.sortOrder },
|
|
1262
|
+
...input.isActive !== void 0 && { isActive: input.isActive }
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
async function deleteKnowledgeEntry(db, id) {
|
|
1267
|
+
const existing = await db.agentKnowledge.findUnique({ where: { id } });
|
|
1268
|
+
if (!existing) {
|
|
1269
|
+
throw agentKnowledgeError("AGENTKNOW_405", { id });
|
|
1270
|
+
}
|
|
1271
|
+
return db.agentKnowledge.delete({ where: { id } });
|
|
1272
|
+
}
|
|
1273
|
+
async function ensureKnowledgeDefaults(db, seedProvider) {
|
|
1274
|
+
const count = await db.agentKnowledge.count();
|
|
1275
|
+
if (count > 0) return;
|
|
1276
|
+
const seedData = seedProvider?.getKnowledgeSeedData?.();
|
|
1277
|
+
if (!seedData || seedData.length === 0) return;
|
|
1278
|
+
try {
|
|
1279
|
+
await db.agentKnowledge.createMany({
|
|
1280
|
+
data: seedData.map((entry) => ({
|
|
1281
|
+
...entry,
|
|
1282
|
+
metadata: entry.metadata === null ? Prisma2.JsonNull : entry.metadata
|
|
1283
|
+
})),
|
|
1284
|
+
skipDuplicates: true
|
|
1285
|
+
});
|
|
1286
|
+
} catch (error) {
|
|
1287
|
+
console.error(
|
|
1288
|
+
"[AGENTKNOW_409] Failed to seed knowledge defaults:",
|
|
1289
|
+
error instanceof Error ? error.message : error
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
async function resetKnowledgeDefaults(db, seedProvider) {
|
|
1294
|
+
const seedData = seedProvider?.getKnowledgeSeedData?.();
|
|
1295
|
+
if (!seedData || seedData.length === 0) {
|
|
1296
|
+
throw agentKnowledgeError("AGENTKNOW_410", { error: "No seed data provider available" });
|
|
1297
|
+
}
|
|
1298
|
+
try {
|
|
1299
|
+
await db.$transaction(async (tx) => {
|
|
1300
|
+
await tx.agentKnowledge.deleteMany({ where: { source: "seed" } });
|
|
1301
|
+
await tx.agentKnowledge.createMany({
|
|
1302
|
+
data: seedData.map((entry) => ({
|
|
1303
|
+
...entry,
|
|
1304
|
+
metadata: entry.metadata === null ? Prisma2.JsonNull : entry.metadata
|
|
1305
|
+
})),
|
|
1306
|
+
skipDuplicates: true
|
|
1307
|
+
});
|
|
1308
|
+
});
|
|
1309
|
+
seedChecked = false;
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
throw agentKnowledgeError("AGENTKNOW_410", {
|
|
1312
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
function resetSeedCheck() {
|
|
1317
|
+
seedChecked = false;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// src/agent/preferences.ts
|
|
1321
|
+
import { Prisma as Prisma3 } from "@prisma/client";
|
|
1322
|
+
async function getPreferences(db, input) {
|
|
1323
|
+
const { entity, userId, field } = input;
|
|
1324
|
+
const where = {
|
|
1325
|
+
entity,
|
|
1326
|
+
isActive: true
|
|
1327
|
+
};
|
|
1328
|
+
if (field) where.field = field;
|
|
1329
|
+
if (userId) {
|
|
1330
|
+
where.OR = [{ userId: null }, { userId }];
|
|
1331
|
+
} else {
|
|
1332
|
+
where.userId = null;
|
|
1333
|
+
}
|
|
1334
|
+
return db.agentPreference.findMany({
|
|
1335
|
+
where,
|
|
1336
|
+
orderBy: [{ priority: "desc" }, { createdAt: "asc" }]
|
|
1337
|
+
});
|
|
1338
|
+
}
|
|
1339
|
+
async function setPreference(db, input) {
|
|
1340
|
+
const existing = await db.agentPreference.findFirst({
|
|
1341
|
+
where: {
|
|
1342
|
+
entity: input.entity,
|
|
1343
|
+
field: input.field,
|
|
1344
|
+
userId: input.userId ?? null
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
if (existing) {
|
|
1348
|
+
return db.agentPreference.update({
|
|
1349
|
+
where: { id: existing.id },
|
|
1350
|
+
data: {
|
|
1351
|
+
value: input.value,
|
|
1352
|
+
label: input.label,
|
|
1353
|
+
conditions: input.conditions ?? Prisma3.JsonNull,
|
|
1354
|
+
priority: input.priority
|
|
1355
|
+
}
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
return db.agentPreference.create({
|
|
1359
|
+
data: {
|
|
1360
|
+
entity: input.entity,
|
|
1361
|
+
field: input.field,
|
|
1362
|
+
value: input.value,
|
|
1363
|
+
label: input.label,
|
|
1364
|
+
conditions: input.conditions ?? Prisma3.JsonNull,
|
|
1365
|
+
userId: input.userId ?? null,
|
|
1366
|
+
priority: input.priority
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
async function deletePreference(db, id) {
|
|
1371
|
+
const existing = await db.agentPreference.findUnique({ where: { id } });
|
|
1372
|
+
if (!existing) {
|
|
1373
|
+
throw agentKnowledgeError("AGENTKNOW_408", { id });
|
|
1374
|
+
}
|
|
1375
|
+
return db.agentPreference.delete({ where: { id } });
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// src/agent/analytics.ts
|
|
1379
|
+
var INTENT_PATTERNS = [
|
|
1380
|
+
{ pattern: /^\/report\b/i, intent: "report" },
|
|
1381
|
+
{ pattern: /^\/tour\b/i, intent: "tour" },
|
|
1382
|
+
{ pattern: /^\/automation\b/i, intent: "command" },
|
|
1383
|
+
{ pattern: /\b(generate|create|make|build|add|set up|configure)\b/i, intent: "command" },
|
|
1384
|
+
{ pattern: /\b(delete|remove|disable|turn off)\b/i, intent: "command" },
|
|
1385
|
+
{ pattern: /\b(update|change|modify|edit|fix)\b/i, intent: "command" },
|
|
1386
|
+
{ pattern: /\b(how|what|why|when|where|explain|tell me|describe)\b/i, intent: "question" },
|
|
1387
|
+
{ pattern: /\b(show|find|search|list|get|check|look up)\b/i, intent: "search" }
|
|
1388
|
+
];
|
|
1389
|
+
var TOPIC_KEYWORDS = {
|
|
1390
|
+
"email campaigns": ["email campaign", "newsletter", "mass email", "email blast", "email marketing"],
|
|
1391
|
+
"email templates": ["email template", "template", "email format"],
|
|
1392
|
+
"fabric stock": ["fabric", "stock", "in stock", "out of stock", "availability"],
|
|
1393
|
+
"order management": ["order", "placement", "order status", "delivery"],
|
|
1394
|
+
"client management": ["client", "customer", "contact", "profile"],
|
|
1395
|
+
"fit profiles": ["fit profile", "measurement", "try-on", "size", "adjustment"],
|
|
1396
|
+
"tax operations": ["tax", "transaction", "categorize", "deduction", "irs"],
|
|
1397
|
+
"reports": ["report", "export", "pdf", "csv", "analytics", "summary"],
|
|
1398
|
+
"automations": ["automation", "workflow", "trigger", "automate"],
|
|
1399
|
+
"scheduling": ["schedule", "calendar", "deadline", "reminder"],
|
|
1400
|
+
"billing": ["billing", "invoice", "payment", "charge"],
|
|
1401
|
+
"inventory": ["inventory", "stock", "supply", "reorder"],
|
|
1402
|
+
"social media": ["social media", "post", "tweet", "instagram", "facebook"],
|
|
1403
|
+
"customer support": ["support", "ticket", "complaint", "help desk"],
|
|
1404
|
+
"onboarding": ["onboarding", "training", "getting started", "tutorial"]
|
|
1405
|
+
};
|
|
1406
|
+
function classifyIntent(query) {
|
|
1407
|
+
const normalized = query.trim().toLowerCase();
|
|
1408
|
+
for (const { pattern, intent } of INTENT_PATTERNS) {
|
|
1409
|
+
if (pattern.test(normalized)) return intent;
|
|
1410
|
+
}
|
|
1411
|
+
if (normalized.includes("?")) return "question";
|
|
1412
|
+
return "search";
|
|
1413
|
+
}
|
|
1414
|
+
function extractTopic(query) {
|
|
1415
|
+
const normalized = query.toLowerCase();
|
|
1416
|
+
let bestTopic = null;
|
|
1417
|
+
let bestScore = 0;
|
|
1418
|
+
for (const [topic, keywords] of Object.entries(TOPIC_KEYWORDS)) {
|
|
1419
|
+
for (const keyword of keywords) {
|
|
1420
|
+
if (normalized.includes(keyword)) {
|
|
1421
|
+
const score = keyword.length;
|
|
1422
|
+
if (score > bestScore) {
|
|
1423
|
+
bestScore = score;
|
|
1424
|
+
bestTopic = topic;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
return bestTopic;
|
|
1430
|
+
}
|
|
1431
|
+
function extractModules(toolNames) {
|
|
1432
|
+
const MODULE_MAP = {
|
|
1433
|
+
email: "email",
|
|
1434
|
+
template: "email",
|
|
1435
|
+
automation: "automation",
|
|
1436
|
+
order: "orders",
|
|
1437
|
+
client: "clients",
|
|
1438
|
+
fabric: "fabric",
|
|
1439
|
+
tax: "tax",
|
|
1440
|
+
transaction: "tax",
|
|
1441
|
+
report: "reports",
|
|
1442
|
+
tour: "tours",
|
|
1443
|
+
task: "tasks",
|
|
1444
|
+
activity: "activities",
|
|
1445
|
+
navigate: "navigation",
|
|
1446
|
+
setting: "settings"
|
|
1447
|
+
};
|
|
1448
|
+
const modules = /* @__PURE__ */ new Set();
|
|
1449
|
+
for (const tool of toolNames) {
|
|
1450
|
+
const lower = tool.toLowerCase();
|
|
1451
|
+
for (const [keyword, mod] of Object.entries(MODULE_MAP)) {
|
|
1452
|
+
if (lower.includes(keyword)) {
|
|
1453
|
+
modules.add(mod);
|
|
1454
|
+
break;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
return Array.from(modules);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// src/agent/interactions.ts
|
|
1462
|
+
async function logInteraction(db, input) {
|
|
1463
|
+
const intent = classifyIntent(input.query);
|
|
1464
|
+
const topic = extractTopic(input.query);
|
|
1465
|
+
const modules = extractModules(input.toolsUsed ?? []);
|
|
1466
|
+
return db.agentInteraction.create({
|
|
1467
|
+
data: {
|
|
1468
|
+
userId: input.userId,
|
|
1469
|
+
query: input.query,
|
|
1470
|
+
intent,
|
|
1471
|
+
topic,
|
|
1472
|
+
modules,
|
|
1473
|
+
toolsUsed: input.toolsUsed ?? [],
|
|
1474
|
+
sessionId: input.sessionId ?? null,
|
|
1475
|
+
satisfied: null
|
|
1476
|
+
}
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
async function markSatisfied(db, id, satisfied) {
|
|
1480
|
+
return db.agentInteraction.update({
|
|
1481
|
+
where: { id },
|
|
1482
|
+
data: { satisfied }
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
async function getTopicDistribution(db, days = 30) {
|
|
1486
|
+
const since = /* @__PURE__ */ new Date();
|
|
1487
|
+
since.setDate(since.getDate() - days);
|
|
1488
|
+
const interactions = await db.agentInteraction.findMany({
|
|
1489
|
+
where: {
|
|
1490
|
+
createdAt: { gte: since },
|
|
1491
|
+
topic: { not: null }
|
|
1492
|
+
},
|
|
1493
|
+
select: {
|
|
1494
|
+
topic: true,
|
|
1495
|
+
toolsUsed: true,
|
|
1496
|
+
modules: true
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
const topicMap = /* @__PURE__ */ new Map();
|
|
1500
|
+
for (const interaction of interactions) {
|
|
1501
|
+
if (!interaction.topic) continue;
|
|
1502
|
+
const existing = topicMap.get(interaction.topic) ?? {
|
|
1503
|
+
count: 0,
|
|
1504
|
+
modules: /* @__PURE__ */ new Set(),
|
|
1505
|
+
tools: /* @__PURE__ */ new Set()
|
|
1506
|
+
};
|
|
1507
|
+
existing.count++;
|
|
1508
|
+
for (const m of interaction.modules) existing.modules.add(m);
|
|
1509
|
+
for (const t of interaction.toolsUsed) existing.tools.add(t);
|
|
1510
|
+
topicMap.set(interaction.topic, existing);
|
|
1511
|
+
}
|
|
1512
|
+
return Array.from(topicMap.entries()).map(([topic, data]) => ({
|
|
1513
|
+
topic,
|
|
1514
|
+
count: data.count,
|
|
1515
|
+
modules: Array.from(data.modules),
|
|
1516
|
+
tools: Array.from(data.tools)
|
|
1517
|
+
})).sort((a, b) => b.count - a.count);
|
|
1518
|
+
}
|
|
1519
|
+
async function getRecentInteractions(db, userId, limit = 20) {
|
|
1520
|
+
return db.agentInteraction.findMany({
|
|
1521
|
+
where: { userId },
|
|
1522
|
+
orderBy: { createdAt: "desc" },
|
|
1523
|
+
take: limit
|
|
1524
|
+
});
|
|
1525
|
+
}
|
|
1526
|
+
async function getInteractionCount(db, days = 30) {
|
|
1527
|
+
const since = /* @__PURE__ */ new Date();
|
|
1528
|
+
since.setDate(since.getDate() - days);
|
|
1529
|
+
return db.agentInteraction.count({
|
|
1530
|
+
where: { createdAt: { gte: since } }
|
|
1531
|
+
});
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
// src/agent/suggestions.ts
|
|
1535
|
+
var TOPIC_TO_AGENT = {
|
|
1536
|
+
"email campaigns": {
|
|
1537
|
+
name: "Marketing Agent",
|
|
1538
|
+
category: "communication",
|
|
1539
|
+
description: "Handles email campaigns, newsletter scheduling, and audience segmentation."
|
|
1540
|
+
},
|
|
1541
|
+
"social media": {
|
|
1542
|
+
name: "Social Media Agent",
|
|
1543
|
+
category: "communication",
|
|
1544
|
+
description: "Manages social media posts, scheduling, and engagement tracking."
|
|
1545
|
+
},
|
|
1546
|
+
"customer support": {
|
|
1547
|
+
name: "Support Agent",
|
|
1548
|
+
category: "service",
|
|
1549
|
+
description: "Handles support tickets, customer complaints, and FAQ automation."
|
|
1550
|
+
},
|
|
1551
|
+
"reports": {
|
|
1552
|
+
name: "Analytics Agent",
|
|
1553
|
+
category: "analytics",
|
|
1554
|
+
description: "Generates custom reports, dashboards, and data analysis."
|
|
1555
|
+
},
|
|
1556
|
+
"inventory": {
|
|
1557
|
+
name: "Inventory Agent",
|
|
1558
|
+
category: "operations",
|
|
1559
|
+
description: "Tracks stock levels, reorder points, and supply chain status."
|
|
1560
|
+
},
|
|
1561
|
+
"billing": {
|
|
1562
|
+
name: "Billing Agent",
|
|
1563
|
+
category: "finance",
|
|
1564
|
+
description: "Manages invoices, payment tracking, and billing automation."
|
|
1565
|
+
},
|
|
1566
|
+
"onboarding": {
|
|
1567
|
+
name: "Onboarding Agent",
|
|
1568
|
+
category: "workflow",
|
|
1569
|
+
description: "Guides new users through setup, training, and documentation."
|
|
1570
|
+
},
|
|
1571
|
+
"scheduling": {
|
|
1572
|
+
name: "Scheduling Agent",
|
|
1573
|
+
category: "workflow",
|
|
1574
|
+
description: "Manages appointments, calendars, and deadline tracking."
|
|
1575
|
+
}
|
|
1576
|
+
};
|
|
1577
|
+
var SPECIALIST_TOPICS = /* @__PURE__ */ new Set([
|
|
1578
|
+
"tax operations",
|
|
1579
|
+
"order management",
|
|
1580
|
+
"fabric stock",
|
|
1581
|
+
"client management",
|
|
1582
|
+
"fit profiles",
|
|
1583
|
+
"email templates",
|
|
1584
|
+
"automations"
|
|
1585
|
+
]);
|
|
1586
|
+
var MIN_QUERY_THRESHOLD = 10;
|
|
1587
|
+
var DISMISS_COOLDOWN_DAYS = 90;
|
|
1588
|
+
async function analyzeAndSuggest(db) {
|
|
1589
|
+
const topics = await getTopicDistribution(db, 30);
|
|
1590
|
+
const totalCount = await getInteractionCount(db, 30);
|
|
1591
|
+
let created = 0;
|
|
1592
|
+
for (const topicData of topics) {
|
|
1593
|
+
if (SPECIALIST_TOPICS.has(topicData.topic)) continue;
|
|
1594
|
+
if (topicData.count < MIN_QUERY_THRESHOLD) continue;
|
|
1595
|
+
const proposal = TOPIC_TO_AGENT[topicData.topic];
|
|
1596
|
+
if (!proposal) continue;
|
|
1597
|
+
const topicConsistency = totalCount > 0 ? topicData.count / totalCount : 0;
|
|
1598
|
+
const volumeScore = Math.min(1, topicData.count / 50);
|
|
1599
|
+
const confidence = Number((volumeScore * (0.5 + topicConsistency * 0.5)).toFixed(3));
|
|
1600
|
+
const evidence = `${topicData.count} queries about "${topicData.topic}" in the last 30 days (${Math.round(topicConsistency * 100)}% of all queries).`;
|
|
1601
|
+
const existing = await db.agentSuggestion.findFirst({
|
|
1602
|
+
where: { name: proposal.name }
|
|
1603
|
+
});
|
|
1604
|
+
if (existing) {
|
|
1605
|
+
if (existing.status === "dismissed" && existing.dismissedAt && Date.now() - existing.dismissedAt.getTime() < DISMISS_COOLDOWN_DAYS * 24 * 60 * 60 * 1e3) {
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
await db.agentSuggestion.update({
|
|
1609
|
+
where: { id: existing.id },
|
|
1610
|
+
data: {
|
|
1611
|
+
confidence,
|
|
1612
|
+
evidence,
|
|
1613
|
+
queryCount: topicData.count,
|
|
1614
|
+
suggestedTools: topicData.tools,
|
|
1615
|
+
status: existing.status === "dismissed" ? "pending" : existing.status,
|
|
1616
|
+
dismissedAt: existing.status === "dismissed" ? null : existing.dismissedAt,
|
|
1617
|
+
dismissedBy: existing.status === "dismissed" ? null : existing.dismissedBy
|
|
1618
|
+
}
|
|
1619
|
+
});
|
|
1620
|
+
} else {
|
|
1621
|
+
await db.agentSuggestion.create({
|
|
1622
|
+
data: {
|
|
1623
|
+
name: proposal.name,
|
|
1624
|
+
description: proposal.description,
|
|
1625
|
+
category: proposal.category,
|
|
1626
|
+
evidence,
|
|
1627
|
+
confidence,
|
|
1628
|
+
queryCount: topicData.count,
|
|
1629
|
+
suggestedTools: topicData.tools,
|
|
1630
|
+
status: "pending"
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
created++;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
return created;
|
|
1637
|
+
}
|
|
1638
|
+
async function getSuggestions(db, status) {
|
|
1639
|
+
return db.agentSuggestion.findMany({
|
|
1640
|
+
where: status ? { status } : void 0,
|
|
1641
|
+
orderBy: { confidence: "desc" }
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
async function dismissSuggestion(db, id, userId) {
|
|
1645
|
+
return db.agentSuggestion.update({
|
|
1646
|
+
where: { id },
|
|
1647
|
+
data: {
|
|
1648
|
+
status: "dismissed",
|
|
1649
|
+
dismissedBy: userId,
|
|
1650
|
+
dismissedAt: /* @__PURE__ */ new Date()
|
|
1651
|
+
}
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
async function approveSuggestion(db, id) {
|
|
1655
|
+
return db.agentSuggestion.update({
|
|
1656
|
+
where: { id },
|
|
1657
|
+
data: { status: "approved" }
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// src/notes/notes.ts
|
|
1662
|
+
async function createNote(db, input, callbacks) {
|
|
1663
|
+
const note = await db.clientNote.create({
|
|
1664
|
+
data: {
|
|
1665
|
+
clientId: input.entityId,
|
|
1666
|
+
content: input.content,
|
|
1667
|
+
contentHtml: input.contentHtml ?? null,
|
|
1668
|
+
isDraft: input.isDraft ?? false,
|
|
1669
|
+
authorId: input.authorId ?? null,
|
|
1670
|
+
source: input.source ?? "MANUAL",
|
|
1671
|
+
...input.attachments && input.attachments.length > 0 ? {
|
|
1672
|
+
attachments: {
|
|
1673
|
+
create: input.attachments.map((a) => ({
|
|
1674
|
+
filename: a.filename,
|
|
1675
|
+
mimeType: a.mimeType,
|
|
1676
|
+
size: a.size,
|
|
1677
|
+
storagePath: a.storagePath
|
|
1678
|
+
}))
|
|
1679
|
+
}
|
|
1680
|
+
} : {}
|
|
1681
|
+
},
|
|
1682
|
+
include: {
|
|
1683
|
+
author: { select: { id: true, name: true } },
|
|
1684
|
+
attachments: true
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1687
|
+
if (!input.isDraft) {
|
|
1688
|
+
try {
|
|
1689
|
+
const truncated = truncateText(input.content, 200);
|
|
1690
|
+
await callbacks?.logActivity?.(
|
|
1691
|
+
"CLIENT_NOTE_ADDED",
|
|
1692
|
+
truncated,
|
|
1693
|
+
{
|
|
1694
|
+
userId: input.authorId ?? null,
|
|
1695
|
+
clientId: input.entityId,
|
|
1696
|
+
metadata: {
|
|
1697
|
+
noteId: note.id,
|
|
1698
|
+
contentPreview: truncateText(input.content, 500),
|
|
1699
|
+
source: input.source ?? "MANUAL"
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
);
|
|
1703
|
+
} catch {
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
if (input.fields && Object.keys(input.fields).length > 0) {
|
|
1707
|
+
try {
|
|
1708
|
+
const source = input.source === "AGENT" ? "agent" : "note";
|
|
1709
|
+
await callbacks?.setEntityFields?.(
|
|
1710
|
+
input.entityType,
|
|
1711
|
+
input.entityId,
|
|
1712
|
+
input.fields,
|
|
1713
|
+
source,
|
|
1714
|
+
note.id,
|
|
1715
|
+
1,
|
|
1716
|
+
input.authorId ?? void 0
|
|
1717
|
+
);
|
|
1718
|
+
} catch {
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
return note;
|
|
1722
|
+
}
|
|
1723
|
+
async function updateNote(db, noteId, entityId, input, callbacks) {
|
|
1724
|
+
const note = await db.clientNote.findFirst({
|
|
1725
|
+
where: { id: noteId, clientId: entityId }
|
|
1726
|
+
});
|
|
1727
|
+
if (!note) return null;
|
|
1728
|
+
const isPublishing = note.isDraft && input.isDraft === false;
|
|
1729
|
+
const updated = await db.clientNote.update({
|
|
1730
|
+
where: { id: noteId },
|
|
1731
|
+
data: {
|
|
1732
|
+
...input.content !== void 0 ? { content: input.content } : {},
|
|
1733
|
+
...input.contentHtml !== void 0 ? { contentHtml: input.contentHtml } : {},
|
|
1734
|
+
...input.isDraft !== void 0 ? { isDraft: input.isDraft } : {}
|
|
1735
|
+
},
|
|
1736
|
+
include: {
|
|
1737
|
+
author: { select: { id: true, name: true } },
|
|
1738
|
+
attachments: true
|
|
1739
|
+
}
|
|
1740
|
+
});
|
|
1741
|
+
if (input.attachments !== void 0) {
|
|
1742
|
+
await db.noteAttachment.deleteMany({ where: { noteId } });
|
|
1743
|
+
if (input.attachments.length > 0) {
|
|
1744
|
+
await db.noteAttachment.createMany({
|
|
1745
|
+
data: input.attachments.map((a) => ({
|
|
1746
|
+
noteId,
|
|
1747
|
+
filename: a.filename,
|
|
1748
|
+
mimeType: a.mimeType,
|
|
1749
|
+
size: a.size,
|
|
1750
|
+
storagePath: a.storagePath
|
|
1751
|
+
}))
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
if (isPublishing) {
|
|
1756
|
+
try {
|
|
1757
|
+
const noteContent = input.content ?? note.content;
|
|
1758
|
+
const truncated = truncateText(noteContent, 200);
|
|
1759
|
+
await callbacks?.logActivity?.(
|
|
1760
|
+
"CLIENT_NOTE_ADDED",
|
|
1761
|
+
truncated,
|
|
1762
|
+
{
|
|
1763
|
+
userId: note.authorId ?? null,
|
|
1764
|
+
clientId: entityId,
|
|
1765
|
+
metadata: {
|
|
1766
|
+
noteId,
|
|
1767
|
+
contentPreview: truncateText(noteContent, 500),
|
|
1768
|
+
source: note.source ?? "MANUAL"
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
);
|
|
1772
|
+
} catch {
|
|
1773
|
+
}
|
|
1774
|
+
}
|
|
1775
|
+
return db.clientNote.findUnique({
|
|
1776
|
+
where: { id: noteId },
|
|
1777
|
+
include: {
|
|
1778
|
+
author: { select: { id: true, name: true } },
|
|
1779
|
+
attachments: true
|
|
1780
|
+
}
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
async function getNotes(db, { entityId, limit = 50, cursor, isDraft }) {
|
|
1784
|
+
const notes = await db.clientNote.findMany({
|
|
1785
|
+
where: {
|
|
1786
|
+
clientId: entityId,
|
|
1787
|
+
...isDraft !== void 0 ? { isDraft } : { isDraft: false }
|
|
1788
|
+
},
|
|
1789
|
+
orderBy: { createdAt: "desc" },
|
|
1790
|
+
take: limit + 1,
|
|
1791
|
+
...cursor ? { cursor: { id: cursor }, skip: 1 } : {},
|
|
1792
|
+
include: {
|
|
1793
|
+
author: { select: { id: true, name: true } },
|
|
1794
|
+
attachments: true
|
|
1795
|
+
}
|
|
1796
|
+
});
|
|
1797
|
+
const hasMore = notes.length > limit;
|
|
1798
|
+
const items = hasMore ? notes.slice(0, limit) : notes;
|
|
1799
|
+
const nextCursor = hasMore ? items[items.length - 1]?.id : void 0;
|
|
1800
|
+
return { items, nextCursor, hasMore };
|
|
1801
|
+
}
|
|
1802
|
+
async function deleteNote(db, noteId, entityId) {
|
|
1803
|
+
const note = await db.clientNote.findFirst({
|
|
1804
|
+
where: { id: noteId, clientId: entityId }
|
|
1805
|
+
});
|
|
1806
|
+
if (!note) return null;
|
|
1807
|
+
await db.clientNote.delete({ where: { id: noteId } });
|
|
1808
|
+
return note;
|
|
1809
|
+
}
|
|
1810
|
+
function truncateText(text, maxLength) {
|
|
1811
|
+
if (!text || text.length <= maxLength) return text;
|
|
1812
|
+
return text.slice(0, maxLength).trimEnd() + "...";
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1815
|
+
// src/contacts/contacts.ts
|
|
1816
|
+
async function getAllContacts(db, options) {
|
|
1817
|
+
const {
|
|
1818
|
+
limit = 50,
|
|
1819
|
+
offset = 0,
|
|
1820
|
+
source,
|
|
1821
|
+
search,
|
|
1822
|
+
excludeConverted = true
|
|
1823
|
+
} = options ?? {};
|
|
1824
|
+
const where = {};
|
|
1825
|
+
if (source) where.source = source;
|
|
1826
|
+
if (excludeConverted) where.convertedToClientId = null;
|
|
1827
|
+
if (search) {
|
|
1828
|
+
where.OR = [
|
|
1829
|
+
{ name: { contains: search, mode: "insensitive" } },
|
|
1830
|
+
{ email: { contains: search, mode: "insensitive" } }
|
|
1831
|
+
];
|
|
1832
|
+
}
|
|
1833
|
+
const [contacts, total] = await Promise.all([
|
|
1834
|
+
db.contact.findMany({
|
|
1835
|
+
where,
|
|
1836
|
+
include: {
|
|
1837
|
+
tags: { include: { tag: true } },
|
|
1838
|
+
_count: { select: { linkedEmails: true } }
|
|
1839
|
+
},
|
|
1840
|
+
orderBy: { createdAt: "desc" },
|
|
1841
|
+
take: limit,
|
|
1842
|
+
skip: offset
|
|
1843
|
+
}),
|
|
1844
|
+
db.contact.count({ where })
|
|
1845
|
+
]);
|
|
1846
|
+
return { contacts, total, limit, offset };
|
|
1847
|
+
}
|
|
1848
|
+
async function getContactById(db, id) {
|
|
1849
|
+
const contact = await db.contact.findUnique({
|
|
1850
|
+
where: { id },
|
|
1851
|
+
include: {
|
|
1852
|
+
tags: { include: { tag: true } },
|
|
1853
|
+
linkedEmails: {
|
|
1854
|
+
take: 10,
|
|
1855
|
+
orderBy: { emailDate: "desc" },
|
|
1856
|
+
select: {
|
|
1857
|
+
id: true,
|
|
1858
|
+
subject: true,
|
|
1859
|
+
snippet: true,
|
|
1860
|
+
emailDate: true,
|
|
1861
|
+
status: true
|
|
1862
|
+
}
|
|
1863
|
+
},
|
|
1864
|
+
_count: { select: { linkedEmails: true } }
|
|
1865
|
+
}
|
|
1866
|
+
});
|
|
1867
|
+
if (!contact) {
|
|
1868
|
+
throw new ContactNotFoundError(id);
|
|
1869
|
+
}
|
|
1870
|
+
return contact;
|
|
1871
|
+
}
|
|
1872
|
+
async function getContactByEmail(db, email) {
|
|
1873
|
+
return db.contact.findUnique({
|
|
1874
|
+
where: { email: email.toLowerCase() }
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
async function createContact(db, params, callbacks) {
|
|
1878
|
+
const normalizedEmail = params.email.toLowerCase();
|
|
1879
|
+
const existing = await db.contact.findUnique({
|
|
1880
|
+
where: { email: normalizedEmail }
|
|
1881
|
+
});
|
|
1882
|
+
if (existing) {
|
|
1883
|
+
throw new ContactExistsError(normalizedEmail);
|
|
1884
|
+
}
|
|
1885
|
+
const contact = await db.contact.create({
|
|
1886
|
+
data: {
|
|
1887
|
+
email: normalizedEmail,
|
|
1888
|
+
name: params.name ?? null,
|
|
1889
|
+
notes: params.notes ?? null,
|
|
1890
|
+
source: params.source ?? "MANUAL"
|
|
1891
|
+
}
|
|
1892
|
+
});
|
|
1893
|
+
try {
|
|
1894
|
+
await callbacks?.emitEvent?.("CONTACT_CREATED", {
|
|
1895
|
+
entityType: "contact",
|
|
1896
|
+
entityId: contact.id,
|
|
1897
|
+
data: {
|
|
1898
|
+
email: contact.email,
|
|
1899
|
+
name: contact.name,
|
|
1900
|
+
source: contact.source
|
|
1901
|
+
}
|
|
1902
|
+
});
|
|
1903
|
+
} catch {
|
|
1904
|
+
}
|
|
1905
|
+
if (params.fields && Object.keys(params.fields).length > 0) {
|
|
1906
|
+
try {
|
|
1907
|
+
await callbacks?.setEntityFields?.(
|
|
1908
|
+
"contact",
|
|
1909
|
+
contact.id,
|
|
1910
|
+
params.fields,
|
|
1911
|
+
params.source ?? "MANUAL"
|
|
1912
|
+
);
|
|
1913
|
+
} catch {
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
return contact;
|
|
1917
|
+
}
|
|
1918
|
+
async function updateContact(db, id, data) {
|
|
1919
|
+
const contact = await db.contact.findUnique({ where: { id } });
|
|
1920
|
+
if (!contact) {
|
|
1921
|
+
throw new ContactNotFoundError(id);
|
|
1922
|
+
}
|
|
1923
|
+
return db.contact.update({
|
|
1924
|
+
where: { id },
|
|
1925
|
+
data,
|
|
1926
|
+
include: {
|
|
1927
|
+
tags: { include: { tag: true } }
|
|
1928
|
+
}
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
async function deleteContact(db, id) {
|
|
1932
|
+
const contact = await db.contact.findUnique({ where: { id } });
|
|
1933
|
+
if (!contact) {
|
|
1934
|
+
throw new ContactNotFoundError(id);
|
|
1935
|
+
}
|
|
1936
|
+
await db.contact.delete({ where: { id } });
|
|
1937
|
+
return { success: true };
|
|
1938
|
+
}
|
|
1939
|
+
async function findOrCreateContact(db, email, name, source = "EMAIL_SYNC") {
|
|
1940
|
+
const normalizedEmail = email.toLowerCase();
|
|
1941
|
+
const existing = await db.contact.findUnique({
|
|
1942
|
+
where: { email: normalizedEmail }
|
|
1943
|
+
});
|
|
1944
|
+
if (existing) {
|
|
1945
|
+
if (name && !existing.name) {
|
|
1946
|
+
return db.contact.update({
|
|
1947
|
+
where: { id: existing.id },
|
|
1948
|
+
data: { name }
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
return existing;
|
|
1952
|
+
}
|
|
1953
|
+
return db.contact.create({
|
|
1954
|
+
data: {
|
|
1955
|
+
email: normalizedEmail,
|
|
1956
|
+
name: name ?? null,
|
|
1957
|
+
source
|
|
1958
|
+
}
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
// src/knowledge/contact-fields-seed.ts
|
|
1963
|
+
var CONTACT_FIELD_DEFINITIONS = [
|
|
1964
|
+
// --- Communication ---
|
|
1965
|
+
{ entityType: "contact", key: "preferred_contact_method", label: "Preferred Contact Method", dataType: "enum", category: "communication", enumOptions: ["email", "phone", "text", "in-person"], sortOrder: 0 },
|
|
1966
|
+
{ entityType: "contact", key: "preferred_contact_time", label: "Preferred Contact Time", dataType: "text", category: "communication", extractionHint: "Time of day or day of week the contact prefers to be reached", sortOrder: 1 },
|
|
1967
|
+
{ entityType: "contact", key: "timezone", label: "Timezone", dataType: "text", category: "communication", sortOrder: 2 },
|
|
1968
|
+
// --- Professional ---
|
|
1969
|
+
{ entityType: "contact", key: "company", label: "Company", dataType: "text", category: "professional", sortOrder: 0 },
|
|
1970
|
+
{ entityType: "contact", key: "title", label: "Job Title", dataType: "text", category: "professional", sortOrder: 1 },
|
|
1971
|
+
{ entityType: "contact", key: "industry", label: "Industry", dataType: "text", category: "professional", sortOrder: 2 },
|
|
1972
|
+
// --- Relationship ---
|
|
1973
|
+
{ entityType: "contact", key: "referral_source", label: "Referral Source", dataType: "text", category: "relationship", sortOrder: 0 },
|
|
1974
|
+
{ entityType: "contact", key: "relationship_type", label: "Relationship Type", dataType: "enum", category: "relationship", enumOptions: ["prospect", "vendor", "partner", "personal", "other"], sortOrder: 1 },
|
|
1975
|
+
{ entityType: "contact", key: "last_interaction_notes", label: "Last Interaction Notes", dataType: "text", category: "relationship", extractionHint: "Brief summary of last meaningful interaction", sortOrder: 2 }
|
|
1976
|
+
];
|
|
1977
|
+
async function seedContactFieldDefinitions(db) {
|
|
1978
|
+
const result = await db.entityFieldDefinition.createMany({
|
|
1979
|
+
data: CONTACT_FIELD_DEFINITIONS.map((d) => ({
|
|
1980
|
+
entityType: d.entityType,
|
|
1981
|
+
key: d.key,
|
|
1982
|
+
label: d.label,
|
|
1983
|
+
dataType: d.dataType,
|
|
1984
|
+
category: d.category,
|
|
1985
|
+
enumOptions: "enumOptions" in d ? d.enumOptions : null,
|
|
1986
|
+
extractionHint: "extractionHint" in d ? d.extractionHint : null,
|
|
1987
|
+
sortOrder: d.sortOrder,
|
|
1988
|
+
status: "approved",
|
|
1989
|
+
isActive: true
|
|
1990
|
+
})),
|
|
1991
|
+
skipDuplicates: true
|
|
1992
|
+
});
|
|
1993
|
+
return result.count;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
// src/knowledge/schemas.ts
|
|
1997
|
+
import { z } from "zod";
|
|
1998
|
+
var ENTITY_TYPES = ["client", "contact", "order"];
|
|
1999
|
+
var FIELD_DATA_TYPES = ["text", "number", "date", "boolean", "enum", "multi-select"];
|
|
2000
|
+
var FIELD_STATUSES = ["suggested", "approved", "dismissed", "merged"];
|
|
2001
|
+
var LEDGER_SOURCES = ["manual", "note", "email", "agent", "ai-batch", "predicted"];
|
|
2002
|
+
var SEARCH_SOURCES = ["search_bar", "agent_chat", "api"];
|
|
2003
|
+
var CreateFieldDefinitionSchema = z.object({
|
|
2004
|
+
entityType: z.enum(ENTITY_TYPES),
|
|
2005
|
+
key: z.string().min(1).max(100).regex(/^[a-z][a-z0-9_]*$/, "Key must be lowercase snake_case"),
|
|
2006
|
+
label: z.string().min(1).max(200),
|
|
2007
|
+
dataType: z.enum(FIELD_DATA_TYPES),
|
|
2008
|
+
category: z.string().min(1).max(100),
|
|
2009
|
+
enumOptions: z.array(z.string()).optional(),
|
|
2010
|
+
extractionHint: z.string().max(500).optional(),
|
|
2011
|
+
sortOrder: z.number().int().min(0).default(0),
|
|
2012
|
+
halfLifeDays: z.number().int().min(1).nullable().optional()
|
|
2013
|
+
});
|
|
2014
|
+
var UpdateFieldDefinitionSchema = z.object({
|
|
2015
|
+
label: z.string().min(1).max(200).optional(),
|
|
2016
|
+
category: z.string().min(1).max(100).optional(),
|
|
2017
|
+
enumOptions: z.array(z.string()).optional(),
|
|
2018
|
+
extractionHint: z.string().max(500).optional(),
|
|
2019
|
+
status: z.enum(FIELD_STATUSES).optional(),
|
|
2020
|
+
mergedIntoId: z.string().optional(),
|
|
2021
|
+
sortOrder: z.number().int().min(0).optional(),
|
|
2022
|
+
isActive: z.boolean().optional(),
|
|
2023
|
+
halfLifeDays: z.number().int().min(1).nullable().optional(),
|
|
2024
|
+
constraints: z.array(z.object({
|
|
2025
|
+
targetField: z.string(),
|
|
2026
|
+
operator: z.enum(["not_in", "in", "not_equal", "less_than", "greater_than", "regex"]),
|
|
2027
|
+
values: z.array(z.unknown()).optional(),
|
|
2028
|
+
value: z.unknown().optional(),
|
|
2029
|
+
pattern: z.string().optional(),
|
|
2030
|
+
message: z.string(),
|
|
2031
|
+
severity: z.enum(["warning", "error"])
|
|
2032
|
+
})).nullable().optional()
|
|
2033
|
+
});
|
|
2034
|
+
var ListFieldDefinitionsQuery = z.object({
|
|
2035
|
+
entityType: z.enum(ENTITY_TYPES).optional(),
|
|
2036
|
+
status: z.enum(FIELD_STATUSES).optional()
|
|
2037
|
+
});
|
|
2038
|
+
var SetEntityFieldsSchema = z.object({
|
|
2039
|
+
fields: z.record(z.string(), z.unknown()).refine(
|
|
2040
|
+
(obj) => Object.keys(obj).length > 0,
|
|
2041
|
+
"At least one field is required"
|
|
2042
|
+
),
|
|
2043
|
+
source: z.enum(LEDGER_SOURCES).default("manual"),
|
|
2044
|
+
sourceId: z.string().optional(),
|
|
2045
|
+
confidence: z.number().min(0).max(1).default(1)
|
|
2046
|
+
});
|
|
2047
|
+
var SearchByFieldsSchema = z.object({
|
|
2048
|
+
entityType: z.enum(ENTITY_TYPES),
|
|
2049
|
+
filters: z.record(z.string(), z.unknown()).refine(
|
|
2050
|
+
(obj) => Object.keys(obj).length > 0,
|
|
2051
|
+
"At least one filter is required"
|
|
2052
|
+
),
|
|
2053
|
+
limit: z.number().int().min(1).max(100).default(50)
|
|
2054
|
+
});
|
|
2055
|
+
var LogSearchQuerySchema = z.object({
|
|
2056
|
+
query: z.string().min(1).max(500),
|
|
2057
|
+
source: z.enum(SEARCH_SOURCES),
|
|
2058
|
+
userId: z.string().optional(),
|
|
2059
|
+
entityType: z.enum(ENTITY_TYPES).optional(),
|
|
2060
|
+
resultCount: z.number().int().min(0).optional()
|
|
2061
|
+
});
|
|
2062
|
+
var FieldHistoryQuery = z.object({
|
|
2063
|
+
fieldId: z.string().optional(),
|
|
2064
|
+
limit: z.coerce.number().int().min(1).max(200).default(50)
|
|
2065
|
+
});
|
|
2066
|
+
|
|
2067
|
+
// src/agent/schemas.ts
|
|
2068
|
+
import { z as z2 } from "zod";
|
|
2069
|
+
var QueryKnowledgeSchema = z2.object({
|
|
2070
|
+
entity: z2.string().min(1).max(50),
|
|
2071
|
+
field: z2.string().min(1).max(100).optional(),
|
|
2072
|
+
search: z2.string().min(1).optional(),
|
|
2073
|
+
category: z2.string().min(1).max(100).optional(),
|
|
2074
|
+
limit: z2.coerce.number().int().min(1).max(200).default(50)
|
|
2075
|
+
});
|
|
2076
|
+
var CreateKnowledgeEntrySchema = z2.object({
|
|
2077
|
+
entity: z2.string().min(1).max(50),
|
|
2078
|
+
field: z2.string().min(1).max(100),
|
|
2079
|
+
value: z2.string().min(1),
|
|
2080
|
+
label: z2.string().min(1),
|
|
2081
|
+
aliases: z2.array(z2.string().min(1)).default([]),
|
|
2082
|
+
category: z2.string().min(1).max(100).optional(),
|
|
2083
|
+
metadata: z2.record(z2.string(), z2.unknown()).optional(),
|
|
2084
|
+
sortOrder: z2.number().int().default(0)
|
|
2085
|
+
});
|
|
2086
|
+
var UpdateKnowledgeEntrySchema = z2.object({
|
|
2087
|
+
label: z2.string().min(1).optional(),
|
|
2088
|
+
aliases: z2.array(z2.string().min(1)).optional(),
|
|
2089
|
+
category: z2.string().min(1).max(100).optional(),
|
|
2090
|
+
metadata: z2.record(z2.string(), z2.unknown()).optional(),
|
|
2091
|
+
sortOrder: z2.number().int().optional(),
|
|
2092
|
+
isActive: z2.boolean().optional()
|
|
2093
|
+
});
|
|
2094
|
+
var GetPreferencesSchema = z2.object({
|
|
2095
|
+
entity: z2.string().min(1).max(50),
|
|
2096
|
+
userId: z2.string().optional(),
|
|
2097
|
+
field: z2.string().min(1).max(100).optional()
|
|
2098
|
+
});
|
|
2099
|
+
var SetPreferenceSchema = z2.object({
|
|
2100
|
+
entity: z2.string().min(1).max(50),
|
|
2101
|
+
field: z2.string().min(1).max(100),
|
|
2102
|
+
value: z2.string().min(1),
|
|
2103
|
+
label: z2.string().min(1),
|
|
2104
|
+
conditions: z2.record(z2.string(), z2.unknown()).optional(),
|
|
2105
|
+
userId: z2.string().optional(),
|
|
2106
|
+
priority: z2.number().int().default(0)
|
|
2107
|
+
});
|
|
2108
|
+
|
|
2109
|
+
// src/notes/schemas.ts
|
|
2110
|
+
import { z as z3 } from "zod";
|
|
2111
|
+
var AttachmentSchema = z3.object({
|
|
2112
|
+
filename: z3.string().min(1).max(255),
|
|
2113
|
+
mimeType: z3.string().min(1).max(100),
|
|
2114
|
+
size: z3.number().int().min(1).max(10 * 1024 * 1024),
|
|
2115
|
+
// 10MB
|
|
2116
|
+
storagePath: z3.string().min(1).max(500)
|
|
2117
|
+
});
|
|
2118
|
+
var CreateNoteSchema = z3.object({
|
|
2119
|
+
content: z3.string().min(1, "Note content is required").max(5e3, "Note content must be 5000 characters or fewer"),
|
|
2120
|
+
contentHtml: z3.string().max(5e4).optional(),
|
|
2121
|
+
isDraft: z3.boolean().default(false),
|
|
2122
|
+
source: z3.enum(["MANUAL", "AGENT", "SYSTEM"]).default("MANUAL"),
|
|
2123
|
+
attachments: z3.array(AttachmentSchema).max(5, "Maximum 5 attachments").default([]),
|
|
2124
|
+
fields: z3.record(z3.string(), z3.unknown()).optional()
|
|
2125
|
+
});
|
|
2126
|
+
var UpdateNoteSchema = z3.object({
|
|
2127
|
+
content: z3.string().min(1).max(5e3).optional(),
|
|
2128
|
+
contentHtml: z3.string().max(5e4).optional(),
|
|
2129
|
+
isDraft: z3.boolean().optional(),
|
|
2130
|
+
attachments: z3.array(AttachmentSchema).max(5).optional()
|
|
2131
|
+
});
|
|
2132
|
+
var ListNotesQuery = z3.object({
|
|
2133
|
+
limit: z3.coerce.number().int().min(1).max(100).default(50),
|
|
2134
|
+
cursor: z3.string().optional(),
|
|
2135
|
+
isDraft: z3.enum(["true", "false"]).optional()
|
|
2136
|
+
});
|
|
2137
|
+
|
|
2138
|
+
// src/contacts/schemas.ts
|
|
2139
|
+
import { z as z4 } from "zod";
|
|
2140
|
+
var CreateContactSchema = z4.object({
|
|
2141
|
+
email: z4.string().email().max(255),
|
|
2142
|
+
name: z4.string().max(255).optional(),
|
|
2143
|
+
notes: z4.string().max(5e3).optional(),
|
|
2144
|
+
source: z4.enum(["EMAIL_SYNC", "MANUAL", "AUTOMATION", "IMPORT"]).default("MANUAL"),
|
|
2145
|
+
fields: z4.record(z4.string(), z4.unknown()).optional()
|
|
2146
|
+
});
|
|
2147
|
+
var UpdateContactSchema = z4.object({
|
|
2148
|
+
name: z4.string().max(255).optional().nullable(),
|
|
2149
|
+
notes: z4.string().max(5e3).optional().nullable()
|
|
2150
|
+
});
|
|
2151
|
+
var ListContactsQuery = z4.object({
|
|
2152
|
+
limit: z4.coerce.number().int().min(1).max(100).default(50),
|
|
2153
|
+
offset: z4.coerce.number().int().min(0).default(0),
|
|
2154
|
+
source: z4.enum(["EMAIL_SYNC", "MANUAL", "AUTOMATION", "IMPORT"]).optional(),
|
|
2155
|
+
search: z4.string().max(200).optional(),
|
|
2156
|
+
excludeConverted: z4.enum(["true", "false"]).default("true")
|
|
2157
|
+
});
|
|
2158
|
+
export {
|
|
2159
|
+
AGENTKNOW_ERRORS,
|
|
2160
|
+
AttachmentSchema,
|
|
2161
|
+
CONTACT_FIELD_DEFINITIONS,
|
|
2162
|
+
ContactAlreadyConvertedError,
|
|
2163
|
+
ContactExistsError,
|
|
2164
|
+
ContactNotFoundError,
|
|
2165
|
+
CreateContactSchema,
|
|
2166
|
+
CreateFieldDefinitionSchema,
|
|
2167
|
+
CreateKnowledgeEntrySchema,
|
|
2168
|
+
CreateNoteSchema,
|
|
2169
|
+
DEFAULT_CONFLICT_RULES,
|
|
2170
|
+
ENTITY_TYPES,
|
|
2171
|
+
FIELD_DATA_TYPES,
|
|
2172
|
+
FIELD_STATUSES,
|
|
2173
|
+
FieldHistoryQuery,
|
|
2174
|
+
GetPreferencesSchema,
|
|
2175
|
+
KNOWLEDGE_ERRORS,
|
|
2176
|
+
LEDGER_SOURCES,
|
|
2177
|
+
ListContactsQuery,
|
|
2178
|
+
ListFieldDefinitionsQuery,
|
|
2179
|
+
ListNotesQuery,
|
|
2180
|
+
LogSearchQuerySchema,
|
|
2181
|
+
QueryKnowledgeSchema,
|
|
2182
|
+
SEARCH_SOURCES,
|
|
2183
|
+
SOURCE_RELIABILITY,
|
|
2184
|
+
SearchByFieldsSchema,
|
|
2185
|
+
SetEntityFieldsSchema,
|
|
2186
|
+
SetPreferenceSchema,
|
|
2187
|
+
UpdateContactSchema,
|
|
2188
|
+
UpdateFieldDefinitionSchema,
|
|
2189
|
+
UpdateKnowledgeEntrySchema,
|
|
2190
|
+
UpdateNoteSchema,
|
|
2191
|
+
agentKnowledgeError,
|
|
2192
|
+
analyzeAndSuggest,
|
|
2193
|
+
approveSuggestion,
|
|
2194
|
+
buildCoOccurrenceMap,
|
|
2195
|
+
checkConstraints,
|
|
2196
|
+
classifyIntent,
|
|
2197
|
+
computeComplexityScore,
|
|
2198
|
+
computeDecayedConfidence,
|
|
2199
|
+
computeEntityHealth,
|
|
2200
|
+
computeEntityTruthScores,
|
|
2201
|
+
computeOverallScore,
|
|
2202
|
+
computeRecencyFactor,
|
|
2203
|
+
computeRelationshipStrength,
|
|
2204
|
+
computeTruthScore,
|
|
2205
|
+
createContact,
|
|
2206
|
+
createFieldDefinition,
|
|
2207
|
+
createKnowledgeEntry,
|
|
2208
|
+
createNote,
|
|
2209
|
+
deleteContact,
|
|
2210
|
+
deleteKnowledgeEntry,
|
|
2211
|
+
deleteNote,
|
|
2212
|
+
deletePreference,
|
|
2213
|
+
detectConflicts,
|
|
2214
|
+
dismissSuggestion,
|
|
2215
|
+
ensureKnowledgeDefaults,
|
|
2216
|
+
evaluateConflictRule,
|
|
2217
|
+
evaluateConstraint,
|
|
2218
|
+
extractModules,
|
|
2219
|
+
extractTopic,
|
|
2220
|
+
findOrCreateContact,
|
|
2221
|
+
findRelatedEntities,
|
|
2222
|
+
fireObservers,
|
|
2223
|
+
formatStalenessWarnings,
|
|
2224
|
+
getAllContacts,
|
|
2225
|
+
getContactByEmail,
|
|
2226
|
+
getContactById,
|
|
2227
|
+
getEntityFields,
|
|
2228
|
+
getEntityRelationships,
|
|
2229
|
+
getEntityTypeHealth,
|
|
2230
|
+
getFieldDefinitionById,
|
|
2231
|
+
getFieldDefinitions,
|
|
2232
|
+
getFieldHistory,
|
|
2233
|
+
getFieldValueAt,
|
|
2234
|
+
getInteractionCount,
|
|
2235
|
+
getKnowledgeAt,
|
|
2236
|
+
getKnowledgeTimeline,
|
|
2237
|
+
getNotes,
|
|
2238
|
+
getPreferences,
|
|
2239
|
+
getRecentInteractions,
|
|
2240
|
+
getStaleFields,
|
|
2241
|
+
getSuggestions,
|
|
2242
|
+
getTopicDistribution,
|
|
2243
|
+
getWeekOfYear,
|
|
2244
|
+
isStale,
|
|
2245
|
+
knowledgeError,
|
|
2246
|
+
logInteraction,
|
|
2247
|
+
logSearchQuery,
|
|
2248
|
+
markSatisfied,
|
|
2249
|
+
predictGhostFields,
|
|
2250
|
+
queryKnowledge,
|
|
2251
|
+
resetKnowledgeDefaults,
|
|
2252
|
+
resetSeedCheck,
|
|
2253
|
+
scanForConflicts,
|
|
2254
|
+
searchByFields,
|
|
2255
|
+
seedContactFieldDefinitions,
|
|
2256
|
+
setEntityFields,
|
|
2257
|
+
setEntityFieldsWithBranching,
|
|
2258
|
+
setPreference,
|
|
2259
|
+
shouldBranch,
|
|
2260
|
+
updateContact,
|
|
2261
|
+
updateFieldDefinition,
|
|
2262
|
+
updateKnowledgeEntry,
|
|
2263
|
+
updateNote,
|
|
2264
|
+
writeGhostFields
|
|
2265
|
+
};
|