morpheus-cli 0.4.14 → 0.5.0
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 +275 -1116
- package/dist/channels/telegram.js +210 -73
- package/dist/cli/commands/doctor.js +34 -0
- package/dist/cli/commands/init.js +128 -0
- package/dist/cli/commands/restart.js +17 -0
- package/dist/cli/commands/start.js +15 -0
- package/dist/config/manager.js +51 -0
- package/dist/config/schemas.js +7 -0
- package/dist/devkit/tools/network.js +1 -1
- package/dist/http/api.js +177 -10
- package/dist/runtime/apoc.js +139 -32
- package/dist/runtime/memory/sati/repository.js +30 -2
- package/dist/runtime/memory/sati/service.js +46 -15
- package/dist/runtime/memory/sati/system-prompts.js +71 -29
- package/dist/runtime/memory/sqlite.js +24 -0
- package/dist/runtime/neo.js +134 -0
- package/dist/runtime/oracle.js +244 -133
- package/dist/runtime/providers/factory.js +1 -12
- package/dist/runtime/tasks/context.js +53 -0
- package/dist/runtime/tasks/dispatcher.js +70 -0
- package/dist/runtime/tasks/notifier.js +68 -0
- package/dist/runtime/tasks/repository.js +370 -0
- package/dist/runtime/tasks/types.js +1 -0
- package/dist/runtime/tasks/worker.js +96 -0
- package/dist/runtime/tools/apoc-tool.js +61 -8
- package/dist/runtime/tools/delegation-guard.js +29 -0
- package/dist/runtime/tools/index.js +1 -0
- package/dist/runtime/tools/neo-tool.js +99 -0
- package/dist/runtime/tools/task-query-tool.js +76 -0
- package/dist/runtime/webhooks/dispatcher.js +10 -19
- package/dist/types/config.js +10 -0
- package/dist/ui/assets/index-20lLB1sM.js +112 -0
- package/dist/ui/assets/index-BJ56bRfs.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-LemKVRjC.js +0 -112
- package/dist/ui/assets/index-TCQ7VNYO.css +0 -1
|
@@ -15,6 +15,9 @@ import { ProviderError } from '../../runtime/errors.js';
|
|
|
15
15
|
import { HttpServer } from '../../http/server.js';
|
|
16
16
|
import { getVersion } from '../utils/version.js';
|
|
17
17
|
import { startSessionEmbeddingScheduler } from '../../runtime/session-embedding-scheduler.js';
|
|
18
|
+
import { TaskWorker } from '../../runtime/tasks/worker.js';
|
|
19
|
+
import { TaskNotifier } from '../../runtime/tasks/notifier.js';
|
|
20
|
+
import { TaskDispatcher } from '../../runtime/tasks/dispatcher.js';
|
|
18
21
|
export const startCommand = new Command('start')
|
|
19
22
|
.description('Start the Morpheus agent')
|
|
20
23
|
.option('--ui', 'Enable web UI', true)
|
|
@@ -119,6 +122,9 @@ export const startCommand = new Command('start')
|
|
|
119
122
|
}
|
|
120
123
|
const adapters = [];
|
|
121
124
|
let httpServer;
|
|
125
|
+
const taskWorker = new TaskWorker();
|
|
126
|
+
const taskNotifier = new TaskNotifier();
|
|
127
|
+
const asyncTasksEnabled = config.runtime?.async_tasks?.enabled !== false;
|
|
122
128
|
// Initialize Web UI
|
|
123
129
|
if (options.ui && config.ui.enabled) {
|
|
124
130
|
try {
|
|
@@ -139,6 +145,7 @@ export const startCommand = new Command('start')
|
|
|
139
145
|
await telegram.connect(config.channels.telegram.token, config.channels.telegram.allowedUsers || []);
|
|
140
146
|
// Wire Telegram adapter to webhook dispatcher for proactive notifications
|
|
141
147
|
WebhookDispatcher.setTelegramAdapter(telegram);
|
|
148
|
+
TaskDispatcher.setTelegramAdapter(telegram);
|
|
142
149
|
adapters.push(telegram);
|
|
143
150
|
}
|
|
144
151
|
catch (e) {
|
|
@@ -151,6 +158,10 @@ export const startCommand = new Command('start')
|
|
|
151
158
|
}
|
|
152
159
|
// Start Background Services
|
|
153
160
|
startSessionEmbeddingScheduler();
|
|
161
|
+
if (asyncTasksEnabled) {
|
|
162
|
+
taskWorker.start();
|
|
163
|
+
taskNotifier.start();
|
|
164
|
+
}
|
|
154
165
|
// Handle graceful shutdown
|
|
155
166
|
const shutdown = async (signal) => {
|
|
156
167
|
display.stopSpinner();
|
|
@@ -161,6 +172,10 @@ export const startCommand = new Command('start')
|
|
|
161
172
|
for (const adapter of adapters) {
|
|
162
173
|
await adapter.disconnect();
|
|
163
174
|
}
|
|
175
|
+
if (asyncTasksEnabled) {
|
|
176
|
+
taskWorker.stop();
|
|
177
|
+
taskNotifier.stop();
|
|
178
|
+
}
|
|
164
179
|
await clearPid();
|
|
165
180
|
process.exit(0);
|
|
166
181
|
};
|
package/dist/config/manager.js
CHANGED
|
@@ -85,6 +85,45 @@ export class ConfigManager {
|
|
|
85
85
|
timeout_ms: config.apoc.timeout_ms !== undefined ? resolveNumeric('MORPHEUS_APOC_TIMEOUT_MS', config.apoc.timeout_ms, 30_000) : 30_000
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
|
+
// Apply precedence to Neo config
|
|
89
|
+
const neoEnvVars = [
|
|
90
|
+
'MORPHEUS_NEO_PROVIDER',
|
|
91
|
+
'MORPHEUS_NEO_MODEL',
|
|
92
|
+
'MORPHEUS_NEO_TEMPERATURE',
|
|
93
|
+
'MORPHEUS_NEO_MAX_TOKENS',
|
|
94
|
+
'MORPHEUS_NEO_API_KEY',
|
|
95
|
+
'MORPHEUS_NEO_BASE_URL',
|
|
96
|
+
'MORPHEUS_NEO_CONTEXT_WINDOW',
|
|
97
|
+
];
|
|
98
|
+
const hasNeoEnvOverrides = neoEnvVars.some((envVar) => process.env[envVar] !== undefined);
|
|
99
|
+
const resolveOptionalNumeric = (envVar, configValue, fallbackValue) => {
|
|
100
|
+
if (fallbackValue !== undefined) {
|
|
101
|
+
return resolveNumeric(envVar, configValue, fallbackValue);
|
|
102
|
+
}
|
|
103
|
+
if (process.env[envVar] !== undefined && process.env[envVar] !== '') {
|
|
104
|
+
const parsed = Number(process.env[envVar]);
|
|
105
|
+
if (!Number.isNaN(parsed)) {
|
|
106
|
+
return parsed;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return configValue;
|
|
110
|
+
};
|
|
111
|
+
let neoConfig;
|
|
112
|
+
if (config.neo || hasNeoEnvOverrides) {
|
|
113
|
+
const neoProvider = resolveProvider('MORPHEUS_NEO_PROVIDER', config.neo?.provider, llmConfig.provider);
|
|
114
|
+
const neoBaseUrl = resolveString('MORPHEUS_NEO_BASE_URL', config.neo?.base_url, llmConfig.base_url || '');
|
|
115
|
+
const neoMaxTokensFallback = config.neo?.max_tokens ?? llmConfig.max_tokens;
|
|
116
|
+
const neoContextWindowFallback = config.neo?.context_window ?? llmConfig.context_window;
|
|
117
|
+
neoConfig = {
|
|
118
|
+
provider: neoProvider,
|
|
119
|
+
model: resolveModel(neoProvider, 'MORPHEUS_NEO_MODEL', config.neo?.model || llmConfig.model),
|
|
120
|
+
temperature: resolveNumeric('MORPHEUS_NEO_TEMPERATURE', config.neo?.temperature, llmConfig.temperature),
|
|
121
|
+
max_tokens: resolveOptionalNumeric('MORPHEUS_NEO_MAX_TOKENS', config.neo?.max_tokens, neoMaxTokensFallback),
|
|
122
|
+
api_key: resolveApiKey(neoProvider, 'MORPHEUS_NEO_API_KEY', config.neo?.api_key || llmConfig.api_key),
|
|
123
|
+
base_url: neoBaseUrl || undefined,
|
|
124
|
+
context_window: resolveOptionalNumeric('MORPHEUS_NEO_CONTEXT_WINDOW', config.neo?.context_window, neoContextWindowFallback),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
88
127
|
// Apply precedence to audio config
|
|
89
128
|
const audioProvider = resolveString('MORPHEUS_AUDIO_PROVIDER', config.audio.provider, DEFAULT_CONFIG.audio.provider);
|
|
90
129
|
// AudioProvider uses 'google' but resolveApiKey expects LLMProvider which uses 'gemini'
|
|
@@ -128,6 +167,7 @@ export class ConfigManager {
|
|
|
128
167
|
agent: agentConfig,
|
|
129
168
|
llm: llmConfig,
|
|
130
169
|
sati: satiConfig,
|
|
170
|
+
neo: neoConfig,
|
|
131
171
|
apoc: apocConfig,
|
|
132
172
|
audio: audioConfig,
|
|
133
173
|
channels: channelsConfig,
|
|
@@ -185,4 +225,15 @@ export class ConfigManager {
|
|
|
185
225
|
timeout_ms: 30_000
|
|
186
226
|
};
|
|
187
227
|
}
|
|
228
|
+
getNeoConfig() {
|
|
229
|
+
if (this.config.neo) {
|
|
230
|
+
return {
|
|
231
|
+
...this.config.neo
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
// Fallback to main LLM config
|
|
235
|
+
return {
|
|
236
|
+
...this.config.llm,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
188
239
|
}
|
package/dist/config/schemas.js
CHANGED
|
@@ -26,6 +26,7 @@ export const ApocConfigSchema = LLMConfigSchema.extend({
|
|
|
26
26
|
working_dir: z.string().optional(),
|
|
27
27
|
timeout_ms: z.number().int().positive().optional(),
|
|
28
28
|
});
|
|
29
|
+
export const NeoConfigSchema = LLMConfigSchema;
|
|
29
30
|
export const WebhookConfigSchema = z.object({
|
|
30
31
|
telegram_notify_all: z.boolean().optional(),
|
|
31
32
|
}).optional();
|
|
@@ -37,12 +38,18 @@ export const ConfigSchema = z.object({
|
|
|
37
38
|
}).default(DEFAULT_CONFIG.agent),
|
|
38
39
|
llm: LLMConfigSchema.default(DEFAULT_CONFIG.llm),
|
|
39
40
|
sati: SatiConfigSchema.optional(),
|
|
41
|
+
neo: NeoConfigSchema.optional(),
|
|
40
42
|
apoc: ApocConfigSchema.optional(),
|
|
41
43
|
webhooks: WebhookConfigSchema,
|
|
42
44
|
audio: AudioConfigSchema.default(DEFAULT_CONFIG.audio),
|
|
43
45
|
memory: z.object({
|
|
44
46
|
limit: z.number().int().positive().optional(),
|
|
45
47
|
}).default(DEFAULT_CONFIG.memory),
|
|
48
|
+
runtime: z.object({
|
|
49
|
+
async_tasks: z.object({
|
|
50
|
+
enabled: z.boolean().default(DEFAULT_CONFIG.runtime?.async_tasks.enabled ?? true),
|
|
51
|
+
}).default(DEFAULT_CONFIG.runtime?.async_tasks ?? { enabled: true }),
|
|
52
|
+
}).optional(),
|
|
46
53
|
channels: z.object({
|
|
47
54
|
telegram: z.object({
|
|
48
55
|
enabled: z.boolean().default(false),
|
|
@@ -64,7 +64,7 @@ export function createNetworkTools(ctx) {
|
|
|
64
64
|
});
|
|
65
65
|
}, {
|
|
66
66
|
name: 'ping',
|
|
67
|
-
description: '
|
|
67
|
+
description: 'Preferred connectivity check tool. Verify if a host is reachable on a given port (TCP connect check). Use this instead of shell ping for routine reachability checks.',
|
|
68
68
|
schema: z.object({
|
|
69
69
|
host: z.string().describe('Hostname or IP'),
|
|
70
70
|
port: z.number().int().optional().describe('Port to check, default 80'),
|
package/dist/http/api.js
CHANGED
|
@@ -11,6 +11,7 @@ import { z } from 'zod';
|
|
|
11
11
|
import { MCPManager } from '../config/mcp-manager.js';
|
|
12
12
|
import { MCPServerConfigSchema } from '../config/schemas.js';
|
|
13
13
|
import { Construtor } from '../runtime/tools/factory.js';
|
|
14
|
+
import { TaskRepository } from '../runtime/tasks/repository.js';
|
|
14
15
|
async function readLastLines(filePath, n) {
|
|
15
16
|
try {
|
|
16
17
|
const content = await fs.readFile(filePath, 'utf8');
|
|
@@ -25,6 +26,7 @@ export function createApiRouter(oracle) {
|
|
|
25
26
|
const router = Router();
|
|
26
27
|
const configManager = ConfigManager.getInstance();
|
|
27
28
|
const history = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
|
|
29
|
+
const taskRepository = TaskRepository.getInstance();
|
|
28
30
|
// --- Session Management ---
|
|
29
31
|
router.get('/sessions', async (req, res) => {
|
|
30
32
|
try {
|
|
@@ -83,18 +85,76 @@ export function createApiRouter(oracle) {
|
|
|
83
85
|
const { id } = req.params;
|
|
84
86
|
const sessionHistory = new SQLiteChatMessageHistory({ sessionId: id, limit: 100 });
|
|
85
87
|
try {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
const relatedSessionIds = id.startsWith('sati-evaluation-')
|
|
89
|
+
? [id]
|
|
90
|
+
: [id, `sati-evaluation-${id}`];
|
|
91
|
+
const rows = await sessionHistory.getRawMessagesBySessionIds(relatedSessionIds, 200);
|
|
92
|
+
const normalizedMessages = rows.map((row) => {
|
|
93
|
+
let content = row.content;
|
|
94
|
+
let tool_calls;
|
|
95
|
+
let tool_name;
|
|
96
|
+
let tool_call_id;
|
|
97
|
+
if (row.type === 'ai') {
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(row.content);
|
|
100
|
+
if (parsed && typeof parsed === 'object') {
|
|
101
|
+
if (Array.isArray(parsed.tool_calls)) {
|
|
102
|
+
tool_calls = parsed.tool_calls;
|
|
103
|
+
}
|
|
104
|
+
if (typeof parsed.text === 'string') {
|
|
105
|
+
content = parsed.text;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Keep raw content for legacy/plain-text messages.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (row.type === 'tool') {
|
|
114
|
+
try {
|
|
115
|
+
const parsed = JSON.parse(row.content);
|
|
116
|
+
if (parsed && typeof parsed === 'object') {
|
|
117
|
+
if (parsed.content !== undefined) {
|
|
118
|
+
const parsedContent = parsed.content;
|
|
119
|
+
content =
|
|
120
|
+
typeof parsedContent === 'string'
|
|
121
|
+
? parsedContent
|
|
122
|
+
: JSON.stringify(parsedContent, null, 2);
|
|
123
|
+
}
|
|
124
|
+
if (typeof parsed.name === 'string') {
|
|
125
|
+
tool_name = parsed.name;
|
|
126
|
+
}
|
|
127
|
+
if (typeof parsed.tool_call_id === 'string') {
|
|
128
|
+
tool_call_id = parsed.tool_call_id;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// Keep raw content for legacy/plain-text tool messages.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const usage_metadata = row.total_tokens != null
|
|
137
|
+
? {
|
|
138
|
+
input_tokens: row.input_tokens || 0,
|
|
139
|
+
output_tokens: row.output_tokens || 0,
|
|
140
|
+
total_tokens: row.total_tokens || 0,
|
|
141
|
+
input_token_details: row.cache_read_tokens
|
|
142
|
+
? { cache_read: row.cache_read_tokens }
|
|
143
|
+
: undefined,
|
|
144
|
+
}
|
|
145
|
+
: undefined;
|
|
90
146
|
return {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
147
|
+
session_id: row.session_id,
|
|
148
|
+
created_at: row.created_at,
|
|
149
|
+
type: row.type,
|
|
150
|
+
content,
|
|
151
|
+
tool_calls,
|
|
152
|
+
tool_name,
|
|
153
|
+
tool_call_id,
|
|
154
|
+
usage_metadata,
|
|
95
155
|
};
|
|
96
156
|
});
|
|
97
|
-
//
|
|
157
|
+
// Convert DESC to ASC for UI rendering
|
|
98
158
|
res.json(normalizedMessages.reverse());
|
|
99
159
|
}
|
|
100
160
|
catch (err) {
|
|
@@ -117,13 +177,74 @@ export function createApiRouter(oracle) {
|
|
|
117
177
|
try {
|
|
118
178
|
const { message, sessionId } = parsed.data;
|
|
119
179
|
await oracle.setSessionId(sessionId);
|
|
120
|
-
const response = await oracle.chat(message
|
|
180
|
+
const response = await oracle.chat(message, undefined, false, {
|
|
181
|
+
origin_channel: 'ui',
|
|
182
|
+
session_id: sessionId,
|
|
183
|
+
});
|
|
121
184
|
res.json({ response });
|
|
122
185
|
}
|
|
123
186
|
catch (err) {
|
|
124
187
|
res.status(500).json({ error: err.message });
|
|
125
188
|
}
|
|
126
189
|
});
|
|
190
|
+
const TaskStatusSchema = z.enum(['pending', 'running', 'completed', 'failed', 'cancelled']);
|
|
191
|
+
const TaskAgentSchema = z.enum(['apoc', 'neo', 'trinit']);
|
|
192
|
+
const OriginChannelSchema = z.enum(['telegram', 'discord', 'ui', 'api', 'webhook', 'cli']);
|
|
193
|
+
router.get('/tasks', (req, res) => {
|
|
194
|
+
try {
|
|
195
|
+
const status = req.query.status;
|
|
196
|
+
const agent = req.query.agent;
|
|
197
|
+
const originChannel = req.query.origin_channel;
|
|
198
|
+
const sessionId = req.query.session_id;
|
|
199
|
+
const limit = req.query.limit;
|
|
200
|
+
const parsedStatus = typeof status === 'string' ? TaskStatusSchema.safeParse(status) : null;
|
|
201
|
+
const parsedAgent = typeof agent === 'string' ? TaskAgentSchema.safeParse(agent) : null;
|
|
202
|
+
const parsedOrigin = typeof originChannel === 'string' ? OriginChannelSchema.safeParse(originChannel) : null;
|
|
203
|
+
const tasks = taskRepository.listTasks({
|
|
204
|
+
status: parsedStatus?.success ? parsedStatus.data : undefined,
|
|
205
|
+
agent: parsedAgent?.success ? parsedAgent.data : undefined,
|
|
206
|
+
origin_channel: parsedOrigin?.success ? parsedOrigin.data : undefined,
|
|
207
|
+
session_id: typeof sessionId === 'string' ? sessionId : undefined,
|
|
208
|
+
limit: typeof limit === 'string' ? Math.max(1, Math.min(500, Number(limit) || 200)) : 200,
|
|
209
|
+
});
|
|
210
|
+
res.json(tasks);
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
res.status(500).json({ error: err.message });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
router.get('/tasks/stats', (req, res) => {
|
|
217
|
+
try {
|
|
218
|
+
res.json(taskRepository.getStats());
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
res.status(500).json({ error: err.message });
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
router.get('/tasks/:id', (req, res) => {
|
|
225
|
+
try {
|
|
226
|
+
const task = taskRepository.getTaskById(req.params.id);
|
|
227
|
+
if (!task) {
|
|
228
|
+
return res.status(404).json({ error: 'Task not found' });
|
|
229
|
+
}
|
|
230
|
+
res.json(task);
|
|
231
|
+
}
|
|
232
|
+
catch (err) {
|
|
233
|
+
res.status(500).json({ error: err.message });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
router.post('/tasks/:id/retry', (req, res) => {
|
|
237
|
+
try {
|
|
238
|
+
const ok = taskRepository.retryTask(req.params.id);
|
|
239
|
+
if (!ok) {
|
|
240
|
+
return res.status(404).json({ error: 'Failed task not found for retry' });
|
|
241
|
+
}
|
|
242
|
+
res.json({ success: true });
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
res.status(500).json({ error: err.message });
|
|
246
|
+
}
|
|
247
|
+
});
|
|
127
248
|
// Legacy /session/reset (keep for backward compat or redirect to POST /sessions)
|
|
128
249
|
router.post('/session/reset', async (req, res) => {
|
|
129
250
|
try {
|
|
@@ -396,6 +517,52 @@ export function createApiRouter(oracle) {
|
|
|
396
517
|
res.status(500).json({ error: error.message });
|
|
397
518
|
}
|
|
398
519
|
});
|
|
520
|
+
// Neo config endpoints
|
|
521
|
+
router.get('/config/neo', (req, res) => {
|
|
522
|
+
try {
|
|
523
|
+
const neoConfig = configManager.getNeoConfig();
|
|
524
|
+
res.json(neoConfig);
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
res.status(500).json({ error: error.message });
|
|
528
|
+
}
|
|
529
|
+
});
|
|
530
|
+
router.post('/config/neo', async (req, res) => {
|
|
531
|
+
try {
|
|
532
|
+
const config = configManager.get();
|
|
533
|
+
await configManager.save({ ...config, neo: req.body });
|
|
534
|
+
const display = DisplayManager.getInstance();
|
|
535
|
+
display.log('Neo configuration updated via UI', {
|
|
536
|
+
source: 'Zaion',
|
|
537
|
+
level: 'info'
|
|
538
|
+
});
|
|
539
|
+
res.json({ success: true });
|
|
540
|
+
}
|
|
541
|
+
catch (error) {
|
|
542
|
+
if (error.name === 'ZodError') {
|
|
543
|
+
res.status(400).json({ error: 'Validation failed', details: error.errors });
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
res.status(500).json({ error: error.message });
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
router.delete('/config/neo', async (req, res) => {
|
|
551
|
+
try {
|
|
552
|
+
const config = configManager.get();
|
|
553
|
+
const { neo: _neo, ...restConfig } = config;
|
|
554
|
+
await configManager.save(restConfig);
|
|
555
|
+
const display = DisplayManager.getInstance();
|
|
556
|
+
display.log('Neo configuration removed via UI (falling back to Oracle config)', {
|
|
557
|
+
source: 'Zaion',
|
|
558
|
+
level: 'info'
|
|
559
|
+
});
|
|
560
|
+
res.json({ success: true });
|
|
561
|
+
}
|
|
562
|
+
catch (error) {
|
|
563
|
+
res.status(500).json({ error: error.message });
|
|
564
|
+
}
|
|
565
|
+
});
|
|
399
566
|
router.post('/config/apoc', async (req, res) => {
|
|
400
567
|
try {
|
|
401
568
|
const config = configManager.get();
|
package/dist/runtime/apoc.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
|
|
1
|
+
import { HumanMessage, SystemMessage, AIMessage } from "@langchain/core/messages";
|
|
2
2
|
import { ConfigManager } from "../config/manager.js";
|
|
3
3
|
import { ProviderFactory } from "./providers/factory.js";
|
|
4
4
|
import { ProviderError } from "./errors.js";
|
|
@@ -41,7 +41,7 @@ export class Apoc {
|
|
|
41
41
|
}
|
|
42
42
|
async initialize() {
|
|
43
43
|
const apocConfig = this.config.apoc || this.config.llm;
|
|
44
|
-
console.log(`Apoc configuration: ${JSON.stringify(apocConfig)}`);
|
|
44
|
+
// console.log(`Apoc configuration: ${JSON.stringify(apocConfig)}`);
|
|
45
45
|
const working_dir = this.config.apoc?.working_dir || process.cwd();
|
|
46
46
|
const timeout_ms = this.config.apoc?.timeout_ms || 30_000;
|
|
47
47
|
// Import all devkit tool factories (side-effect registration)
|
|
@@ -95,21 +95,125 @@ OPERATING RULES:
|
|
|
95
95
|
3. Report clearly what was done and what the result was.
|
|
96
96
|
4. If something fails, report the error and what you tried.
|
|
97
97
|
5. Stay focused on the delegated task only.
|
|
98
|
+
6. Respond in the language requested by the user. If not explicit, use the dominant language of the task/context.
|
|
99
|
+
7. For connectivity checks, prefer the dedicated network tool "ping" (TCP reachability) instead of shell "ping".
|
|
100
|
+
8. Only use shell ping when explicitly required by the user. If shell ping is needed, detect OS first:
|
|
101
|
+
- Windows: use "-n" (never use "-c")
|
|
102
|
+
- Linux/macOS: use "-c"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
────────────────────────────────────────
|
|
106
|
+
BROWSER AUTOMATION PROTOCOL
|
|
107
|
+
────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
When using browser tools (browser_navigate, browser_get_dom, browser_click, browser_fill), follow this protocol exactly.
|
|
110
|
+
|
|
111
|
+
GENERAL PRINCIPLES
|
|
112
|
+
- Never guess selectors.
|
|
113
|
+
- Never assume page state.
|
|
114
|
+
- Always verify page transitions.
|
|
115
|
+
- Always extract evidence of success.
|
|
116
|
+
- If required user data is missing, STOP and return to Oracle immediately.
|
|
117
|
+
|
|
118
|
+
PHASE 1 — Navigation
|
|
119
|
+
1. ALWAYS call browser_navigate first.
|
|
120
|
+
2. Use:
|
|
121
|
+
- wait_until: "networkidle0" for SPAs or JS-heavy pages.
|
|
122
|
+
- wait_until: "domcontentloaded" for simple pages.
|
|
123
|
+
3. After navigation, confirm current_url and title.
|
|
124
|
+
4. If navigation fails, report the error and stop.
|
|
125
|
+
|
|
126
|
+
PHASE 2 — DOM Inspection (MANDATORY BEFORE ACTION)
|
|
127
|
+
1. ALWAYS call browser_get_dom before browser_click or browser_fill.
|
|
128
|
+
2. Identify stable selectors (prefer id > name > role > unique class).
|
|
129
|
+
3. Understand page structure and expected flow before interacting.
|
|
130
|
+
4. Never click or fill blindly.
|
|
131
|
+
|
|
132
|
+
PHASE 3 — Interaction
|
|
133
|
+
When clicking:
|
|
134
|
+
- Prefer stable selectors.
|
|
135
|
+
- If ambiguous, refine selector.
|
|
136
|
+
- Use visible text only if selector is unstable.
|
|
137
|
+
|
|
138
|
+
When filling:
|
|
139
|
+
- Confirm correct input field via DOM.
|
|
140
|
+
- Fill field.
|
|
141
|
+
- Submit using press_enter OR clicking submit button.
|
|
142
|
+
|
|
143
|
+
If login or personal data is required:
|
|
144
|
+
STOP and return required fields clearly.
|
|
145
|
+
|
|
146
|
+
PHASE 4 — State Verification (MANDATORY)
|
|
147
|
+
After ANY interaction:
|
|
148
|
+
1. Call browser_get_dom again.
|
|
149
|
+
2. Verify URL change or content change.
|
|
150
|
+
3. Confirm success or detect error message.
|
|
151
|
+
|
|
152
|
+
If expected change did not occur:
|
|
153
|
+
- Reinspect DOM.
|
|
154
|
+
- Attempt one justified alternative.
|
|
155
|
+
- If still failing, report failure clearly.
|
|
156
|
+
|
|
157
|
+
Maximum 2 attempts per step.
|
|
158
|
+
Never assume success.
|
|
159
|
+
|
|
160
|
+
PHASE 5 — Reporting
|
|
161
|
+
Include:
|
|
162
|
+
- Step-by-step actions
|
|
163
|
+
- Final URL
|
|
164
|
+
- Evidence of success
|
|
165
|
+
- Errors encountered
|
|
166
|
+
- Completion status (true/false)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
────────────────────────────────────────
|
|
170
|
+
WEB RESEARCH PROTOCOL
|
|
171
|
+
────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
When using browser_search for factual verification, follow this protocol strictly.
|
|
174
|
+
|
|
175
|
+
PHASE 1 — Query Design
|
|
176
|
+
1. Identify core entity, information type, and time constraint.
|
|
177
|
+
2. Build a precise search query.
|
|
178
|
+
3. If time-sensitive, include the current year.
|
|
179
|
+
|
|
180
|
+
PHASE 2 — Source Discovery
|
|
181
|
+
1. Call browser_search.
|
|
182
|
+
2. Collect results.
|
|
183
|
+
3. Prioritize:
|
|
184
|
+
- Official sources
|
|
185
|
+
- Major authoritative publications
|
|
186
|
+
4. Reformulate query if necessary.
|
|
187
|
+
|
|
188
|
+
PHASE 3 — Source Validation (MANDATORY)
|
|
189
|
+
1. Open at least 3 distinct URLs with browser_navigate.
|
|
190
|
+
2. Read actual page content.
|
|
191
|
+
3. NEVER rely only on search snippets.
|
|
192
|
+
4. Ignore inaccessible pages.
|
|
193
|
+
|
|
194
|
+
PHASE 4 — Cross-Verification
|
|
195
|
+
1. Extract relevant information from each source.
|
|
196
|
+
2. Compare findings:
|
|
197
|
+
- Agreement → verified
|
|
198
|
+
- Minor differences → report variation
|
|
199
|
+
- Conflict → report discrepancy
|
|
200
|
+
3. Require confirmation from at least 2 reliable sources.
|
|
201
|
+
4. If not confirmed, state clearly:
|
|
202
|
+
"Information could not be confidently verified."
|
|
203
|
+
|
|
204
|
+
PHASE 5 — Structured Report
|
|
205
|
+
Include:
|
|
206
|
+
- Direct answer
|
|
207
|
+
- Short explanation
|
|
208
|
+
- Source URLs
|
|
209
|
+
- Confidence level (High / Medium / Low)
|
|
210
|
+
|
|
211
|
+
ANTI-HALLUCINATION RULES
|
|
212
|
+
- Never answer from prior knowledge without verification.
|
|
213
|
+
- Never stop after reading only one source.
|
|
214
|
+
- Treat time-sensitive information as volatile.
|
|
215
|
+
|
|
98
216
|
|
|
99
|
-
BROWSER WORKFLOW RULES (when using browser tools):
|
|
100
|
-
1. ALWAYS call browser_navigate first to load the page.
|
|
101
|
-
2. ALWAYS call browser_get_dom before browser_click or browser_fill to inspect the page structure and choose the correct CSS selectors. Never guess selectors — analyze the DOM.
|
|
102
|
-
3. Analyze the DOM to identify interactive elements (inputs, buttons, links), their selectors (id, class, name), and the page flow.
|
|
103
|
-
4. If the task requires information you don't have (e.g. email, password, form fields, personal data), DO NOT proceed. Instead, immediately return to Oracle with a clear message listing exactly what information is needed from the user. Example: "To complete the login form I need: email address and password."
|
|
104
|
-
5. After clicking or filling, call browser_get_dom again to verify the page changed as expected.
|
|
105
|
-
6. Report what was done, the final URL, and any relevant content extracted.
|
|
106
|
-
|
|
107
|
-
SEARCH & FACT-CHECKING RULES (when using browser_search to answer factual questions):
|
|
108
|
-
1. Call browser_search first to get a list of relevant sources.
|
|
109
|
-
2. ALWAYS open at least 3 of the returned URLs with browser_navigate to read the actual content. Do not rely solely on the snippet — snippets may be outdated or incomplete.
|
|
110
|
-
3. Cross-reference the information across the sources. If they agree, report the fact with confidence. If they disagree, report all versions found and indicate the discrepancy.
|
|
111
|
-
4. Prefer authoritative sources (official team sites, major sports outlets, official event pages) over aggregators.
|
|
112
|
-
5. Include the source URLs in your final report so Oracle can pass them to the user.
|
|
113
217
|
|
|
114
218
|
${context ? `CONTEXT FROM ORACLE:\n${context}` : ""}
|
|
115
219
|
`);
|
|
@@ -117,27 +221,30 @@ ${context ? `CONTEXT FROM ORACLE:\n${context}` : ""}
|
|
|
117
221
|
const messages = [systemMessage, userMessage];
|
|
118
222
|
try {
|
|
119
223
|
const response = await this.agent.invoke({ messages });
|
|
120
|
-
// Persist
|
|
121
|
-
// Use
|
|
122
|
-
// otherwise fall back to 'apoc'.
|
|
224
|
+
// Persist one AI message per delegated task so usage can be parameterized later.
|
|
225
|
+
// Use task session id when provided.
|
|
123
226
|
const apocConfig = this.config.apoc || this.config.llm;
|
|
124
|
-
const newMessages = response.messages.slice(messages.length);
|
|
125
|
-
if (newMessages.length > 0) {
|
|
126
|
-
const targetSession = sessionId ?? Apoc.currentSessionId ?? 'apoc';
|
|
127
|
-
const history = new SQLiteChatMessageHistory({ sessionId: targetSession });
|
|
128
|
-
for (const msg of newMessages) {
|
|
129
|
-
msg.provider_metadata = {
|
|
130
|
-
provider: apocConfig.provider,
|
|
131
|
-
model: apocConfig.model,
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
await history.addMessages(newMessages);
|
|
135
|
-
history.close();
|
|
136
|
-
}
|
|
137
227
|
const lastMessage = response.messages[response.messages.length - 1];
|
|
138
228
|
const content = typeof lastMessage.content === "string"
|
|
139
229
|
? lastMessage.content
|
|
140
230
|
: JSON.stringify(lastMessage.content);
|
|
231
|
+
const targetSession = sessionId ?? Apoc.currentSessionId ?? "apoc";
|
|
232
|
+
const history = new SQLiteChatMessageHistory({ sessionId: targetSession });
|
|
233
|
+
try {
|
|
234
|
+
const persisted = new AIMessage(content);
|
|
235
|
+
persisted.usage_metadata = lastMessage.usage_metadata
|
|
236
|
+
?? lastMessage.response_metadata?.usage
|
|
237
|
+
?? lastMessage.response_metadata?.tokenUsage
|
|
238
|
+
?? lastMessage.usage;
|
|
239
|
+
persisted.provider_metadata = {
|
|
240
|
+
provider: apocConfig.provider,
|
|
241
|
+
model: apocConfig.model,
|
|
242
|
+
};
|
|
243
|
+
await history.addMessage(persisted);
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
history.close();
|
|
247
|
+
}
|
|
141
248
|
this.display.log("Apoc task completed.", { source: "Apoc" });
|
|
142
249
|
return content;
|
|
143
250
|
}
|
|
@@ -220,7 +220,7 @@ export class SatiRepository {
|
|
|
220
220
|
m.category as category,
|
|
221
221
|
m.importance as importance,
|
|
222
222
|
'long_term' as source_type,
|
|
223
|
-
(1 - vec_distance_cosine(v.embedding, ?)) *
|
|
223
|
+
(1 - vec_distance_cosine(v.embedding, ?)) * 0.8 as distance
|
|
224
224
|
FROM memory_vec v
|
|
225
225
|
JOIN memory_embedding_map map ON map.vec_rowid = v.rowid
|
|
226
226
|
JOIN long_term_memory m ON m.id = map.memory_id
|
|
@@ -236,7 +236,7 @@ export class SatiRepository {
|
|
|
236
236
|
'session' as category,
|
|
237
237
|
'medium' as importance,
|
|
238
238
|
'session_chunk' as source_type,
|
|
239
|
-
(1 - vec_distance_cosine(v.embedding, ?)) * 0.
|
|
239
|
+
(1 - vec_distance_cosine(v.embedding, ?)) * 0.2 as distance
|
|
240
240
|
FROM session_vec v
|
|
241
241
|
JOIN session_embedding_map map ON map.vec_rowid = v.rowid
|
|
242
242
|
JOIN session_chunks sc ON sc.id = map.session_chunk_id
|
|
@@ -431,6 +431,34 @@ export class SatiRepository {
|
|
|
431
431
|
archived: Boolean(row.archived)
|
|
432
432
|
};
|
|
433
433
|
}
|
|
434
|
+
update(id, data) {
|
|
435
|
+
if (!this.db)
|
|
436
|
+
this.initialize();
|
|
437
|
+
const existing = this.db.prepare('SELECT * FROM long_term_memory WHERE id = ?').get(id);
|
|
438
|
+
if (!existing)
|
|
439
|
+
return null;
|
|
440
|
+
const setClauses = ['updated_at = @updated_at', 'version = version + 1'];
|
|
441
|
+
const params = { id, updated_at: new Date().toISOString() };
|
|
442
|
+
if (data.importance !== undefined) {
|
|
443
|
+
setClauses.push('importance = @importance');
|
|
444
|
+
params.importance = data.importance;
|
|
445
|
+
}
|
|
446
|
+
if (data.category !== undefined) {
|
|
447
|
+
setClauses.push('category = @category');
|
|
448
|
+
params.category = data.category;
|
|
449
|
+
}
|
|
450
|
+
if (data.summary !== undefined) {
|
|
451
|
+
setClauses.push('summary = @summary');
|
|
452
|
+
params.summary = data.summary;
|
|
453
|
+
}
|
|
454
|
+
if (data.details !== undefined) {
|
|
455
|
+
setClauses.push('details = @details');
|
|
456
|
+
params.details = data.details;
|
|
457
|
+
}
|
|
458
|
+
this.db.prepare(`UPDATE long_term_memory SET ${setClauses.join(', ')} WHERE id = @id`).run(params);
|
|
459
|
+
const updated = this.db.prepare('SELECT * FROM long_term_memory WHERE id = ?').get(id);
|
|
460
|
+
return updated ? this.mapRowToRecord(updated) : null;
|
|
461
|
+
}
|
|
434
462
|
archiveMemory(id) {
|
|
435
463
|
if (!this.db)
|
|
436
464
|
this.initialize();
|