morpheus-cli 0.2.8 → 0.3.1
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/README.md +18 -8
- package/dist/channels/telegram.js +101 -6
- package/dist/cli/commands/doctor.js +10 -10
- package/dist/cli/commands/init.js +34 -34
- package/dist/cli/commands/restart.js +1 -1
- package/dist/cli/commands/start.js +1 -1
- package/dist/config/manager.js +16 -15
- package/dist/config/schemas.js +2 -1
- package/dist/http/__tests__/config_api.test.js +6 -1
- package/dist/http/api.js +138 -6
- package/dist/http/server.js +4 -2
- package/dist/runtime/memory/sati/repository.js +14 -11
- package/dist/runtime/memory/sati/service.js +14 -12
- package/dist/runtime/memory/session-embedding-worker.js +4 -9
- package/dist/runtime/memory/sqlite.js +19 -2
- package/dist/runtime/migration.js +40 -0
- package/dist/runtime/oracle.js +56 -0
- package/dist/types/config.js +8 -0
- package/dist/ui/assets/index-DqzvLXXS.js +109 -0
- package/dist/ui/assets/index-f1sqiqOo.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +2 -2
- package/dist/ui/assets/index-Dx1lwaMu.js +0 -96
- package/dist/ui/assets/index-QHZ08tDL.css +0 -1
package/README.md
CHANGED
|
@@ -97,6 +97,14 @@ Morpheus is built with **Node.js** and **TypeScript**, using **LangChain** as th
|
|
|
97
97
|
### 🖥️ Web Dashboard
|
|
98
98
|
Local React-based UI to manage recordings, chat history, and system status across your agent instances.
|
|
99
99
|
|
|
100
|
+
**New: Interactive Web Chat**
|
|
101
|
+
- Full-featured chat interface accessible from the browser
|
|
102
|
+
- Session management: create, archive, delete, and rename sessions
|
|
103
|
+
- Cross-channel visibility: view and interact with sessions started on any channel (Telegram, Web, etc.)
|
|
104
|
+
- Real-time messaging with the Oracle agent
|
|
105
|
+
- Responsive design with collapsible sidebar
|
|
106
|
+
- Full support for Light and Dark (Matrix) themes
|
|
107
|
+
|
|
100
108
|
#### 🔒 UI Authentication
|
|
101
109
|
To protect your Web UI, use the `THE_ARCHITECT_PASS` environment variable. This ensures only authorized users can access the dashboard and API.
|
|
102
110
|
|
|
@@ -125,13 +133,15 @@ The system also supports generic environment variables that apply to all provide
|
|
|
125
133
|
| `MORPHEUS_LLM_MAX_TOKENS` | Maximum tokens for LLM | llm.max_tokens |
|
|
126
134
|
| `MORPHEUS_LLM_CONTEXT_WINDOW` | Context window size for LLM | llm.context_window |
|
|
127
135
|
| `MORPHEUS_LLM_API_KEY` | Generic API key for LLM (lower precedence than provider-specific keys) | llm.api_key |
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
136
|
+
| `MORPHEUS_SATI_PROVIDER` | Sati provider to use | santi.provider |
|
|
137
|
+
| `MORPHEUS_SATI_MODEL` | Model name for Sati | santi.model |
|
|
138
|
+
| `MORPHEUS_SATI_TEMPERATURE` | Temperature setting for Sati | santi.temperature |
|
|
139
|
+
| `MORPHEUS_SATI_MAX_TOKENS` | Maximum tokens for Sati | santi.max_tokens |
|
|
140
|
+
| `MORPHEUS_SATI_CONTEXT_WINDOW` | Context window size for Sati | santi.context_window |
|
|
141
|
+
| `MORPHEUS_SATI_API_KEY` | Generic API key for Sati (lower precedence than provider-specific keys) | santi.api_key |
|
|
142
|
+
| `MORPHEUS_SATI_MEMORY_LIMIT` | Memory retrieval limit for Sati | santi.memory_limit |
|
|
143
|
+
| `MORPHEUS_SATI_MEMORY_LIMIT` | Memory retrieval limit for Sati | santi.memory_limit |
|
|
144
|
+
| `MORPHEUS_SATI_ENABLED_ARCHIVED_SESSIONS`| Enable/disable retrieval of archived sessions in Sati | santi.enableArchivedSessions |
|
|
135
145
|
| `MORPHEUS_AUDIO_MODEL` | Model name for audio processing | audio.model |
|
|
136
146
|
| `MORPHEUS_AUDIO_ENABLED` | Enable/disable audio processing | audio.enabled |
|
|
137
147
|
| `MORPHEUS_AUDIO_API_KEY` | Generic API key for audio (lower precedence than provider-specific keys) | audio.apiKey |
|
|
@@ -214,7 +224,7 @@ The Morpheus Telegram bot supports several commands for interacting with the age
|
|
|
214
224
|
- `/zaion` - Show system configurations
|
|
215
225
|
- `/sati <qnt>` - Show specific memories
|
|
216
226
|
- `/newsession` - Archive current session and start fresh
|
|
217
|
-
- `/sessions` - List all sessions
|
|
227
|
+
- `/sessions` - List all sessions with options to switch, archive, or delete
|
|
218
228
|
- `/restart` - Restart the Morpheus agent
|
|
219
229
|
- `/mcp` or `/mcps` - List registered MCP servers
|
|
220
230
|
|
|
@@ -187,9 +187,84 @@ export class TelegramAdapter {
|
|
|
187
187
|
ctx.reply(`✅ Switched to session ID: ${sessionId}`);
|
|
188
188
|
}
|
|
189
189
|
catch (error) {
|
|
190
|
-
await ctx.answerCbQuery(`Error switching session: ${error.message}
|
|
190
|
+
await ctx.answerCbQuery(`Error switching session: ${error.message}`, { show_alert: true });
|
|
191
191
|
}
|
|
192
192
|
});
|
|
193
|
+
// --- Archive Flow ---
|
|
194
|
+
this.bot.action(/^ask_archive_session_/, async (ctx) => {
|
|
195
|
+
const data = ctx.callbackQuery.data;
|
|
196
|
+
const sessionId = data.replace('ask_archive_session_', '');
|
|
197
|
+
// Fetch session title for better UX (optional, but nice) - for now just use ID
|
|
198
|
+
await ctx.reply(`⚠️ **ARCHIVE SESSION?**\n\nAre you sure you want to archive session \`${sessionId}\`?\n\nIt will be moved to long-term memory (SATI) and removed from the active list. This action cannot be easily undone via Telegram.`, {
|
|
199
|
+
parse_mode: 'Markdown',
|
|
200
|
+
reply_markup: {
|
|
201
|
+
inline_keyboard: [
|
|
202
|
+
[
|
|
203
|
+
{ text: '✅ Yes, Archive', callback_data: `confirm_archive_session_${sessionId}` },
|
|
204
|
+
{ text: '❌ Cancel', callback_data: 'cancel_session_action' }
|
|
205
|
+
]
|
|
206
|
+
]
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
await ctx.answerCbQuery();
|
|
210
|
+
});
|
|
211
|
+
this.bot.action(/^confirm_archive_session_/, async (ctx) => {
|
|
212
|
+
const data = ctx.callbackQuery.data;
|
|
213
|
+
const sessionId = data.replace('confirm_archive_session_', '');
|
|
214
|
+
try {
|
|
215
|
+
const history = new SQLiteChatMessageHistory({ sessionId: "" });
|
|
216
|
+
await history.archiveSession(sessionId);
|
|
217
|
+
await ctx.answerCbQuery('Session archived successfully');
|
|
218
|
+
if (ctx.updateType === 'callback_query') {
|
|
219
|
+
ctx.deleteMessage().catch(() => { });
|
|
220
|
+
}
|
|
221
|
+
await ctx.reply(`✅ Session \`${sessionId}\` has been archived and moved to long-term memory.`, { parse_mode: 'Markdown' });
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
await ctx.answerCbQuery(`Error archiving: ${error.message}`, { show_alert: true });
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
// --- Delete Flow ---
|
|
228
|
+
this.bot.action(/^ask_delete_session_/, async (ctx) => {
|
|
229
|
+
const data = ctx.callbackQuery.data;
|
|
230
|
+
const sessionId = data.replace('ask_delete_session_', '');
|
|
231
|
+
await ctx.reply(`🚫 **DELETE SESSION?**\n\nAre you sure you want to PERMANENTLY DELETE session \`${sessionId}\`?\n\nThis action is **IRREVERSIBLE**. All data will be lost.`, {
|
|
232
|
+
parse_mode: 'Markdown',
|
|
233
|
+
reply_markup: {
|
|
234
|
+
inline_keyboard: [
|
|
235
|
+
[
|
|
236
|
+
{ text: '🗑️ Yes, DELETE PERMANENTLY', callback_data: `confirm_delete_session_${sessionId}` },
|
|
237
|
+
{ text: '❌ Cancel', callback_data: 'cancel_session_action' }
|
|
238
|
+
]
|
|
239
|
+
]
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
await ctx.answerCbQuery();
|
|
243
|
+
});
|
|
244
|
+
this.bot.action(/^confirm_delete_session_/, async (ctx) => {
|
|
245
|
+
const data = ctx.callbackQuery.data;
|
|
246
|
+
const sessionId = data.replace('confirm_delete_session_', '');
|
|
247
|
+
try {
|
|
248
|
+
const history = new SQLiteChatMessageHistory({ sessionId: "" });
|
|
249
|
+
await history.deleteSession(sessionId);
|
|
250
|
+
await ctx.answerCbQuery('Session deleted successfully');
|
|
251
|
+
if (ctx.updateType === 'callback_query') {
|
|
252
|
+
ctx.deleteMessage().catch(() => { });
|
|
253
|
+
}
|
|
254
|
+
await ctx.reply(`🗑️ Session \`${sessionId}\` has been permanently deleted.`, { parse_mode: 'Markdown' });
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
await ctx.answerCbQuery(`Error deleting: ${error.message}`, { show_alert: true });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
// --- Cancel Action ---
|
|
261
|
+
this.bot.action('cancel_session_action', async (ctx) => {
|
|
262
|
+
await ctx.answerCbQuery('Action cancelled');
|
|
263
|
+
if (ctx.updateType === 'callback_query') {
|
|
264
|
+
ctx.deleteMessage().catch(() => { });
|
|
265
|
+
}
|
|
266
|
+
await ctx.reply('Action cancelled.');
|
|
267
|
+
});
|
|
193
268
|
this.bot.launch().catch((err) => {
|
|
194
269
|
if (this.isConnected) {
|
|
195
270
|
this.display.log(`Telegram bot error: ${err}`, { source: 'Telegram', level: 'error' });
|
|
@@ -331,12 +406,22 @@ export class TelegramAdapter {
|
|
|
331
406
|
response += `- Status: ${session.status}\n`;
|
|
332
407
|
response += `- Started: ${new Date(session.started_at).toLocaleString()}\n\n`;
|
|
333
408
|
// Adicionar botão inline para alternar para esta sessão
|
|
409
|
+
const sessionButtons = [];
|
|
334
410
|
if (session.status !== 'active') {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
411
|
+
sessionButtons.push({
|
|
412
|
+
text: `➡️ Switch`,
|
|
413
|
+
callback_data: `switch_session_${session.id}`
|
|
414
|
+
});
|
|
339
415
|
}
|
|
416
|
+
sessionButtons.push({
|
|
417
|
+
text: `📂 Archive`,
|
|
418
|
+
callback_data: `ask_archive_session_${session.id}`
|
|
419
|
+
});
|
|
420
|
+
sessionButtons.push({
|
|
421
|
+
text: `🗑️ Delete`,
|
|
422
|
+
callback_data: `ask_delete_session_${session.id}`
|
|
423
|
+
});
|
|
424
|
+
keyboard.push(sessionButtons);
|
|
340
425
|
}
|
|
341
426
|
await ctx.reply(response, {
|
|
342
427
|
parse_mode: 'Markdown',
|
|
@@ -535,6 +620,7 @@ How can I assist you today?`;
|
|
|
535
620
|
async handleRestartCommand(ctx, user) {
|
|
536
621
|
// Store the user ID who requested the restart
|
|
537
622
|
const userId = ctx.from.id;
|
|
623
|
+
const updateId = ctx.update.update_id;
|
|
538
624
|
// Save the user ID to a temporary file so the restarted process can notify them
|
|
539
625
|
const restartNotificationFile = path.join(os.tmpdir(), 'morpheus_restart_notification.json');
|
|
540
626
|
try {
|
|
@@ -545,6 +631,15 @@ How can I assist you today?`;
|
|
|
545
631
|
}
|
|
546
632
|
// Respond to the user first
|
|
547
633
|
await ctx.reply('🔄 Restart initiated. The Morpheus agent will restart shortly.');
|
|
634
|
+
// Acknowledge this update to Telegram by advancing the offset past it.
|
|
635
|
+
// Without this, Telegraf's in-memory offset is lost on process.exit() and the
|
|
636
|
+
// /restart message gets re-delivered on the next startup, causing an infinite loop.
|
|
637
|
+
try {
|
|
638
|
+
await ctx.telegram.callApi('getUpdates', { offset: updateId + 1, limit: 1, timeout: 0 });
|
|
639
|
+
}
|
|
640
|
+
catch (e) {
|
|
641
|
+
// Best-effort — proceed with restart regardless
|
|
642
|
+
}
|
|
548
643
|
// Schedule the restart after a short delay to ensure the response is sent
|
|
549
644
|
setTimeout(() => {
|
|
550
645
|
// Stop the bot to prevent processing more messages
|
|
@@ -564,7 +659,7 @@ How can I assist you today?`;
|
|
|
564
659
|
restartProcess.unref();
|
|
565
660
|
// Exit the current process
|
|
566
661
|
process.exit(0);
|
|
567
|
-
}, 500);
|
|
662
|
+
}, 500);
|
|
568
663
|
}
|
|
569
664
|
async checkAndSendRestartNotification() {
|
|
570
665
|
const restartNotificationFile = path.join(os.tmpdir(), 'morpheus_restart_notification.json');
|
|
@@ -49,7 +49,7 @@ export const doctorCommand = new Command('doctor')
|
|
|
49
49
|
}
|
|
50
50
|
// Check API keys availability for active providers
|
|
51
51
|
const llmProvider = config.llm?.provider;
|
|
52
|
-
const
|
|
52
|
+
const satiProvider = config.sati?.provider;
|
|
53
53
|
// Check LLM provider API key
|
|
54
54
|
if (llmProvider && llmProvider !== 'ollama') {
|
|
55
55
|
const hasLlmApiKey = config.llm?.api_key ||
|
|
@@ -65,18 +65,18 @@ export const doctorCommand = new Command('doctor')
|
|
|
65
65
|
allPassed = false;
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
-
// Check
|
|
69
|
-
if (
|
|
70
|
-
const hasSantiApiKey = config.
|
|
71
|
-
(
|
|
72
|
-
(
|
|
73
|
-
(
|
|
74
|
-
(
|
|
68
|
+
// Check Sati provider API key
|
|
69
|
+
if (satiProvider && satiProvider !== 'ollama') {
|
|
70
|
+
const hasSantiApiKey = config.sati?.api_key ||
|
|
71
|
+
(satiProvider === 'openai' && process.env.OPENAI_API_KEY) ||
|
|
72
|
+
(satiProvider === 'anthropic' && process.env.ANTHROPIC_API_KEY) ||
|
|
73
|
+
(satiProvider === 'gemini' && process.env.GOOGLE_API_KEY) ||
|
|
74
|
+
(satiProvider === 'openrouter' && process.env.OPENROUTER_API_KEY);
|
|
75
75
|
if (hasSantiApiKey) {
|
|
76
|
-
console.log(chalk.green('✓') + `
|
|
76
|
+
console.log(chalk.green('✓') + ` Sati API key available for ${satiProvider}`);
|
|
77
77
|
}
|
|
78
78
|
else {
|
|
79
|
-
console.log(chalk.red('✗') + `
|
|
79
|
+
console.log(chalk.red('✗') + ` Sati API key missing for ${satiProvider}. Either set in config or define environment variable.`);
|
|
80
80
|
allPassed = false;
|
|
81
81
|
}
|
|
82
82
|
}
|
|
@@ -117,15 +117,15 @@ export const initCommand = new Command('init')
|
|
|
117
117
|
],
|
|
118
118
|
default: 'no',
|
|
119
119
|
});
|
|
120
|
-
let
|
|
121
|
-
let
|
|
122
|
-
let
|
|
120
|
+
let satiProvider = provider;
|
|
121
|
+
let satiModel = model;
|
|
122
|
+
let satiApiKey = apiKey;
|
|
123
123
|
// If using main settings and no new key provided, use existing if available
|
|
124
|
-
if (configureSati === 'no' && !
|
|
125
|
-
|
|
124
|
+
if (configureSati === 'no' && !satiApiKey && hasExistingKey) {
|
|
125
|
+
satiApiKey = currentConfig.llm.api_key;
|
|
126
126
|
}
|
|
127
127
|
if (configureSati === 'yes') {
|
|
128
|
-
|
|
128
|
+
satiProvider = await select({
|
|
129
129
|
message: 'Select Sati LLM Provider:',
|
|
130
130
|
choices: [
|
|
131
131
|
{ name: 'OpenAI', value: 'openai' },
|
|
@@ -134,10 +134,10 @@ export const initCommand = new Command('init')
|
|
|
134
134
|
{ name: 'Ollama', value: 'ollama' },
|
|
135
135
|
{ name: 'Google Gemini', value: 'gemini' },
|
|
136
136
|
],
|
|
137
|
-
default: currentConfig.
|
|
137
|
+
default: currentConfig.sati?.provider || provider,
|
|
138
138
|
});
|
|
139
139
|
let defaultSatiModel = 'gpt-3.5-turbo';
|
|
140
|
-
switch (
|
|
140
|
+
switch (satiProvider) {
|
|
141
141
|
case 'openai':
|
|
142
142
|
defaultSatiModel = 'gpt-4o';
|
|
143
143
|
break;
|
|
@@ -154,59 +154,59 @@ export const initCommand = new Command('init')
|
|
|
154
154
|
defaultSatiModel = 'gemini-pro';
|
|
155
155
|
break;
|
|
156
156
|
}
|
|
157
|
-
if (
|
|
158
|
-
defaultSatiModel = currentConfig.
|
|
157
|
+
if (satiProvider === currentConfig.sati?.provider) {
|
|
158
|
+
defaultSatiModel = currentConfig.sati?.model || defaultSatiModel;
|
|
159
159
|
}
|
|
160
|
-
|
|
160
|
+
satiModel = await input({
|
|
161
161
|
message: 'Enter Sati Model Name:',
|
|
162
162
|
default: defaultSatiModel,
|
|
163
163
|
});
|
|
164
|
-
const hasExistingSatiKey = !!currentConfig.
|
|
165
|
-
let
|
|
164
|
+
const hasExistingSatiKey = !!currentConfig.sati?.api_key;
|
|
165
|
+
let satiKeyMsg = hasExistingSatiKey
|
|
166
166
|
? 'Enter Sati API Key (leave empty to preserve existing, or if using env vars):'
|
|
167
167
|
: 'Enter Sati API Key (leave empty if using env vars):';
|
|
168
168
|
// Add info about environment variables to the message
|
|
169
|
-
if (
|
|
170
|
-
|
|
169
|
+
if (satiProvider === 'openai') {
|
|
170
|
+
satiKeyMsg = `${satiKeyMsg} (Env var: OPENAI_API_KEY)`;
|
|
171
171
|
}
|
|
172
|
-
else if (
|
|
173
|
-
|
|
172
|
+
else if (satiProvider === 'anthropic') {
|
|
173
|
+
satiKeyMsg = `${satiKeyMsg} (Env var: ANTHROPIC_API_KEY)`;
|
|
174
174
|
}
|
|
175
|
-
else if (
|
|
176
|
-
|
|
175
|
+
else if (satiProvider === 'gemini') {
|
|
176
|
+
satiKeyMsg = `${satiKeyMsg} (Env var: GOOGLE_API_KEY)`;
|
|
177
177
|
}
|
|
178
|
-
else if (
|
|
179
|
-
|
|
178
|
+
else if (satiProvider === 'openrouter') {
|
|
179
|
+
satiKeyMsg = `${satiKeyMsg} (Env var: OPENROUTER_API_KEY)`;
|
|
180
180
|
}
|
|
181
|
-
const keyInput = await password({ message:
|
|
181
|
+
const keyInput = await password({ message: satiKeyMsg });
|
|
182
182
|
if (keyInput) {
|
|
183
|
-
|
|
183
|
+
satiApiKey = keyInput;
|
|
184
184
|
}
|
|
185
185
|
else if (hasExistingSatiKey) {
|
|
186
|
-
|
|
186
|
+
satiApiKey = currentConfig.sati?.api_key;
|
|
187
187
|
}
|
|
188
188
|
else {
|
|
189
|
-
|
|
189
|
+
satiApiKey = undefined; // Ensure we don't accidentally carry over invalid state
|
|
190
190
|
}
|
|
191
191
|
// Base URL Configuration for Sati OpenRouter
|
|
192
|
-
if (
|
|
192
|
+
if (satiProvider === 'openrouter') {
|
|
193
193
|
const satiBaseUrl = await input({
|
|
194
194
|
message: 'Enter Sati OpenRouter Base URL:',
|
|
195
|
-
default: currentConfig.
|
|
195
|
+
default: currentConfig.sati?.base_url || 'https://openrouter.ai/api/v1',
|
|
196
196
|
});
|
|
197
|
-
await configManager.set('
|
|
197
|
+
await configManager.set('sati.base_url', satiBaseUrl);
|
|
198
198
|
}
|
|
199
199
|
}
|
|
200
200
|
const memoryLimit = await input({
|
|
201
201
|
message: 'Sati Memory Retrieval Limit (messages):',
|
|
202
|
-
default: currentConfig.
|
|
202
|
+
default: currentConfig.sati?.memory_limit?.toString() || '1000',
|
|
203
203
|
validate: (val) => !isNaN(Number(val)) && Number(val) > 0 || 'Must be a positive number'
|
|
204
204
|
});
|
|
205
|
-
await configManager.set('
|
|
206
|
-
await configManager.set('
|
|
207
|
-
await configManager.set('
|
|
208
|
-
if (
|
|
209
|
-
await configManager.set('
|
|
205
|
+
await configManager.set('sati.provider', satiProvider);
|
|
206
|
+
await configManager.set('sati.model', satiModel);
|
|
207
|
+
await configManager.set('sati.memory_limit', Number(memoryLimit));
|
|
208
|
+
if (satiApiKey) {
|
|
209
|
+
await configManager.set('sati.api_key', satiApiKey);
|
|
210
210
|
}
|
|
211
211
|
// Audio Configuration
|
|
212
212
|
const audioEnabled = await confirm({
|
|
@@ -99,7 +99,7 @@ export const restartCommand = new Command('restart')
|
|
|
99
99
|
// Initialize Web UI
|
|
100
100
|
if (options.ui && config.ui.enabled) {
|
|
101
101
|
try {
|
|
102
|
-
httpServer = new HttpServer();
|
|
102
|
+
httpServer = new HttpServer(oracle);
|
|
103
103
|
// Use CLI port if provided and valid, otherwise fallback to config or default
|
|
104
104
|
const port = parseInt(options.port) || config.ui.port || 3333;
|
|
105
105
|
httpServer.start(port);
|
|
@@ -79,7 +79,7 @@ export const startCommand = new Command('start')
|
|
|
79
79
|
// Initialize Web UI
|
|
80
80
|
if (options.ui && config.ui.enabled) {
|
|
81
81
|
try {
|
|
82
|
-
httpServer = new HttpServer();
|
|
82
|
+
httpServer = new HttpServer(oracle);
|
|
83
83
|
// Use CLI port if provided and valid, otherwise fallback to config or default
|
|
84
84
|
const port = parseInt(options.port) || config.ui.port || 3333;
|
|
85
85
|
httpServer.start(port);
|
package/dist/config/manager.js
CHANGED
|
@@ -54,18 +54,19 @@ export class ConfigManager {
|
|
|
54
54
|
context_window: config.llm.context_window !== undefined ? resolveNumeric('MORPHEUS_LLM_CONTEXT_WINDOW', config.llm.context_window, DEFAULT_CONFIG.llm.context_window) : undefined
|
|
55
55
|
};
|
|
56
56
|
// Apply precedence to Sati config
|
|
57
|
-
let
|
|
58
|
-
if (config.
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
provider:
|
|
62
|
-
model: resolveModel(
|
|
63
|
-
temperature: resolveNumeric('
|
|
64
|
-
max_tokens: config.
|
|
65
|
-
api_key: resolveApiKey(
|
|
66
|
-
base_url: config.
|
|
67
|
-
context_window: config.
|
|
68
|
-
memory_limit: config.
|
|
57
|
+
let satiConfig;
|
|
58
|
+
if (config.sati) {
|
|
59
|
+
const satiProvider = resolveProvider('MORPHEUS_SATI_PROVIDER', config.sati.provider, llmConfig.provider);
|
|
60
|
+
satiConfig = {
|
|
61
|
+
provider: satiProvider,
|
|
62
|
+
model: resolveModel(satiProvider, 'MORPHEUS_SATI_MODEL', config.sati.model || llmConfig.model),
|
|
63
|
+
temperature: resolveNumeric('MORPHEUS_SATI_TEMPERATURE', config.sati.temperature, llmConfig.temperature),
|
|
64
|
+
max_tokens: config.sati.max_tokens !== undefined ? resolveNumeric('MORPHEUS_SATI_MAX_TOKENS', config.sati.max_tokens, config.sati.max_tokens) : llmConfig.max_tokens,
|
|
65
|
+
api_key: resolveApiKey(satiProvider, 'MORPHEUS_SATI_API_KEY', config.sati.api_key || llmConfig.api_key),
|
|
66
|
+
base_url: config.sati.base_url || config.llm.base_url,
|
|
67
|
+
context_window: config.sati.context_window !== undefined ? resolveNumeric('MORPHEUS_SATI_CONTEXT_WINDOW', config.sati.context_window, config.sati.context_window) : llmConfig.context_window,
|
|
68
|
+
memory_limit: config.sati.memory_limit !== undefined ? resolveNumeric('MORPHEUS_SATI_MEMORY_LIMIT', config.sati.memory_limit, config.sati.memory_limit) : undefined,
|
|
69
|
+
enabled_archived_sessions: resolveBoolean('MORPHEUS_SATI_ENABLED_ARCHIVED_SESSIONS', config.sati.enabled_archived_sessions, true)
|
|
69
70
|
};
|
|
70
71
|
}
|
|
71
72
|
// Apply precedence to audio config
|
|
@@ -107,7 +108,7 @@ export class ConfigManager {
|
|
|
107
108
|
return {
|
|
108
109
|
agent: agentConfig,
|
|
109
110
|
llm: llmConfig,
|
|
110
|
-
|
|
111
|
+
sati: satiConfig,
|
|
111
112
|
audio: audioConfig,
|
|
112
113
|
channels: channelsConfig,
|
|
113
114
|
ui: uiConfig,
|
|
@@ -137,10 +138,10 @@ export class ConfigManager {
|
|
|
137
138
|
return this.config.llm;
|
|
138
139
|
}
|
|
139
140
|
getSatiConfig() {
|
|
140
|
-
if (this.config.
|
|
141
|
+
if (this.config.sati) {
|
|
141
142
|
return {
|
|
142
143
|
memory_limit: 10, // Default if undefined
|
|
143
|
-
...this.config.
|
|
144
|
+
...this.config.sati
|
|
144
145
|
};
|
|
145
146
|
}
|
|
146
147
|
// Fallback to main LLM config
|
package/dist/config/schemas.js
CHANGED
|
@@ -19,6 +19,7 @@ export const LLMConfigSchema = z.object({
|
|
|
19
19
|
});
|
|
20
20
|
export const SatiConfigSchema = LLMConfigSchema.extend({
|
|
21
21
|
memory_limit: z.number().int().positive().optional(),
|
|
22
|
+
enabled_archived_sessions: z.boolean().default(true),
|
|
22
23
|
});
|
|
23
24
|
// Zod Schema matching MorpheusConfig interface
|
|
24
25
|
export const ConfigSchema = z.object({
|
|
@@ -27,7 +28,7 @@ export const ConfigSchema = z.object({
|
|
|
27
28
|
personality: z.string().default(DEFAULT_CONFIG.agent.personality),
|
|
28
29
|
}).default(DEFAULT_CONFIG.agent),
|
|
29
30
|
llm: LLMConfigSchema.default(DEFAULT_CONFIG.llm),
|
|
30
|
-
|
|
31
|
+
sati: SatiConfigSchema.optional(),
|
|
31
32
|
audio: AudioConfigSchema.default(DEFAULT_CONFIG.audio),
|
|
32
33
|
memory: z.object({
|
|
33
34
|
limit: z.number().int().positive().optional(),
|
|
@@ -27,10 +27,15 @@ describe('Config API', () => {
|
|
|
27
27
|
log: vi.fn(),
|
|
28
28
|
};
|
|
29
29
|
DisplayManager.getInstance.mockReturnValue(mockDisplayManager);
|
|
30
|
+
// Mock Oracle instance
|
|
31
|
+
const mockOracle = {
|
|
32
|
+
think: vi.fn(),
|
|
33
|
+
getMemory: vi.fn(),
|
|
34
|
+
};
|
|
30
35
|
// Setup App
|
|
31
36
|
app = express();
|
|
32
37
|
app.use(bodyParser.json());
|
|
33
|
-
app.use('/api', createApiRouter());
|
|
38
|
+
app.use('/api', createApiRouter(mockOracle));
|
|
34
39
|
});
|
|
35
40
|
afterEach(() => {
|
|
36
41
|
vi.restoreAllMocks();
|
package/dist/http/api.js
CHANGED
|
@@ -20,14 +20,146 @@ async function readLastLines(filePath, n) {
|
|
|
20
20
|
return [];
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
-
export function createApiRouter() {
|
|
23
|
+
export function createApiRouter(oracle) {
|
|
24
24
|
const router = Router();
|
|
25
25
|
const configManager = ConfigManager.getInstance();
|
|
26
26
|
const history = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
|
|
27
|
+
// --- Session Management ---
|
|
28
|
+
router.get('/sessions', async (req, res) => {
|
|
29
|
+
try {
|
|
30
|
+
const allSessions = await history.listSessions();
|
|
31
|
+
res.json(allSessions);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
res.status(500).json({ error: err.message });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
router.post('/sessions', async (req, res) => {
|
|
38
|
+
try {
|
|
39
|
+
await history.createNewSession();
|
|
40
|
+
const newSessionId = await history.getCurrentSessionOrCreate(); // Should be the new one
|
|
41
|
+
res.json({ success: true, id: newSessionId, message: 'New session started' });
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
res.status(500).json({ error: err.message });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
router.delete('/sessions/:id', async (req, res) => {
|
|
48
|
+
try {
|
|
49
|
+
const { id } = req.params;
|
|
50
|
+
await history.deleteSession(id);
|
|
51
|
+
res.json({ success: true, message: 'Session deleted' });
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
res.status(500).json({ error: err.message });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
router.post('/sessions/:id/archive', async (req, res) => {
|
|
58
|
+
try {
|
|
59
|
+
const { id } = req.params;
|
|
60
|
+
await history.archiveSession(id);
|
|
61
|
+
res.json({ success: true, message: 'Session archived' });
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
res.status(500).json({ error: err.message });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
router.patch('/sessions/:id/title', async (req, res) => {
|
|
68
|
+
try {
|
|
69
|
+
const { id } = req.params;
|
|
70
|
+
const { title } = req.body;
|
|
71
|
+
if (!title) {
|
|
72
|
+
return res.status(400).json({ error: 'Title is required' });
|
|
73
|
+
}
|
|
74
|
+
await history.renameSession(id, title);
|
|
75
|
+
res.json({ success: true, message: 'Session renamed' });
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
res.status(500).json({ error: err.message });
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
router.get('/sessions/:id/messages', async (req, res) => {
|
|
82
|
+
try {
|
|
83
|
+
const { id } = req.params;
|
|
84
|
+
const sessionHistory = new SQLiteChatMessageHistory({ sessionId: id, limit: 100 });
|
|
85
|
+
const messages = await sessionHistory.getMessages();
|
|
86
|
+
// Normalize messages for UI
|
|
87
|
+
const key = (msg) => msg._getType(); // Access internal type if available, or infer
|
|
88
|
+
const normalizedMessages = messages.map((msg) => {
|
|
89
|
+
const type = msg._getType ? msg._getType() : 'unknown';
|
|
90
|
+
return {
|
|
91
|
+
type,
|
|
92
|
+
content: msg.content,
|
|
93
|
+
tool_calls: msg.tool_calls,
|
|
94
|
+
usage_metadata: msg.usage_metadata
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
// Reverse to chronological order for UI
|
|
98
|
+
res.json(normalizedMessages.reverse());
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
res.status(500).json({ error: err.message });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
// --- Chat Interaction ---
|
|
105
|
+
router.post('/chat', async (req, res) => {
|
|
106
|
+
try {
|
|
107
|
+
const { message, sessionId } = req.body;
|
|
108
|
+
if (!message || !sessionId) {
|
|
109
|
+
return res.status(400).json({ error: 'Message and Session ID are required' });
|
|
110
|
+
}
|
|
111
|
+
// We need to ensure the Oracle uses the correct session history.
|
|
112
|
+
// The Oracle class uses its own internal history instance.
|
|
113
|
+
// We might need to refactor Oracle to accept a session ID per request or
|
|
114
|
+
// instantiate a temporary Oracle wrapper/context?
|
|
115
|
+
//
|
|
116
|
+
// ACTUALLY: The Oracle class uses `this.history`.
|
|
117
|
+
// `SQLiteChatMessageHistory` takes `sessionId` in constructor.
|
|
118
|
+
// To support multi-session chat via API, we should probably allow passing sessionId to `chat()`
|
|
119
|
+
// OR (cleaner for now) we can rely on the fact that `Oracle` might not support swapping sessions easily without
|
|
120
|
+
// re-initialization or we extend `oracle.chat` to support overriding session.
|
|
121
|
+
//
|
|
122
|
+
// Let's look at `Oracle.chat`: it uses `this.history`.
|
|
123
|
+
// And `SQLiteChatMessageHistory` is tied to a sessionId.
|
|
124
|
+
//
|
|
125
|
+
// Quick fix for this feature:
|
|
126
|
+
// We can use a trick: `Oracle` allows `overrides` in constructor but that's for db path.
|
|
127
|
+
// `Oracle` initializes `this.history` in `initialize`.
|
|
128
|
+
// Better approach:
|
|
129
|
+
// We can temporarily switch the session of the Oracle's history if it exposes it,
|
|
130
|
+
// OR we just instantiate a fresh history for the chat request and use the provider?
|
|
131
|
+
// No, `oracle.chat` encapsulates the provider invocation.
|
|
132
|
+
// Let's check `Oracle` class again. (I viewed it earlier).
|
|
133
|
+
// It has `private history`.
|
|
134
|
+
// SOLUTION:
|
|
135
|
+
// I will add a `setSessionId(id: string)` method to `Oracle` interface and class.
|
|
136
|
+
// OR pass `sessionId` to `chat`.
|
|
137
|
+
// For now, I will assume I can update `Oracle` to support dynamic sessions.
|
|
138
|
+
// I'll modify `Oracle.chat` signature in a separate step if needed.
|
|
139
|
+
// Wait, `Oracle` is a singleton-ish in `start.ts`.
|
|
140
|
+
// Let's modify `Oracle` to accept `sessionId` in `chat`?
|
|
141
|
+
// `chat(message: string, extraUsage?: UsageMetadata, isTelephonist?: boolean)`
|
|
142
|
+
//
|
|
143
|
+
// Adding `sessionId` to `chat` seems invasive if not threaded fast.
|
|
144
|
+
//
|
|
145
|
+
// Alternative:
|
|
146
|
+
// `router.post('/chat')` instantiates a *new* Oracle? No, expensive (provider factory).
|
|
147
|
+
//
|
|
148
|
+
// Ideally `Oracle` should be stateless regarding session, or easily switchable.
|
|
149
|
+
// `SQLiteChatMessageHistory` is cheap to instantiate.
|
|
150
|
+
//
|
|
151
|
+
// Let's update `Oracle` to allow switching session.
|
|
152
|
+
// `oracle.switchSession(sessionId)`
|
|
153
|
+
await oracle.setSessionId(sessionId); // Type cast for now, will implement next
|
|
154
|
+
const response = await oracle.chat(message);
|
|
155
|
+
res.json({ response });
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
res.status(500).json({ error: err.message });
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
// Legacy /session/reset (keep for backward compat or redirect to POST /sessions)
|
|
27
162
|
router.post('/session/reset', async (req, res) => {
|
|
28
|
-
// if (!oracle) {
|
|
29
|
-
// return res.status(503).json({ error: 'Oracle unavailable' });
|
|
30
|
-
// }
|
|
31
163
|
try {
|
|
32
164
|
await history.createNewSession();
|
|
33
165
|
res.json({ success: true, message: 'New session started' });
|
|
@@ -179,7 +311,7 @@ export function createApiRouter() {
|
|
|
179
311
|
router.post('/config/sati', async (req, res) => {
|
|
180
312
|
try {
|
|
181
313
|
const config = configManager.get();
|
|
182
|
-
await configManager.save({ ...config,
|
|
314
|
+
await configManager.save({ ...config, sati: req.body });
|
|
183
315
|
const display = DisplayManager.getInstance();
|
|
184
316
|
display.log('Sati configuration updated via UI', {
|
|
185
317
|
source: 'Zaion',
|
|
@@ -199,7 +331,7 @@ export function createApiRouter() {
|
|
|
199
331
|
router.delete('/config/sati', async (req, res) => {
|
|
200
332
|
try {
|
|
201
333
|
const config = configManager.get();
|
|
202
|
-
const {
|
|
334
|
+
const { sati: sati, ...restConfig } = config;
|
|
203
335
|
await configManager.save(restConfig);
|
|
204
336
|
const display = DisplayManager.getInstance();
|
|
205
337
|
display.log('Sati configuration removed via UI (falling back to Oracle config)', {
|