neoagent 1.5.1 → 1.5.3
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/package.json
CHANGED
|
@@ -29,9 +29,10 @@ function readUpdateStatus() {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
// Get supported models metadata
|
|
32
|
-
router.get('/meta/models', (req, res) => {
|
|
33
|
-
const {
|
|
34
|
-
|
|
32
|
+
router.get('/meta/models', async (req, res) => {
|
|
33
|
+
const { getSupportedModels } = require('../services/ai/models');
|
|
34
|
+
const models = await getSupportedModels();
|
|
35
|
+
res.json({ models });
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
// Get all settings
|
|
@@ -18,8 +18,9 @@ function generateTitle(task) {
|
|
|
18
18
|
return cleaned.slice(0, 90);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function getProviderForUser(userId, task = '', isSubagent = false, modelOverride = null) {
|
|
22
|
-
const {
|
|
21
|
+
async function getProviderForUser(userId, task = '', isSubagent = false, modelOverride = null) {
|
|
22
|
+
const { getSupportedModels, createProviderInstance } = require('./models');
|
|
23
|
+
const models = await getSupportedModels();
|
|
23
24
|
|
|
24
25
|
let enabledIds = [];
|
|
25
26
|
let defaultChatModel = 'auto';
|
|
@@ -49,16 +50,16 @@ function getProviderForUser(userId, task = '', isSubagent = false, modelOverride
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
if (!Array.isArray(enabledIds) || enabledIds.length === 0) {
|
|
52
|
-
enabledIds =
|
|
53
|
+
enabledIds = models.map((m) => m.id);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
const availableModels =
|
|
56
|
-
const fallbackModel = availableModels.length > 0 ? availableModels[0] :
|
|
56
|
+
const availableModels = models.filter((m) => enabledIds.includes(m.id));
|
|
57
|
+
const fallbackModel = availableModels.length > 0 ? availableModels[0] : models[0];
|
|
57
58
|
let selectedModelDef = fallbackModel;
|
|
58
59
|
const userSelectedDefault = isSubagent ? defaultSubagentModel : defaultChatModel;
|
|
59
60
|
|
|
60
61
|
if (modelOverride && typeof modelOverride === 'string') {
|
|
61
|
-
const requested =
|
|
62
|
+
const requested = models.find((m) => m.id === modelOverride.trim());
|
|
62
63
|
if (requested && enabledIds.includes(requested.id)) {
|
|
63
64
|
selectedModelDef = requested;
|
|
64
65
|
return {
|
|
@@ -70,10 +71,10 @@ function getProviderForUser(userId, task = '', isSubagent = false, modelOverride
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
if (userSelectedDefault && userSelectedDefault !== 'auto') {
|
|
73
|
-
selectedModelDef =
|
|
74
|
+
selectedModelDef = models.find((m) => m.id === userSelectedDefault) || fallbackModel;
|
|
74
75
|
} else {
|
|
75
76
|
const taskStr = String(task || '').toLowerCase();
|
|
76
|
-
|
|
77
|
+
|
|
77
78
|
// Basic detection
|
|
78
79
|
let isPlanning = /\b(plan|think|analy[sz]e|complex|step by step)\b/.test(taskStr);
|
|
79
80
|
let isCoding = false;
|
|
@@ -83,7 +84,7 @@ function getProviderForUser(userId, task = '', isSubagent = false, modelOverride
|
|
|
83
84
|
isPlanning = isPlanning || /\b(reason|strategy|logical|math|complex)\b/.test(taskStr);
|
|
84
85
|
isCoding = /\b(code|program|script|debug|refactor|function|implementation|logic)\b/.test(taskStr);
|
|
85
86
|
}
|
|
86
|
-
|
|
87
|
+
|
|
87
88
|
if (isPlanning) {
|
|
88
89
|
selectedModelDef = availableModels.find((m) => m.purpose === 'planning') || fallbackModel;
|
|
89
90
|
} else if (isCoding) {
|
|
@@ -240,7 +241,7 @@ class AgentEngine {
|
|
|
240
241
|
const triggerType = options.triggerType || 'user';
|
|
241
242
|
ensureDefaultAiSettings(userId);
|
|
242
243
|
const aiSettings = getAiSettings(userId);
|
|
243
|
-
const { provider, model, providerName } = getProviderForUser(userId, userMessage, triggerType === 'subagent', _modelOverride);
|
|
244
|
+
const { provider, model, providerName } = await getProviderForUser(userId, userMessage, triggerType === 'subagent', _modelOverride);
|
|
244
245
|
|
|
245
246
|
const runId = options.runId || uuidv4();
|
|
246
247
|
const conversationId = options.conversationId;
|
|
@@ -345,19 +346,19 @@ class AgentEngine {
|
|
|
345
346
|
console.error(`[Engine] Model call failed (${model}):`, err.message);
|
|
346
347
|
if (retryForFallback && aiSettings.fallback_model_id && aiSettings.fallback_model_id !== model) {
|
|
347
348
|
console.log(`[Engine] Attempting fallback to: ${aiSettings.fallback_model_id}`);
|
|
348
|
-
const fallback = getProviderForUser(userId, userMessage, triggerType === 'subagent', aiSettings.fallback_model_id);
|
|
349
|
+
const fallback = await getProviderForUser(userId, userMessage, triggerType === 'subagent', aiSettings.fallback_model_id);
|
|
349
350
|
// Update local state for the retry
|
|
350
351
|
const nextProvider = fallback.provider;
|
|
351
352
|
const nextModel = fallback.model;
|
|
352
353
|
const nextProviderName = fallback.providerName;
|
|
353
|
-
|
|
354
|
+
|
|
354
355
|
// Recursive call once
|
|
355
356
|
const retryOptions = { ...callOptions, model: nextModel, reasoningEffort: this.getReasoningEffort(nextProviderName, options) };
|
|
356
|
-
|
|
357
|
+
|
|
357
358
|
if (options.stream !== false) {
|
|
358
359
|
const gen = nextProvider.stream(messages, tools, retryOptions);
|
|
359
360
|
for await (const chunk of gen) {
|
|
360
|
-
|
|
361
|
+
if (chunk.type === 'content') {
|
|
361
362
|
streamContent += chunk.content;
|
|
362
363
|
this.emit(userId, 'run:stream', { runId, content: streamContent, iteration });
|
|
363
364
|
}
|
|
@@ -3,7 +3,7 @@ const { OpenAIProvider } = require('./providers/openai');
|
|
|
3
3
|
const { GoogleProvider } = require('./providers/google');
|
|
4
4
|
const { OllamaProvider } = require('./providers/ollama');
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const STATIC_MODELS = [
|
|
7
7
|
{
|
|
8
8
|
id: 'grok-4-1-fast-reasoning',
|
|
9
9
|
label: 'Grok 4.1 (Personality / Default)',
|
|
@@ -54,6 +54,46 @@ const SUPPORTED_MODELS = [
|
|
|
54
54
|
}
|
|
55
55
|
];
|
|
56
56
|
|
|
57
|
+
let dynamicModels = [];
|
|
58
|
+
let lastRefresh = 0;
|
|
59
|
+
const REFRESH_INTERVAL = 30000; // 30 seconds
|
|
60
|
+
|
|
61
|
+
async function getSupportedModels() {
|
|
62
|
+
const now = Date.now();
|
|
63
|
+
if (now - lastRefresh > REFRESH_INTERVAL) {
|
|
64
|
+
await refreshDynamicModels();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const all = [...STATIC_MODELS];
|
|
68
|
+
const staticIds = new Set(STATIC_MODELS.map(m => m.id));
|
|
69
|
+
|
|
70
|
+
for (const dm of dynamicModels) {
|
|
71
|
+
if (!staticIds.has(dm.id)) {
|
|
72
|
+
all.push(dm);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return all;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function refreshDynamicModels() {
|
|
80
|
+
try {
|
|
81
|
+
const ollama = new OllamaProvider({ baseUrl: process.env.OLLAMA_URL });
|
|
82
|
+
const models = await ollama.listModels();
|
|
83
|
+
|
|
84
|
+
dynamicModels = models.map(name => ({
|
|
85
|
+
id: name,
|
|
86
|
+
label: `${name} (Ollama / Local)`,
|
|
87
|
+
provider: 'ollama',
|
|
88
|
+
purpose: 'general'
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
lastRefresh = Date.now();
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.warn('[Models] Failed to refresh Ollama models:', err.message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
57
97
|
function createProviderInstance(providerStr) {
|
|
58
98
|
if (providerStr === 'grok') {
|
|
59
99
|
return new GrokProvider({ apiKey: process.env.XAI_API_KEY });
|
|
@@ -68,6 +108,7 @@ function createProviderInstance(providerStr) {
|
|
|
68
108
|
}
|
|
69
109
|
|
|
70
110
|
module.exports = {
|
|
71
|
-
SUPPORTED_MODELS,
|
|
111
|
+
SUPPORTED_MODELS: STATIC_MODELS, // Backward compatibility
|
|
112
|
+
getSupportedModels,
|
|
72
113
|
createProviderInstance
|
|
73
114
|
};
|
|
@@ -4,44 +4,44 @@ const db = require('../../db/database');
|
|
|
4
4
|
const { DATA_DIR } = require('../../../runtime/paths');
|
|
5
5
|
|
|
6
6
|
function compactText(text, maxChars = 120) {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
const str = String(text || '').replace(/\s+/g, ' ').trim();
|
|
8
|
+
if (str.length <= maxChars) return str;
|
|
9
|
+
const trimmed = str.slice(0, maxChars);
|
|
10
|
+
const sentenceBreak = Math.max(trimmed.lastIndexOf('. '), trimmed.lastIndexOf('; '), trimmed.lastIndexOf(', '));
|
|
11
|
+
if (sentenceBreak > 40) return trimmed.slice(0, sentenceBreak + 1).trim();
|
|
12
|
+
return `${trimmed.trim()}...`;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
function compactToolDefinition(tool, options = {}) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
const compact = {
|
|
17
|
+
name: tool.name,
|
|
18
|
+
parameters: {
|
|
19
|
+
...(tool.parameters || { type: 'object', properties: {} }),
|
|
20
|
+
properties: {}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
if (options.includeDescriptions) {
|
|
25
|
+
compact.description = compactText(tool.description, 120);
|
|
21
26
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
|
|
28
|
+
if (tool.parameters?.properties) {
|
|
29
|
+
const properties = {};
|
|
30
|
+
for (const [key, value] of Object.entries(tool.parameters.properties)) {
|
|
31
|
+
properties[key] = { ...value };
|
|
32
|
+
if (options.includeDescriptions && value.description) {
|
|
33
|
+
properties[key].description = compactText(value.description, 70);
|
|
34
|
+
} else {
|
|
35
|
+
delete properties[key].description;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
compact.parameters = {
|
|
39
|
+
...compact.parameters,
|
|
40
|
+
properties
|
|
41
|
+
};
|
|
37
42
|
}
|
|
38
|
-
compact.parameters = {
|
|
39
|
-
...compact.parameters,
|
|
40
|
-
properties
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
return compact;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/**
|
|
@@ -1199,7 +1199,7 @@ async function executeTool(toolName, args, context, engine) {
|
|
|
1199
1199
|
const mimeMap = { '.png': 'image/png', '.gif': 'image/gif', '.webp': 'image/webp', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg' };
|
|
1200
1200
|
const mime = mimeMap[ext] || 'image/jpeg';
|
|
1201
1201
|
const { getProviderForUser } = require('./engine');
|
|
1202
|
-
const { provider: visionProvider, model: visionModel } = getProviderForUser(userId);
|
|
1202
|
+
const { provider: visionProvider, model: visionModel } = await getProviderForUser(userId);
|
|
1203
1203
|
const visionResponse = await visionProvider.chat(
|
|
1204
1204
|
[{
|
|
1205
1205
|
role: 'user', content: [
|
|
@@ -11,13 +11,10 @@ const {
|
|
|
11
11
|
} = require('./embeddings');
|
|
12
12
|
const { AGENT_DATA_DIR } = require('../../../runtime/paths');
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
* Derive the active AI provider name from user settings so the right
|
|
16
|
-
* embedding model is selected automatically (e.g. Gemini when using Google).
|
|
17
|
-
*/
|
|
18
|
-
function getActiveProvider(userId) {
|
|
14
|
+
async function getActiveProvider(userId) {
|
|
19
15
|
try {
|
|
20
|
-
const {
|
|
16
|
+
const { getSupportedModels } = require('../ai/models');
|
|
17
|
+
const models = await getSupportedModels();
|
|
21
18
|
const rows = db.prepare('SELECT key, value FROM user_settings WHERE user_id = ? AND key IN (?, ?)')
|
|
22
19
|
.all(userId || 1, 'default_chat_model', 'enabled_models');
|
|
23
20
|
|
|
@@ -36,7 +33,7 @@ function getActiveProvider(userId) {
|
|
|
36
33
|
: (Array.isArray(enabledIds) && enabledIds.length > 0 ? enabledIds[0] : null);
|
|
37
34
|
|
|
38
35
|
if (modelId) {
|
|
39
|
-
const def =
|
|
36
|
+
const def = models.find(m => m.id === modelId);
|
|
40
37
|
if (def) return def.provider;
|
|
41
38
|
}
|
|
42
39
|
} catch { }
|
|
@@ -118,7 +115,7 @@ class MemoryManager {
|
|
|
118
115
|
category = CATEGORIES.includes(category) ? category : 'episodic';
|
|
119
116
|
importance = Math.max(1, Math.min(10, Number(importance) || 5));
|
|
120
117
|
|
|
121
|
-
const embedding = await getEmbedding(content, getActiveProvider(userId));
|
|
118
|
+
const embedding = await getEmbedding(content, await getActiveProvider(userId));
|
|
122
119
|
|
|
123
120
|
// Dedup check: compare against existing non-archived memories for this user
|
|
124
121
|
const existing = db.prepare(
|
|
@@ -171,7 +168,7 @@ class MemoryManager {
|
|
|
171
168
|
|
|
172
169
|
if (!all.length) return [];
|
|
173
170
|
|
|
174
|
-
const queryVec = await getEmbedding(query, getActiveProvider(userId));
|
|
171
|
+
const queryVec = await getEmbedding(query, await getActiveProvider(userId));
|
|
175
172
|
|
|
176
173
|
const scored = all.map(mem => {
|
|
177
174
|
let score = 0;
|
|
@@ -235,7 +232,7 @@ class MemoryManager {
|
|
|
235
232
|
|
|
236
233
|
let newEmbed = mem.embedding;
|
|
237
234
|
if (content && content !== mem.content) {
|
|
238
|
-
const vec = await getEmbedding(newContent, getActiveProvider(null));
|
|
235
|
+
const vec = await getEmbedding(newContent, await getActiveProvider(null));
|
|
239
236
|
newEmbed = vec ? serializeEmbedding(vec) : mem.embedding;
|
|
240
237
|
}
|
|
241
238
|
|
|
@@ -81,7 +81,7 @@ function setupWebSocket(io, services) {
|
|
|
81
81
|
.run(userId, result.runId, 'assistant', result.content, JSON.stringify({ tokens: result.totalTokens }));
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
const { provider, model } = getProviderForUser(userId, task, false, options?.model || null);
|
|
84
|
+
const { provider, model } = await getProviderForUser(userId, task, false, options?.model || null);
|
|
85
85
|
refreshWebChatSummary(userId, provider, model, aiSettings.chat_history_window).catch((summaryErr) => {
|
|
86
86
|
console.error('[WS] Web summary refresh failed:', summaryErr.message);
|
|
87
87
|
});
|