morpheus-cli 0.2.7 → 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 +51 -8
- package/dist/channels/telegram.js +229 -22
- package/dist/cli/commands/doctor.js +11 -11
- package/dist/cli/commands/init.js +34 -34
- package/dist/cli/commands/restart.js +1 -1
- package/dist/cli/commands/session.js +79 -0
- package/dist/cli/commands/start.js +4 -1
- package/dist/cli/index.js +3 -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 +160 -3
- package/dist/http/server.js +4 -2
- package/dist/runtime/memory/backfill-embeddings.js +54 -0
- package/dist/runtime/memory/embedding.service.js +21 -0
- package/dist/runtime/memory/sati/index.js +5 -5
- package/dist/runtime/memory/sati/repository.js +323 -116
- package/dist/runtime/memory/sati/service.js +58 -33
- package/dist/runtime/memory/sati/system-prompts.js +19 -8
- package/dist/runtime/memory/session-embedding-worker.js +94 -0
- package/dist/runtime/memory/sqlite-vec.js +6 -0
- package/dist/runtime/memory/sqlite.js +432 -3
- package/dist/runtime/migration.js +40 -0
- package/dist/runtime/oracle.js +69 -1
- package/dist/runtime/session-embedding-scheduler.js +21 -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 +11 -4
- package/dist/ui/assets/index-Dx1lwaMu.js +0 -96
- package/dist/ui/assets/index-QHZ08tDL.css +0 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { ConfigManager } from '../../config/manager.js';
|
|
4
|
+
import { confirm } from '@inquirer/prompts';
|
|
5
|
+
const session = new Command('session')
|
|
6
|
+
.description('Manage chat sessions');
|
|
7
|
+
session.command('new')
|
|
8
|
+
.description('Archive current session and start a new one')
|
|
9
|
+
.action(async () => {
|
|
10
|
+
const confirmNew = await confirm({
|
|
11
|
+
message: 'Are you sure you want to start a new session?',
|
|
12
|
+
default: false,
|
|
13
|
+
});
|
|
14
|
+
if (confirmNew) {
|
|
15
|
+
const config = ConfigManager.getInstance().get();
|
|
16
|
+
const port = config.ui.port || 3333;
|
|
17
|
+
const authPass = process.env.THE_ARCHITECT_PASS || 'iamthearchitect';
|
|
18
|
+
try {
|
|
19
|
+
const response = await fetch(`http://localhost:${port}/api/session/reset`, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
headers: {
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
'x-architect-pass': authPass
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
if (response.ok) {
|
|
27
|
+
console.log(chalk.green('✓ New session started successfully on running Morpheus instance.'));
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
const errorText = await response.text();
|
|
31
|
+
console.log(chalk.red(`Failed: ${response.status} ${response.statusText}`));
|
|
32
|
+
if (errorText)
|
|
33
|
+
console.log(chalk.gray(errorText));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
console.log(chalk.red('Could not connect to Morpheus daemon.'));
|
|
38
|
+
console.log(chalk.yellow(`Ensure Morpheus is running and listening on port ${port}.`));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
console.log(chalk.yellow('Session reset cancelled. Current session is intact.'));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
session.command('status')
|
|
46
|
+
.description('Get current session status')
|
|
47
|
+
.action(async () => {
|
|
48
|
+
const config = ConfigManager.getInstance().get();
|
|
49
|
+
const port = config.ui.port || 3333;
|
|
50
|
+
const authPass = process.env.THE_ARCHITECT_PASS || 'iamthearchitect';
|
|
51
|
+
try {
|
|
52
|
+
const response = await fetch(`http://localhost:${port}/api/session/status`, {
|
|
53
|
+
method: 'GET',
|
|
54
|
+
headers: {
|
|
55
|
+
'Content-Type': 'application/json',
|
|
56
|
+
'x-architect-pass': authPass
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
if (response.ok) {
|
|
60
|
+
const data = await response.json();
|
|
61
|
+
console.log(chalk.bold('Current Session Status:'));
|
|
62
|
+
console.log(`- Session ID: ${data.id}`);
|
|
63
|
+
console.log(`- Messages in Session: ${data.messageCount}`);
|
|
64
|
+
console.log(`- Embedded: ${data.embedded}`);
|
|
65
|
+
console.log(`- Embedding Status: ${data.embedding_status}`);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const errorText = await response.text();
|
|
69
|
+
console.log(chalk.red(`Failed: ${response.status} ${response.statusText}`));
|
|
70
|
+
if (errorText)
|
|
71
|
+
console.log(chalk.gray(errorText));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
console.log(chalk.red('Could not connect to Morpheus daemon.'));
|
|
76
|
+
console.log(chalk.yellow(`Ensure Morpheus is running and listening on port ${port}.`));
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
export const sessionCommand = session;
|
|
@@ -12,6 +12,7 @@ import { Oracle } from '../../runtime/oracle.js';
|
|
|
12
12
|
import { ProviderError } from '../../runtime/errors.js';
|
|
13
13
|
import { HttpServer } from '../../http/server.js';
|
|
14
14
|
import { getVersion } from '../utils/version.js';
|
|
15
|
+
import { startSessionEmbeddingScheduler } from '../../runtime/session-embedding-scheduler.js';
|
|
15
16
|
export const startCommand = new Command('start')
|
|
16
17
|
.description('Start the Morpheus agent')
|
|
17
18
|
.option('--ui', 'Enable web UI', true)
|
|
@@ -78,7 +79,7 @@ export const startCommand = new Command('start')
|
|
|
78
79
|
// Initialize Web UI
|
|
79
80
|
if (options.ui && config.ui.enabled) {
|
|
80
81
|
try {
|
|
81
|
-
httpServer = new HttpServer();
|
|
82
|
+
httpServer = new HttpServer(oracle);
|
|
82
83
|
// Use CLI port if provided and valid, otherwise fallback to config or default
|
|
83
84
|
const port = parseInt(options.port) || config.ui.port || 3333;
|
|
84
85
|
httpServer.start(port);
|
|
@@ -103,6 +104,8 @@ export const startCommand = new Command('start')
|
|
|
103
104
|
display.log(chalk.yellow('Telegram enabled but no token provided. Skipping.'));
|
|
104
105
|
}
|
|
105
106
|
}
|
|
107
|
+
// Start Background Services
|
|
108
|
+
startSessionEmbeddingScheduler();
|
|
106
109
|
// Handle graceful shutdown
|
|
107
110
|
const shutdown = async (signal) => {
|
|
108
111
|
display.stopSpinner();
|
package/dist/cli/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { configCommand } from './commands/config.js';
|
|
|
6
6
|
import { doctorCommand } from './commands/doctor.js';
|
|
7
7
|
import { initCommand } from './commands/init.js';
|
|
8
8
|
import { restartCommand } from './commands/restart.js';
|
|
9
|
+
import { sessionCommand } from './commands/session.js';
|
|
9
10
|
import { scaffold } from '../runtime/scaffold.js';
|
|
10
11
|
import { getVersion } from './utils/version.js';
|
|
11
12
|
export async function cli() {
|
|
@@ -24,7 +25,8 @@ export async function cli() {
|
|
|
24
25
|
program.addCommand(statusCommand);
|
|
25
26
|
program.addCommand(configCommand);
|
|
26
27
|
program.addCommand(doctorCommand);
|
|
27
|
-
program.
|
|
28
|
+
program.addCommand(sessionCommand);
|
|
29
|
+
await program.parseAsync(process.argv);
|
|
28
30
|
}
|
|
29
31
|
// Support direct execution via tsx
|
|
30
32
|
if (import.meta.url.startsWith('file:') && (process.argv[1]?.endsWith('index.ts') || process.argv[1]?.endsWith('cli/index.js'))) {
|
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,9 +20,166 @@ 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
|
+
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)
|
|
162
|
+
router.post('/session/reset', async (req, res) => {
|
|
163
|
+
try {
|
|
164
|
+
await history.createNewSession();
|
|
165
|
+
res.json({ success: true, message: 'New session started' });
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
res.status(500).json({ error: err.message });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
router.post('/session/status', async (req, res) => {
|
|
172
|
+
try {
|
|
173
|
+
const sessionStatus = await history.getSessionStatus();
|
|
174
|
+
if (!sessionStatus) {
|
|
175
|
+
return res.status(404).json({ error: 'No session found' });
|
|
176
|
+
}
|
|
177
|
+
res.json(sessionStatus);
|
|
178
|
+
}
|
|
179
|
+
catch (err) {
|
|
180
|
+
res.status(500).json({ error: err.message });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
26
183
|
router.get('/status', async (req, res) => {
|
|
27
184
|
let version = 'unknown';
|
|
28
185
|
try {
|
|
@@ -154,7 +311,7 @@ export function createApiRouter() {
|
|
|
154
311
|
router.post('/config/sati', async (req, res) => {
|
|
155
312
|
try {
|
|
156
313
|
const config = configManager.get();
|
|
157
|
-
await configManager.save({ ...config,
|
|
314
|
+
await configManager.save({ ...config, sati: req.body });
|
|
158
315
|
const display = DisplayManager.getInstance();
|
|
159
316
|
display.log('Sati configuration updated via UI', {
|
|
160
317
|
source: 'Zaion',
|
|
@@ -174,7 +331,7 @@ export function createApiRouter() {
|
|
|
174
331
|
router.delete('/config/sati', async (req, res) => {
|
|
175
332
|
try {
|
|
176
333
|
const config = configManager.get();
|
|
177
|
-
const {
|
|
334
|
+
const { sati: sati, ...restConfig } = config;
|
|
178
335
|
await configManager.save(restConfig);
|
|
179
336
|
const display = DisplayManager.getInstance();
|
|
180
337
|
display.log('Sati configuration removed via UI (falling back to Oracle config)', {
|
package/dist/http/server.js
CHANGED
|
@@ -12,8 +12,10 @@ const __dirname = path.dirname(__filename);
|
|
|
12
12
|
export class HttpServer {
|
|
13
13
|
app;
|
|
14
14
|
server;
|
|
15
|
-
|
|
15
|
+
oracle;
|
|
16
|
+
constructor(oracle) {
|
|
16
17
|
this.app = express();
|
|
18
|
+
this.oracle = oracle;
|
|
17
19
|
this.setupMiddleware();
|
|
18
20
|
this.setupRoutes();
|
|
19
21
|
}
|
|
@@ -43,7 +45,7 @@ export class HttpServer {
|
|
|
43
45
|
uptime: process.uptime()
|
|
44
46
|
});
|
|
45
47
|
});
|
|
46
|
-
this.app.use('/api', authMiddleware, createApiRouter());
|
|
48
|
+
this.app.use('/api', authMiddleware, createApiRouter(this.oracle));
|
|
47
49
|
// Serve static frontend from compiled output
|
|
48
50
|
const uiPath = path.resolve(__dirname, '../ui');
|
|
49
51
|
this.app.use(express.static(uiPath));
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import { EmbeddingService } from './embedding.service.js';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
import loadVecExtension from './sqlite-vec.js';
|
|
6
|
+
const db = new Database(path.join(homedir(), '.morpheus', 'memory', 'sati-memory.db'));
|
|
7
|
+
db.pragma('journal_mode = WAL');
|
|
8
|
+
// 🔥 ISSO AQUI É O QUE ESTÁ FALTANDO
|
|
9
|
+
loadVecExtension(db);
|
|
10
|
+
const embeddingService = await EmbeddingService.getInstance();
|
|
11
|
+
const BATCH_SIZE = 50;
|
|
12
|
+
async function run() {
|
|
13
|
+
console.log('🔎 Buscando memórias sem embedding vetorial...');
|
|
14
|
+
while (true) {
|
|
15
|
+
const rows = db.prepare(`
|
|
16
|
+
SELECT m.rowid, m.summary, m.details
|
|
17
|
+
FROM long_term_memory m
|
|
18
|
+
LEFT JOIN memory_vec v ON m.rowid = v.rowid
|
|
19
|
+
WHERE v.rowid IS NULL
|
|
20
|
+
LIMIT ?
|
|
21
|
+
`).all(BATCH_SIZE);
|
|
22
|
+
if (rows.length === 0) {
|
|
23
|
+
console.log('✅ Todas as memórias já possuem embedding.');
|
|
24
|
+
break;
|
|
25
|
+
}
|
|
26
|
+
console.log(`⚙️ Processando batch de ${rows.length} memórias...`);
|
|
27
|
+
const vectors = [];
|
|
28
|
+
for (const row of rows) {
|
|
29
|
+
const text = `${row.summary} ${row.details || ''}`.trim();
|
|
30
|
+
const vector = await embeddingService.generate(text);
|
|
31
|
+
vectors.push({ rowid: row.rowid, vector });
|
|
32
|
+
}
|
|
33
|
+
const insertVec = db.prepare(`
|
|
34
|
+
INSERT INTO memory_vec (embedding)
|
|
35
|
+
VALUES (?)
|
|
36
|
+
`);
|
|
37
|
+
const insertMap = db.prepare(`
|
|
38
|
+
INSERT INTO memory_embedding_map (memory_id, vec_rowid)
|
|
39
|
+
VALUES (?, ?)
|
|
40
|
+
`);
|
|
41
|
+
const transaction = db.transaction((items) => {
|
|
42
|
+
for (const item of items) {
|
|
43
|
+
const result = insertVec.run(new Float32Array(item.vector));
|
|
44
|
+
const vecRowId = result.lastInsertRowid;
|
|
45
|
+
insertMap.run(item.memory_id, vecRowId);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
transaction(vectors);
|
|
49
|
+
}
|
|
50
|
+
console.log('🎉 Backfill concluído.');
|
|
51
|
+
}
|
|
52
|
+
run().catch(err => {
|
|
53
|
+
console.error('❌ Erro no backfill:', err);
|
|
54
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { pipeline } from '@xenova/transformers';
|
|
2
|
+
export class EmbeddingService {
|
|
3
|
+
static instance;
|
|
4
|
+
extractor;
|
|
5
|
+
constructor() { }
|
|
6
|
+
static async getInstance() {
|
|
7
|
+
if (!EmbeddingService.instance) {
|
|
8
|
+
const service = new EmbeddingService();
|
|
9
|
+
service.extractor = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
|
|
10
|
+
EmbeddingService.instance = service;
|
|
11
|
+
}
|
|
12
|
+
return EmbeddingService.instance;
|
|
13
|
+
}
|
|
14
|
+
async generate(text) {
|
|
15
|
+
const output = await this.extractor(text, {
|
|
16
|
+
pooling: 'mean',
|
|
17
|
+
normalize: true,
|
|
18
|
+
});
|
|
19
|
+
return Array.from(output.data);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -18,16 +18,16 @@ export class SatiMemoryMiddleware {
|
|
|
18
18
|
try {
|
|
19
19
|
// Extract recent messages content strings for context
|
|
20
20
|
const recentText = history.slice(-10).map(m => m.content.toString());
|
|
21
|
-
display.log(`
|
|
21
|
+
display.log(`Searching memories for: "${currentMessage.substring(0, 50)}${currentMessage.length > 50 ? '...' : ''}"`, { source: 'Sati' });
|
|
22
22
|
const result = await this.service.recover(currentMessage, recentText);
|
|
23
23
|
if (result.relevant_memories.length === 0) {
|
|
24
|
-
display.log('
|
|
24
|
+
display.log('No relevant memories found', { source: 'Sati' });
|
|
25
25
|
return null;
|
|
26
26
|
}
|
|
27
27
|
const memoryContext = result.relevant_memories
|
|
28
28
|
.map(m => `- [${m.category.toUpperCase()}] ${m.summary}`)
|
|
29
29
|
.join('\n');
|
|
30
|
-
display.log(`
|
|
30
|
+
display.log(`Retrieved ${result.relevant_memories.length} memories.`, { source: 'Sati' });
|
|
31
31
|
return new AIMessage(`
|
|
32
32
|
### LONG-TERM MEMORY (SATI)
|
|
33
33
|
The following information was retrieved from previous sessions. Use it if relevant:
|
|
@@ -36,7 +36,7 @@ export class SatiMemoryMiddleware {
|
|
|
36
36
|
`);
|
|
37
37
|
}
|
|
38
38
|
catch (error) {
|
|
39
|
-
display.log(`
|
|
39
|
+
display.log(`Error in beforeAgent: ${error}`, { source: 'Sati' });
|
|
40
40
|
// Fail open: return null so execution continues without memory
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
@@ -52,7 +52,7 @@ export class SatiMemoryMiddleware {
|
|
|
52
52
|
]);
|
|
53
53
|
}
|
|
54
54
|
catch (error) {
|
|
55
|
-
|
|
55
|
+
display.log(`Error in afterAgent: ${error}`, { source: 'Sati' });
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
}
|