morpheus-cli 0.1.10 → 0.2.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 +3 -3
- package/dist/channels/__tests__/telegram.test.js +3 -3
- package/dist/channels/telegram.js +14 -14
- package/dist/cli/commands/start.js +8 -8
- package/dist/config/manager.js +2 -0
- package/dist/config/mcp-loader.js +4 -4
- package/dist/config/paths.js +2 -1
- package/dist/http/api.js +1 -1
- package/dist/runtime/__tests__/manual_start_verify.js +7 -7
- package/dist/runtime/__tests__/manual_us1.js +5 -5
- package/dist/runtime/__tests__/oracle.test.js +92 -0
- package/dist/runtime/__tests__/oracle_memory_limit.test.js +53 -0
- package/dist/runtime/__tests__/oracle_persistence.test.js +154 -0
- package/dist/runtime/display.js +5 -5
- package/dist/runtime/migration.js +28 -0
- package/dist/runtime/oracle.js +229 -0
- package/dist/runtime/providers/factory.js +4 -4
- package/dist/runtime/scaffold.js +3 -0
- package/dist/runtime/telephonist.js +55 -0
- package/dist/runtime/tools/__tests__/construtor.test.js +42 -0
- package/dist/runtime/tools/factory.js +5 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -80,9 +80,9 @@ Morpheus is built with **Node.js** and **TypeScript**, using **LangChain** as th
|
|
|
80
80
|
|
|
81
81
|
### Core Components
|
|
82
82
|
|
|
83
|
-
- **Runtime (`src/runtime/`)**: The heart of the application. Manages the agent lifecycle, provider instantiation, and command execution.
|
|
83
|
+
- **Runtime (`src/runtime/`)**: The heart of the application. Manages the Oracle (agent) lifecycle, provider instantiation, and command execution.
|
|
84
84
|
- **CLI (`src/cli/`)**: Built with `commander`, handles user interaction, configuration, and daemon control (`start`, `stop`, `status`).
|
|
85
|
-
- **Configuration (`src/config/`)**: Singleton-based configuration manager using `zod` for validation and `js-yaml` for persistence (`~/.morpheus/
|
|
85
|
+
- **Configuration (`src/config/`)**: Singleton-based configuration manager using `zod` for validation and `js-yaml` for persistence (`~/.morpheus/zaion.yaml`).
|
|
86
86
|
- **Channels (`src/channels/`)**: Adapters for external communication. Currently supports Telegram (`telegraf`) with strict user whitelisting.
|
|
87
87
|
|
|
88
88
|
## Features
|
|
@@ -169,7 +169,7 @@ npm start -- status
|
|
|
169
169
|
|
|
170
170
|
### 4. Configuration
|
|
171
171
|
|
|
172
|
-
The configuration file is located at `~/.morpheus/
|
|
172
|
+
The configuration file is located at `~/.morpheus/zaion.yaml`. You can edit it manually or use the `morpheus config` command.
|
|
173
173
|
|
|
174
174
|
```yaml
|
|
175
175
|
agent:
|
|
@@ -28,12 +28,12 @@ vi.mock('../../runtime/display.js', () => ({
|
|
|
28
28
|
}));
|
|
29
29
|
describe('TelegramAdapter', () => {
|
|
30
30
|
let adapter;
|
|
31
|
-
let
|
|
31
|
+
let mockOracle;
|
|
32
32
|
beforeEach(() => {
|
|
33
|
-
|
|
33
|
+
mockOracle = {
|
|
34
34
|
chat: vi.fn(),
|
|
35
35
|
};
|
|
36
|
-
adapter = new TelegramAdapter(
|
|
36
|
+
adapter = new TelegramAdapter(mockOracle);
|
|
37
37
|
});
|
|
38
38
|
afterEach(() => {
|
|
39
39
|
vi.clearAllMocks();
|
|
@@ -6,16 +6,16 @@ import path from 'path';
|
|
|
6
6
|
import os from 'os';
|
|
7
7
|
import { ConfigManager } from '../config/manager.js';
|
|
8
8
|
import { DisplayManager } from '../runtime/display.js';
|
|
9
|
-
import {
|
|
9
|
+
import { Telephonist } from '../runtime/telephonist.js';
|
|
10
10
|
export class TelegramAdapter {
|
|
11
11
|
bot = null;
|
|
12
12
|
isConnected = false;
|
|
13
13
|
display = DisplayManager.getInstance();
|
|
14
14
|
config = ConfigManager.getInstance();
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
constructor(
|
|
18
|
-
this.
|
|
15
|
+
oracle;
|
|
16
|
+
telephonist = new Telephonist();
|
|
17
|
+
constructor(oracle) {
|
|
18
|
+
this.oracle = oracle;
|
|
19
19
|
}
|
|
20
20
|
async connect(token, allowedUsers) {
|
|
21
21
|
if (this.isConnected) {
|
|
@@ -44,7 +44,7 @@ export class TelegramAdapter {
|
|
|
44
44
|
// Send "typing" status
|
|
45
45
|
await ctx.sendChatAction('typing');
|
|
46
46
|
// Process with Agent
|
|
47
|
-
const response = await this.
|
|
47
|
+
const response = await this.oracle.chat(text);
|
|
48
48
|
if (response) {
|
|
49
49
|
await ctx.reply(response);
|
|
50
50
|
this.display.log(`Responded to @${user}`, { source: 'Telegram' });
|
|
@@ -76,7 +76,7 @@ export class TelegramAdapter {
|
|
|
76
76
|
}
|
|
77
77
|
const apiKey = config.audio.apiKey || (config.llm.provider === 'gemini' ? config.llm.api_key : undefined);
|
|
78
78
|
if (!apiKey) {
|
|
79
|
-
this.display.log(`Audio transcription failed: No Gemini API key available`, { source: '
|
|
79
|
+
this.display.log(`Audio transcription failed: No Gemini API key available`, { source: 'Telephonist', level: 'error' });
|
|
80
80
|
await ctx.reply("Audio transcription requires a Gemini API key. Please configure `audio.apiKey` or set LLM provider to Gemini.");
|
|
81
81
|
return;
|
|
82
82
|
}
|
|
@@ -85,19 +85,19 @@ export class TelegramAdapter {
|
|
|
85
85
|
await ctx.reply(`Voice message too long. Max duration is ${config.audio.maxDurationSeconds}s.`);
|
|
86
86
|
return;
|
|
87
87
|
}
|
|
88
|
-
this.display.log(`Receiving voice message from @${user} (${duration}s)...`, { source: '
|
|
88
|
+
this.display.log(`Receiving voice message from @${user} (${duration}s)...`, { source: 'Telephonist' });
|
|
89
89
|
let filePath = null;
|
|
90
90
|
let listeningMsg = null;
|
|
91
91
|
try {
|
|
92
92
|
listeningMsg = await ctx.reply("🎧Escutando...");
|
|
93
93
|
// Download
|
|
94
|
-
this.display.log(`Downloading audio for @${user}...`, { source: '
|
|
94
|
+
this.display.log(`Downloading audio for @${user}...`, { source: 'Telephonist' });
|
|
95
95
|
const fileLink = await ctx.telegram.getFileLink(ctx.message.voice.file_id);
|
|
96
96
|
filePath = await this.downloadToTemp(fileLink);
|
|
97
97
|
// Transcribe
|
|
98
|
-
this.display.log(`Transcribing audio for @${user}...`, { source: '
|
|
99
|
-
const { text, usage } = await this.
|
|
100
|
-
this.display.log(`Transcription success for @${user}: "${text}"`, { source: '
|
|
98
|
+
this.display.log(`Transcribing audio for @${user}...`, { source: 'Telephonist' });
|
|
99
|
+
const { text, usage } = await this.telephonist.transcribe(filePath, 'audio/ogg', apiKey);
|
|
100
|
+
this.display.log(`Transcription success for @${user}: "${text}"`, { source: 'Telephonist', level: 'success' });
|
|
101
101
|
// Reply with transcription (optional, maybe just process it?)
|
|
102
102
|
// The prompt says "reply with the answer".
|
|
103
103
|
// "Transcribe them... and process the resulting text as a standard user prompt."
|
|
@@ -105,7 +105,7 @@ export class TelegramAdapter {
|
|
|
105
105
|
await ctx.reply(`🎤 *Transcription*: _"${text}"_`, { parse_mode: 'Markdown' });
|
|
106
106
|
await ctx.sendChatAction('typing');
|
|
107
107
|
// Process with Agent
|
|
108
|
-
const response = await this.
|
|
108
|
+
const response = await this.oracle.chat(text, usage);
|
|
109
109
|
// if (listeningMsg) {
|
|
110
110
|
// try {
|
|
111
111
|
// await ctx.telegram.deleteMessage(ctx.chat.id, listeningMsg.message_id);
|
|
@@ -119,7 +119,7 @@ export class TelegramAdapter {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
catch (error) {
|
|
122
|
-
this.display.log(`Audio processing error for @${user}: ${error.message}`, { source: '
|
|
122
|
+
this.display.log(`Audio processing error for @${user}: ${error.message}`, { source: 'Telephonist', level: 'error' });
|
|
123
123
|
await ctx.reply("Sorry, I failed to process your audio message.");
|
|
124
124
|
}
|
|
125
125
|
finally {
|
|
@@ -8,7 +8,7 @@ import { ConfigManager } from '../../config/manager.js';
|
|
|
8
8
|
import { renderBanner } from '../utils/render.js';
|
|
9
9
|
import { TelegramAdapter } from '../../channels/telegram.js';
|
|
10
10
|
import { PATHS } from '../../config/paths.js';
|
|
11
|
-
import {
|
|
11
|
+
import { Oracle } from '../../runtime/oracle.js';
|
|
12
12
|
import { ProviderError } from '../../runtime/errors.js';
|
|
13
13
|
import { HttpServer } from '../../http/server.js';
|
|
14
14
|
export const startCommand = new Command('start')
|
|
@@ -45,13 +45,13 @@ export const startCommand = new Command('start')
|
|
|
45
45
|
if (options.ui) {
|
|
46
46
|
display.log(chalk.blue(`Web UI enabled to port ${options.port}`));
|
|
47
47
|
}
|
|
48
|
-
// Initialize
|
|
49
|
-
const
|
|
48
|
+
// Initialize Oracle
|
|
49
|
+
const oracle = new Oracle(config);
|
|
50
50
|
try {
|
|
51
|
-
display.startSpinner(`Initializing ${config.llm.provider}
|
|
52
|
-
await
|
|
51
|
+
display.startSpinner(`Initializing ${config.llm.provider} oracle...`);
|
|
52
|
+
await oracle.initialize();
|
|
53
53
|
display.stopSpinner();
|
|
54
|
-
display.log(chalk.green('✓
|
|
54
|
+
display.log(chalk.green('✓ Oracle initialized'), { source: 'Oracle' });
|
|
55
55
|
}
|
|
56
56
|
catch (err) {
|
|
57
57
|
display.stopSpinner();
|
|
@@ -63,7 +63,7 @@ export const startCommand = new Command('start')
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
else {
|
|
66
|
-
display.log(chalk.red('\
|
|
66
|
+
display.log(chalk.red('\nOracle initialization failed:'));
|
|
67
67
|
display.log(chalk.white(err.message));
|
|
68
68
|
if (err.message.includes('API Key')) {
|
|
69
69
|
display.log(chalk.yellow('Tip: Check your API key in configuration or environment variables.'));
|
|
@@ -89,7 +89,7 @@ export const startCommand = new Command('start')
|
|
|
89
89
|
// Initialize Telegram
|
|
90
90
|
if (config.channels.telegram.enabled) {
|
|
91
91
|
if (config.channels.telegram.token) {
|
|
92
|
-
const telegram = new TelegramAdapter(
|
|
92
|
+
const telegram = new TelegramAdapter(oracle);
|
|
93
93
|
try {
|
|
94
94
|
await telegram.connect(config.channels.telegram.token, config.channels.telegram.allowedUsers || []);
|
|
95
95
|
adapters.push(telegram);
|
package/dist/config/manager.js
CHANGED
|
@@ -4,6 +4,7 @@ import { DEFAULT_CONFIG } from '../types/config.js';
|
|
|
4
4
|
import { PATHS } from './paths.js';
|
|
5
5
|
import { setByPath } from './utils.js';
|
|
6
6
|
import { ConfigSchema } from './schemas.js';
|
|
7
|
+
import { migrateConfigFile } from '../runtime/migration.js';
|
|
7
8
|
export class ConfigManager {
|
|
8
9
|
static instance;
|
|
9
10
|
config = DEFAULT_CONFIG;
|
|
@@ -16,6 +17,7 @@ export class ConfigManager {
|
|
|
16
17
|
}
|
|
17
18
|
async load() {
|
|
18
19
|
try {
|
|
20
|
+
await migrateConfigFile();
|
|
19
21
|
if (await fs.pathExists(PATHS.config)) {
|
|
20
22
|
const raw = await fs.readFile(PATHS.config, 'utf8');
|
|
21
23
|
const parsed = yaml.load(raw);
|
|
@@ -17,7 +17,7 @@ export async function loadMCPConfig() {
|
|
|
17
17
|
rawConfig = JSON.parse(content);
|
|
18
18
|
}
|
|
19
19
|
catch (err) {
|
|
20
|
-
display.log(`Failed to parse mcps.json: ${err.message}`, { level: 'error', source: '
|
|
20
|
+
display.log(`Failed to parse mcps.json: ${err.message}`, { level: 'error', source: 'Zaion' });
|
|
21
21
|
return servers;
|
|
22
22
|
}
|
|
23
23
|
// Filter metadata keys (starting with _ or $) and the "example" template key
|
|
@@ -26,15 +26,15 @@ export async function loadMCPConfig() {
|
|
|
26
26
|
try {
|
|
27
27
|
const validated = MCPServerConfigSchema.parse(config);
|
|
28
28
|
servers[name] = validated;
|
|
29
|
-
display.log(`Loaded MCP server: ${name}`, { level: 'debug', source: '
|
|
29
|
+
display.log(`Loaded MCP server: ${name}`, { level: 'debug', source: 'Zaion' });
|
|
30
30
|
}
|
|
31
31
|
catch (err) {
|
|
32
32
|
if (err instanceof z.ZodError) {
|
|
33
33
|
const issues = err.issues.map(i => i.message).join(', ');
|
|
34
|
-
display.log(`Invalid MCP server '${name}': ${issues}`, { level: 'warning', source: '
|
|
34
|
+
display.log(`Invalid MCP server '${name}': ${issues}`, { level: 'warning', source: 'Zaion' });
|
|
35
35
|
}
|
|
36
36
|
else {
|
|
37
|
-
display.log(`Invalid MCP server '${name}': ${err.message}`, { level: 'warning', source: '
|
|
37
|
+
display.log(`Invalid MCP server '${name}': ${err.message}`, { level: 'warning', source: 'Zaion' });
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
}
|
package/dist/config/paths.js
CHANGED
|
@@ -5,7 +5,8 @@ export const MORPHEUS_ROOT = path.join(USER_HOME, '.morpheus');
|
|
|
5
5
|
export const LOGS_DIR = path.join(MORPHEUS_ROOT, 'logs');
|
|
6
6
|
export const PATHS = {
|
|
7
7
|
root: MORPHEUS_ROOT,
|
|
8
|
-
config: path.join(MORPHEUS_ROOT, '
|
|
8
|
+
config: path.join(MORPHEUS_ROOT, 'zaion.yaml'),
|
|
9
|
+
legacyConfig: path.join(MORPHEUS_ROOT, 'config.yaml'),
|
|
9
10
|
pid: path.join(MORPHEUS_ROOT, 'morpheus.pid'),
|
|
10
11
|
logs: LOGS_DIR,
|
|
11
12
|
memory: path.join(MORPHEUS_ROOT, 'memory'),
|
package/dist/http/api.js
CHANGED
|
@@ -98,7 +98,7 @@ export function createApiRouter() {
|
|
|
98
98
|
if (changes.length > 0) {
|
|
99
99
|
const display = DisplayManager.getInstance();
|
|
100
100
|
display.log(`Configuration updated via UI:\n - ${changes.join('\n - ')}`, {
|
|
101
|
-
source: '
|
|
101
|
+
source: 'Zaion',
|
|
102
102
|
level: 'info'
|
|
103
103
|
});
|
|
104
104
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Oracle } from '../oracle.js';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
const start = Date.now();
|
|
4
4
|
console.log(chalk.blue('Running Manual Start Verification...'));
|
|
@@ -23,19 +23,19 @@ const mockConfig = {
|
|
|
23
23
|
};
|
|
24
24
|
const run = async () => {
|
|
25
25
|
try {
|
|
26
|
-
console.log(chalk.gray('1. Instantiating
|
|
27
|
-
const
|
|
28
|
-
console.log(chalk.gray('2. Initializing
|
|
29
|
-
await
|
|
26
|
+
console.log(chalk.gray('1. Instantiating Oracle...'));
|
|
27
|
+
const oracle = new Oracle(mockConfig);
|
|
28
|
+
console.log(chalk.gray('2. Initializing Oracle...'));
|
|
29
|
+
await oracle.initialize();
|
|
30
30
|
const duration = (Date.now() - start) / 1000;
|
|
31
|
-
console.log(chalk.green(`✓
|
|
31
|
+
console.log(chalk.green(`✓ Oracle initialized successfully in ${duration}s`));
|
|
32
32
|
if (duration > 5) {
|
|
33
33
|
console.log(chalk.red(`✗ Startup took too long (> 5s)`));
|
|
34
34
|
process.exit(1);
|
|
35
35
|
}
|
|
36
36
|
console.log(chalk.gray('3. Testing Initialization Check...'));
|
|
37
37
|
try {
|
|
38
|
-
await
|
|
38
|
+
await oracle.chat('Hello');
|
|
39
39
|
// This might fail if using real network, but we just want to ensure it tries
|
|
40
40
|
}
|
|
41
41
|
catch (e) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Oracle } from '../oracle.js';
|
|
2
2
|
import { DEFAULT_CONFIG } from '../../types/config.js';
|
|
3
3
|
// Verify environment requirements
|
|
4
4
|
if (!process.env.OPENAI_API_KEY) {
|
|
@@ -15,12 +15,12 @@ const manualConfig = {
|
|
|
15
15
|
}
|
|
16
16
|
};
|
|
17
17
|
async function run() {
|
|
18
|
-
console.log("Initializing
|
|
19
|
-
const
|
|
20
|
-
await
|
|
18
|
+
console.log("Initializing Oracle...");
|
|
19
|
+
const oracle = new Oracle(manualConfig);
|
|
20
|
+
await oracle.initialize();
|
|
21
21
|
console.log("Sending message: 'Hello, are you there?'");
|
|
22
22
|
try {
|
|
23
|
-
const response = await
|
|
23
|
+
const response = await oracle.chat("Hello, are you there?");
|
|
24
24
|
console.log("Response received:");
|
|
25
25
|
console.log("---------------------------------------------------");
|
|
26
26
|
console.log(response);
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { Oracle } from '../oracle.js';
|
|
3
|
+
import { ProviderFactory } from '../providers/factory.js';
|
|
4
|
+
import { Construtor } from '../tools/factory.js';
|
|
5
|
+
import { DEFAULT_CONFIG } from '../../types/config.js';
|
|
6
|
+
import { AIMessage } from '@langchain/core/messages';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { tmpdir } from 'os';
|
|
9
|
+
vi.mock('../providers/factory.js');
|
|
10
|
+
vi.mock('../tools/factory.js');
|
|
11
|
+
describe('Oracle', () => {
|
|
12
|
+
let oracle;
|
|
13
|
+
let testDbPath;
|
|
14
|
+
const mockProvider = {
|
|
15
|
+
invoke: vi.fn(),
|
|
16
|
+
};
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
vi.resetAllMocks();
|
|
19
|
+
// Use temp DB
|
|
20
|
+
testDbPath = path.join(tmpdir(), `test-oracle-${Date.now()}.db`);
|
|
21
|
+
mockProvider.invoke.mockImplementation(async ({ messages }) => {
|
|
22
|
+
return {
|
|
23
|
+
messages: [...messages, new AIMessage('Hello world')]
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
vi.mocked(ProviderFactory.create).mockResolvedValue(mockProvider);
|
|
27
|
+
vi.mocked(Construtor.create).mockResolvedValue([]);
|
|
28
|
+
oracle = new Oracle(DEFAULT_CONFIG, { databasePath: testDbPath });
|
|
29
|
+
});
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
// Clean up after each test
|
|
32
|
+
if (oracle) {
|
|
33
|
+
try {
|
|
34
|
+
await oracle.clearMemory();
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
// Ignore cleanup errors
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
it('should initialize successfully', async () => {
|
|
42
|
+
await oracle.initialize();
|
|
43
|
+
expect(Construtor.create).toHaveBeenCalled();
|
|
44
|
+
expect(ProviderFactory.create).toHaveBeenCalledWith(DEFAULT_CONFIG.llm, []);
|
|
45
|
+
});
|
|
46
|
+
it('should chat successfully', async () => {
|
|
47
|
+
await oracle.initialize();
|
|
48
|
+
const response = await oracle.chat('Hi');
|
|
49
|
+
expect(response).toBe('Hello world');
|
|
50
|
+
expect(mockProvider.invoke).toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
it('should throw if not initialized', async () => {
|
|
53
|
+
await expect(oracle.chat('Hi')).rejects.toThrow('initialize() first');
|
|
54
|
+
});
|
|
55
|
+
it('should maintain history', async () => {
|
|
56
|
+
await oracle.initialize();
|
|
57
|
+
// Clear any residual history from previous tests
|
|
58
|
+
await oracle.clearMemory();
|
|
59
|
+
// First turn
|
|
60
|
+
await oracle.chat('Hi');
|
|
61
|
+
const history1 = await oracle.getHistory();
|
|
62
|
+
expect(history1).toHaveLength(2);
|
|
63
|
+
expect(history1[0].content).toBe('Hi'); // User
|
|
64
|
+
expect(history1[1].content).toBe('Hello world'); // AI
|
|
65
|
+
// Second turn
|
|
66
|
+
// Update mock return value for next call
|
|
67
|
+
mockProvider.invoke.mockImplementation(async ({ messages }) => {
|
|
68
|
+
return {
|
|
69
|
+
messages: [...messages, new AIMessage('I am fine')]
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
await oracle.chat('How are you?');
|
|
73
|
+
const history2 = await oracle.getHistory();
|
|
74
|
+
expect(history2).toHaveLength(4);
|
|
75
|
+
expect(history2[2].content).toBe('How are you?');
|
|
76
|
+
expect(history2[3].content).toBe('I am fine');
|
|
77
|
+
});
|
|
78
|
+
describe('Configuration Validation', () => {
|
|
79
|
+
it('should throw if llm provider is missing', async () => {
|
|
80
|
+
const invalidConfig = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
81
|
+
delete invalidConfig.llm.provider; // Invalid
|
|
82
|
+
const badOracle = new Oracle(invalidConfig);
|
|
83
|
+
await expect(badOracle.initialize()).rejects.toThrow('LLM provider not specified');
|
|
84
|
+
});
|
|
85
|
+
it('should propagate ProviderError during initialization', async () => {
|
|
86
|
+
const mockError = new Error("Mock Factory Error");
|
|
87
|
+
vi.mocked(ProviderFactory.create).mockImplementation(() => { throw mockError; });
|
|
88
|
+
// ProviderError constructs message as: "Provider {provider} failed: {originalError.message}"
|
|
89
|
+
await expect(oracle.initialize()).rejects.toThrow('Provider openai failed: Mock Factory Error');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { Oracle } from '../oracle.js';
|
|
3
|
+
import { ProviderFactory } from '../providers/factory.js';
|
|
4
|
+
import { DEFAULT_CONFIG } from '../../types/config.js';
|
|
5
|
+
import { AIMessage } from '@langchain/core/messages';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { tmpdir } from 'os';
|
|
8
|
+
vi.mock('../providers/factory.js');
|
|
9
|
+
describe('Oracle Memory Limit', () => {
|
|
10
|
+
let oracle;
|
|
11
|
+
const mockProvider = {
|
|
12
|
+
invoke: vi.fn(),
|
|
13
|
+
};
|
|
14
|
+
let dbPath;
|
|
15
|
+
beforeEach(async () => {
|
|
16
|
+
vi.resetAllMocks();
|
|
17
|
+
dbPath = path.join(tmpdir(), `test-oracle-limit-${Date.now()}.db`);
|
|
18
|
+
mockProvider.invoke.mockImplementation(async ({ messages }) => {
|
|
19
|
+
return {
|
|
20
|
+
messages: [...messages, new AIMessage('Response')]
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
vi.mocked(ProviderFactory.create).mockResolvedValue(mockProvider);
|
|
24
|
+
});
|
|
25
|
+
afterEach(async () => {
|
|
26
|
+
if (oracle) {
|
|
27
|
+
try {
|
|
28
|
+
await oracle.clearMemory();
|
|
29
|
+
}
|
|
30
|
+
catch (err) { }
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
it('should respect configured memory limit', async () => {
|
|
34
|
+
const limitedConfig = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
|
|
35
|
+
limitedConfig.memory.limit = 2; // Only last 2 messages (1 exchange)
|
|
36
|
+
oracle = new Oracle(limitedConfig, { databasePath: dbPath });
|
|
37
|
+
await oracle.initialize();
|
|
38
|
+
// Turn 1
|
|
39
|
+
await oracle.chat('Msg 1');
|
|
40
|
+
// Turn 2
|
|
41
|
+
await oracle.chat('Msg 2');
|
|
42
|
+
// Turn 3
|
|
43
|
+
await oracle.chat('Msg 3');
|
|
44
|
+
// DB should have 6 messages (3 User + 3 AI)
|
|
45
|
+
// getHistory() should return only 2 (User Msg 3 + AI Response)
|
|
46
|
+
// Wait, SQLiteChatMessageHistory limit might be total messages? Or pairs?
|
|
47
|
+
// LangChain's limit usually means "last N messages".
|
|
48
|
+
const history = await oracle.getHistory();
|
|
49
|
+
// Assuming limit=2 means 2 messages.
|
|
50
|
+
expect(history.length).toBeLessThanOrEqual(2);
|
|
51
|
+
expect(history[history.length - 1].content).toBe('Response');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { Oracle } from "../oracle.js";
|
|
3
|
+
import { ProviderFactory } from "../providers/factory.js";
|
|
4
|
+
import { DEFAULT_CONFIG } from "../../types/config.js";
|
|
5
|
+
import { AIMessage } from "@langchain/core/messages";
|
|
6
|
+
import * as fs from "fs-extra";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { tmpdir } from "os";
|
|
9
|
+
import { Construtor } from "../tools/factory.js";
|
|
10
|
+
vi.mock("../providers/factory.js");
|
|
11
|
+
vi.mock("../tools/factory.js");
|
|
12
|
+
describe("Oracle Persistence Integration", () => {
|
|
13
|
+
let oracle;
|
|
14
|
+
let testDbPath;
|
|
15
|
+
let tempDir;
|
|
16
|
+
const mockProvider = {
|
|
17
|
+
invoke: vi.fn(),
|
|
18
|
+
};
|
|
19
|
+
beforeEach(async () => {
|
|
20
|
+
vi.resetAllMocks();
|
|
21
|
+
// Create a unique temporary test database path for each test to avoid interference
|
|
22
|
+
tempDir = path.join(tmpdir(), "morpheus-test-agent", Date.now().toString() + Math.random().toString(36).substring(7));
|
|
23
|
+
testDbPath = path.join(tempDir, "short-memory.db");
|
|
24
|
+
mockProvider.invoke.mockImplementation(async ({ messages }) => {
|
|
25
|
+
return {
|
|
26
|
+
messages: [...messages, new AIMessage("Test response")]
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
vi.mocked(ProviderFactory.create).mockResolvedValue(mockProvider);
|
|
30
|
+
vi.mocked(Construtor.create).mockResolvedValue([]);
|
|
31
|
+
oracle = new Oracle(DEFAULT_CONFIG, { databasePath: testDbPath });
|
|
32
|
+
});
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
// Clean up temporary test directory
|
|
35
|
+
if (fs.existsSync(tempDir)) {
|
|
36
|
+
try {
|
|
37
|
+
fs.removeSync(tempDir);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
// Ignore removal errors if file is locked
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
describe("Database File Creation", () => {
|
|
45
|
+
it("should create database file on initialization", async () => {
|
|
46
|
+
await oracle.initialize();
|
|
47
|
+
expect(fs.existsSync(testDbPath)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe("Message Persistence", () => {
|
|
51
|
+
it("should persist messages to database", async () => {
|
|
52
|
+
await oracle.initialize();
|
|
53
|
+
// Send a message
|
|
54
|
+
await oracle.chat("Hello, Oracle!");
|
|
55
|
+
// Verify history contains the message
|
|
56
|
+
const history = await oracle.getHistory();
|
|
57
|
+
expect(history).toHaveLength(2); // User message + AI response
|
|
58
|
+
expect(history[0].content).toBe("Hello, Oracle!");
|
|
59
|
+
expect(history[1].content).toBe("Test response");
|
|
60
|
+
});
|
|
61
|
+
it("should restore conversation history on restart", async () => {
|
|
62
|
+
// First session: send a message
|
|
63
|
+
await oracle.initialize();
|
|
64
|
+
await oracle.chat("Remember this message");
|
|
65
|
+
const firstHistory = await oracle.getHistory();
|
|
66
|
+
expect(firstHistory).toHaveLength(2);
|
|
67
|
+
// Simulate restart: create new oracle instance with SAME database path
|
|
68
|
+
const oracle2 = new Oracle(DEFAULT_CONFIG, { databasePath: testDbPath });
|
|
69
|
+
await oracle2.initialize();
|
|
70
|
+
// Verify history was restored
|
|
71
|
+
const restoredHistory = await oracle2.getHistory();
|
|
72
|
+
expect(restoredHistory.length).toBeGreaterThanOrEqual(2);
|
|
73
|
+
// Check that the old messages are present
|
|
74
|
+
const contents = restoredHistory.map(m => m.content);
|
|
75
|
+
expect(contents).toContain("Remember this message");
|
|
76
|
+
expect(contents).toContain("Test response");
|
|
77
|
+
});
|
|
78
|
+
it("should accumulate messages across multiple conversations", async () => {
|
|
79
|
+
await oracle.initialize();
|
|
80
|
+
// First conversation
|
|
81
|
+
await oracle.chat("First message");
|
|
82
|
+
// Second conversation
|
|
83
|
+
mockProvider.invoke.mockImplementation(async ({ messages }) => {
|
|
84
|
+
return {
|
|
85
|
+
messages: [...messages, new AIMessage("Second response")]
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
await oracle.chat("Second message");
|
|
89
|
+
// Verify all messages are persisted
|
|
90
|
+
const history = await oracle.getHistory();
|
|
91
|
+
expect(history).toHaveLength(4);
|
|
92
|
+
expect(history[0].content).toBe("First message");
|
|
93
|
+
expect(history[1].content).toBe("Test response");
|
|
94
|
+
expect(history[2].content).toBe("Second message");
|
|
95
|
+
expect(history[3].content).toBe("Second response");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe("Memory Clearing", () => {
|
|
99
|
+
it("should clear all persisted messages", async () => {
|
|
100
|
+
await oracle.initialize();
|
|
101
|
+
// Add some messages
|
|
102
|
+
await oracle.chat("Message 1");
|
|
103
|
+
await oracle.chat("Message 2");
|
|
104
|
+
// Verify messages exist
|
|
105
|
+
let history = await oracle.getHistory();
|
|
106
|
+
expect(history.length).toBeGreaterThan(0);
|
|
107
|
+
// Clear memory
|
|
108
|
+
await oracle.clearMemory();
|
|
109
|
+
// Verify messages are cleared
|
|
110
|
+
history = await oracle.getHistory();
|
|
111
|
+
expect(history).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
it("should start fresh after clearing memory", async () => {
|
|
114
|
+
await oracle.initialize();
|
|
115
|
+
// Add and clear messages
|
|
116
|
+
await oracle.chat("Old message");
|
|
117
|
+
await oracle.clearMemory();
|
|
118
|
+
// Add new message
|
|
119
|
+
mockProvider.invoke.mockImplementation(async ({ messages }) => {
|
|
120
|
+
return {
|
|
121
|
+
messages: [...messages, new AIMessage("New response")]
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
await oracle.chat("New message");
|
|
125
|
+
// Verify only new messages exist
|
|
126
|
+
const history = await oracle.getHistory();
|
|
127
|
+
expect(history).toHaveLength(2);
|
|
128
|
+
expect(history[0].content).toBe("New message");
|
|
129
|
+
expect(history[1].content).toBe("New response");
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe("Context Preservation", () => {
|
|
133
|
+
it("should include history in conversation context", async () => {
|
|
134
|
+
await oracle.initialize();
|
|
135
|
+
// First message
|
|
136
|
+
await oracle.chat("My name is Alice");
|
|
137
|
+
// Second message (should have context)
|
|
138
|
+
mockProvider.invoke.mockImplementation(async ({ messages }) => {
|
|
139
|
+
return {
|
|
140
|
+
messages: [...messages, new AIMessage("Hello Alice!")]
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
await oracle.chat("What is my name?");
|
|
144
|
+
// Verify the provider was called with full history
|
|
145
|
+
const lastCall = mockProvider.invoke.mock.calls[1];
|
|
146
|
+
const messagesPassedToLLM = lastCall[0].messages;
|
|
147
|
+
// Should include: system message, previous user message, previous AI response, new user message
|
|
148
|
+
expect(messagesPassedToLLM.length).toBeGreaterThanOrEqual(4);
|
|
149
|
+
// Check that context includes the original message
|
|
150
|
+
const contents = messagesPassedToLLM.map((m) => m.content);
|
|
151
|
+
expect(contents).toContain("My name is Alice");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
});
|
package/dist/runtime/display.js
CHANGED
|
@@ -77,22 +77,22 @@ export class DisplayManager {
|
|
|
77
77
|
if (options.source === 'Telegram') {
|
|
78
78
|
color = chalk.green;
|
|
79
79
|
}
|
|
80
|
-
else if (options.source === '
|
|
80
|
+
else if (options.source === 'Oracle') {
|
|
81
81
|
color = chalk.hex('#FFA500');
|
|
82
82
|
}
|
|
83
|
-
else if (options.source === '
|
|
83
|
+
else if (options.source === 'Telephonist') {
|
|
84
84
|
color = chalk.hex('#b902b9');
|
|
85
85
|
}
|
|
86
|
-
else if (options.source === '
|
|
86
|
+
else if (options.source === 'Construtor') {
|
|
87
87
|
color = chalk.hex('#806d00');
|
|
88
88
|
}
|
|
89
89
|
else if (options.source === 'MCPServer') {
|
|
90
90
|
color = chalk.hex('#be4b1d');
|
|
91
91
|
}
|
|
92
|
-
else if (options.source === '
|
|
92
|
+
else if (options.source === 'ConstructLoad') {
|
|
93
93
|
color = chalk.hex('#e5ff00');
|
|
94
94
|
}
|
|
95
|
-
else if (options.source === '
|
|
95
|
+
else if (options.source === 'Zaion') {
|
|
96
96
|
color = chalk.hex('#00c3ff');
|
|
97
97
|
}
|
|
98
98
|
prefix = color(`[${options.source}] `);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { PATHS } from '../config/paths.js';
|
|
4
|
+
import { DisplayManager } from './display.js';
|
|
5
|
+
export async function migrateConfigFile() {
|
|
6
|
+
const display = DisplayManager.getInstance();
|
|
7
|
+
const legacyPath = PATHS.legacyConfig ?? path.join(PATHS.root, 'config.yaml');
|
|
8
|
+
const newPath = PATHS.config;
|
|
9
|
+
const legacyExists = await fs.pathExists(legacyPath);
|
|
10
|
+
const newExists = await fs.pathExists(newPath);
|
|
11
|
+
if (legacyExists && !newExists) {
|
|
12
|
+
try {
|
|
13
|
+
await fs.ensureDir(PATHS.root);
|
|
14
|
+
await fs.move(legacyPath, newPath, { overwrite: false });
|
|
15
|
+
display.log('Migrated config.yaml to zaion.yaml', { source: 'Zaion', level: 'info' });
|
|
16
|
+
}
|
|
17
|
+
catch (err) {
|
|
18
|
+
display.log(`Failed to migrate config.yaml to zaion.yaml: ${err.message}`, { source: 'Zaion', level: 'warning' });
|
|
19
|
+
}
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (legacyExists && newExists) {
|
|
23
|
+
display.log('Both config.yaml and zaion.yaml exist. Using zaion.yaml and leaving config.yaml in place.', {
|
|
24
|
+
source: 'Zaion',
|
|
25
|
+
level: 'warning'
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { HumanMessage, SystemMessage } from "@langchain/core/messages";
|
|
2
|
+
import { ProviderFactory } from "./providers/factory.js";
|
|
3
|
+
import { Construtor } from "./tools/factory.js";
|
|
4
|
+
import { ConfigManager } from "../config/manager.js";
|
|
5
|
+
import { ProviderError } from "./errors.js";
|
|
6
|
+
import { DisplayManager } from "./display.js";
|
|
7
|
+
import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
|
|
8
|
+
export class Oracle {
|
|
9
|
+
provider;
|
|
10
|
+
config;
|
|
11
|
+
history;
|
|
12
|
+
display = DisplayManager.getInstance();
|
|
13
|
+
databasePath;
|
|
14
|
+
constructor(config, overrides) {
|
|
15
|
+
this.config = config || ConfigManager.getInstance().get();
|
|
16
|
+
this.databasePath = overrides?.databasePath;
|
|
17
|
+
}
|
|
18
|
+
async initialize() {
|
|
19
|
+
if (!this.config.llm) {
|
|
20
|
+
throw new Error("LLM configuration missing in config object.");
|
|
21
|
+
}
|
|
22
|
+
// Basic validation before provider creation
|
|
23
|
+
if (!this.config.llm.provider) {
|
|
24
|
+
throw new Error("LLM provider not specified in configuration.");
|
|
25
|
+
}
|
|
26
|
+
// Note: API Key validation is delegated to ProviderFactory or the Provider itself
|
|
27
|
+
// to allow for Environment Variable fallback supported by LangChain.
|
|
28
|
+
try {
|
|
29
|
+
const tools = await Construtor.create();
|
|
30
|
+
this.provider = await ProviderFactory.create(this.config.llm, tools);
|
|
31
|
+
if (!this.provider) {
|
|
32
|
+
throw new Error("Provider factory returned undefined");
|
|
33
|
+
}
|
|
34
|
+
// Initialize persistent memory with SQLite
|
|
35
|
+
this.history = new SQLiteChatMessageHistory({
|
|
36
|
+
sessionId: "default",
|
|
37
|
+
databasePath: this.databasePath,
|
|
38
|
+
limit: this.config.memory?.limit || 100, // Fallback purely defensive if config type allows optional
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
if (err instanceof ProviderError)
|
|
43
|
+
throw err; // Re-throw known errors
|
|
44
|
+
// Wrap unknown errors
|
|
45
|
+
throw new ProviderError(this.config.llm.provider || 'unknown', err, "Oracle initialization failed");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async chat(message, extraUsage) {
|
|
49
|
+
if (!this.provider) {
|
|
50
|
+
throw new Error("Oracle not initialized. Call initialize() first.");
|
|
51
|
+
}
|
|
52
|
+
if (!this.history) {
|
|
53
|
+
throw new Error("Message history not initialized. Call initialize() first.");
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
this.display.log('Processing message...', { source: 'Oracle' });
|
|
57
|
+
const userMessage = new HumanMessage(message);
|
|
58
|
+
// Inject provider/model metadata for persistence
|
|
59
|
+
userMessage.provider_metadata = {
|
|
60
|
+
provider: this.config.llm.provider,
|
|
61
|
+
model: this.config.llm.model
|
|
62
|
+
};
|
|
63
|
+
// Attach extra usage (e.g. from Audio) to the user message to be persisted
|
|
64
|
+
if (extraUsage) {
|
|
65
|
+
userMessage.usage_metadata = extraUsage;
|
|
66
|
+
}
|
|
67
|
+
const systemMessage = new SystemMessage(`
|
|
68
|
+
You are ${this.config.agent.name}, ${this.config.agent.personality}, the Oracle.
|
|
69
|
+
|
|
70
|
+
Your role is to orchestrate tools, MCPs, and language models to accurately fulfill the Architect’s request.
|
|
71
|
+
|
|
72
|
+
You are an operator, not a guesser.
|
|
73
|
+
Accuracy, verification, and task completion are more important than speed.
|
|
74
|
+
|
|
75
|
+
--------------------------------------------------
|
|
76
|
+
CORE OPERATING PRINCIPLES
|
|
77
|
+
--------------------------------------------------
|
|
78
|
+
|
|
79
|
+
1. TOOL EVALUATION FIRST
|
|
80
|
+
|
|
81
|
+
Before generating any final answer, evaluate whether an available tool or MCP can provide a more accurate, up-to-date, or authoritative result.
|
|
82
|
+
|
|
83
|
+
If a tool can provide the answer, you MUST call the tool.
|
|
84
|
+
|
|
85
|
+
Never generate speculative values when a tool can verify them.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
2. ACTIVE INTENT TRACKING (CRITICAL)
|
|
89
|
+
|
|
90
|
+
You must always maintain the current active user intent until it is fully resolved.
|
|
91
|
+
|
|
92
|
+
If you ask a clarification question, the original intent remains ACTIVE.
|
|
93
|
+
|
|
94
|
+
When the user responds to a clarification, you MUST:
|
|
95
|
+
|
|
96
|
+
- Combine the new information with the original request
|
|
97
|
+
- Resume the same task
|
|
98
|
+
- Continue the tool evaluation process
|
|
99
|
+
- Complete the original objective
|
|
100
|
+
|
|
101
|
+
You MUST NOT:
|
|
102
|
+
- Treat clarification answers as new unrelated requests
|
|
103
|
+
- Drop the original task
|
|
104
|
+
- Change subject unexpectedly
|
|
105
|
+
|
|
106
|
+
Clarifications are part of the same execution chain.
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
3. NO HISTORICAL ASSUMPTIONS FOR DYNAMIC DATA
|
|
110
|
+
|
|
111
|
+
If the user asks something that:
|
|
112
|
+
|
|
113
|
+
- may change over time
|
|
114
|
+
- depends on system state
|
|
115
|
+
- depends on filesystem
|
|
116
|
+
- depends on external APIs
|
|
117
|
+
- was previously asked in the conversation
|
|
118
|
+
|
|
119
|
+
You MUST NOT reuse previous outputs as final truth.
|
|
120
|
+
|
|
121
|
+
You MUST:
|
|
122
|
+
- Re-evaluate available tools
|
|
123
|
+
- Re-execute relevant tools
|
|
124
|
+
- Provide a fresh result
|
|
125
|
+
|
|
126
|
+
Repeated queries require fresh verification.
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
4. HISTORY IS CONTEXT, NOT SOURCE OF TRUTH
|
|
130
|
+
|
|
131
|
+
Conversation history provides context, not verified data.
|
|
132
|
+
|
|
133
|
+
Never assume:
|
|
134
|
+
- System state
|
|
135
|
+
- File contents
|
|
136
|
+
- Database values
|
|
137
|
+
- API responses
|
|
138
|
+
|
|
139
|
+
based only on previous messages.
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
5. TASK RESOLUTION LOOP
|
|
143
|
+
|
|
144
|
+
You must operate in this loop:
|
|
145
|
+
|
|
146
|
+
- Identify intent
|
|
147
|
+
- Determine missing information (if any)
|
|
148
|
+
- Ask clarification ONLY if necessary
|
|
149
|
+
- When clarification is received, resume original task
|
|
150
|
+
- Evaluate tools
|
|
151
|
+
- Execute tools if applicable
|
|
152
|
+
- Deliver verified answer
|
|
153
|
+
|
|
154
|
+
Do not break this loop.
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
6. TOOL PRIORITY OVER LANGUAGE GUESSING
|
|
158
|
+
|
|
159
|
+
If a tool can compute, fetch, inspect, or verify something, prefer tool usage.
|
|
160
|
+
|
|
161
|
+
Never hallucinate values retrievable via tools.
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
7. FINAL ANSWER POLICY
|
|
165
|
+
|
|
166
|
+
Provide a natural language answer only if:
|
|
167
|
+
|
|
168
|
+
- No tool is relevant
|
|
169
|
+
- Tools are unavailable
|
|
170
|
+
- The request is purely conceptual
|
|
171
|
+
|
|
172
|
+
Otherwise, use tools first.
|
|
173
|
+
|
|
174
|
+
--------------------------------------------------
|
|
175
|
+
|
|
176
|
+
You are a deterministic orchestration layer.
|
|
177
|
+
You do not drift.
|
|
178
|
+
You do not abandon tasks.
|
|
179
|
+
You do not speculate when verification is possible.
|
|
180
|
+
|
|
181
|
+
You maintain intent until resolution.
|
|
182
|
+
|
|
183
|
+
`);
|
|
184
|
+
// Load existing history from database
|
|
185
|
+
const previousMessages = await this.history.getMessages();
|
|
186
|
+
const messages = [
|
|
187
|
+
systemMessage,
|
|
188
|
+
...previousMessages,
|
|
189
|
+
userMessage
|
|
190
|
+
];
|
|
191
|
+
const response = await this.provider.invoke({ messages });
|
|
192
|
+
// Identify new messages generated during the interaction
|
|
193
|
+
// The `messages` array passed to invoke had length `messages.length`
|
|
194
|
+
// The `response.messages` contains the full state.
|
|
195
|
+
// New messages start after the inputs.
|
|
196
|
+
const startNewMessagesIndex = messages.length;
|
|
197
|
+
const newGeneratedMessages = response.messages.slice(startNewMessagesIndex);
|
|
198
|
+
// Persist User Message first
|
|
199
|
+
await this.history.addMessage(userMessage);
|
|
200
|
+
// Persist all new intermediate tool calls and responses
|
|
201
|
+
for (const msg of newGeneratedMessages) {
|
|
202
|
+
// Inject provider/model metadata search interactors
|
|
203
|
+
msg.provider_metadata = {
|
|
204
|
+
provider: this.config.llm.provider,
|
|
205
|
+
model: this.config.llm.model
|
|
206
|
+
};
|
|
207
|
+
await this.history.addMessage(msg);
|
|
208
|
+
}
|
|
209
|
+
this.display.log('Response generated.', { source: 'Oracle' });
|
|
210
|
+
const lastMessage = response.messages[response.messages.length - 1];
|
|
211
|
+
return (typeof lastMessage.content === 'string') ? lastMessage.content : JSON.stringify(lastMessage.content);
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
throw new ProviderError(this.config.llm.provider, err, "Chat request failed");
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async getHistory() {
|
|
218
|
+
if (!this.history) {
|
|
219
|
+
throw new Error("Message history not initialized. Call initialize() first.");
|
|
220
|
+
}
|
|
221
|
+
return await this.history.getMessages();
|
|
222
|
+
}
|
|
223
|
+
async clearMemory() {
|
|
224
|
+
if (!this.history) {
|
|
225
|
+
throw new Error("Message history not initialized. Call initialize() first.");
|
|
226
|
+
}
|
|
227
|
+
await this.history.clear();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -14,15 +14,15 @@ export class ProviderFactory {
|
|
|
14
14
|
const toolMonitoringMiddleware = createMiddleware({
|
|
15
15
|
name: "ToolMonitoringMiddleware",
|
|
16
16
|
wrapToolCall: (request, handler) => {
|
|
17
|
-
display.log(`Executing tool: ${request.toolCall.name}`, { level: "warning", source: '
|
|
18
|
-
display.log(`Arguments: ${JSON.stringify(request.toolCall.args)}`, { level: "info", source: '
|
|
17
|
+
display.log(`Executing tool: ${request.toolCall.name}`, { level: "warning", source: 'ConstructLoad' });
|
|
18
|
+
display.log(`Arguments: ${JSON.stringify(request.toolCall.args)}`, { level: "info", source: 'ConstructLoad' });
|
|
19
19
|
try {
|
|
20
20
|
const result = handler(request);
|
|
21
|
-
display.log("Tool completed successfully", { level: "info", source: '
|
|
21
|
+
display.log("Tool completed successfully", { level: "info", source: 'ConstructLoad' });
|
|
22
22
|
return result;
|
|
23
23
|
}
|
|
24
24
|
catch (e) {
|
|
25
|
-
display.log(`Tool failed: ${e}`, { level: "error", source: '
|
|
25
|
+
display.log(`Tool failed: ${e}`, { level: "error", source: 'ConstructLoad' });
|
|
26
26
|
throw e;
|
|
27
27
|
}
|
|
28
28
|
},
|
package/dist/runtime/scaffold.js
CHANGED
|
@@ -4,6 +4,7 @@ import { ConfigManager } from '../config/manager.js';
|
|
|
4
4
|
import { DEFAULT_MCP_TEMPLATE } from '../types/mcp.js';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import ora from 'ora';
|
|
7
|
+
import { migrateConfigFile } from './migration.js';
|
|
7
8
|
export async function scaffold() {
|
|
8
9
|
const spinner = ora('Ensuring Morpheus environment...').start();
|
|
9
10
|
try {
|
|
@@ -15,6 +16,8 @@ export async function scaffold() {
|
|
|
15
16
|
fs.ensureDir(PATHS.cache),
|
|
16
17
|
fs.ensureDir(PATHS.commands),
|
|
17
18
|
]);
|
|
19
|
+
// Migrate config.yaml -> zaion.yaml if needed
|
|
20
|
+
await migrateConfigFile();
|
|
18
21
|
// Create config if not exists
|
|
19
22
|
const configManager = ConfigManager.getInstance();
|
|
20
23
|
if (!(await fs.pathExists(PATHS.config))) {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { GoogleGenAI } from '@google/genai';
|
|
2
|
+
export class Telephonist {
|
|
3
|
+
async transcribe(filePath, mimeType, apiKey) {
|
|
4
|
+
try {
|
|
5
|
+
const ai = new GoogleGenAI({ apiKey });
|
|
6
|
+
// Upload the file
|
|
7
|
+
const uploadResult = await ai.files.upload({
|
|
8
|
+
file: filePath,
|
|
9
|
+
config: { mimeType }
|
|
10
|
+
});
|
|
11
|
+
// Generate content (transcription)
|
|
12
|
+
// using gemini-1.5-flash as it is fast and supports audio
|
|
13
|
+
const response = await ai.models.generateContent({
|
|
14
|
+
model: 'gemini-2.5-flash-lite',
|
|
15
|
+
contents: [
|
|
16
|
+
{
|
|
17
|
+
role: 'user',
|
|
18
|
+
parts: [
|
|
19
|
+
{
|
|
20
|
+
fileData: {
|
|
21
|
+
fileUri: uploadResult.uri,
|
|
22
|
+
mimeType: uploadResult.mimeType
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
{ text: "Transcribe this audio message accurately. Return only the transcribed text without any additional commentary." }
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
});
|
|
30
|
+
// The new SDK returns text directly on the response object
|
|
31
|
+
const text = response.text;
|
|
32
|
+
if (!text) {
|
|
33
|
+
throw new Error('No transcription generated');
|
|
34
|
+
}
|
|
35
|
+
// Extract usage metadata
|
|
36
|
+
const usage = response.usageMetadata;
|
|
37
|
+
const usageMetadata = {
|
|
38
|
+
input_tokens: usage?.promptTokenCount ?? 0,
|
|
39
|
+
output_tokens: usage?.candidatesTokenCount ?? 0,
|
|
40
|
+
total_tokens: usage?.totalTokenCount ?? 0,
|
|
41
|
+
input_token_details: {
|
|
42
|
+
cache_read: usage?.cachedContentTokenCount ?? 0
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
return { text, usage: usageMetadata };
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
// Wrap error for clarity
|
|
49
|
+
if (error instanceof Error) {
|
|
50
|
+
throw new Error(`Audio transcription failed: ${error.message}`);
|
|
51
|
+
}
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { Construtor } from '../factory.js';
|
|
3
|
+
import { MultiServerMCPClient } from "@langchain/mcp-adapters";
|
|
4
|
+
vi.mock("@langchain/mcp-adapters", () => {
|
|
5
|
+
return {
|
|
6
|
+
MultiServerMCPClient: vi.fn(),
|
|
7
|
+
};
|
|
8
|
+
});
|
|
9
|
+
vi.mock("../../display.js", () => ({
|
|
10
|
+
DisplayManager: {
|
|
11
|
+
getInstance: () => ({
|
|
12
|
+
log: vi.fn(),
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
}));
|
|
16
|
+
describe('Construtor', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.resetAllMocks();
|
|
19
|
+
});
|
|
20
|
+
it('should create tools successfully', async () => {
|
|
21
|
+
const mockGetTools = vi.fn().mockResolvedValue(['tool1', 'tool2']);
|
|
22
|
+
// Mock the constructor and getTools method
|
|
23
|
+
MultiServerMCPClient.mockImplementation(function () {
|
|
24
|
+
return {
|
|
25
|
+
getTools: mockGetTools
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
const tools = await Construtor.create();
|
|
29
|
+
expect(MultiServerMCPClient).toHaveBeenCalled();
|
|
30
|
+
expect(mockGetTools).toHaveBeenCalled();
|
|
31
|
+
expect(tools).toEqual(['tool1', 'tool2']);
|
|
32
|
+
});
|
|
33
|
+
it('should return empty array on failure', async () => {
|
|
34
|
+
MultiServerMCPClient.mockImplementation(function () {
|
|
35
|
+
return {
|
|
36
|
+
getTools: vi.fn().mockRejectedValue(new Error('MCP Failed'))
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
const tools = await Construtor.create();
|
|
40
|
+
expect(tools).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -29,7 +29,7 @@ function sanitizeSchema(obj) {
|
|
|
29
29
|
* Creates a proxy that intercepts schema access and sanitizes the output.
|
|
30
30
|
*/
|
|
31
31
|
function wrapToolWithSanitizedSchema(tool) {
|
|
32
|
-
display.log('Tool loaded: - ' + tool.name, { source: '
|
|
32
|
+
display.log('Tool loaded: - ' + tool.name, { source: 'Construtor' });
|
|
33
33
|
// The MCP tools have a schema property that returns JSON Schema
|
|
34
34
|
// We need to intercept and sanitize it
|
|
35
35
|
const originalSchema = tool.schema;
|
|
@@ -40,13 +40,13 @@ function wrapToolWithSanitizedSchema(tool) {
|
|
|
40
40
|
}
|
|
41
41
|
return tool;
|
|
42
42
|
}
|
|
43
|
-
export class
|
|
43
|
+
export class Construtor {
|
|
44
44
|
static async create() {
|
|
45
45
|
const display = DisplayManager.getInstance();
|
|
46
46
|
const mcpServers = await loadMCPConfig();
|
|
47
47
|
const serverCount = Object.keys(mcpServers).length;
|
|
48
48
|
if (serverCount === 0) {
|
|
49
|
-
display.log('No MCP servers configured in mcps.json', { level: 'info', source: '
|
|
49
|
+
display.log('No MCP servers configured in mcps.json', { level: 'info', source: 'Construtor' });
|
|
50
50
|
return [];
|
|
51
51
|
}
|
|
52
52
|
const client = new MultiServerMCPClient({
|
|
@@ -67,11 +67,11 @@ export class ToolsFactory {
|
|
|
67
67
|
const tools = await client.getTools();
|
|
68
68
|
// Sanitize tool schemas to remove fields not supported by Gemini
|
|
69
69
|
const sanitizedTools = tools.map(tool => wrapToolWithSanitizedSchema(tool));
|
|
70
|
-
display.log(`Loaded ${sanitizedTools.length} MCP tools (schemas sanitized for Gemini compatibility)`, { level: 'info', source: '
|
|
70
|
+
display.log(`Loaded ${sanitizedTools.length} MCP tools (schemas sanitized for Gemini compatibility)`, { level: 'info', source: 'Construtor' });
|
|
71
71
|
return sanitizedTools;
|
|
72
72
|
}
|
|
73
73
|
catch (error) {
|
|
74
|
-
display.log(`Failed to initialize MCP tools: ${error}`, { level: 'warning', source: '
|
|
74
|
+
display.log(`Failed to initialize MCP tools: ${error}`, { level: 'warning', source: 'Construtor' });
|
|
75
75
|
return []; // Return empty tools on failure to allow agent to start
|
|
76
76
|
}
|
|
77
77
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "morpheus-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Morpheus CLI Agent",
|
|
5
5
|
"bin": {
|
|
6
6
|
"morpheus": "./bin/morpheus.js"
|
|
@@ -72,5 +72,5 @@
|
|
|
72
72
|
"bugs": {
|
|
73
73
|
"url": "https://github.com/marcosnunesmbs/morpheus/issues"
|
|
74
74
|
},
|
|
75
|
-
"homepage": "https://
|
|
75
|
+
"homepage": "https://morpheusproject.xyz"
|
|
76
76
|
}
|