@inetafrica/open-claudia 2.6.4 → 2.6.5
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/core/recall-filter.js +17 -3
- package/core/system-prompt.js +57 -14
- package/package.json +1 -1
package/core/recall-filter.js
CHANGED
|
@@ -20,21 +20,35 @@ const FILTER_SYSTEM_PROMPT = [
|
|
|
20
20
|
"Reply with ONLY a JSON array of candidate ids — no prose, no markdown fences.",
|
|
21
21
|
].join("\n");
|
|
22
22
|
|
|
23
|
-
// candidates: [{ id, name, description }].
|
|
24
|
-
//
|
|
25
|
-
|
|
23
|
+
// candidates: [{ id, name, description }]. opts.context: surrounding
|
|
24
|
+
// conversation text (e.g. the message the user replied to) used to
|
|
25
|
+
// disambiguate pronouns like "the app" — context only, not a topic
|
|
26
|
+
// source in its own right. Returns a Set of kept ids, or null to
|
|
27
|
+
// signal fail-open (caller decides what that keeps).
|
|
28
|
+
async function judgeRelevance(message, candidates, opts = {}) {
|
|
26
29
|
if (!enabled() || candidates.length === 0) return null;
|
|
27
30
|
const msg = String(message || "").slice(0, MESSAGE_CLIP_CHARS);
|
|
31
|
+
const context = String(opts.context || "").slice(0, MESSAGE_CLIP_CHARS);
|
|
28
32
|
const lines = candidates.map((c) => `- ${c.id}: ${c.name}${c.description ? ` — ${c.description}` : ""}`);
|
|
29
33
|
const prompt = [
|
|
30
34
|
"A user sent this message to their assistant:",
|
|
31
35
|
"<<<",
|
|
32
36
|
msg,
|
|
33
37
|
">>>",
|
|
38
|
+
...(context
|
|
39
|
+
? [
|
|
40
|
+
"",
|
|
41
|
+
"It was a reply to this earlier message (context for resolving references like \"the app\" or \"it\" — judge relevance against what the user's message is about in light of it):",
|
|
42
|
+
"<<<",
|
|
43
|
+
context,
|
|
44
|
+
">>>",
|
|
45
|
+
]
|
|
46
|
+
: []),
|
|
34
47
|
"",
|
|
35
48
|
"These stored memory notes were keyword-matched as possibly relevant.",
|
|
36
49
|
"Keep only the ones the message is GENUINELY about — the topic, project, or person itself.",
|
|
37
50
|
"Sharing an incidental word with the note (e.g. \"zoom\" matching a video tool, a person's employer matching a project name) is NOT relevance.",
|
|
51
|
+
"If the conversation context shows the user means a different project than a candidate (e.g. they are discussing app A, and a candidate is the unrelated app B that merely shares words like \"mobile\" or \"gradient\"), drop that candidate.",
|
|
38
52
|
"",
|
|
39
53
|
"Candidates:",
|
|
40
54
|
...lines,
|
package/core/system-prompt.js
CHANGED
|
@@ -395,15 +395,32 @@ function buildEntityBlock(matches) {
|
|
|
395
395
|
// router.js wraps Telegram reply-quotes in "Replying to …:\n---\n…\n---",
|
|
396
396
|
// and quoting a 📖 recall announcement echoes every entity named in it.
|
|
397
397
|
// Matching recall over that text re-injects whatever was quoted (names
|
|
398
|
-
// score 2 — instant threshold)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
398
|
+
// score 2 — instant threshold). So we split: the user's own words drive
|
|
399
|
+
// recall as before, and the quoted text is kept separately as context.
|
|
400
|
+
// Context-derived candidates only survive if the LLM relevance judge
|
|
401
|
+
// confirms them — without a working judge they are dropped, so a quote
|
|
402
|
+
// can never force-inject on keywords alone.
|
|
403
|
+
function stripRecallAnnouncements(text) {
|
|
404
|
+
return String(text || "").split("\n").filter((l) => !l.includes("📖")).join("\n").trim();
|
|
404
405
|
}
|
|
405
406
|
|
|
406
|
-
|
|
407
|
+
function recallMatchParts(prompt) {
|
|
408
|
+
const text = String(prompt || "");
|
|
409
|
+
const m = text.match(/^Replying to [^\n]*:\n---\n([\s\S]*?)\n---\n+/);
|
|
410
|
+
return {
|
|
411
|
+
userText: stripRecallAnnouncements(m ? text.slice(m[0].length) : text),
|
|
412
|
+
contextText: stripRecallAnnouncements(m ? m[1] : ""),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// packMatches/entityMatches carry an `origin` of "user" or "context".
|
|
417
|
+
// Fail-open keeps user-text candidates (the keyword baseline) but drops
|
|
418
|
+
// context-only ones — those are only trustworthy with a working judge.
|
|
419
|
+
async function filterMatches(userText, contextText, packMatches, entityMatches) {
|
|
420
|
+
const failOpen = () => ({
|
|
421
|
+
packMatches: packMatches.filter((m) => m.origin === "user"),
|
|
422
|
+
entityMatches: entityMatches.filter((m) => m.origin === "user"),
|
|
423
|
+
});
|
|
407
424
|
if (packMatches.length === 0 && entityMatches.length === 0) {
|
|
408
425
|
return { packMatches, entityMatches };
|
|
409
426
|
}
|
|
@@ -419,26 +436,52 @@ async function filterMatches(matchText, packMatches, entityMatches) {
|
|
|
419
436
|
const e = entitiesLib.readEntity(m.slug);
|
|
420
437
|
candidates.push({ id: `entity:${m.slug}`, name: m.name, description: e?.description || "" });
|
|
421
438
|
}
|
|
422
|
-
const kept = await require("./recall-filter").judgeRelevance(
|
|
423
|
-
if (!kept) return
|
|
439
|
+
const kept = await require("./recall-filter").judgeRelevance(userText, candidates, { context: contextText });
|
|
440
|
+
if (!kept) return failOpen();
|
|
424
441
|
return {
|
|
425
442
|
packMatches: packMatches.filter((m) => kept.has(`pack:${m.dir}`)),
|
|
426
443
|
entityMatches: entityMatches.filter((m) => kept.has(`entity:${m.slug}`)),
|
|
427
444
|
};
|
|
428
445
|
} catch (e) {
|
|
429
|
-
return
|
|
446
|
+
return failOpen();
|
|
430
447
|
}
|
|
431
448
|
}
|
|
432
449
|
|
|
450
|
+
function mergeMatches(primary, secondary, keyOf) {
|
|
451
|
+
const out = primary.map((m) => ({ ...m, origin: "user" }));
|
|
452
|
+
const seen = new Set(primary.map(keyOf));
|
|
453
|
+
for (const m of secondary) {
|
|
454
|
+
if (seen.has(keyOf(m))) continue;
|
|
455
|
+
out.push({ ...m, origin: "context" });
|
|
456
|
+
}
|
|
457
|
+
return out;
|
|
458
|
+
}
|
|
459
|
+
|
|
433
460
|
async function promptWithDynamicContext(prompt) {
|
|
434
461
|
lastInjected = { packs: [], entities: [] };
|
|
435
462
|
try {
|
|
436
|
-
const
|
|
463
|
+
const { userText, contextText } = recallMatchParts(prompt);
|
|
464
|
+
const packsLib = require("./packs");
|
|
465
|
+
const entitiesLib = require("./entities");
|
|
437
466
|
let packMatches = [];
|
|
438
467
|
let entityMatches = [];
|
|
439
|
-
try {
|
|
440
|
-
|
|
441
|
-
|
|
468
|
+
try {
|
|
469
|
+
packMatches = mergeMatches(
|
|
470
|
+
packsLib.matchPacks(userText, { limit: 3 }),
|
|
471
|
+
contextText ? packsLib.matchPacks(contextText, { limit: 3 }) : [],
|
|
472
|
+
(m) => m.dir,
|
|
473
|
+
);
|
|
474
|
+
} catch (e) {}
|
|
475
|
+
try {
|
|
476
|
+
entityMatches = mergeMatches(
|
|
477
|
+
entitiesLib.matchEntities(userText, { limit: 4 }),
|
|
478
|
+
contextText ? entitiesLib.matchEntities(contextText, { limit: 4 }) : [],
|
|
479
|
+
(m) => m.slug,
|
|
480
|
+
);
|
|
481
|
+
} catch (e) {}
|
|
482
|
+
({ packMatches, entityMatches } = await filterMatches(userText, contextText, packMatches, entityMatches));
|
|
483
|
+
packMatches = packMatches.slice(0, 3);
|
|
484
|
+
entityMatches = entityMatches.slice(0, 4);
|
|
442
485
|
return `${buildDynamicContextBlock()}${buildPackBlock(packMatches)}${buildEntityBlock(entityMatches)}\n\nCurrent user request:\n${prompt}`;
|
|
443
486
|
} catch (e) {
|
|
444
487
|
return prompt;
|
package/package.json
CHANGED