morpheus-cli 0.1.3 → 0.1.5

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 CHANGED
@@ -85,6 +85,16 @@ Morpheus is built with **Node.js** and **TypeScript**, using **LangChain** as th
85
85
  - **Configuration (`src/config/`)**: Singleton-based configuration manager using `zod` for validation and `js-yaml` for persistence (`~/.morpheus/config.yaml`).
86
86
  - **Channels (`src/channels/`)**: Adapters for external communication. Currently supports Telegram (`telegraf`) with strict user whitelisting.
87
87
 
88
+ ## Features
89
+
90
+ ### 🎙️ Audio Transcription (Telegram)
91
+ Send voice messages directly to the Telegram bot. Morpheus will:
92
+ 1. Transcribe the audio using **Google Gemini**.
93
+ 2. Process the text as a standard prompt.
94
+ 3. Reply with the answer.
95
+
96
+ *Requires a Google Gemini API Key.*
97
+
88
98
  ## Development Setup
89
99
 
90
100
  This guide is for developers contributing to the Morpheus codebase.
@@ -139,11 +149,19 @@ llm:
139
149
  model: "gpt-4-turbo"
140
150
  temperature: 0.7
141
151
  api_key: "sk-..."
152
+ memory:
153
+ limit: 100 # Number of messages to retain in context
142
154
  channels:
143
155
  telegram:
144
156
  enabled: true
145
157
  token: "YOUR_TELEGRAM_BOT_TOKEN"
146
158
  allowedUsers: ["123456789"] # Your Telegram User ID
