utilityInference layer 2
This commit is contained in:
@@ -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');
|
||||||
|
|||||||
@@ -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 };
|
||||||
Reference in New Issue
Block a user