trivial turn patch
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
const {getDB} = require('../db');
|
||||
const { EPISODIC, getEnv, SERVICES, parseRow, formatEpisodeText, SUMMARIES, logger } = require('@nexusai/shared');
|
||||
const { EPISODIC, getEnv, SERVICES, parseRow, formatEpisodeText, SUMMARIES, logger, isTrivialTurn } = require('@nexusai/shared');
|
||||
const semantic = require('../semantic');
|
||||
const { extractAndStoreEntities } = require('../entities/extraction')
|
||||
|
||||
@@ -162,8 +162,16 @@ async function createEpisode(sessionId, userMessage, aiResponse, tokenCount = nu
|
||||
}))
|
||||
.catch(err => logger.error(`Failed to embed episode ${episode.id}:`, err.message));
|
||||
|
||||
// Skip entity extraction on contentless social turns (greetings, sign-offs).
|
||||
// They carry nothing worth storing, and running extraction on them was a
|
||||
// source of junk/confabulated entities. The informational content lives in
|
||||
// substantive turns, which still extract normally.
|
||||
if (isTrivialTurn(userMessage)) {
|
||||
logger.debug(`[entities] Skipping extraction for episode ${episode.id} — trivial turn`);
|
||||
} else {
|
||||
extractAndStoreEntities(userMessage, aiResponse, episode.id, projectId)
|
||||
.catch(err => logger.error(`Failed to extract entities for episode ${episode.id}:`, err.message));
|
||||
}
|
||||
|
||||
|
||||
return episode;
|
||||
|
||||
@@ -2,7 +2,7 @@ const memory = require("../services/memory");
|
||||
const inference = require("../services/inference");
|
||||
const embedding = require("../services/embedding");
|
||||
const qdrant = require("../services/qdrant");
|
||||
const { ORCHESTRATION, RETRIEVAL, logger } = require("@nexusai/shared");
|
||||
const { ORCHESTRATION, RETRIEVAL, logger, isTrivialTurn } = require("@nexusai/shared");
|
||||
const appSettings = require("../config/settings");
|
||||
const {triggerSummary} = require('../services/summarization')
|
||||
const graph = require('../services/graph');
|
||||
@@ -283,8 +283,13 @@ async function assembleContext(externalId, userMessage) {
|
||||
const isFirstMessage = recentEpisodes.length === 0;
|
||||
const recentIds = new Set(recentEpisodes.map(e => e.id));
|
||||
|
||||
// 4. Embed the query once — the vector is shared by semantic episode search
|
||||
// and entity search, so embedding it twice was a wasted round-trip + Ollama call.
|
||||
// 4. Retrieval — skipped entirely on contentless social turns (greetings,
|
||||
// sign-offs). On those, recent history alone is the right context; running
|
||||
// semantic/keyword/entity retrieval only surfaces marginal noise the model
|
||||
// then confabulates around. Embed once (shared by episode + entity search).
|
||||
let fusedWithScores = [];
|
||||
let entityResults = [];
|
||||
if (!isTrivialTurn(userMessage)) {
|
||||
let queryVector = null;
|
||||
try {
|
||||
queryVector = await embedding.embed(userMessage);
|
||||
@@ -292,11 +297,13 @@ async function assembleContext(externalId, userMessage) {
|
||||
logger.warn('[orchestration] Query embedding failed; semantic + entity search disabled this turn:', err.message);
|
||||
}
|
||||
|
||||
// 4b. Fused retrieval + entity search in parallel (both are independent)
|
||||
const [fusedWithScores, entityResults] = await Promise.all([
|
||||
[fusedWithScores, entityResults] = await Promise.all([
|
||||
getFusedEpisodes(userMessage, session, recentIds, projectSessionIds, queryVector, { semanticLimit, scoreThreshold, semanticWeight, keywordWeight }),
|
||||
getRelevantEntities(queryVector, session.project_id ?? null),
|
||||
]);
|
||||
} else {
|
||||
logger.debug('[orchestration] Trivial turn — skipping semantic/keyword/entity retrieval');
|
||||
}
|
||||
|
||||
// 5. Entity-linked episode IDs for scoring bonus
|
||||
const entityIds = entityResults.map(e => e.id);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const {getEnv} = require('./config/env');
|
||||
const {QDRANT, COLLECTIONS, EPISODIC, SERVICES, OLLAMA, PORTS, LLAMACPP, INFERENCE_DEFAULTS, SQLITE, ORCHESTRATION, SUMMARIES, ENTITIES, RETRIEVAL } = require('./config/constants');
|
||||
const {parseRow, formatEpisodeText} = require('./utils')
|
||||
const {parseRow, formatEpisodeText, isTrivialTurn} = require('./utils')
|
||||
const logger = require('./utils/logger');
|
||||
|
||||
module.exports = {
|
||||
@@ -17,6 +17,7 @@ module.exports = {
|
||||
ORCHESTRATION,
|
||||
parseRow,
|
||||
formatEpisodeText,
|
||||
isTrivialTurn,
|
||||
SUMMARIES,
|
||||
ENTITIES,
|
||||
logger,
|
||||
|
||||
@@ -10,4 +10,46 @@ function formatEpisodeText(userMessage, aiResponse) {
|
||||
return `User: ${userMessage}\nAssistant: ${aiResponse}`;
|
||||
}
|
||||
|
||||
module.exports = { parseRow, formatEpisodeText };
|
||||
// Contentless "social" turns — greetings, sign-offs, acknowledgements — that
|
||||
// carry no information to store or recall. Used to skip entity extraction and
|
||||
// noisy retrieval on such turns.
|
||||
const TRIVIAL_TURNS = new Set([
|
||||
'good morning', 'good night', 'good evening', 'good afternoon', 'morning', 'evening',
|
||||
'hello', 'hi', 'hey', 'hey there', 'yo', 'sup', 'whats up', 'hiya', 'howdy',
|
||||
'goodbye', 'bye', 'see you', 'see ya', 'see you later', 'talk later', 'talk soon',
|
||||
'later', 'catch you later', 'gtg', 'gotta go', 'im off', 'heading out',
|
||||
'thanks', 'thank you', 'thanks again', 'thank you so much', 'ty', 'thx', 'cheers',
|
||||
'no worries', 'no problem', 'np', 'youre welcome', 'my pleasure',
|
||||
'ok', 'okay', 'k', 'kk', 'alright', 'sure', 'sounds good', 'got it', 'gotcha',
|
||||
'cool', 'nice', 'great', 'awesome', 'perfect', 'lol', 'haha',
|
||||
'just saying hi', 'just saying hello', 'just checking in', 'just dropping in',
|
||||
'stopping by', 'just stopping by', 'just wanted to say hi',
|
||||
]);
|
||||
|
||||
// Trailing filler words that don't change a phrase's social nature, so
|
||||
// "good morning again" / "thanks everyone" collapse to a base phrase.
|
||||
const TRIVIAL_FILLER = new Set(['again', 'there', 'everyone', 'all', 'yall', 'folks', 'man', 'dude', 'friend', 'buddy']);
|
||||
|
||||
// Conservative, HIGH-PRECISION check: is this a contentless social turn?
|
||||
// It exists to skip retrieval/extraction on turns with nothing to remember or
|
||||
// recall, and deliberately errs toward "substantive" — a real short query like
|
||||
// "capital of France" must NOT be treated as trivial. Genuine intent
|
||||
// classification is a separate, later concern (confidence-based routing), not
|
||||
// this heuristic. Extend TRIVIAL_TURNS as new pure-social phrases show up.
|
||||
function isTrivialTurn(message) {
|
||||
if (!message) return true;
|
||||
const norm = String(message)
|
||||
.toLowerCase()
|
||||
.replace(/[^\p{L}\p{N}\s]/gu, ' ') // punctuation → space (unicode-aware)
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
if (!norm) return true; // empty / punctuation-only
|
||||
if (TRIVIAL_TURNS.has(norm)) return true;
|
||||
|
||||
// Drop trailing filler and re-check ("good morning again" → "good morning")
|
||||
const words = norm.split(' ');
|
||||
while (words.length > 1 && TRIVIAL_FILLER.has(words[words.length - 1])) words.pop();
|
||||
return TRIVIAL_TURNS.has(words.join(' '));
|
||||
}
|
||||
|
||||
module.exports = { parseRow, formatEpisodeText, isTrivialTurn };
|
||||
@@ -0,0 +1,42 @@
|
||||
// Trivial-turn guard — tests the REAL isTrivialTurn from @nexusai/shared, which
|
||||
// gates whether a turn skips retrieval + entity extraction. The critical property
|
||||
// is high precision: greetings/pleasantries are caught, but real short queries
|
||||
// must NOT be (a false positive would suppress retrieval on a genuine question).
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const { isTrivialTurn } = require('@nexusai/shared');
|
||||
|
||||
test('plain greetings and pleasantries are trivial', () => {
|
||||
for (const m of ['good morning', 'Good morning!', 'hello', 'hey there', 'hi',
|
||||
'thanks', 'thank you so much', 'bye', 'ok', 'sounds good',
|
||||
'just dropping in', 'just checking in', 'cheers']) {
|
||||
assert.strictEqual(isTrivialTurn(m), true, `"${m}" should be trivial`);
|
||||
}
|
||||
});
|
||||
|
||||
test('trailing filler collapses to a base greeting', () => {
|
||||
assert.strictEqual(isTrivialTurn('good morning again'), true);
|
||||
assert.strictEqual(isTrivialTurn('thanks everyone'), true);
|
||||
assert.strictEqual(isTrivialTurn('hello there'), true);
|
||||
});
|
||||
|
||||
test('empty or punctuation-only input is trivial', () => {
|
||||
assert.strictEqual(isTrivialTurn(''), true);
|
||||
assert.strictEqual(isTrivialTurn(' '), true);
|
||||
assert.strictEqual(isTrivialTurn('!!!'), true);
|
||||
assert.strictEqual(isTrivialTurn(null), true);
|
||||
});
|
||||
|
||||
test('real queries are NOT trivial — even short ones (the precision guarantee)', () => {
|
||||
for (const m of ['what is the capital of France', 'France capital', 'capital of France?',
|
||||
'how do I configure Qdrant', 'One Piece', 'help me debug this',
|
||||
'what did we decide about the schema', 'morning routine ideas']) {
|
||||
assert.strictEqual(isTrivialTurn(m), false, `"${m}" should be substantive`);
|
||||
}
|
||||
});
|
||||
|
||||
test('a greeting prefix does not make a substantive message trivial', () => {
|
||||
// "good morning, can you help with X" carries a real request — must not be skipped
|
||||
assert.strictEqual(isTrivialTurn('good morning, can you help me with the migration'), false);
|
||||
assert.strictEqual(isTrivialTurn('hey, what is the One Piece'), false);
|
||||
});
|
||||
Reference in New Issue
Block a user