morpheus-cli 0.3.8 → 0.4.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/dist/channels/telegram.js +21 -1
- package/dist/http/api.js +91 -50
- package/dist/runtime/memory/embedding.service.js +15 -1
- package/dist/runtime/memory/sati/index.js +2 -2
- package/dist/runtime/memory/sati/repository.js +48 -34
- package/dist/runtime/memory/sati/service.js +3 -2
- package/dist/runtime/memory/sqlite.js +160 -27
- package/dist/runtime/oracle.js +9 -6
- package/dist/runtime/telephonist.js +19 -1
- package/dist/ui/assets/index-CovGlIO5.js +109 -0
- package/dist/ui/assets/index-LrqT6MpO.css +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/sw.js +1 -1
- package/package.json +1 -1
- package/dist/ui/assets/index-CwvCMGLo.css +0 -1
- package/dist/ui/assets/index-D9REy_tK.js +0 -109
|
@@ -23,6 +23,16 @@ export class TelegramAdapter {
|
|
|
23
23
|
telephonistProvider = null;
|
|
24
24
|
telephonistModel = null;
|
|
25
25
|
history = new SQLiteChatMessageHistory({ sessionId: '' });
|
|
26
|
+
RATE_LIMIT_MS = 3000; // minimum ms between requests per user
|
|
27
|
+
rateLimiter = new Map(); // userId -> last request timestamp
|
|
28
|
+
isRateLimited(userId) {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const last = this.rateLimiter.get(userId);
|
|
31
|
+
if (last !== undefined && now - last < this.RATE_LIMIT_MS)
|
|
32
|
+
return true;
|
|
33
|
+
this.rateLimiter.set(userId, now);
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
26
36
|
HELP_MESSAGE = `/start - Show this welcome message and available commands
|
|
27
37
|
/status - Check the status of the Morpheus agent
|
|
28
38
|
/doctor - Diagnose environment and configuration issues
|
|
@@ -61,11 +71,16 @@ export class TelegramAdapter {
|
|
|
61
71
|
return; // Silent fail for security
|
|
62
72
|
}
|
|
63
73
|
this.display.log(`@${user}: ${text}`, { source: 'Telegram' });
|
|
64
|
-
// Handle system commands
|
|
74
|
+
// Handle system commands (commands bypass rate limit)
|
|
65
75
|
if (text.startsWith('/')) {
|
|
66
76
|
await this.handleSystemCommand(ctx, text, user);
|
|
67
77
|
return;
|
|
68
78
|
}
|
|
79
|
+
// Rate limit check
|
|
80
|
+
if (this.isRateLimited(userId)) {
|
|
81
|
+
await ctx.reply('Please wait a moment before sending another message.');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
69
84
|
try {
|
|
70
85
|
// Send "typing" status
|
|
71
86
|
await ctx.sendChatAction('typing');
|
|
@@ -96,6 +111,11 @@ export class TelegramAdapter {
|
|
|
96
111
|
this.display.log(`Unauthorized audio attempt by @${user} (ID: ${userId})`, { source: 'Telegram', level: 'warning' });
|
|
97
112
|
return;
|
|
98
113
|
}
|
|
114
|
+
// Rate limit check
|
|
115
|
+
if (this.isRateLimited(userId)) {
|
|
116
|
+
await ctx.reply('Please wait a moment before sending another message.');
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
99
119
|
if (!config.audio.enabled) {
|
|
100
120
|
await ctx.reply("Audio transcription is currently disabled.");
|
|
101
121
|
return;
|
package/dist/http/api.js
CHANGED
|
@@ -80,12 +80,11 @@ export function createApiRouter(oracle) {
|
|
|
80
80
|
}
|
|
81
81
|
});
|
|
82
82
|
router.get('/sessions/:id/messages', async (req, res) => {
|
|
83
|
+
const { id } = req.params;
|
|
84
|
+
const sessionHistory = new SQLiteChatMessageHistory({ sessionId: id, limit: 100 });
|
|
83
85
|
try {
|
|
84
|
-
const { id } = req.params;
|
|
85
|
-
const sessionHistory = new SQLiteChatMessageHistory({ sessionId: id, limit: 100 });
|
|
86
86
|
const messages = await sessionHistory.getMessages();
|
|
87
87
|
// Normalize messages for UI
|
|
88
|
-
const key = (msg) => msg._getType(); // Access internal type if available, or infer
|
|
89
88
|
const normalizedMessages = messages.map((msg) => {
|
|
90
89
|
const type = msg._getType ? msg._getType() : 'unknown';
|
|
91
90
|
return {
|
|
@@ -101,57 +100,23 @@ export function createApiRouter(oracle) {
|
|
|
101
100
|
catch (err) {
|
|
102
101
|
res.status(500).json({ error: err.message });
|
|
103
102
|
}
|
|
103
|
+
finally {
|
|
104
|
+
sessionHistory.close();
|
|
105
|
+
}
|
|
104
106
|
});
|
|
105
107
|
// --- Chat Interaction ---
|
|
108
|
+
const ChatSchema = z.object({
|
|
109
|
+
message: z.string().min(1).max(32_000),
|
|
110
|
+
sessionId: z.string().min(1)
|
|
111
|
+
});
|
|
106
112
|
router.post('/chat', async (req, res) => {
|
|
113
|
+
const parsed = ChatSchema.safeParse(req.body);
|
|
114
|
+
if (!parsed.success) {
|
|
115
|
+
return res.status(400).json({ error: 'Invalid input', details: parsed.error.message });
|
|
116
|
+
}
|
|
107
117
|
try {
|
|
108
|
-
const { message, sessionId } =
|
|
109
|
-
|
|
110
|
-
return res.status(400).json({ error: 'Message and Session ID are required' });
|
|
111
|
-
}
|
|
112
|
-
// We need to ensure the Oracle uses the correct session history.
|
|
113
|
-
// The Oracle class uses its own internal history instance.
|
|
114
|
-
// We might need to refactor Oracle to accept a session ID per request or
|
|
115
|
-
// instantiate a temporary Oracle wrapper/context?
|
|
116
|
-
//
|
|
117
|
-
// ACTUALLY: The Oracle class uses `this.history`.
|
|
118
|
-
// `SQLiteChatMessageHistory` takes `sessionId` in constructor.
|
|
119
|
-
// To support multi-session chat via API, we should probably allow passing sessionId to `chat()`
|
|
120
|
-
// OR (cleaner for now) we can rely on the fact that `Oracle` might not support swapping sessions easily without
|
|
121
|
-
// re-initialization or we extend `oracle.chat` to support overriding session.
|
|
122
|
-
//
|
|
123
|
-
// Let's look at `Oracle.chat`: it uses `this.history`.
|
|
124
|
-
// And `SQLiteChatMessageHistory` is tied to a sessionId.
|
|
125
|
-
//
|
|
126
|
-
// Quick fix for this feature:
|
|
127
|
-
// We can use a trick: `Oracle` allows `overrides` in constructor but that's for db path.
|
|
128
|
-
// `Oracle` initializes `this.history` in `initialize`.
|
|
129
|
-
// Better approach:
|
|
130
|
-
// We can temporarily switch the session of the Oracle's history if it exposes it,
|
|
131
|
-
// OR we just instantiate a fresh history for the chat request and use the provider?
|
|
132
|
-
// No, `oracle.chat` encapsulates the provider invocation.
|
|
133
|
-
// Let's check `Oracle` class again. (I viewed it earlier).
|
|
134
|
-
// It has `private history`.
|
|
135
|
-
// SOLUTION:
|
|
136
|
-
// I will add a `setSessionId(id: string)` method to `Oracle` interface and class.
|
|
137
|
-
// OR pass `sessionId` to `chat`.
|
|
138
|
-
// For now, I will assume I can update `Oracle` to support dynamic sessions.
|
|
139
|
-
// I'll modify `Oracle.chat` signature in a separate step if needed.
|
|
140
|
-
// Wait, `Oracle` is a singleton-ish in `start.ts`.
|
|
141
|
-
// Let's modify `Oracle` to accept `sessionId` in `chat`?
|
|
142
|
-
// `chat(message: string, extraUsage?: UsageMetadata, isTelephonist?: boolean)`
|
|
143
|
-
//
|
|
144
|
-
// Adding `sessionId` to `chat` seems invasive if not threaded fast.
|
|
145
|
-
//
|
|
146
|
-
// Alternative:
|
|
147
|
-
// `router.post('/chat')` instantiates a *new* Oracle? No, expensive (provider factory).
|
|
148
|
-
//
|
|
149
|
-
// Ideally `Oracle` should be stateless regarding session, or easily switchable.
|
|
150
|
-
// `SQLiteChatMessageHistory` is cheap to instantiate.
|
|
151
|
-
//
|
|
152
|
-
// Let's update `Oracle` to allow switching session.
|
|
153
|
-
// `oracle.switchSession(sessionId)`
|
|
154
|
-
await oracle.setSessionId(sessionId); // Type cast for now, will implement next
|
|
118
|
+
const { message, sessionId } = parsed.data;
|
|
119
|
+
await oracle.setSessionId(sessionId);
|
|
155
120
|
const response = await oracle.chat(message);
|
|
156
121
|
res.json({ response });
|
|
157
122
|
}
|
|
@@ -248,6 +213,82 @@ export function createApiRouter(oracle) {
|
|
|
248
213
|
res.status(500).json({ error: error.message });
|
|
249
214
|
}
|
|
250
215
|
});
|
|
216
|
+
// --- Model Pricing ---
|
|
217
|
+
const ModelPricingSchema = z.object({
|
|
218
|
+
provider: z.string().min(1),
|
|
219
|
+
model: z.string().min(1),
|
|
220
|
+
input_price_per_1m: z.number().nonnegative(),
|
|
221
|
+
output_price_per_1m: z.number().nonnegative()
|
|
222
|
+
});
|
|
223
|
+
router.get('/model-pricing', (req, res) => {
|
|
224
|
+
try {
|
|
225
|
+
const h = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
|
|
226
|
+
const entries = h.listModelPricing();
|
|
227
|
+
h.close();
|
|
228
|
+
res.json(entries);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
res.status(500).json({ error: error.message });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
router.post('/model-pricing', (req, res) => {
|
|
235
|
+
const parsed = ModelPricingSchema.safeParse(req.body);
|
|
236
|
+
if (!parsed.success) {
|
|
237
|
+
return res.status(400).json({ error: 'Invalid payload', details: parsed.error.issues });
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const h = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
|
|
241
|
+
h.upsertModelPricing(parsed.data);
|
|
242
|
+
h.close();
|
|
243
|
+
res.json({ success: true });
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
res.status(500).json({ error: error.message });
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
router.put('/model-pricing/:provider/:model', (req, res) => {
|
|
250
|
+
const { provider, model } = req.params;
|
|
251
|
+
const partial = z.object({
|
|
252
|
+
input_price_per_1m: z.number().nonnegative().optional(),
|
|
253
|
+
output_price_per_1m: z.number().nonnegative().optional()
|
|
254
|
+
}).safeParse(req.body);
|
|
255
|
+
if (!partial.success) {
|
|
256
|
+
return res.status(400).json({ error: 'Invalid payload', details: partial.error.issues });
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
const h = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
|
|
260
|
+
const existing = h.listModelPricing().find(e => e.provider === provider && e.model === model);
|
|
261
|
+
if (!existing) {
|
|
262
|
+
h.close();
|
|
263
|
+
return res.status(404).json({ error: 'Pricing entry not found' });
|
|
264
|
+
}
|
|
265
|
+
h.upsertModelPricing({
|
|
266
|
+
provider,
|
|
267
|
+
model,
|
|
268
|
+
input_price_per_1m: partial.data.input_price_per_1m ?? existing.input_price_per_1m,
|
|
269
|
+
output_price_per_1m: partial.data.output_price_per_1m ?? existing.output_price_per_1m
|
|
270
|
+
});
|
|
271
|
+
h.close();
|
|
272
|
+
res.json({ success: true });
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
res.status(500).json({ error: error.message });
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
router.delete('/model-pricing/:provider/:model', (req, res) => {
|
|
279
|
+
const { provider, model } = req.params;
|
|
280
|
+
try {
|
|
281
|
+
const h = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
|
|
282
|
+
const changes = h.deleteModelPricing(provider, model);
|
|
283
|
+
h.close();
|
|
284
|
+
if (changes === 0)
|
|
285
|
+
return res.status(404).json({ error: 'Pricing entry not found' });
|
|
286
|
+
res.json({ success: true });
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
res.status(500).json({ error: error.message });
|
|
290
|
+
}
|
|
291
|
+
});
|
|
251
292
|
// Calculate diff between two objects
|
|
252
293
|
const getDiff = (obj1, obj2, prefix = '') => {
|
|
253
294
|
const changes = [];
|
|
@@ -2,6 +2,8 @@ import { pipeline } from '@xenova/transformers';
|
|
|
2
2
|
export class EmbeddingService {
|
|
3
3
|
static instance;
|
|
4
4
|
extractor;
|
|
5
|
+
MAX_CACHE_SIZE = 256;
|
|
6
|
+
cache = new Map(); // text prefix -> embedding
|
|
5
7
|
constructor() { }
|
|
6
8
|
static async getInstance() {
|
|
7
9
|
if (!EmbeddingService.instance) {
|
|
@@ -12,10 +14,22 @@ export class EmbeddingService {
|
|
|
12
14
|
return EmbeddingService.instance;
|
|
13
15
|
}
|
|
14
16
|
async generate(text) {
|
|
17
|
+
const cacheKey = text.slice(0, 200);
|
|
18
|
+
const cached = this.cache.get(cacheKey);
|
|
19
|
+
if (cached)
|
|
20
|
+
return cached;
|
|
15
21
|
const output = await this.extractor(text, {
|
|
16
22
|
pooling: 'mean',
|
|
17
23
|
normalize: true,
|
|
18
24
|
});
|
|
19
|
-
|
|
25
|
+
const embedding = Array.from(output.data);
|
|
26
|
+
if (this.cache.size >= this.MAX_CACHE_SIZE) {
|
|
27
|
+
// Evict oldest entry (FIFO)
|
|
28
|
+
const firstKey = this.cache.keys().next().value;
|
|
29
|
+
if (firstKey !== undefined)
|
|
30
|
+
this.cache.delete(firstKey);
|
|
31
|
+
}
|
|
32
|
+
this.cache.set(cacheKey, embedding);
|
|
33
|
+
return embedding;
|
|
20
34
|
}
|
|
21
35
|
}
|
|
@@ -41,7 +41,7 @@ export class SatiMemoryMiddleware {
|
|
|
41
41
|
return null;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
async afterAgent(generatedResponse, history) {
|
|
44
|
+
async afterAgent(generatedResponse, history, userSessionId) {
|
|
45
45
|
try {
|
|
46
46
|
await this.service.evaluateAndPersist([
|
|
47
47
|
...history.slice(-5).map(m => ({
|
|
@@ -49,7 +49,7 @@ export class SatiMemoryMiddleware {
|
|
|
49
49
|
content: m.content.toString()
|
|
50
50
|
})),
|
|
51
51
|
{ role: 'assistant', content: generatedResponse }
|
|
52
|
-
]);
|
|
52
|
+
], userSessionId);
|
|
53
53
|
}
|
|
54
54
|
catch (error) {
|
|
55
55
|
display.log(`Error in afterAgent: ${error}`, { source: 'Sati' });
|
|
@@ -83,6 +83,9 @@ export class SatiRepository {
|
|
|
83
83
|
vec_rowid INTEGER NOT NULL
|
|
84
84
|
);
|
|
85
85
|
|
|
86
|
+
CREATE INDEX IF NOT EXISTS idx_embedding_map_vec_rowid
|
|
87
|
+
ON memory_embedding_map(vec_rowid);
|
|
88
|
+
|
|
86
89
|
-- ===============================
|
|
87
90
|
-- 4️⃣ TRIGGERS FTS
|
|
88
91
|
-- ===============================
|
|
@@ -129,6 +132,9 @@ export class SatiRepository {
|
|
|
129
132
|
vec_rowid INTEGER NOT NULL
|
|
130
133
|
);
|
|
131
134
|
|
|
135
|
+
CREATE INDEX IF NOT EXISTS idx_session_embedding_map_vec_rowid
|
|
136
|
+
ON session_embedding_map(vec_rowid);
|
|
137
|
+
|
|
132
138
|
`);
|
|
133
139
|
}
|
|
134
140
|
// 🔥 NOVO — Salvar embedding
|
|
@@ -162,41 +168,47 @@ export class SatiRepository {
|
|
|
162
168
|
transaction();
|
|
163
169
|
}
|
|
164
170
|
// 🔥 NOVO — Busca vetorial
|
|
165
|
-
searchByVector(
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
171
|
+
// private searchByVector(
|
|
172
|
+
// embedding: number[],
|
|
173
|
+
// limit: number
|
|
174
|
+
// ): IMemoryRecord[] {
|
|
175
|
+
// if (!this.db) return [];
|
|
176
|
+
// const SIMILARITY_THRESHOLD = 0.5; // ajuste fino depois
|
|
177
|
+
// const stmt = this.db.prepare(`
|
|
178
|
+
// SELECT
|
|
179
|
+
// m.*,
|
|
180
|
+
// vec_distance_cosine(v.embedding, ?) as distance
|
|
181
|
+
// FROM memory_vec v
|
|
182
|
+
// JOIN memory_embedding_map map ON map.vec_rowid = v.rowid
|
|
183
|
+
// JOIN long_term_memory m ON m.id = map.memory_id
|
|
184
|
+
// WHERE m.archived = 0
|
|
185
|
+
// ORDER BY distance ASC
|
|
186
|
+
// LIMIT ?
|
|
187
|
+
// `);
|
|
188
|
+
// const rows = stmt.all(
|
|
189
|
+
// new Float32Array(embedding),
|
|
190
|
+
// limit
|
|
191
|
+
// ) as any[];
|
|
192
|
+
// // 🔥 Filtrar por similaridade real
|
|
193
|
+
// const ranked = rows
|
|
194
|
+
// .map(r => ({
|
|
195
|
+
// ...r,
|
|
196
|
+
// similarity: 1 - r.distance
|
|
197
|
+
// }));
|
|
198
|
+
// const filtered = ranked
|
|
199
|
+
// .filter(r => r.distance >= SIMILARITY_THRESHOLD)
|
|
200
|
+
// .sort((a, b) => b.similarity - a.similarity);
|
|
201
|
+
// if (filtered.length > 0) {
|
|
202
|
+
// console.log(
|
|
203
|
+
// `[SatiRepository] Vector hit (${filtered.length})`
|
|
204
|
+
// );
|
|
205
|
+
// }
|
|
206
|
+
// return filtered.map(this.mapRowToRecord);
|
|
207
|
+
// }
|
|
196
208
|
searchUnifiedVector(embedding, limit) {
|
|
197
209
|
if (!this.db)
|
|
198
210
|
return [];
|
|
199
|
-
const SIMILARITY_THRESHOLD = 0.
|
|
211
|
+
const SIMILARITY_THRESHOLD = 0.8;
|
|
200
212
|
const stmt = this.db.prepare(`
|
|
201
213
|
SELECT *
|
|
202
214
|
FROM (
|
|
@@ -208,7 +220,7 @@ export class SatiRepository {
|
|
|
208
220
|
m.category as category,
|
|
209
221
|
m.importance as importance,
|
|
210
222
|
'long_term' as source_type,
|
|
211
|
-
(1 - vec_distance_cosine(v.embedding, ?)) *
|
|
223
|
+
(1 - vec_distance_cosine(v.embedding, ?)) * 1.7 as distance
|
|
212
224
|
FROM memory_vec v
|
|
213
225
|
JOIN memory_embedding_map map ON map.vec_rowid = v.rowid
|
|
214
226
|
JOIN long_term_memory m ON m.id = map.memory_id
|
|
@@ -224,7 +236,7 @@ export class SatiRepository {
|
|
|
224
236
|
'session' as category,
|
|
225
237
|
'medium' as importance,
|
|
226
238
|
'session_chunk' as source_type,
|
|
227
|
-
(1 - vec_distance_cosine(v.embedding, ?)) * 0.
|
|
239
|
+
(1 - vec_distance_cosine(v.embedding, ?)) * 0.5 as distance
|
|
228
240
|
FROM session_vec v
|
|
229
241
|
JOIN session_embedding_map map ON map.vec_rowid = v.rowid
|
|
230
242
|
JOIN session_chunks sc ON sc.id = map.session_chunk_id
|
|
@@ -240,6 +252,8 @@ export class SatiRepository {
|
|
|
240
252
|
// rows.forEach((row, index) => {
|
|
241
253
|
// console.log(`[SatiRepository] Row ${index + 1}:`, row);
|
|
242
254
|
// });
|
|
255
|
+
// Note: the SQL query already computes distance as (1 - cosine_distance) * weight,
|
|
256
|
+
// so higher values mean higher similarity. Use distance directly as similarity score.
|
|
243
257
|
const ranked = rows
|
|
244
258
|
.map(r => ({
|
|
245
259
|
...r,
|
|
@@ -50,7 +50,7 @@ export class SatiService {
|
|
|
50
50
|
}))
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
-
async evaluateAndPersist(conversation) {
|
|
53
|
+
async evaluateAndPersist(conversation, userSessionId) {
|
|
54
54
|
try {
|
|
55
55
|
const satiConfig = ConfigManager.getInstance().getSatiConfig();
|
|
56
56
|
if (!satiConfig)
|
|
@@ -74,7 +74,8 @@ export class SatiService {
|
|
|
74
74
|
new SystemMessage(SATI_EVALUATION_PROMPT),
|
|
75
75
|
new HumanMessage(JSON.stringify(inputPayload, null, 2))
|
|
76
76
|
];
|
|
77
|
-
const
|
|
77
|
+
const satiSessionId = userSessionId ? `sati-evaluation-${userSessionId}` : 'sati-evaluation';
|
|
78
|
+
const history = new SQLiteChatMessageHistory({ sessionId: satiSessionId });
|
|
78
79
|
try {
|
|
79
80
|
const inputMsg = new ToolMessage({
|
|
80
81
|
content: JSON.stringify(inputPayload, null, 2),
|