nyxora 26.6.25 → 26.6.26
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/dist/packages/core/src/agent/llmProvider.js +3 -1
- package/dist/packages/core/src/agent/osAgent.js +15 -83
- package/dist/packages/core/src/agent/reasoning.js +15 -115
- package/dist/packages/core/src/agent/web3Agent.js +15 -81
- package/dist/packages/core/src/config/parser.js +14 -1
- package/dist/packages/core/src/gateway/server.js +3 -0
- package/dist/packages/core/src/gateway/telegram.js +5 -5
- package/dist/packages/core/src/memory/logger.js +11 -0
- package/dist/packages/core/src/memory/reflection.js +2 -2
- package/dist/packages/core/src/system/skills/audioTranscribe.js +2 -2
- package/dist/packages/core/src/system/skills/summarizeText.js +2 -2
- package/dist/packages/core/src/utils/historySanitizer.js +27 -17
- package/dist/packages/core/src/utils/llmUtils.js +126 -0
- package/dist/packages/core/src/web3/utils/vaultClient.js +2 -1
- package/dist/packages/policy/src/server.js +31 -26
- package/package.json +1 -1
- package/packages/core/package.json +1 -1
- package/packages/core/src/agent/llmProvider.ts +3 -1
- package/packages/core/src/agent/osAgent.ts +15 -92
- package/packages/core/src/agent/reasoning.ts +16 -125
- package/packages/core/src/agent/web3Agent.ts +15 -88
- package/packages/core/src/config/parser.ts +22 -7
- package/packages/core/src/gateway/server.ts +3 -0
- package/packages/core/src/gateway/telegram.ts +5 -5
- package/packages/core/src/memory/logger.ts +11 -0
- package/packages/core/src/memory/reflection.ts +1 -1
- package/packages/core/src/system/skills/audioTranscribe.ts +1 -1
- package/packages/core/src/system/skills/summarizeText.ts +1 -1
- package/packages/core/src/utils/historySanitizer.ts +24 -16
- package/packages/core/src/utils/llmUtils.ts +139 -0
- package/packages/core/src/web3/utils/vaultClient.ts +4 -1
- package/packages/dashboard/dist/assets/{index-BW-OzhTX.js → index-C_WmWSch.js} +2 -2
- package/packages/dashboard/dist/index.html +1 -1
- package/packages/dashboard/dist/routers/coingecko.png +0 -0
- package/packages/dashboard/package.json +3 -2
- package/packages/mcp-server/package.json +5 -3
- package/packages/policy/package.json +4 -2
- package/packages/policy/src/server.ts +27 -29
- package/packages/signer/package.json +2 -1
- package/packages/signer/src/server.ts +0 -1
|
@@ -162,7 +162,9 @@ class GeminiAdapter {
|
|
|
162
162
|
catch (e) { }
|
|
163
163
|
});
|
|
164
164
|
}
|
|
165
|
-
|
|
165
|
+
if (parts.length > 0) {
|
|
166
|
+
contents.push({ role: 'model', parts: parts });
|
|
167
|
+
}
|
|
166
168
|
}
|
|
167
169
|
else if (m.role === 'tool') {
|
|
168
170
|
contents.push({
|
|
@@ -4,9 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.logger = void 0;
|
|
7
|
-
exports.getOpenAI = getOpenAI;
|
|
8
7
|
exports.processOsIntent = processOsIntent;
|
|
9
|
-
const openai_1 = require("openai");
|
|
10
8
|
const parser_1 = require("../config/parser");
|
|
11
9
|
const logger_1 = require("../memory/logger");
|
|
12
10
|
const tracker_1 = require("../gateway/tracker");
|
|
@@ -15,73 +13,7 @@ const skillManager_1 = require("../utils/skillManager");
|
|
|
15
13
|
const registry_1 = require("../plugin/registry");
|
|
16
14
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
17
15
|
exports.logger = new logger_1.Logger();
|
|
18
|
-
const
|
|
19
|
-
ollama: { baseURL: process.env.OLLAMA_BASE_URL ? `${process.env.OLLAMA_BASE_URL}/v1` : 'http://localhost:11434/v1', requiresApiKey: false },
|
|
20
|
-
gemini: { baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/', requiresApiKey: true },
|
|
21
|
-
openrouter: { baseURL: 'https://openrouter.ai/api/v1', requiresApiKey: true },
|
|
22
|
-
groq: { baseURL: 'https://api.groq.com/openai/v1', requiresApiKey: true },
|
|
23
|
-
mistral: { baseURL: 'https://api.mistral.ai/v1', requiresApiKey: true },
|
|
24
|
-
xai: { baseURL: 'https://api.x.ai/v1', requiresApiKey: true },
|
|
25
|
-
deepseek: { baseURL: 'https://api.deepseek.com', requiresApiKey: true },
|
|
26
|
-
openai: { requiresApiKey: true }
|
|
27
|
-
};
|
|
28
|
-
async function getOpenAI() {
|
|
29
|
-
const config = (0, parser_1.loadConfig)();
|
|
30
|
-
const vaultKeys = await (0, parser_1.loadApiKeys)();
|
|
31
|
-
const providerName = config.llm.provider || 'openai';
|
|
32
|
-
const providerConf = PROVIDER_CONFIGS[providerName] || PROVIDER_CONFIGS['openai'];
|
|
33
|
-
let apiKey = 'local';
|
|
34
|
-
if (providerConf.requiresApiKey) {
|
|
35
|
-
apiKey = '';
|
|
36
|
-
const keyName = `${providerName}_key`;
|
|
37
|
-
apiKey = vaultKeys[keyName] || config.credentials?.[keyName] || '';
|
|
38
|
-
if (!apiKey) {
|
|
39
|
-
throw new Error(`[Security] No API Key found for ${providerName} in OS Keyring. Please run 'nyxora set-key ${providerName} <key>' or 'nyxora setup'.`);
|
|
40
|
-
}
|
|
41
|
-
console.log(`[LLM] Using API Key securely unlocked from OS Keyring vault.`);
|
|
42
|
-
}
|
|
43
|
-
return new openai_1.OpenAI({
|
|
44
|
-
baseURL: providerConf.baseURL,
|
|
45
|
-
apiKey: apiKey,
|
|
46
|
-
timeout: 120 * 1000,
|
|
47
|
-
maxRetries: 0
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
async function executeWithRetry(requestBuilder, maxRetries = 3) {
|
|
51
|
-
let retries = 0;
|
|
52
|
-
while (retries <= maxRetries) {
|
|
53
|
-
try {
|
|
54
|
-
const client = await getOpenAI();
|
|
55
|
-
return await requestBuilder(client);
|
|
56
|
-
}
|
|
57
|
-
catch (error) {
|
|
58
|
-
const status = error?.status || error?.response?.status;
|
|
59
|
-
// 401 Unauthorized or 400 Bad Request - don't retry, it's fatal
|
|
60
|
-
if (status === 401 || status === 400) {
|
|
61
|
-
console.error(`[LLM] Fatal Error ${status}: ${error.message}. Aborting.`);
|
|
62
|
-
throw error;
|
|
63
|
-
}
|
|
64
|
-
// 429 Rate Limit - rotate provider/key immediately and retry
|
|
65
|
-
if (status === 429) {
|
|
66
|
-
console.warn(`[LLM] Rate Limit (429) hit. Rotating key...`);
|
|
67
|
-
// getOpenAI() automatically rotates to next key if available
|
|
68
|
-
retries++;
|
|
69
|
-
if (retries > maxRetries)
|
|
70
|
-
throw error;
|
|
71
|
-
continue; // Try next key immediately
|
|
72
|
-
}
|
|
73
|
-
// 500, 502, 503, Timeout, Network error - Exponential Backoff
|
|
74
|
-
retries++;
|
|
75
|
-
if (retries > maxRetries) {
|
|
76
|
-
console.error(`[LLM] Max retries reached.`);
|
|
77
|
-
throw error;
|
|
78
|
-
}
|
|
79
|
-
const delayMs = Math.pow(2, retries) * 1000; // 2s, 4s, 8s
|
|
80
|
-
console.warn(`[LLM] API Error (${status || error.message}). Retrying in ${delayMs}ms...`);
|
|
81
|
-
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
16
|
+
const llmUtils_1 = require("../utils/llmUtils");
|
|
85
17
|
function getSystemPrompt(context = 'os') {
|
|
86
18
|
const config = (0, parser_1.loadConfig)();
|
|
87
19
|
const currentDateTime = new Date().toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' });
|
|
@@ -122,25 +54,21 @@ async function processOsIntent(input, role = 'user', onProgress, sessionId) {
|
|
|
122
54
|
let activeTools = [...registry_1.pluginManager.getAllToolDefinitions()];
|
|
123
55
|
activeTools = activeTools.filter(t => (0, skillManager_1.isSkillActive)(t.function.name));
|
|
124
56
|
const { sanitizeHistoryForLLM } = require('../utils/historySanitizer');
|
|
125
|
-
const sanitizedHistory = sanitizeHistoryForLLM(history, activeTools);
|
|
57
|
+
const sanitizedHistory = sanitizeHistoryForLLM(history, activeTools, config.llm.provider);
|
|
126
58
|
let messages = [
|
|
127
59
|
{ role: 'system', content: getSystemPrompt('os') },
|
|
128
60
|
...sanitizedHistory
|
|
129
61
|
];
|
|
130
62
|
try {
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
activeTools = activeTools.filter(t => (0, skillManager_1.isSkillActive)(t.function.name));
|
|
134
|
-
const response = await executeWithRetry(async (client) => {
|
|
135
|
-
return await client.chat.completions.create({
|
|
63
|
+
const response = await (0, llmUtils_1.executeWithRetry)(async (client) => {
|
|
64
|
+
return await client.chat({
|
|
136
65
|
model: config.llm.model,
|
|
137
66
|
temperature: config.llm.temperature,
|
|
138
67
|
messages: messages,
|
|
139
|
-
tools: activeTools
|
|
140
|
-
tool_choice: "auto",
|
|
68
|
+
tools: activeTools
|
|
141
69
|
});
|
|
142
70
|
});
|
|
143
|
-
const responseMessage = response.
|
|
71
|
+
const responseMessage = response.message;
|
|
144
72
|
tracker_1.Tracker.addMessage();
|
|
145
73
|
if (response.usage?.total_tokens) {
|
|
146
74
|
tracker_1.Tracker.addTokens(response.usage.total_tokens, config.llm.provider);
|
|
@@ -230,13 +158,13 @@ async function processOsIntent(input, role = 'user', onProgress, sessionId) {
|
|
|
230
158
|
return finalContent;
|
|
231
159
|
}
|
|
232
160
|
// Second call to get the final answer after tool execution
|
|
233
|
-
const secondSanitized = sanitizeHistoryForLLM(exports.logger.getHistory(sessionId), activeTools);
|
|
161
|
+
const secondSanitized = sanitizeHistoryForLLM(exports.logger.getHistory(sessionId), activeTools, config.llm.provider);
|
|
234
162
|
const secondMessages = [
|
|
235
163
|
{ role: 'system', content: getSystemPrompt('os') },
|
|
236
164
|
...secondSanitized
|
|
237
165
|
];
|
|
238
|
-
const secondResponse = await executeWithRetry(async (client) => {
|
|
239
|
-
return await client.chat
|
|
166
|
+
const secondResponse = await (0, llmUtils_1.executeWithRetry)(async (client) => {
|
|
167
|
+
return await client.chat({
|
|
240
168
|
model: config.llm.model,
|
|
241
169
|
messages: secondMessages,
|
|
242
170
|
});
|
|
@@ -245,7 +173,7 @@ async function processOsIntent(input, role = 'user', onProgress, sessionId) {
|
|
|
245
173
|
tracker_1.Tracker.addTokens(secondResponse.usage.total_tokens, config.llm.provider);
|
|
246
174
|
}
|
|
247
175
|
tracker_1.Tracker.addEvent('llm.final_response', { provider: config.llm.provider });
|
|
248
|
-
let finalContent = secondResponse.
|
|
176
|
+
let finalContent = secondResponse.message.content || "";
|
|
249
177
|
// Clean up orphaned <think> blocks that forgot to output </think>
|
|
250
178
|
finalContent = finalContent.replace(/<thought>[\s\S]*?<\/thought>\n?/gi, '');
|
|
251
179
|
finalContent = finalContent.replace(/<think>[\s\S]*?<\/think>\n?/gi, '');
|
|
@@ -270,7 +198,11 @@ async function processOsIntent(input, role = 'user', onProgress, sessionId) {
|
|
|
270
198
|
}
|
|
271
199
|
catch (error) {
|
|
272
200
|
console.error("LLM Error:", error);
|
|
273
|
-
const
|
|
201
|
+
const status = error?.status || error?.response?.status;
|
|
202
|
+
let errorMsg = '⚠️ All models are temporarily rate-limited. Please try again in a few minutes.';
|
|
203
|
+
if (status === 400 || (error.message && error.message.toLowerCase().includes('invalid'))) {
|
|
204
|
+
errorMsg = '⚠️ Terjadi kesalahan pemahaman instruksi. LLM kesulitan menentukan format alat (skill) yang cocok. Silakan coba deskripsikan perintah Anda dengan lebih spesifik.';
|
|
205
|
+
}
|
|
274
206
|
exports.logger.addEntry({ role: 'assistant', content: errorMsg }, sessionId);
|
|
275
207
|
return errorMsg;
|
|
276
208
|
}
|
|
@@ -4,121 +4,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.logger = void 0;
|
|
7
|
-
exports.getOpenAI = getOpenAI;
|
|
8
|
-
exports.getLLMClient = getLLMClient;
|
|
9
7
|
exports.processUserInput = processUserInput;
|
|
10
8
|
const fs_1 = __importDefault(require("fs"));
|
|
11
|
-
const openai_1 = require("openai");
|
|
12
|
-
const llmProvider_1 = require("./llmProvider");
|
|
13
|
-
const sdk_1 = __importDefault(require("@anthropic-ai/sdk"));
|
|
14
9
|
const parser_1 = require("../config/parser");
|
|
15
10
|
const logger_1 = require("../memory/logger");
|
|
16
11
|
const episodic_1 = require("../memory/episodic");
|
|
17
12
|
const paths_1 = require("../config/paths");
|
|
18
13
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
19
14
|
exports.logger = new logger_1.Logger();
|
|
20
|
-
const
|
|
21
|
-
ollama: { baseURL: process.env.OLLAMA_BASE_URL ? `${process.env.OLLAMA_BASE_URL}/v1` : 'http://localhost:11434/v1', requiresApiKey: false },
|
|
22
|
-
gemini: { baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/', requiresApiKey: true },
|
|
23
|
-
openrouter: { baseURL: 'https://openrouter.ai/api/v1', requiresApiKey: true },
|
|
24
|
-
groq: { baseURL: 'https://api.groq.com/openai/v1', requiresApiKey: true },
|
|
25
|
-
mistral: { baseURL: 'https://api.mistral.ai/v1', requiresApiKey: true },
|
|
26
|
-
xai: { baseURL: 'https://api.x.ai/v1', requiresApiKey: true },
|
|
27
|
-
deepseek: { baseURL: 'https://api.deepseek.com', requiresApiKey: true },
|
|
28
|
-
openai: { requiresApiKey: true }
|
|
29
|
-
};
|
|
30
|
-
async function getOpenAI() {
|
|
31
|
-
const config = (0, parser_1.loadConfig)();
|
|
32
|
-
const vaultKeys = await (0, parser_1.loadApiKeys)();
|
|
33
|
-
const providerName = config.llm.provider || 'openai';
|
|
34
|
-
// Audio Transcription Fallback: Always try to use OpenAI/Groq if Anthropic/Gemini
|
|
35
|
-
let actualProvider = (providerName === 'anthropic' || providerName === 'gemini') ? 'openai' : providerName;
|
|
36
|
-
const providerConf = PROVIDER_CONFIGS[actualProvider] || PROVIDER_CONFIGS['openai'];
|
|
37
|
-
let apiKey = 'local';
|
|
38
|
-
if (providerConf.requiresApiKey) {
|
|
39
|
-
const keyName = `${actualProvider}_key`;
|
|
40
|
-
apiKey = vaultKeys[keyName] || config.credentials?.[keyName] || '';
|
|
41
|
-
if (!apiKey && actualProvider === 'openai') {
|
|
42
|
-
// Last resort fallback to groq for audio if openai key is missing
|
|
43
|
-
actualProvider = 'groq';
|
|
44
|
-
apiKey = vaultKeys['groq_key'] || config.credentials?.['groq_key'] || '';
|
|
45
|
-
}
|
|
46
|
-
if (!apiKey) {
|
|
47
|
-
throw new Error(`[Security] No Audio Transcription API Key found (OpenAI/Groq). Please run 'nyxora set-key openai <key>'.`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return new openai_1.OpenAI({
|
|
51
|
-
baseURL: (PROVIDER_CONFIGS[actualProvider] || PROVIDER_CONFIGS['openai']).baseURL,
|
|
52
|
-
apiKey: apiKey,
|
|
53
|
-
timeout: 120 * 1000,
|
|
54
|
-
maxRetries: 0
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
async function getLLMClient() {
|
|
58
|
-
const config = (0, parser_1.loadConfig)();
|
|
59
|
-
const vaultKeys = await (0, parser_1.loadApiKeys)();
|
|
60
|
-
const providerName = config.llm.provider || 'openai';
|
|
61
|
-
let apiKey = '';
|
|
62
|
-
const keyName = `${providerName}_key`;
|
|
63
|
-
apiKey = vaultKeys[keyName] || config.credentials?.[keyName] || '';
|
|
64
|
-
if (!apiKey && providerName !== 'ollama') {
|
|
65
|
-
throw new Error(`[Security] No API Key found for ${providerName} in OS Keyring. Please run 'nyxora set-key ${providerName} <key>' or 'nyxora setup'.`);
|
|
66
|
-
}
|
|
67
|
-
if (providerName !== 'ollama') {
|
|
68
|
-
console.log(`[LLM] Using API Key securely unlocked from OS Keyring vault for ${providerName}.`);
|
|
69
|
-
}
|
|
70
|
-
if (providerName === 'anthropic') {
|
|
71
|
-
const client = new sdk_1.default({ apiKey });
|
|
72
|
-
return new llmProvider_1.AnthropicAdapter(client);
|
|
73
|
-
}
|
|
74
|
-
if (providerName === 'gemini') {
|
|
75
|
-
return new llmProvider_1.GeminiAdapter(apiKey);
|
|
76
|
-
}
|
|
77
|
-
// Default fallback (OpenAI, Groq, OpenRouter, xAI, Mistral, DeepSeek)
|
|
78
|
-
const providerConf = PROVIDER_CONFIGS[providerName] || PROVIDER_CONFIGS['openai'];
|
|
79
|
-
const client = new openai_1.OpenAI({
|
|
80
|
-
baseURL: providerConf.baseURL,
|
|
81
|
-
apiKey: apiKey || 'local',
|
|
82
|
-
timeout: 120 * 1000,
|
|
83
|
-
maxRetries: 0
|
|
84
|
-
});
|
|
85
|
-
return new llmProvider_1.OpenAIAdapter(client);
|
|
86
|
-
}
|
|
87
|
-
async function executeWithRetry(requestBuilder, maxRetries = 3) {
|
|
88
|
-
let retries = 0;
|
|
89
|
-
while (retries <= maxRetries) {
|
|
90
|
-
try {
|
|
91
|
-
const client = await getLLMClient();
|
|
92
|
-
return await requestBuilder(client);
|
|
93
|
-
}
|
|
94
|
-
catch (error) {
|
|
95
|
-
const status = error?.status || error?.response?.status;
|
|
96
|
-
// 401 Unauthorized or 400 Bad Request - don't retry, it's fatal
|
|
97
|
-
if (status === 401 || status === 400) {
|
|
98
|
-
console.error(`[LLM] Fatal Error ${status}: ${error.message}. Aborting.`);
|
|
99
|
-
throw error;
|
|
100
|
-
}
|
|
101
|
-
// 429 Rate Limit - rotate provider/key immediately and retry
|
|
102
|
-
if (status === 429) {
|
|
103
|
-
console.warn(`[LLM] Rate Limit (429) hit. Rotating key...`);
|
|
104
|
-
// getOpenAI() automatically rotates to next key if available
|
|
105
|
-
retries++;
|
|
106
|
-
if (retries > maxRetries)
|
|
107
|
-
throw error;
|
|
108
|
-
continue; // Try next key immediately
|
|
109
|
-
}
|
|
110
|
-
// 500, 502, 503, Timeout, Network error - Exponential Backoff
|
|
111
|
-
retries++;
|
|
112
|
-
if (retries > maxRetries) {
|
|
113
|
-
console.error(`[LLM] Max retries reached.`);
|
|
114
|
-
throw error;
|
|
115
|
-
}
|
|
116
|
-
const delayMs = Math.pow(2, retries) * 1000; // 2s, 4s, 8s
|
|
117
|
-
console.warn(`[LLM] API Error (${status || error.message}). Retrying in ${delayMs}ms...`);
|
|
118
|
-
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
15
|
+
const llmUtils_1 = require("../utils/llmUtils");
|
|
122
16
|
function getSystemPrompt(context = 'general') {
|
|
123
17
|
const config = (0, parser_1.loadConfig)();
|
|
124
18
|
const currentDateTime = new Date().toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' });
|
|
@@ -283,7 +177,7 @@ Reply with EXACTLY ONE WORD.`;
|
|
|
283
177
|
];
|
|
284
178
|
let context = 'general';
|
|
285
179
|
try {
|
|
286
|
-
const routerResponse = await executeWithRetry(async (client) => {
|
|
180
|
+
const routerResponse = await (0, llmUtils_1.executeWithRetry)(async (client) => {
|
|
287
181
|
return await client.chat({
|
|
288
182
|
model: config.llm.model,
|
|
289
183
|
messages: routerMessages,
|
|
@@ -314,21 +208,20 @@ Reply with EXACTLY ONE WORD.`;
|
|
|
314
208
|
// General Agent: Use osAgent logic but without execution tools to save tokens.
|
|
315
209
|
// Wait, osAgent has activeTools. We can just route to osAgent for now, but general agent shouldn't have tools.
|
|
316
210
|
// Let's create a dummy general intent processing directly here.
|
|
317
|
-
const config = (0, parser_1.loadConfig)();
|
|
318
211
|
exports.logger.addEntry({ role, content: input }, sessionId);
|
|
319
|
-
const history = exports.logger.getHistory(sessionId);
|
|
320
212
|
const messages = [
|
|
321
|
-
{ role: 'system', content:
|
|
322
|
-
...textOnlyHistory
|
|
213
|
+
{ role: 'system', content: getSystemPrompt('general') },
|
|
214
|
+
...textOnlyHistory,
|
|
215
|
+
{ role: 'user', content: input }
|
|
323
216
|
];
|
|
324
217
|
try {
|
|
325
|
-
const response = await executeWithRetry(async (client) => {
|
|
218
|
+
const response = await (0, llmUtils_1.executeWithRetry)(async (client) => {
|
|
326
219
|
return await client.chat({
|
|
327
220
|
model: config.llm.model,
|
|
328
221
|
messages: messages
|
|
329
222
|
});
|
|
330
223
|
});
|
|
331
|
-
let finalContent = response.message?.content || "
|
|
224
|
+
let finalContent = response.message?.content || "";
|
|
332
225
|
finalContent = finalContent.replace(/<thought>[\s\S]*?<\/thought>\n?/gi, '');
|
|
333
226
|
finalContent = finalContent.replace(/<think>[\s\S]*?<\/think>\n?/gi, '');
|
|
334
227
|
if (finalContent.includes('<think>')) {
|
|
@@ -336,12 +229,19 @@ Reply with EXACTLY ONE WORD.`;
|
|
|
336
229
|
finalContent = finalContent.replace(/<think>[\s\S]*$/i, '');
|
|
337
230
|
}
|
|
338
231
|
finalContent = finalContent.trim();
|
|
232
|
+
if (!finalContent) {
|
|
233
|
+
finalContent = "⚠️ LLM mengembalikan respons kosong atau terputus. Hal ini biasanya terjadi karena fluktuasi koneksi API atau limitasi sesaat. Silakan coba lagi.";
|
|
234
|
+
}
|
|
339
235
|
exports.logger.addEntry({ role: 'assistant', content: finalContent }, sessionId);
|
|
340
236
|
return finalContent;
|
|
341
237
|
}
|
|
342
238
|
catch (error) {
|
|
343
239
|
console.error("General LLM Error:", error);
|
|
344
|
-
const
|
|
240
|
+
const status = error?.status || error?.response?.status;
|
|
241
|
+
let errorMsg = '⚠️ Sistem sedang mengalami limitasi koneksi LLM (Rate Limit). Harap tunggu beberapa detik dan coba lagi.';
|
|
242
|
+
if (status === 400 || (error.message && error.message.toLowerCase().includes('invalid'))) {
|
|
243
|
+
errorMsg = '⚠️ Terjadi kesalahan. Format pesan atau alat (skill) tidak dimengerti oleh LLM.';
|
|
244
|
+
}
|
|
345
245
|
exports.logger.addEntry({ role: 'assistant', content: errorMsg }, sessionId);
|
|
346
246
|
return errorMsg;
|
|
347
247
|
}
|
|
@@ -4,9 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.logger = void 0;
|
|
7
|
-
exports.getOpenAI = getOpenAI;
|
|
8
7
|
exports.processWeb3Intent = processWeb3Intent;
|
|
9
|
-
const openai_1 = require("openai");
|
|
10
8
|
const parser_1 = require("../config/parser");
|
|
11
9
|
const logger_1 = require("../memory/logger");
|
|
12
10
|
const tracker_1 = require("../gateway/tracker");
|
|
@@ -15,73 +13,7 @@ const skillManager_1 = require("../utils/skillManager");
|
|
|
15
13
|
const registry_1 = require("../plugin/registry");
|
|
16
14
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
17
15
|
exports.logger = new logger_1.Logger();
|
|
18
|
-
const
|
|
19
|
-
ollama: { baseURL: process.env.OLLAMA_BASE_URL ? `${process.env.OLLAMA_BASE_URL}/v1` : 'http://localhost:11434/v1', requiresApiKey: false },
|
|
20
|
-
gemini: { baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai/', requiresApiKey: true },
|
|
21
|
-
openrouter: { baseURL: 'https://openrouter.ai/api/v1', requiresApiKey: true },
|
|
22
|
-
groq: { baseURL: 'https://api.groq.com/openai/v1', requiresApiKey: true },
|
|
23
|
-
mistral: { baseURL: 'https://api.mistral.ai/v1', requiresApiKey: true },
|
|
24
|
-
xai: { baseURL: 'https://api.x.ai/v1', requiresApiKey: true },
|
|
25
|
-
deepseek: { baseURL: 'https://api.deepseek.com', requiresApiKey: true },
|
|
26
|
-
openai: { requiresApiKey: true }
|
|
27
|
-
};
|
|
28
|
-
async function getOpenAI() {
|
|
29
|
-
const config = (0, parser_1.loadConfig)();
|
|
30
|
-
const vaultKeys = await (0, parser_1.loadApiKeys)();
|
|
31
|
-
const providerName = config.llm.provider || 'openai';
|
|
32
|
-
const providerConf = PROVIDER_CONFIGS[providerName] || PROVIDER_CONFIGS['openai'];
|
|
33
|
-
let apiKey = 'local';
|
|
34
|
-
if (providerConf.requiresApiKey) {
|
|
35
|
-
apiKey = '';
|
|
36
|
-
const keyName = `${providerName}_key`;
|
|
37
|
-
apiKey = vaultKeys[keyName] || config.credentials?.[keyName] || '';
|
|
38
|
-
if (!apiKey) {
|
|
39
|
-
throw new Error(`[Security] No API Key found for ${providerName} in OS Keyring. Please run 'nyxora set-key ${providerName} <key>' or 'nyxora setup'.`);
|
|
40
|
-
}
|
|
41
|
-
console.log(`[LLM] Using API Key securely unlocked from OS Keyring vault.`);
|
|
42
|
-
}
|
|
43
|
-
return new openai_1.OpenAI({
|
|
44
|
-
baseURL: providerConf.baseURL,
|
|
45
|
-
apiKey: apiKey,
|
|
46
|
-
timeout: 120 * 1000,
|
|
47
|
-
maxRetries: 0
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
async function executeWithRetry(requestBuilder, maxRetries = 3) {
|
|
51
|
-
let retries = 0;
|
|
52
|
-
while (retries <= maxRetries) {
|
|
53
|
-
try {
|
|
54
|
-
const client = await getOpenAI();
|
|
55
|
-
return await requestBuilder(client);
|
|
56
|
-
}
|
|
57
|
-
catch (error) {
|
|
58
|
-
const status = error?.status || error?.response?.status;
|
|
59
|
-
// 401 Unauthorized or 400 Bad Request - don't retry, it's fatal
|
|
60
|
-
if (status === 401 || status === 400) {
|
|
61
|
-
console.error(`[LLM] Fatal Error ${status}: ${error.message}. Aborting.`);
|
|
62
|
-
throw error;
|
|
63
|
-
}
|
|
64
|
-
// 429 Rate Limit - rotate provider/key immediately and retry
|
|
65
|
-
if (status === 429) {
|
|
66
|
-
console.warn(`[LLM] Rate Limit (429) hit. Rotating key...`);
|
|
67
|
-
// getOpenAI() automatically rotates to next key if available
|
|
68
|
-
retries++;
|
|
69
|
-
if (retries > maxRetries)
|
|
70
|
-
throw error;
|
|
71
|
-
continue; // Try next key immediately
|
|
72
|
-
}
|
|
73
|
-
// 500, 502, 503, Timeout, Network error - Exponential Backoff
|
|
74
|
-
retries++;
|
|
75
|
-
if (retries > maxRetries) {
|
|
76
|
-
console.error(`[LLM] Max retries reached.`);
|
|
77
|
-
throw error;
|
|
78
|
-
}
|
|
79
|
-
const delayMs = Math.pow(2, retries) * 1000; // 2s, 4s, 8s
|
|
80
|
-
console.warn(`[LLM] API Error (${status || error.message}). Retrying in ${delayMs}ms...`);
|
|
81
|
-
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
16
|
+
const llmUtils_1 = require("../utils/llmUtils");
|
|
85
17
|
function getSystemPrompt(context = 'web3') {
|
|
86
18
|
const config = (0, parser_1.loadConfig)();
|
|
87
19
|
const currentDateTime = new Date().toLocaleString('id-ID', { timeZone: 'Asia/Jakarta' });
|
|
@@ -128,26 +60,24 @@ async function processWeb3Intent(input, role = 'user', onProgress, sessionId) {
|
|
|
128
60
|
let activeTools = [...pluginTools];
|
|
129
61
|
activeTools = activeTools.filter(t => (0, skillManager_1.isSkillActive)(t.function.name));
|
|
130
62
|
const { sanitizeHistoryForLLM } = require('../utils/historySanitizer');
|
|
131
|
-
const sanitizedHistory = sanitizeHistoryForLLM(history, activeTools);
|
|
63
|
+
const sanitizedHistory = sanitizeHistoryForLLM(history, activeTools, config.llm.provider);
|
|
132
64
|
let messages = [
|
|
133
65
|
{ role: 'system', content: getSystemPrompt('web3') },
|
|
134
66
|
...sanitizedHistory
|
|
135
67
|
];
|
|
136
68
|
try {
|
|
137
69
|
const context = 'web3';
|
|
138
|
-
const response = await executeWithRetry(async (client) => {
|
|
70
|
+
const response = await (0, llmUtils_1.executeWithRetry)(async (client) => {
|
|
139
71
|
// Debug log to find out why Gemini 400 error happens
|
|
140
72
|
console.log(`[LLM Debug] Sending ${messages.length} messages to LLM.`);
|
|
141
|
-
|
|
142
|
-
return await client.chat.completions.create({
|
|
73
|
+
return await client.chat({
|
|
143
74
|
model: config.llm.model,
|
|
144
75
|
temperature: config.llm.temperature,
|
|
145
76
|
messages: messages,
|
|
146
|
-
tools: activeTools
|
|
147
|
-
tool_choice: "auto",
|
|
77
|
+
tools: activeTools
|
|
148
78
|
});
|
|
149
79
|
});
|
|
150
|
-
const responseMessage = response.
|
|
80
|
+
const responseMessage = response.message;
|
|
151
81
|
tracker_1.Tracker.addMessage();
|
|
152
82
|
if (response.usage?.total_tokens) {
|
|
153
83
|
tracker_1.Tracker.addTokens(response.usage.total_tokens, config.llm.provider);
|
|
@@ -237,13 +167,13 @@ async function processWeb3Intent(input, role = 'user', onProgress, sessionId) {
|
|
|
237
167
|
return finalContent;
|
|
238
168
|
}
|
|
239
169
|
// Second call to get the final answer after tool execution
|
|
240
|
-
const secondSanitized = sanitizeHistoryForLLM(exports.logger.getHistory(sessionId), activeTools);
|
|
170
|
+
const secondSanitized = sanitizeHistoryForLLM(exports.logger.getHistory(sessionId), activeTools, config.llm.provider);
|
|
241
171
|
const secondMessages = [
|
|
242
172
|
{ role: 'system', content: getSystemPrompt('web3') },
|
|
243
173
|
...secondSanitized
|
|
244
174
|
];
|
|
245
|
-
const secondResponse = await executeWithRetry(async (client) => {
|
|
246
|
-
return await client.chat
|
|
175
|
+
const secondResponse = await (0, llmUtils_1.executeWithRetry)(async (client) => {
|
|
176
|
+
return await client.chat({
|
|
247
177
|
model: config.llm.model,
|
|
248
178
|
messages: secondMessages,
|
|
249
179
|
});
|
|
@@ -252,7 +182,7 @@ async function processWeb3Intent(input, role = 'user', onProgress, sessionId) {
|
|
|
252
182
|
tracker_1.Tracker.addTokens(secondResponse.usage.total_tokens, config.llm.provider);
|
|
253
183
|
}
|
|
254
184
|
tracker_1.Tracker.addEvent('llm.final_response', { provider: config.llm.provider });
|
|
255
|
-
let finalContent = secondResponse.
|
|
185
|
+
let finalContent = secondResponse.message.content || "";
|
|
256
186
|
// Clean up orphaned <think> blocks that forgot to output </think>
|
|
257
187
|
finalContent = finalContent.replace(/<thought>[\s\S]*?<\/thought>\n?/gi, '');
|
|
258
188
|
finalContent = finalContent.replace(/<think>[\s\S]*?<\/think>\n?/gi, '');
|
|
@@ -277,7 +207,11 @@ async function processWeb3Intent(input, role = 'user', onProgress, sessionId) {
|
|
|
277
207
|
}
|
|
278
208
|
catch (error) {
|
|
279
209
|
console.error("LLM Error:", error);
|
|
280
|
-
const
|
|
210
|
+
const status = error?.status || error?.response?.status;
|
|
211
|
+
let errorMsg = '⚠️ All models are temporarily rate-limited. Please try again in a few minutes.';
|
|
212
|
+
if (status === 400 || (error.message && error.message.toLowerCase().includes('invalid'))) {
|
|
213
|
+
errorMsg = '⚠️ Terjadi kesalahan pemahaman instruksi. LLM kesulitan menentukan format alat (skill) yang cocok. Silakan coba deskripsikan perintah Anda dengan lebih spesifik.';
|
|
214
|
+
}
|
|
281
215
|
exports.logger.addEntry({ role: 'assistant', content: errorMsg }, sessionId);
|
|
282
216
|
return errorMsg;
|
|
283
217
|
}
|
|
@@ -121,7 +121,13 @@ async function saveApiKeys(newKeys) {
|
|
|
121
121
|
config.credentials = { ...config.credentials, ...newKeys };
|
|
122
122
|
saveConfig(config);
|
|
123
123
|
}
|
|
124
|
+
let cachedNyxoraConfig = null;
|
|
125
|
+
let lastConfigLoadTime = 0;
|
|
124
126
|
function loadConfig() {
|
|
127
|
+
const now = Date.now();
|
|
128
|
+
if (cachedNyxoraConfig && now - lastConfigLoadTime < 5000) {
|
|
129
|
+
return cachedNyxoraConfig;
|
|
130
|
+
}
|
|
125
131
|
const configPath = (0, paths_1.getPath)('config.yaml');
|
|
126
132
|
const rpcPath = (0, paths_1.getPath)('rpc_key.yaml');
|
|
127
133
|
let rpcUrls = loadRpcConfig();
|
|
@@ -208,6 +214,8 @@ function loadConfig() {
|
|
|
208
214
|
},
|
|
209
215
|
skills: parsed.skills
|
|
210
216
|
};
|
|
217
|
+
cachedNyxoraConfig = validatedConfig;
|
|
218
|
+
lastConfigLoadTime = Date.now();
|
|
211
219
|
return validatedConfig;
|
|
212
220
|
}
|
|
213
221
|
catch (error) {
|
|
@@ -220,7 +228,7 @@ function loadConfig() {
|
|
|
220
228
|
else {
|
|
221
229
|
console.error('[Config] Failed to load config.yaml. Using default configuration.', error);
|
|
222
230
|
}
|
|
223
|
-
|
|
231
|
+
const defaultConfig = {
|
|
224
232
|
agent: {
|
|
225
233
|
name: "Nyxora-Default",
|
|
226
234
|
description: "Your Personal Web3 Assistant.",
|
|
@@ -245,11 +253,16 @@ function loadConfig() {
|
|
|
245
253
|
telegram: { enabled: false }
|
|
246
254
|
}
|
|
247
255
|
};
|
|
256
|
+
cachedNyxoraConfig = defaultConfig;
|
|
257
|
+
lastConfigLoadTime = Date.now();
|
|
258
|
+
return defaultConfig;
|
|
248
259
|
}
|
|
249
260
|
}
|
|
250
261
|
function saveConfig(newConfig) {
|
|
251
262
|
const configPath = (0, paths_1.getPath)('config.yaml');
|
|
252
263
|
try {
|
|
264
|
+
cachedNyxoraConfig = null;
|
|
265
|
+
lastConfigLoadTime = 0;
|
|
253
266
|
const configToSave = JSON.parse(JSON.stringify(newConfig));
|
|
254
267
|
if (configToSave.web3 && configToSave.web3.rpc_urls) {
|
|
255
268
|
delete configToSave.web3.rpc_urls;
|
|
@@ -8,6 +8,9 @@ exports.startServer = startServer;
|
|
|
8
8
|
const express_1 = __importDefault(require("express"));
|
|
9
9
|
const cors_1 = __importDefault(require("cors"));
|
|
10
10
|
const httpClient_1 = require("../utils/httpClient");
|
|
11
|
+
BigInt.prototype.toJSON = function () {
|
|
12
|
+
return this.toString();
|
|
13
|
+
};
|
|
11
14
|
process.on('unhandledRejection', (reason, promise) => {
|
|
12
15
|
console.error('[Anti-Crash] Unhandled Rejection at:', promise, 'reason:', reason);
|
|
13
16
|
});
|
|
@@ -135,7 +135,7 @@ function startTelegramBot() {
|
|
|
135
135
|
}
|
|
136
136
|
catch (e) { }
|
|
137
137
|
};
|
|
138
|
-
const response = await (0, reasoning_1.processUserInput)(text, 'user', onProgress, ctx.chat?.id
|
|
138
|
+
const response = await (0, reasoning_1.processUserInput)(text, 'user', onProgress, `telegram_${ctx.chat?.id}`);
|
|
139
139
|
if (progressMsgId) {
|
|
140
140
|
await ctx.api.deleteMessage(ctx.chat.id, progressMsgId).catch(() => { });
|
|
141
141
|
}
|
|
@@ -189,7 +189,7 @@ function startTelegramBot() {
|
|
|
189
189
|
}
|
|
190
190
|
catch (e) { }
|
|
191
191
|
};
|
|
192
|
-
const response = await (0, reasoning_1.processUserInput)(prompt, 'user', onProgress, ctx.chat?.id
|
|
192
|
+
const response = await (0, reasoning_1.processUserInput)(prompt, 'user', onProgress, `telegram_${ctx.chat?.id}`);
|
|
193
193
|
if (progressMsgId) {
|
|
194
194
|
await ctx.api.deleteMessage(ctx.chat.id, progressMsgId).catch(() => { });
|
|
195
195
|
}
|
|
@@ -254,19 +254,19 @@ function startTelegramBot() {
|
|
|
254
254
|
transactionManager_1.txManager.updateStatus(txId, 'executed', result);
|
|
255
255
|
const prettyMsg = (0, formatter_1.formatTransactionSuccess)(tx, result);
|
|
256
256
|
await ctx.reply(formatToTelegramHTML(`✅ **Transaction processed: Success**\n\n${prettyMsg}`), { parse_mode: 'HTML' });
|
|
257
|
-
(0, reasoning_1.processUserInput)(`Transaction ${txId} was APPROVED via Telegram. Result: ${result}`, 'system', undefined, ctx.chat?.id
|
|
257
|
+
(0, reasoning_1.processUserInput)(`Transaction ${txId} was APPROVED via Telegram. Result: ${result}`, 'system', undefined, `telegram_${ctx.chat?.id}`).catch(() => { });
|
|
258
258
|
}
|
|
259
259
|
catch (err) {
|
|
260
260
|
transactionManager_1.txManager.updateStatus(txId, 'failed', err.message);
|
|
261
261
|
const prettyError = (0, formatter_1.formatTransactionError)(tx, err.message);
|
|
262
262
|
await ctx.reply(prettyError);
|
|
263
|
-
(0, reasoning_1.processUserInput)(`Transaction ${txId} FAILED via Telegram. Error: ${err.message}`, 'system', undefined, ctx.chat?.id
|
|
263
|
+
(0, reasoning_1.processUserInput)(`Transaction ${txId} FAILED via Telegram. Error: ${err.message}`, 'system', undefined, `telegram_${ctx.chat?.id}`).catch(() => { });
|
|
264
264
|
}
|
|
265
265
|
});
|
|
266
266
|
bot.callbackQuery(/^reject_(.+)$/, async (ctx) => {
|
|
267
267
|
const txId = ctx.match[1];
|
|
268
268
|
transactionManager_1.txManager.updateStatus(txId, 'rejected');
|
|
269
|
-
(0, reasoning_1.processUserInput)(`Transaction ${txId} was REJECTED via Telegram. CRITICAL: DO NOT retry or recreate this transaction. Acknowledge this cancellation to the user and stop.`, 'system', undefined, ctx.chat?.id
|
|
269
|
+
(0, reasoning_1.processUserInput)(`Transaction ${txId} was REJECTED via Telegram. CRITICAL: DO NOT retry or recreate this transaction. Acknowledge this cancellation to the user and stop.`, 'system', undefined, `telegram_${ctx.chat?.id}`).catch(() => { });
|
|
270
270
|
await ctx.answerCallbackQuery('Transaction cancelled.');
|
|
271
271
|
await ctx.reply(`❌ Transaction cancelled.`);
|
|
272
272
|
await ctx.api.editMessageReplyMarkup(ctx.chat.id, ctx.msg.message_id, { reply_markup: { inline_keyboard: [] } }).catch(() => { });
|
|
@@ -191,6 +191,17 @@ class Logger {
|
|
|
191
191
|
});
|
|
192
192
|
}
|
|
193
193
|
addEntry(entry, sessionId) {
|
|
194
|
+
if (sessionId) {
|
|
195
|
+
// Auto-create session if it doesn't exist (e.g. for Telegram integration)
|
|
196
|
+
try {
|
|
197
|
+
const sessionExists = this.db.prepare('SELECT 1 FROM sessions WHERE id = ?').get(sessionId);
|
|
198
|
+
if (!sessionExists) {
|
|
199
|
+
const title = sessionId.startsWith('telegram_') ? `Telegram Chat` : 'New Session';
|
|
200
|
+
this.db.prepare('INSERT INTO sessions (id, title) VALUES (?, ?)').run(sessionId, title);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch (e) { }
|
|
204
|
+
}
|
|
194
205
|
const insert = this.db.prepare(`
|
|
195
206
|
INSERT INTO messages (session_id, role, content, name, tool_call_id, tool_calls)
|
|
196
207
|
VALUES (@session_id, @role, @content, @name, @tool_call_id, @tool_calls)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ReflectionEngine = void 0;
|
|
4
|
-
const
|
|
4
|
+
const llmUtils_1 = require("../utils/llmUtils");
|
|
5
5
|
const parser_1 = require("../config/parser");
|
|
6
6
|
const logger_1 = require("./logger");
|
|
7
7
|
const validator_1 = require("./validator");
|
|
@@ -19,7 +19,7 @@ class ReflectionEngine {
|
|
|
19
19
|
.join('\n');
|
|
20
20
|
const config = (0, parser_1.loadConfig)();
|
|
21
21
|
const model = config.llm?.model || 'gpt-4o';
|
|
22
|
-
const openai = await (0,
|
|
22
|
+
const openai = await (0, llmUtils_1.getOpenAI)();
|
|
23
23
|
// 2. Build the heavily constrained System Prompt
|
|
24
24
|
const systemPrompt = `
|
|
25
25
|
You are the Self-Reflection Engine for a Web3 AI Agent.
|