159
+
160
+ # Audio Transcription Support
161
+ audio:
162
+ enabled: true
163
+ apiKey: "YOUR_GEMINI_API_KEY" # Optional if llm.provider is 'gemini'
164
+ maxDurationSeconds: 300
147
165
  ```
148
166
 
149
167
  ## Testing
@@ -1,11 +1,19 @@
1
1
  import { Telegraf } from 'telegraf';
2
+ import { message } from 'telegraf/filters';
2
3
  import chalk from 'chalk';
4
+ import fs from 'fs-extra';
5
+ import path from 'path';
6
+ import os from 'os';
7
+ import { ConfigManager } from '../config/manager.js';
3
8
  import { DisplayManager } from '../runtime/display.js';
9
+ import { AudioAgent } from '../runtime/audio-agent.js';
4
10
  export class TelegramAdapter {
5
11
  bot = null;
6
12
  isConnected = false;
7
13
  display = DisplayManager.getInstance();
14
+ config = ConfigManager.getInstance();
8
15
  agent;
16
+ audioAgent = new AudioAgent();
9
17
  constructor(agent) {
10
18
  this.agent = agent;
11
19
  }
@@ -52,6 +60,75 @@ export class TelegramAdapter {
52
60
  }
53
61
  }
54
62
  });
63
+ // Handle Voice Messages
64
+ this.bot.on(message('voice'), async (ctx) => {
65
+ const user = ctx.from.username || ctx.from.first_name;
66
+ const userId = ctx.from.id.toString();
67
+ const config = this.config.get();
68
+ // AUTH GUARD
69
+ if (!this.isAuthorized(userId, allowedUsers)) {
70
+ this.display.log(`Unauthorized audio attempt by @${user} (ID: ${userId})`, { source: 'Telegram', level: 'warning' });
71
+ return;
72
+ }
73
+ if (!config.audio.enabled) {
74
+ await ctx.reply("Audio transcription is currently disabled.");
75
+ return;
76
+ }
77
+ const apiKey = config.audio.apiKey || (config.llm.provider === 'gemini' ? config.llm.api_key : undefined);
78
+ if (!apiKey) {
79
+ this.display.log(`Audio transcription failed: No Gemini API key available`, { source: 'AgentAudio', level: 'error' });
80
+ await ctx.reply("Audio transcription requires a Gemini API key. Please configure `audio.apiKey` or set LLM provider to Gemini.");
81
+ return;
82
+ }
83
+ const duration = ctx.message.voice.duration;
84
+ if (duration > config.audio.maxDurationSeconds) {
85
+ await ctx.reply(`Voice message too long. Max duration is ${config.audio.maxDurationSeconds}s.`);
86
+ return;
87
+ }
88
+ this.display.log(`Receiving voice message from @${user} (${duration}s)...`, { source: 'AgentAudio' });
89
+ let filePath = null;
90
+ let listeningMsg = null;
91
+ try {
92
+ listeningMsg = await ctx.reply("🎧Escutando...");
93
+ // Download
94
+ this.display.log(`Downloading audio for @${user}...`, { source: 'AgentAudio' });
95
+ const fileLink = await ctx.telegram.getFileLink(ctx.message.voice.file_id);
96
+ filePath = await this.downloadToTemp(fileLink);
97
+ // Transcribe
98
+ this.display.log(`Transcribing audio for @${user}...`, { source: 'AgentAudio' });
99
+ const text = await this.audioAgent.transcribe(filePath, 'audio/ogg', apiKey);
100
+ this.display.log(`Transcription success for @${user}: "${text}"`, { source: 'AgentAudio', level: 'success' });
101
+ // Reply with transcription (optional, maybe just process it?)
102
+ // The prompt says "reply with the answer".
103
+ // "Transcribe them... and process the resulting text as a standard user prompt."
104
+ // So I should treat 'text' as if it was a text message.
105
+ await ctx.reply(`🎤 *Transcription*: _"${text}"_`, { parse_mode: 'Markdown' });
106
+ await ctx.sendChatAction('typing');
107
+ // Process with Agent
108
+ const response = await this.agent.chat(text);
109
+ // if (listeningMsg) {
110
+ // try {
111
+ // await ctx.telegram.deleteMessage(ctx.chat.id, listeningMsg.message_id);
112
+ // } catch (e) {
113
+ // // Ignore delete error
114
+ // }
115
+ // }
116
+ if (response) {
117
+ await ctx.reply(response);
118
+ this.display.log(`Responded to @${user} (via audio)`, { source: 'Telegram' });
119
+ }
120
+ }
121
+ catch (error) {
122
+ this.display.log(`Audio processing error for @${user}: ${error.message}`, { source: 'AgentAudio', level: 'error' });
123
+ await ctx.reply("Sorry, I failed to process your audio message.");
124
+ }
125
+ finally {
126
+ // Cleanup
127
+ if (filePath && await fs.pathExists(filePath)) {
128
+ await fs.unlink(filePath).catch(() => { });
129
+ }
130
+ }
131
+ });
55
132
  this.bot.launch().catch((err) => {
56
133
  if (this.isConnected) {
57
134
  this.display.log(`Telegram bot error: ${err}`, { source: 'Telegram', level: 'error' });
@@ -71,6 +148,17 @@ export class TelegramAdapter {
71
148
  isAuthorized(userId, allowedUsers) {
72
149
  return allowedUsers.includes(userId);
73
150
  }
151
+ async downloadToTemp(url, extension = '.ogg') {
152
+ const response = await fetch(url);
153
+ if (!response.ok)
154
+ throw new Error(`Failed to download audio: ${response.statusText}`);
155
+ const tmpDir = os.tmpdir();
156
+ const fileName = `morpheus-audio-${Date.now()}${extension}`;
157
+ const filePath = path.join(tmpDir, fileName);
158
+ const buffer = Buffer.from(await response.arrayBuffer());
159
+ await fs.writeFile(filePath, buffer);
160
+ return filePath;
161
+ }
74
162
  async disconnect() {
75
163
  if (!this.isConnected || !this.bot) {
76
164
  return;
@@ -10,17 +10,19 @@ export const initCommand = new Command('init')
10
10
  .action(async () => {
11
11
  const display = DisplayManager.getInstance();
12
12
  renderBanner();
13
+ const configManager = ConfigManager.getInstance();
14
+ const currentConfig = await configManager.load();
13
15
  // Ensure directory exists
14
16
  await scaffold();
15
17
  display.log(chalk.blue('Let\'s set up your Morpheus agent!'));
16
18
  try {
17
19
  const name = await input({
18
20
  message: 'Name your agent:',
19
- default: 'morpheus',
21
+ default: currentConfig.agent.name,
20
22
  });
21
23
  const personality = await input({
22
24
  message: 'Describe its personality:',
23
- default: 'helpful and concise',
25
+ default: currentConfig.agent.personality,
24
26
  });
25
27
  const provider = await select({
26
28
  message: 'Select LLM Provider:',
@@ -30,6 +32,7 @@ export const initCommand = new Command('init')
30
32
  { name: 'Ollama', value: 'ollama' },
31
33
  { name: 'Google Gemini', value: 'gemini' },
32
34
  ],
35
+ default: currentConfig.llm.provider,
33
36
  });
34
37
  let defaultModel = 'gpt-3.5-turbo';
35
38
  switch (provider) {
@@ -46,17 +49,23 @@ export const initCommand = new Command('init')
46
49
  defaultModel = 'gemini-pro';
47
50
  break;
48
51
  }
52
+ if (provider === currentConfig.llm.provider) {
53
+ defaultModel = currentConfig.llm.model;
54
+ }
49
55
  const model = await input({
50
56
  message: 'Enter Model Name:',
51
57
  default: defaultModel,
52
58
  });
53
59
  let apiKey;
60
+ const hasExistingKey = !!currentConfig.llm.api_key;
61
+ const apiKeyMessage = hasExistingKey
62
+ ? 'Enter API Key (leave empty to preserve existing, or if using env vars):'
63
+ : 'Enter API Key (leave empty if using env vars):';
54
64
  if (provider !== 'ollama') {
55
65
  apiKey = await password({
56
- message: 'Enter API Key (leave empty if using env vars):',
66
+ message: apiKeyMessage,
57
67
  });
58
68
  }
59
- const configManager = ConfigManager.getInstance();
60
69
  // Update config
61
70
  await configManager.set('agent.name', name);
62
71
  await configManager.set('agent.personality', personality);
@@ -65,33 +74,81 @@ export const initCommand = new Command('init')
65
74
  if (apiKey) {
66
75
  await configManager.set('llm.api_key', apiKey);
67
76
  }
77
+ // Audio Configuration
78
+ const audioEnabled = await confirm({
79
+ message: 'Enable Audio Transcription? (Requires Gemini)',
80
+ default: currentConfig.audio?.enabled || false,
81
+ });
82
+ let audioKey;
83
+ let finalAudioEnabled = audioEnabled;
84
+ if (audioEnabled) {
85
+ if (provider === 'gemini') {
86
+ display.log(chalk.gray('Using main Gemini API key for audio.'));
87
+ }
88
+ else {
89
+ const hasExistingAudioKey = !!currentConfig.audio?.apiKey;
90
+ const audioKeyMessage = hasExistingAudioKey
91
+ ? 'Enter Gemini API Key for Audio (leave empty to preserve existing):'
92
+ : 'Enter Gemini API Key for Audio:';
93
+ audioKey = await password({
94
+ message: audioKeyMessage,
95
+ });
96
+ // Check if we have a valid key (new or existing)
97
+ const effectiveKey = audioKey || currentConfig.audio?.apiKey;
98
+ if (!effectiveKey) {
99
+ display.log(chalk.yellow('Audio disabled: Missing Gemini API Key required when using non-Gemini LLM provider.'));
100
+ finalAudioEnabled = false;
101
+ }
102
+ }
103
+ }
104
+ await configManager.set('audio.enabled', finalAudioEnabled);
105
+ if (audioKey) {
106
+ await configManager.set('audio.apiKey', audioKey);
107
+ }
68
108
  // External Channels Configuration
69
109
  const configureChannels = await confirm({
70
110
  message: 'Do you want to configure external channels?',
71
- default: false,
111
+ default: currentConfig.channels.telegram?.enabled || false,
72
112
  });
73
113
  if (configureChannels) {
74
114
  const channels = await checkbox({
75
115
  message: 'Select channels to enable:',
76
116
  choices: [
77
- { name: 'Telegram', value: 'telegram' },
117
+ {
118
+ name: 'Telegram',
119
+ value: 'telegram',
120
+ checked: currentConfig.channels.telegram?.enabled || false
121
+ },
78
122
  ],
79
123
  });
80
124
  if (channels.includes('telegram')) {
81
125
  display.log(chalk.yellow('\n--- Telegram Configuration ---'));
82
126
  display.log(chalk.gray('1. Create a bot via @BotFather to get your token.'));
83
127
  display.log(chalk.gray('2. Get your User ID via @userinfobot.\n'));
128
+ const hasExistingToken = !!currentConfig.channels.telegram?.token;
84
129
  const token = await password({
85
- message: 'Enter Telegram Bot Token:',
86
- validate: (value) => value.length > 0 || 'Token is required.'
130
+ message: hasExistingToken
131
+ ? 'Enter Telegram Bot Token (leave empty to preserve existing):'
132
+ : 'Enter Telegram Bot Token:',
133
+ validate: (value) => {
134
+ if (value.length > 0)
135
+ return true;
136
+ if (hasExistingToken)
137
+ return true;
138
+ return 'Token is required.';
139
+ }
87
140
  });
141
+ const defaultUsers = currentConfig.channels.telegram?.allowedUsers?.join(', ') || '';
88
142
  const allowedUsersInput = await input({
89
143
  message: 'Enter Allowed User IDs (comma separated):',
144
+ default: defaultUsers,
90
145
  validate: (value) => value.length > 0 || 'At least one user ID is required for security.'
91
146
  });
92
147
  const allowedUsers = allowedUsersInput.split(',').map(id => id.trim()).filter(id => id.length > 0);
93
148
  await configManager.set('channels.telegram.enabled', true);
94
- await configManager.set('channels.telegram.token', token);
149
+ if (token) {
150
+ await configManager.set('channels.telegram.token', token);
151
+ }
95
152
  await configManager.set('channels.telegram.allowedUsers', allowedUsers);
96
153
  }
97
154
  }
@@ -130,7 +130,7 @@ export const startCommand = new Command('start')
130
130
  });
131
131
  }
132
132
  // Keep process alive (Mock Agent Loop)
133
- display.startSpinner('Agent active and listening... (Press ESC to stop)');
133
+ display.startSpinner('Agent active and listening... (Press ctrl+c to stop)');
134
134
  // Prevent node from exiting
135
135
  setInterval(() => {
136
136
  // Heartbeat or background tasks would go here
@@ -1,5 +1,11 @@
1
1
  import { z } from 'zod';
2
2
  import { DEFAULT_CONFIG } from '../types/config.js';
3
+ export const AudioConfigSchema = z.object({
4
+ enabled: z.boolean().default(DEFAULT_CONFIG.audio.enabled),
5
+ apiKey: z.string().optional(),
6
+ maxDurationSeconds: z.number().default(DEFAULT_CONFIG.audio.maxDurationSeconds),
7
+ supportedMimeTypes: z.array(z.string()).default(DEFAULT_CONFIG.audio.supportedMimeTypes),
8
+ });
3
9
  // Zod Schema matching MorpheusConfig interface
4
10
  export const ConfigSchema = z.object({
5
11
  agent: z.object({
@@ -12,6 +18,10 @@ export const ConfigSchema = z.object({
12
18
  temperature: z.number().min(0).max(1).default(DEFAULT_CONFIG.llm.temperature),
13
19
  api_key: z.string().optional(),
14
20
  }).default(DEFAULT_CONFIG.llm),
21
+ audio: AudioConfigSchema.default(DEFAULT_CONFIG.audio),
22
+ memory: z.object({
23
+ limit: z.number().int().positive().default(DEFAULT_CONFIG.memory.limit),
24
+ }).default(DEFAULT_CONFIG.memory),
15
25
  channels: z.object({
16
26
  telegram: z.object({
17
27
  enabled: z.boolean().default(false),
@@ -1,12 +1,14 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
2
  import { Agent } from '../agent.js';
3
3
  import { ProviderFactory } from '../providers/factory.js';
4
+ import { ToolsFactory } from '../tools/factory.js';
4
5
  import { DEFAULT_CONFIG } from '../../types/config.js';
5
6
  import { AIMessage } from '@langchain/core/messages';
6
7
  import * as fs from 'fs-extra';
7
8
  import * as path from 'path';
8
9
  import { homedir } from 'os';
9
10
  vi.mock('../providers/factory.js');
11
+ vi.mock('../tools/factory.js');
10
12
  describe('Agent', () => {
11
13
  let agent;
12
14
  const mockProvider = {
@@ -27,8 +29,9 @@ describe('Agent', () => {
27
29
  // Ignore errors if database doesn't exist or is corrupted
28
30
  }
29
31
  }
30
- mockProvider.invoke.mockResolvedValue(new AIMessage('Hello world'));
31
- vi.mocked(ProviderFactory.create).mockReturnValue(mockProvider);
32
+ mockProvider.invoke.mockResolvedValue({ messages: [new AIMessage('Hello world')] });
33
+ vi.mocked(ProviderFactory.create).mockResolvedValue(mockProvider);
34
+ vi.mocked(ToolsFactory.create).mockResolvedValue([]);
32
35
  agent = new Agent(DEFAULT_CONFIG);
33
36
  });
34
37
  afterEach(async () => {
@@ -44,7 +47,8 @@ describe('Agent', () => {
44
47
  });
45
48
  it('should initialize successfully', async () => {
46
49
  await agent.initialize();
47
- expect(ProviderFactory.create).toHaveBeenCalledWith(DEFAULT_CONFIG.llm);
50
+ expect(ToolsFactory.create).toHaveBeenCalled();
51
+ expect(ProviderFactory.create).toHaveBeenCalledWith(DEFAULT_CONFIG.llm, []);
48
52
  });
49
53
  it('should chat successfully', async () => {
50
54
  await agent.initialize();
@@ -67,7 +71,7 @@ describe('Agent', () => {
67
71
  expect(history1[1].content).toBe('Hello world'); // AI
68
72
  // Second turn
69
73
  // Update mock return value for next call
70
- mockProvider.invoke.mockResolvedValue(new AIMessage('I am fine'));
74
+ mockProvider.invoke.mockResolvedValue({ messages: [new AIMessage('I am fine')] });
71
75
  await agent.chat('How are you?');
72
76
  const history2 = await agent.getHistory();
73
77
  expect(history2).toHaveLength(4);
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { Agent } from '../agent.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 { homedir } from 'os';
9
+ vi.mock('../providers/factory.js');
10
+ describe('Agent Memory Limit', () => {
11
+ let agent;
12
+ const mockProvider = {
13
+ invoke: vi.fn(),
14
+ };
15
+ const dbPath = path.join(homedir(), ".morpheus", "memory", "short-memory.db");
16
+ beforeEach(async () => {
17
+ vi.resetAllMocks();
18
+ // Clean up DB
19
+ if (fs.existsSync(dbPath)) {
20
+ try {
21
+ const Database = (await import("better-sqlite3")).default;
22
+ const db = new Database(dbPath);
23
+ db.exec("DELETE FROM messages");
24
+ db.close();
25
+ }
26
+ catch (err) { }
27
+ }
28
+ mockProvider.invoke.mockResolvedValue({
29
+ messages: [new AIMessage('Response')]
30
+ });
31
+ vi.mocked(ProviderFactory.create).mockResolvedValue(mockProvider);
32
+ });
33
+ afterEach(async () => {
34
+ if (agent) {
35
+ try {
36
+ await agent.clearMemory();
37
+ }
38
+ catch (err) { }
39
+ }
40
+ });
41
+ it('should respect configured memory limit', async () => {
42
+ const limitedConfig = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
43
+ limitedConfig.memory.limit = 2; // Only last 2 messages (1 exchange)
44
+ agent = new Agent(limitedConfig);
45
+ await agent.initialize();
46
+ // Turn 1
47
+ await agent.chat('Msg 1');
48
+ // Turn 2
49
+ await agent.chat('Msg 2');
50
+ // Turn 3
51
+ await agent.chat('Msg 3');
52
+ // DB should have 6 messages (3 User + 3 AI)
53
+ // getHistory() should return only 2 (User Msg 3 + AI Response)
54
+ // Wait, SQLiteChatMessageHistory limit might be total messages? Or pairs?
55
+ // LangChain's limit usually means "last N messages".
56
+ const history = await agent.getHistory();
57
+ // Assuming limit=2 means 2 messages.
58
+ expect(history.length).toBeLessThanOrEqual(2);
59
+ expect(history[history.length - 1].content).toBe('Response');
60
+ });
61
+ });
@@ -35,7 +35,7 @@ describe("Agent Persistence Integration", () => {
35
35
  // Mock the SQLiteChatMessageHistory to use test path
36
36
  // We'll use the default ~/.morpheus path for this test
37
37
  mockProvider.invoke.mockResolvedValue(new AIMessage("Test response"));
38
- vi.mocked(ProviderFactory.create).mockReturnValue(mockProvider);
38
+ vi.mocked(ProviderFactory.create).mockResolvedValue(mockProvider);
39
39
  agent = new Agent(DEFAULT_CONFIG);
40
40
  });
41
41
  afterEach(async () => {
@@ -10,7 +10,15 @@ const mockConfig = {
10
10
  discord: { enabled: false }
11
11
  },
12
12
  ui: { enabled: false, port: 3333 },
13
- logging: { enabled: false, level: 'info', retention: '1d' }
13
+ logging: { enabled: false, level: 'info', retention: '1d' },
14
+ audio: {
15
+ enabled: false,
16
+ maxDurationSeconds: 60,
17
+ supportedMimeTypes: ['audio/ogg']
18
+ },
19
+ memory: {
20
+ limit: 100
21
+ }
14
22
  };
15
23
  const run = async () => {
16
24
  try {
@@ -1,5 +1,6 @@
1
1
  import { HumanMessage, SystemMessage, AIMessage } from "@langchain/core/messages";
2
2
  import { ProviderFactory } from "./providers/factory.js";
3
+ import { ToolsFactory } from "./tools/factory.js";
3
4
  import { ConfigManager } from "../config/manager.js";
4
5
  import { ProviderError } from "./errors.js";
5
6
  import { DisplayManager } from "./display.js";
@@ -23,13 +24,15 @@ export class Agent {
23
24
  // Note: API Key validation is delegated to ProviderFactory or the Provider itself
24
25
  // to allow for Environment Variable fallback supported by LangChain.
25
26
  try {
26
- this.provider = ProviderFactory.create(this.config.llm);
27
+ const tools = await ToolsFactory.create();
28
+ this.provider = await ProviderFactory.create(this.config.llm, tools);
27
29
  if (!this.provider) {
28
30
  throw new Error("Provider factory returned undefined");
29
31
  }
30
32
  // Initialize persistent memory with SQLite
31
33
  this.history = new SQLiteChatMessageHistory({
32
34
  sessionId: "default",
35
+ limit: this.config.memory?.limit || 100, // Fallback purely defensive if config type allows optional
33
36
  });
34
37
  }
35
38
  catch (err) {
@@ -57,13 +60,13 @@ export class Agent {
57
60
  ...previousMessages,
58
61
  userMessage
59
62
  ];
60
- const response = await this.provider.invoke(messages);
61
- const content = typeof response.content === 'string' ? response.content : JSON.stringify(response.content);
63
+ const response = await this.provider.invoke({ messages });
64
+ // console.log('Agent response:', response);
62
65
  // Persist messages to database
63
66
  await this.history.addMessage(userMessage);
64
- await this.history.addMessage(new AIMessage(content));
67
+ await this.history.addMessage(new AIMessage(response.messages[response.messages.length - 1].text));
65
68
  this.display.log('Response generated.', { source: 'Agent' });
66
- return content;
69
+ return response.messages[response.messages.length - 1].text;
67
70
  }
68
71
  catch (err) {
69
72
  throw new ProviderError(this.config.llm.provider, err, "Chat request failed");
@@ -0,0 +1,45 @@
1
+ import { GoogleGenAI } from '@google/genai';
2
+ export class AudioAgent {
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
+ return text;
36
+ }
37
+ catch (error) {
38
+ // Wrap error for clarity
39
+ if (error instanceof Error) {
40
+ throw new Error(`Audio transcription failed: ${error.message}`);
41
+ }
42
+ throw error;
43
+ }
44
+ }
45
+ }
@@ -80,6 +80,9 @@ export class DisplayManager {
80
80
  else if (options.source === 'Agent') {
81
81
  color = chalk.hex('#FFA500');
82
82
  }
83
+ else if (options.source === 'AgentAudio') {
84
+ color = chalk.hex('#b902b9');
85
+ }
83
86
  prefix = color(`[${options.source}] `);
84
87
  }
85
88
  let formattedMessage = message;
@@ -8,9 +8,11 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
8
8
  lc_namespace = ["langchain", "stores", "message", "sqlite"];
9
9
  db;
10
10
  sessionId;
11
+ limit;
11
12
  constructor(fields) {
12
13
  super();
13
14
  this.sessionId = fields.sessionId;
15
+ this.limit = fields.limit ? fields.limit : 20;
14
16
  // Default path: ~/.morpheus/memory/short-memory.db
15
17
  const dbPath = fields.databasePath || path.join(homedir(), ".morpheus", "memory", "short-memory.db");
16
18
  // Ensure the directory exists
@@ -97,8 +99,9 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
97
99
  */
98
100
  async getMessages() {
99
101
  try {
100
- const stmt = this.db.prepare("SELECT type, content FROM messages WHERE session_id = ? ORDER BY id ASC");
101
- const rows = stmt.all(this.sessionId);
102
+ // Esta query é válida para SQLite: seleciona os campos type e content da tabela messages filtrando por session_id, ordenando por id e limitando o número de resultados.
103
+ const stmt = this.db.prepare("SELECT type, content FROM messages WHERE session_id = ? ORDER BY id ASC LIMIT ?");
104
+ const rows = stmt.all(this.sessionId, this.limit);
102
105
  return rows.map((row) => {
103
106
  switch (row.type) {
104
107
  case "human":
@@ -3,38 +3,57 @@ import { ChatAnthropic } from "@langchain/anthropic";
3
3
  import { ChatOllama } from "@langchain/ollama";
4
4
  import { ChatGoogleGenerativeAI } from "@langchain/google-genai";
5
5
  import { ProviderError } from "../errors.js";
6
+ import { createAgent } from "langchain";
7
+ // import { MultiServerMCPClient, } from "@langchain/mcp-adapters"; // REMOVED
8
+ import { z } from "zod";
9
+ import { DisplayManager } from "../display.js";
6
10
  export class ProviderFactory {
7
- static create(config) {
11
+ static async create(config, tools = []) {
12
+ let display = DisplayManager.getInstance();
13
+ let model;
14
+ const responseSchema = z.object({
15
+ content: z.string().describe("The main response content from the agent"),
16
+ });
17
+ // Removed direct MCP client instantiation
8
18
  try {
9
19
  switch (config.provider) {
10
20
  case 'openai':
11
- return new ChatOpenAI({
21
+ model = new ChatOpenAI({
12
22
  modelName: config.model,
13
23
  temperature: config.temperature,
14
24
  apiKey: config.api_key, // LangChain will also check process.env.OPENAI_API_KEY
15
25
  });
26
+ break;
16
27
  case 'anthropic':
17
- return new ChatAnthropic({
28
+ model = new ChatAnthropic({
18
29
  modelName: config.model,
19
30
  temperature: config.temperature,
20
31
  apiKey: config.api_key,
21
32
  });
33
+ break;
22
34
  case 'ollama':
23
35
  // Ollama usually runs locally, api_key optional
24
- return new ChatOllama({
36
+ model = new ChatOllama({
25
37
  model: config.model,
26
38
  temperature: config.temperature,
27
39
  baseUrl: config.api_key, // Sometimes users might overload api_key for base URL or similar, but simplified here
28
40
  });
41
+ break;
29
42
  case 'gemini':
30
- return new ChatGoogleGenerativeAI({
43
+ model = new ChatGoogleGenerativeAI({
31
44
  model: config.model,
32
45
  temperature: config.temperature,
33
- apiKey: config.api_key,
46
+ apiKey: config.api_key
34
47
  });
48
+ break;
35
49
  default:
36
50
  throw new Error(`Unsupported provider: ${config.provider}`);
37
51
  }
52
+ const toolsForAgent = tools;
53
+ return createAgent({
54
+ model: model,
55
+ tools: toolsForAgent,
56
+ });
38
57
  }
39
58
  catch (error) {
40
59
  let suggestion = "Check your configuration and API keys.";