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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "1.5.1",
3
+ "version": "1.5.3",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "MIT",
6
6
  "main": "server/index.js",
@@ -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 { SUPPORTED_MODELS } = require('../services/ai/models');
34
- res.json({ models: SUPPORTED_MODELS });
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 { SUPPORTED_MODELS, createProviderInstance } = require('./models');
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 = SUPPORTED_MODELS.map((m) => m.id);
53
+ enabledIds = models.map((m) => m.id);
53
54
  }
54
55
 
55
- const availableModels = SUPPORTED_MODELS.filter((m) => enabledIds.includes(m.id));
56
- const fallbackModel = availableModels.length > 0 ? availableModels[0] : SUPPORTED_MODELS[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 = SUPPORTED_MODELS.find((m) => m.id === modelOverride.trim());
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 = SUPPORTED_MODELS.find((m) => m.id === userSelectedDefault) || fallbackModel;
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
- if (chunk.type === 'content') {
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 SUPPORTED_MODELS = [
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
- 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()}...`;
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
- const compact = {
17
- name: tool.name,
18
- parameters: {
19
- ...(tool.parameters || { type: 'object', properties: {} }),
20
- properties: {}
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
- if (options.includeDescriptions) {
25
- compact.description = compactText(tool.description, 120);
26
- }
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
- }
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
- return compact;
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 { SUPPORTED_MODELS } = require('../ai/models');
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 = SUPPORTED_MODELS.find(m => m.id === modelId);
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
  });