utilityInference layer 2

This commit is contained in:
Storme-bit
2026-08-17 07:41:41 -07:00
parent fab8f32395
commit 2bee4b23d6
4 changed files with 58 additions and 56 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ async function utilityComplete({
model: UTILITY_MODEL, model: UTILITY_MODEL,
messages, messages,
stream: false, stream: false,
...UTILITY(json && {format: 'json' }), ...(json && {format: 'json' }),
options: { options: {
temperature: temperature ?? UTILITY.TEMPERATURE, temperature: temperature ?? UTILITY.TEMPERATURE,
num_predict: maxTokens ?? UTILITY.MAX_TOKENS, num_predict: maxTokens ?? UTILITY.MAX_TOKENS,
@@ -1,9 +1,7 @@
const semantic = require('../semantic') const semantic = require('../semantic')
const { getEnv, SERVICES, formatEpisodeText, ENTITIES, logger } = require('@nexusai/shared'); const { getEnv, SERVICES, formatEpisodeText, ENTITIES, logger, utilityInference } = require('@nexusai/shared');
const { upsertEntity, upsertRelationship, linkEntityToEpisode } = require('./index'); const { upsertEntity, upsertRelationship, linkEntityToEpisode } = require('./index');
const EXTRACTION_URL = getEnv('EXTRACTION_URL', 'http://localhost:11434');
const EXTRACTION_MODEL = getEnv('EXTRACTION_MODEL', 'qwen2.5:3b'); // ChatML format — see buildExtractionPrompt
const EMBEDDING_SERVICE_URL = getEnv('EMBEDDING_SERVICE_URL', SERVICES.EMBEDDING_URL); const EMBEDDING_SERVICE_URL = getEnv('EMBEDDING_SERVICE_URL', SERVICES.EMBEDDING_URL);
const ENTITY_TYPES = ENTITIES.TYPES; const ENTITY_TYPES = ENTITIES.TYPES;
@@ -28,10 +26,9 @@ function mentionedIn(name, haystack) {
return norm(haystack).includes(norm(name)); return norm(haystack).includes(norm(name));
} }
// NOTE: This prompt uses ChatML format (<|im_start|> / <|im_end|> tags), which is // Returns { system, user } for utilityInference. The model's prompt template
// specific to qwen-family models. If EXTRACTION_MODEL is changed to a Llama-family // (ChatML for qwen, etc.) is applied by Ollama via the inference service's
// or other model, this format will need to change — most alternatives use either // /utility/complete route — no template tags belong in this file.
// plain text or [INST] / <<SYS>> tags. Silent degradation is likely if mismatched.
function buildExtractionPrompt(userMessage, aiResponse, knownEntities = []) { function buildExtractionPrompt(userMessage, aiResponse, knownEntities = []) {
const knownBlock = knownEntities.length > 0 const knownBlock = knownEntities.length > 0
? [ ? [
@@ -41,11 +38,9 @@ function buildExtractionPrompt(userMessage, aiResponse, knownEntities = []) {
].join('\n') ].join('\n')
: ''; : '';
return [ return {
'<|im_start|>system', system: 'You are a named entity and relationship extractor. You output only valid JSON.',
'You are a named entity and relationship extractor. You output only valid JSON.', user: [
'<|im_end|>',
'<|im_start|>user',
'Read the conversation below and extract all named entities and the relationships between them.', 'Read the conversation below and extract all named entities and the relationships between them.',
`Entity types: ${ENTITY_TYPES.join(', ')}`, `Entity types: ${ENTITY_TYPES.join(', ')}`,
'Use "character" for any fictional, game, or media characters (e.g. characters from anime, games, books, TV shows, movies)', 'Use "character" for any fictional, game, or media characters (e.g. characters from anime, games, books, TV shows, movies)',
@@ -65,9 +60,8 @@ function buildExtractionPrompt(userMessage, aiResponse, knownEntities = []) {
`User: ${userMessage}`, `User: ${userMessage}`,
`Assistant: ${aiResponse}`, `Assistant: ${aiResponse}`,
'--- END CONVERSATION ---', '--- END CONVERSATION ---',
'<|im_end|>', ].join('\n'),
'<|im_start|>assistant', };
].join('\n');
} }
async function embedEntity(entity) { async function embedEntity(entity) {
@@ -91,30 +85,16 @@ async function extractAndStoreEntities(userMessage, aiResponse, episodeId=null,
// Fetch existing entities to guide the model toward consistent name/type pairs // Fetch existing entities to guide the model toward consistent name/type pairs
const db = require('../db').getDB(); const db = require('../db').getDB();
const knownEntities = db.prepare(`SELECT name, type FROM entities ORDER BY rowid DESC LIMIT 20`).all(); const knownEntities = db.prepare(`SELECT name, type FROM entities ORDER BY rowid DESC LIMIT 20`).all();
const prompt = buildExtractionPrompt(userMessage, aiResponse, knownEntities); const { system, user } = buildExtractionPrompt(userMessage, aiResponse, knownEntities);
const raw = await utilityInference({
const res = await fetch(`${EXTRACTION_URL}/api/generate`, { system,
method: 'POST', user,
headers: { 'Content-Type': 'application/json' }, json: true,
body: JSON.stringify({
model: EXTRACTION_MODEL,
prompt: prompt,
stream: false,
format: 'json',
options: {
temperature: ENTITIES.TEMPERATURE, temperature: ENTITIES.TEMPERATURE,
num_predict: ENTITIES.NUM_PREDICT, maxTokens: ENTITIES.NUM_PREDICT,
},
}),
signal: AbortSignal.timeout(60_000),
}); });
if (!res.ok) throw new Error(`Ollama responded ${res.status}`);
const data = await res.json();
const raw = data.response?.trim() ?? '';
const jsonMatch = raw.match(/\{[\s\S]*\}/); const jsonMatch = raw.match(/\{[\s\S]*\}/);
if (!jsonMatch) { if (!jsonMatch) {
logger.warn('[entities] No JSON object found in response'); logger.warn('[entities] No JSON object found in response');
+2
View File
@@ -17,6 +17,7 @@ const {
} = require('./config/constants'); } = require('./config/constants');
const {parseRow, formatEpisodeText, isTrivialTurn} = require('./utils') const {parseRow, formatEpisodeText, isTrivialTurn} = require('./utils')
const logger = require('./utils/logger'); const logger = require('./utils/logger');
const utilityInference = require('./utils/utilityInference');
module.exports = { module.exports = {
getEnv, getEnv,
@@ -38,4 +39,5 @@ module.exports = {
logger, logger,
RETRIEVAL, RETRIEVAL,
UTILITY, UTILITY,
utilityInference,
}; };
@@ -0,0 +1,20 @@
const { getEnv } = require('../config/env');
const { SERVICES } = require('../config/constants');
// Client for the inference service's /utility/complete route.
// Resolved at call time (not module load) so each service's .env applies.
async function utilityInference({ system, user, json = false, temperature, maxTokens }) {
const base = getEnv('INFERENCE_SERVICE_URL', SERVICES.INFERENCE_URL);
const res = await fetch(`${base}/utility/complete`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ system, user, json, temperature, maxTokens }),
});
if (!res.ok) throw new Error(`Utility inference error: ${res.status}`);
const data = await res.json();
return data.text;
}
module.exports = { utilityInference };