morpheus-cli 0.1.4 → 0.1.6

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.
Files changed (40) hide show
  1. package/README.md +266 -211
  2. package/bin/morpheus.js +30 -0
  3. package/dist/channels/telegram.js +11 -3
  4. package/dist/cli/commands/config.js +18 -3
  5. package/dist/cli/commands/init.js +3 -3
  6. package/dist/cli/index.js +4 -0
  7. package/dist/config/mcp-loader.js +42 -0
  8. package/dist/config/schemas.js +16 -0
  9. package/dist/http/__tests__/auth.test.js +53 -0
  10. package/dist/http/api.js +12 -0
  11. package/dist/http/middleware/auth.js +23 -0
  12. package/dist/http/server.js +2 -1
  13. package/dist/runtime/__tests__/agent.test.js +8 -4
  14. package/dist/runtime/__tests__/agent_memory_limit.test.js +61 -0
  15. package/dist/runtime/__tests__/agent_persistence.test.js +1 -1
  16. package/dist/runtime/__tests__/manual_start_verify.js +4 -0
  17. package/dist/runtime/agent.js +84 -9
  18. package/dist/runtime/audio-agent.js +11 -1
  19. package/dist/runtime/display.js +12 -0
  20. package/dist/runtime/memory/sqlite.js +142 -9
  21. package/dist/runtime/providers/factory.js +50 -6
  22. package/dist/runtime/scaffold.js +5 -0
  23. package/dist/runtime/tools/__tests__/factory.test.js +42 -0
  24. package/dist/runtime/tools/__tests__/tools.test.js +127 -0
  25. package/dist/runtime/tools/analytics-tools.js +73 -0
  26. package/dist/runtime/tools/config-tools.js +70 -0
  27. package/dist/runtime/tools/diagnostic-tools.js +124 -0
  28. package/dist/runtime/tools/factory.js +78 -0
  29. package/dist/runtime/tools/index.js +4 -0
  30. package/dist/types/auth.js +4 -0
  31. package/dist/types/config.js +4 -0
  32. package/dist/types/mcp.js +11 -0
  33. package/dist/types/tools.js +2 -0
  34. package/dist/types/usage.js +1 -0
  35. package/dist/ui/assets/index-4kQpg2wK.js +50 -0
  36. package/dist/ui/assets/index-CwL7mn36.css +1 -0
  37. package/dist/ui/index.html +2 -2
  38. package/package.json +5 -2
  39. package/dist/ui/assets/index-Az60Fu0M.js +0 -50
  40. package/dist/ui/assets/index-nNle8n-Z.css +0 -1
@@ -1,5 +1,5 @@
1
1
  import { BaseListChatMessageHistory } from "@langchain/core/chat_history";
2
- import { HumanMessage, AIMessage, SystemMessage } from "@langchain/core/messages";
2
+ import { HumanMessage, AIMessage, SystemMessage, ToolMessage } from "@langchain/core/messages";
3
3
  import Database from "better-sqlite3";
4
4
  import * as fs from "fs-extra";
5
5
  import * as path from "path";
@@ -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
@@ -80,36 +82,115 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
80
82
  session_id TEXT NOT NULL,
81
83
  type TEXT NOT NULL,
82
84
  content TEXT NOT NULL,
83
- created_at INTEGER NOT NULL
85
+ created_at INTEGER NOT NULL,
86
+ input_tokens INTEGER,
87
+ output_tokens INTEGER,
88
+ total_tokens INTEGER,
89
+ cache_read_tokens INTEGER
84
90
  );
85
91
 
86
92
  CREATE INDEX IF NOT EXISTS idx_messages_session_id
87
93
  ON messages(session_id);
88
94
  `);
95
+ this.migrateTable();
89
96
  }
90
97
  catch (error) {
91
98
  throw new Error(`Failed to create messages table: ${error}`);
92
99
  }
93
100
  }
101
+ /**
102
+ * Checks for missing columns and adds them if necessary.
103
+ */
104
+ migrateTable() {
105
+ try {
106
+ const tableInfo = this.db.pragma('table_info(messages)');
107
+ const columns = new Set(tableInfo.map(c => c.name));
108
+ const newColumns = [
109
+ 'input_tokens',
110
+ 'output_tokens',
111
+ 'total_tokens',
112
+ 'cache_read_tokens'
113
+ ];
114
+ for (const col of newColumns) {
115
+ if (!columns.has(col)) {
116
+ try {
117
+ this.db.exec(`ALTER TABLE messages ADD COLUMN ${col} INTEGER`);
118
+ }
119
+ catch (e) {
120
+ // Ignore error if column already exists (race condition or check failed)
121
+ console.warn(`[SQLite] Failed to add column ${col}: ${e}`);
122
+ }
123
+ }
124
+ }
125
+ }
126
+ catch (error) {
127
+ console.warn(`[SQLite] Migration check failed: ${error}`);
128
+ }
129
+ }
94
130
  /**
95
131
  * Retrieves all messages for the current session from the database.
96
132
  * @returns Promise resolving to an array of BaseMessage objects
97
133
  */
98
134
  async getMessages() {
99
135
  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);
136
+ // Fetch new columns
137
+ const stmt = this.db.prepare("SELECT type, content, input_tokens, output_tokens, total_tokens, cache_read_tokens FROM messages WHERE session_id = ? ORDER BY id ASC LIMIT ?");
138
+ const rows = stmt.all(this.sessionId, this.limit);
102
139
  return rows.map((row) => {
140
+ let msg;
141
+ // Reconstruct usage metadata if present
142
+ const usage_metadata = row.total_tokens != null ? {
143
+ input_tokens: row.input_tokens || 0,
144
+ output_tokens: row.output_tokens || 0,
145
+ total_tokens: row.total_tokens || 0,
146
+ input_token_details: row.cache_read_tokens ? { cache_read: row.cache_read_tokens } : undefined
147
+ } : undefined;
103
148
  switch (row.type) {
104
149
  case "human":
105
- return new HumanMessage(row.content);
150
+ msg = new HumanMessage(row.content);
151
+ break;
106
152
  case "ai":
107
- return new AIMessage(row.content);
153
+ try {
154
+ // Attempt to parse structured content (for tool calls)
155
+ const parsed = JSON.parse(row.content);
156
+ if (parsed && typeof parsed === 'object' && Array.isArray(parsed.tool_calls)) {
157
+ msg = new AIMessage({
158
+ content: parsed.text || "",
159
+ tool_calls: parsed.tool_calls
160
+ });
161
+ }
162
+ else {
163
+ msg = new AIMessage(row.content);
164
+ }
165
+ }
166
+ catch {
167
+ // Fallback for legacy text-only messages
168
+ msg = new AIMessage(row.content);
169
+ }
170
+ break;
108
171
  case "system":
109
- return new SystemMessage(row.content);
172
+ msg = new SystemMessage(row.content);
173
+ break;
174
+ case "tool":
175
+ try {
176
+ const parsed = JSON.parse(row.content);
177
+ msg = new ToolMessage({
178
+ content: parsed.content,
179
+ tool_call_id: parsed.tool_call_id || 'unknown',
180
+ name: parsed.name
181
+ });
182
+ }
183
+ catch {
184
+ msg = new ToolMessage({ content: row.content, tool_call_id: 'unknown' });
185
+ }
186
+ break;
110
187
  default:
111
188
  throw new Error(`Unknown message type: ${row.type}`);
112
189
  }
190
+ if (usage_metadata) {
191
+ msg.usage_metadata = usage_metadata;
192
+ }
193
+ return msg;
113
194
  });
114
195
  }
115
196
  catch (error) {
@@ -136,14 +217,50 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
136
217
  else if (message instanceof SystemMessage) {
137
218
  type = "system";
138
219
  }
220
+ else if (message instanceof ToolMessage) {
221
+ type = "tool";
222
+ }
139
223
  else {
140
224
  throw new Error(`Unsupported message type: ${message.constructor.name}`);
141
225
  }
142
226
  const content = typeof message.content === "string"
143
227
  ? message.content
144
228
  : JSON.stringify(message.content);
145
- const stmt = this.db.prepare("INSERT INTO messages (session_id, type, content, created_at) VALUES (?, ?, ?, ?)");
146
- stmt.run(this.sessionId, type, content, Date.now());
229
+ // Extract usage metadata
230
+ // 1. Try generic usage_metadata (LangChain standard)
231
+ // 2. Try extraUsage (passed via some adapters) - attached to additional_kwargs usually, but we might pass it differently
232
+ // The Spec says we might pass it to chat(), but addMessage receives a BaseMessage.
233
+ // So we should expect usage to be on the message object properties.
234
+ const anyMsg = message;
235
+ const usage = anyMsg.usage_metadata || anyMsg.response_metadata?.usage || anyMsg.response_metadata?.tokenUsage || anyMsg.usage;
236
+ const inputTokens = usage?.input_tokens ?? null;
237
+ const outputTokens = usage?.output_tokens ?? null;
238
+ const totalTokens = usage?.total_tokens ?? null;
239
+ const cacheReadTokens = usage?.input_token_details?.cache_read ?? usage?.cache_read_tokens ?? null;
240
+ // Handle special content serialization for Tools
241
+ let finalContent = "";
242
+ if (type === 'ai' && (message.tool_calls?.length ?? 0) > 0) {
243
+ // Serialize tool calls with content
244
+ finalContent = JSON.stringify({
245
+ text: message.content,
246
+ tool_calls: message.tool_calls
247
+ });
248
+ }
249
+ else if (type === 'tool') {
250
+ const tm = message;
251
+ finalContent = JSON.stringify({
252
+ content: tm.content,
253
+ tool_call_id: tm.tool_call_id,
254
+ name: tm.name
255
+ });
256
+ }
257
+ else {
258
+ finalContent = typeof message.content === "string"
259
+ ? message.content
260
+ : JSON.stringify(message.content);
261
+ }
262
+ const stmt = this.db.prepare("INSERT INTO messages (session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
263
+ stmt.run(this.sessionId, type, finalContent, Date.now(), inputTokens, outputTokens, totalTokens, cacheReadTokens);
147
264
  }
148
265
  catch (error) {
149
266
  // Check for specific SQLite errors
@@ -161,6 +278,22 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
161
278
  throw new Error(`Failed to add message: ${error}`);
162
279
  }
163
280
  }
281
+ /**
282
+ * Retrieves aggregated usage statistics for all messages in the database.
283
+ */
284
+ async getGlobalUsageStats() {
285
+ try {
286
+ const stmt = this.db.prepare("SELECT SUM(input_tokens) as totalInput, SUM(output_tokens) as totalOutput FROM messages");
287
+ const row = stmt.get();
288
+ return {
289
+ totalInputTokens: row.totalInput || 0,
290
+ totalOutputTokens: row.totalOutput || 0
291
+ };
292
+ }
293
+ catch (error) {
294
+ throw new Error(`Failed to get usage stats: ${error}`);
295
+ }
296
+ }
164
297
  /**
165
298
  * Clears all messages for the current session from the database.
166
299
  */
@@ -3,38 +3,82 @@ 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, createMiddleware } from "langchain";
7
+ // import { MultiServerMCPClient, } from "@langchain/mcp-adapters"; // REMOVED
8
+ import { z } from "zod";
9
+ import { DisplayManager } from "../display.js";
10
+ import { ConfigQueryTool, ConfigUpdateTool, DiagnosticTool, MessageCountTool, TokenUsageTool } from "../tools/index.js";
6
11
  export class ProviderFactory {
7
- static create(config) {
12
+ static async create(config, tools = []) {
13
+ let display = DisplayManager.getInstance();
14
+ const toolMonitoringMiddleware = createMiddleware({
15
+ name: "ToolMonitoringMiddleware",
16
+ wrapToolCall: (request, handler) => {
17
+ display.log(`Executing tool: ${request.toolCall.name}`, { level: "warning", source: 'ToolCall' });
18
+ display.log(`Arguments: ${JSON.stringify(request.toolCall.args)}`, { level: "info", source: 'ToolCall' });
19
+ try {
20
+ const result = handler(request);
21
+ display.log("Tool completed successfully", { level: "info", source: 'ToolCall' });
22
+ return result;
23
+ }
24
+ catch (e) {
25
+ display.log(`Tool failed: ${e}`, { level: "error", source: 'ToolCall' });
26
+ throw e;
27
+ }
28
+ },
29
+ });
30
+ let model;
31
+ const responseSchema = z.object({
32
+ content: z.string().describe("The main response content from the agent"),
33
+ });
34
+ // Removed direct MCP client instantiation
8
35
  try {
9
36
  switch (config.provider) {
10
37
  case 'openai':
11
- return new ChatOpenAI({
38
+ model = new ChatOpenAI({
12
39
  modelName: config.model,
13
40
  temperature: config.temperature,
14
41
  apiKey: config.api_key, // LangChain will also check process.env.OPENAI_API_KEY
15
42
  });
43
+ break;
16
44
  case 'anthropic':
17
- return new ChatAnthropic({
45
+ model = new ChatAnthropic({
18
46
  modelName: config.model,
19
47
  temperature: config.temperature,
20
48
  apiKey: config.api_key,
21
49
  });
50
+ break;
22
51
  case 'ollama':
23
52
  // Ollama usually runs locally, api_key optional
24
- return new ChatOllama({
53
+ model = new ChatOllama({
25
54
  model: config.model,
26
55
  temperature: config.temperature,
27
56
  baseUrl: config.api_key, // Sometimes users might overload api_key for base URL or similar, but simplified here
28
57
  });
58
+ break;
29
59
  case 'gemini':
30
- return new ChatGoogleGenerativeAI({
60
+ model = new ChatGoogleGenerativeAI({
31
61
  model: config.model,
32
62
  temperature: config.temperature,
33
- apiKey: config.api_key,
63
+ apiKey: config.api_key
34
64
  });
65
+ break;
35
66
  default:
36
67
  throw new Error(`Unsupported provider: ${config.provider}`);
37
68
  }
69
+ const toolsForAgent = [
70
+ ...tools,
71
+ ConfigQueryTool,
72
+ ConfigUpdateTool,
73
+ DiagnosticTool,
74
+ MessageCountTool,
75
+ TokenUsageTool
76
+ ];
77
+ return createAgent({
78
+ model: model,
79
+ tools: toolsForAgent,
80
+ middleware: [toolMonitoringMiddleware]
81
+ });
38
82
  }
39
83
  catch (error) {
40
84
  let suggestion = "Check your configuration and API keys.";
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs-extra';
2
2
  import { PATHS } from '../config/paths.js';
3
3
  import { ConfigManager } from '../config/manager.js';
4
+ import { DEFAULT_MCP_TEMPLATE } from '../types/mcp.js';
4
5
  import chalk from 'chalk';
5
6
  import ora from 'ora';
6
7
  export async function scaffold() {
@@ -22,6 +23,10 @@ export async function scaffold() {
22
23
  else {
23
24
  await configManager.load(); // Load if exists (although load handles existence check too)
24
25
  }
26
+ // Create mcps.json if not exists
27
+ if (!(await fs.pathExists(PATHS.mcps))) {
28
+ await fs.writeJson(PATHS.mcps, DEFAULT_MCP_TEMPLATE, { spaces: 2 });
29
+ }
25
30
  spinner.succeed('Morpheus environment ready at ' + chalk.cyan(PATHS.root));
26
31
  }
27
32
  catch (error) {
@@ -0,0 +1,42 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import { ToolsFactory } 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('ToolsFactory', () => {
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 ToolsFactory.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 ToolsFactory.create();
40
+ expect(tools).toEqual([]);
41
+ });
42
+ });
@@ -0,0 +1,127 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { ConfigQueryTool, ConfigUpdateTool } from '../config-tools.js';
3
+ import { DiagnosticTool } from '../diagnostic-tools.js';
4
+ import { MessageCountTool, TokenUsageTool } from '../analytics-tools.js';
5
+ import { ConfigManager } from '../../../config/manager.js';
6
+ // Mock the ConfigManager for testing
7
+ vi.mock('../../config/manager.js', () => ({
8
+ ConfigManager: {
9
+ getInstance: vi.fn(() => ({
10
+ load: vi.fn(async () => ({
11
+ llm: { provider: 'openai', model: 'gpt-4' },
12
+ logging: { enabled: true, level: 'info' },
13
+ ui: { enabled: true, port: 3000 }
14
+ })),
15
+ get: vi.fn(() => ({
16
+ llm: { provider: 'openai', model: 'gpt-4' },
17
+ logging: { enabled: true, level: 'info' },
18
+ ui: { enabled: true, port: 3000 }
19
+ })),
20
+ save: vi.fn(async (newConfig) => {
21
+ // Mock save implementation
22
+ })
23
+ }))
24
+ }
25
+ }));
26
+ describe('Config Tools', () => {
27
+ describe('ConfigQueryTool', () => {
28
+ it('should query all configuration values when no key is provided', async () => {
29
+ const result = await ConfigQueryTool.invoke({});
30
+ const parsedResult = JSON.parse(result);
31
+ expect(parsedResult).toHaveProperty('llm');
32
+ expect(parsedResult).toHaveProperty('logging');
33
+ expect(parsedResult).toHaveProperty('ui');
34
+ });
35
+ it('should query specific configuration value when key is provided', async () => {
36
+ const result = await ConfigQueryTool.invoke({ key: 'llm' });
37
+ const parsedResult = JSON.parse(result);
38
+ expect(parsedResult).toHaveProperty('llm');
39
+ const llmConfig = parsedResult.llm;
40
+ expect(llmConfig).toHaveProperty('provider');
41
+ expect(llmConfig).toHaveProperty('model');
42
+ });
43
+ });
44
+ describe('ConfigUpdateTool', () => {
45
+ it('should update configuration values', async () => {
46
+ const updates = { 'ui.port': 4000 };
47
+ const result = await ConfigUpdateTool.invoke({ updates });
48
+ const parsedResult = JSON.parse(result);
49
+ expect(parsedResult).toHaveProperty('success', true);
50
+ });
51
+ it('should return error when update fails', async () => {
52
+ // Create a new mock instance for this specific test
53
+ const originalGetInstance = ConfigManager.getInstance;
54
+ const mockGetInstance = vi.fn(() => ({
55
+ load: vi.fn(async () => ({
56
+ llm: { provider: 'openai', model: 'gpt-4' },
57
+ logging: { enabled: true, level: 'info' },
58
+ ui: { enabled: true, port: 3000 }
59
+ })),
60
+ get: vi.fn(() => ({
61
+ llm: { provider: 'openai', model: 'gpt-4' },
62
+ logging: { enabled: true, level: 'info' },
63
+ ui: { enabled: true, port: 3000 }
64
+ })),
65
+ save: vi.fn(async () => {
66
+ throw new Error('Save failed');
67
+ })
68
+ }));
69
+ // Replace the getInstance method temporarily
70
+ ConfigManager.getInstance = mockGetInstance;
71
+ const updates = { 'invalid.field': 'value' };
72
+ const result = await ConfigUpdateTool.invoke({ updates });
73
+ const parsedResult = JSON.parse(result);
74
+ expect(parsedResult).toHaveProperty('error');
75
+ // Restore the original method
76
+ ConfigManager.getInstance = originalGetInstance;
77
+ });
78
+ });
79
+ });
80
+ describe('DiagnosticTool', () => {
81
+ it('should return a diagnostic report with component statuses', async () => {
82
+ const result = await DiagnosticTool.invoke({});
83
+ const parsedResult = JSON.parse(result);
84
+ expect(parsedResult).toHaveProperty('timestamp');
85
+ expect(parsedResult).toHaveProperty('components');
86
+ expect(parsedResult.components).toHaveProperty('config');
87
+ expect(parsedResult.components).toHaveProperty('storage');
88
+ expect(parsedResult.components).toHaveProperty('network');
89
+ expect(parsedResult.components).toHaveProperty('agent');
90
+ });
91
+ });
92
+ describe('Analytics Tools', () => {
93
+ // Note: These tools now use the existing SQLite class and access the database
94
+ describe('MessageCountTool', () => {
95
+ it('should return message count', async () => {
96
+ const result = await MessageCountTool.invoke({});
97
+ const parsedResult = JSON.parse(result);
98
+ // Should return a number (message count) or an error if database doesn't exist
99
+ if (typeof parsedResult === 'object' && parsedResult.error) {
100
+ // If there's an error (e.g., database doesn't exist), that's acceptable in test environment
101
+ expect(parsedResult).toHaveProperty('error');
102
+ }
103
+ else {
104
+ // Otherwise, should return a number
105
+ expect(typeof parsedResult).toBe('number');
106
+ }
107
+ });
108
+ });
109
+ describe('TokenUsageTool', () => {
110
+ it('should return token usage statistics', async () => {
111
+ const result = await TokenUsageTool.invoke({});
112
+ const parsedResult = JSON.parse(result);
113
+ // Should return token statistics or an error if database doesn't exist
114
+ if (typeof parsedResult === 'object' && parsedResult.error) {
115
+ // If there's an error (e.g., database doesn't exist), that's acceptable in test environment
116
+ expect(parsedResult).toHaveProperty('error');
117
+ }
118
+ else {
119
+ // Otherwise, should return token statistics
120
+ expect(parsedResult).toHaveProperty('totalTokens');
121
+ expect(parsedResult).toHaveProperty('inputTokens');
122
+ expect(parsedResult).toHaveProperty('outputTokens');
123
+ expect(parsedResult).toHaveProperty('timestamp');
124
+ }
125
+ });
126
+ });
127
+ });
@@ -0,0 +1,73 @@
1
+ import { tool } from "langchain";
2
+ import * as z from "zod";
3
+ import path from "path";
4
+ import Database from "better-sqlite3";
5
+ import { homedir } from "os";
6
+ // Tool for querying message counts from the database
7
+ const dbPath = path.join(homedir(), ".morpheus", "memory", "short-memory.db");
8
+ export const MessageCountTool = tool(async ({ timeRange }) => {
9
+ try {
10
+ // Connect to database
11
+ const db = new Database(dbPath);
12
+ let query = "SELECT COUNT(*) as count FROM messages";
13
+ const params = [];
14
+ if (timeRange) {
15
+ query += " WHERE timestamp BETWEEN ? AND ?";
16
+ params.push(timeRange.start);
17
+ params.push(timeRange.end);
18
+ }
19
+ const result = db.prepare(query).get(params);
20
+ db.close();
21
+ return JSON.stringify(result.count);
22
+ }
23
+ catch (error) {
24
+ console.error("Error in MessageCountTool:", error);
25
+ return JSON.stringify({ error: `Failed to count messages: ${error.message}` });
26
+ }
27
+ }, {
28
+ name: "message_count",
29
+ description: "Returns count of stored messages. Accepts an optional 'timeRange' parameter with start and end timestamps for filtering.",
30
+ schema: z.object({
31
+ timeRange: z.object({
32
+ start: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/, "ISO date string"),
33
+ end: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/, "ISO date string"),
34
+ }).optional(),
35
+ }),
36
+ });
37
+ // Tool for querying token usage statistics from the database
38
+ export const TokenUsageTool = tool(async ({ timeRange }) => {
39
+ try {
40
+ // Connect to database
41
+ const db = new Database(dbPath);
42
+ let query = "SELECT SUM(input_tokens) as inputTokens, SUM(output_tokens) as outputTokens, SUM(input_tokens + output_tokens) as totalTokens FROM messages";
43
+ const params = [];
44
+ if (timeRange) {
45
+ query += " WHERE timestamp BETWEEN ? AND ?";
46
+ params.push(timeRange.start);
47
+ params.push(timeRange.end);
48
+ }
49
+ const result = db.prepare(query).get(params);
50
+ db.close();
51
+ // Handle potential null values
52
+ const tokenStats = {
53
+ totalTokens: result.totalTokens || 0,
54
+ inputTokens: result.inputTokens || 0,
55
+ outputTokens: result.outputTokens || 0,
56
+ timestamp: new Date().toISOString()
57
+ };
58
+ return JSON.stringify(tokenStats);
59
+ }
60
+ catch (error) {
61
+ console.error("Error in TokenUsageTool:", error);
62
+ return JSON.stringify({ error: `Failed to get token usage: ${error.message}` });
63
+ }
64
+ }, {
65
+ name: "token_usage",
66
+ description: "Returns token usage statistics. Accepts an optional 'timeRange' parameter with start and end timestamps for filtering.",
67
+ schema: z.object({
68
+ timeRange: z.object({
69
+ start: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/, "ISO date string"),
70
+ end: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/, "ISO date string"),
71
+ }).optional(),
72
+ }),
73
+ });
@@ -0,0 +1,70 @@
1
+ import { tool } from "langchain";
2
+ import * as z from "zod";
3
+ import { ConfigManager } from "../../config/manager.js";
4
+ // Tool for querying configuration values
5
+ export const ConfigQueryTool = tool(async ({ key }) => {
6
+ try {
7
+ const configManager = ConfigManager.getInstance();
8
+ // Load config if not already loaded
9
+ await configManager.load();
10
+ const config = configManager.get();
11
+ if (key) {
12
+ // Return specific configuration value
13
+ const value = config[key];
14
+ return JSON.stringify({ [key]: value });
15
+ }
16
+ else {
17
+ // Return all configuration values
18
+ return JSON.stringify(config);
19
+ }
20
+ }
21
+ catch (error) {
22
+ console.error("Error in ConfigQueryTool:", error);
23
+ return JSON.stringify({ error: "Failed to query configuration" });
24
+ }
25
+ }, {
26
+ name: "config_query",
27
+ description: "Queries current configuration values. Accepts an optional 'key' parameter to get a specific configuration value, or no parameter to get all configuration values.",
28
+ schema: z.object({
29
+ key: z.string().optional(),
30
+ }),
31
+ });
32
+ // Tool for updating configuration values
33
+ export const ConfigUpdateTool = tool(async ({ updates }) => {
34
+ try {
35
+ const configManager = ConfigManager.getInstance();
36
+ // Load current config
37
+ await configManager.load();
38
+ const currentConfig = configManager.get();
39
+ // Create new config with updates
40
+ const newConfig = { ...currentConfig, ...updates };
41
+ // Save the updated config
42
+ await configManager.save(newConfig);
43
+ return JSON.stringify({ success: true, message: "Configuration updated successfully" });
44
+ }
45
+ catch (error) {
46
+ console.error("Error in ConfigUpdateTool:", error);
47
+ return JSON.stringify({ error: `Failed to update configuration: ${error.message}` });
48
+ }
49
+ }, {
50
+ name: "config_update",
51
+ description: "Updates configuration values with validation. Accepts an 'updates' object containing key-value pairs to update.",
52
+ schema: z.object({
53
+ updates: z.object({
54
+ // Define common config properties that might be updated
55
+ // Using optional fields to allow flexible updates
56
+ "llm.provider": z.string().optional(),
57
+ "llm.model": z.string().optional(),
58
+ "llm.temperature": z.number().optional(),
59
+ "llm.api_key": z.string().optional(),
60
+ "ui.enabled": z.boolean().optional(),
61
+ "ui.port": z.number().optional(),
62
+ "logging.enabled": z.boolean().optional(),
63
+ "logging.level": z.enum(['debug', 'info', 'warn', 'error']).optional(),
64
+ "audio.enabled": z.boolean().optional(),
65
+ "audio.provider": z.string().optional(),
66
+ "memory.limit": z.number().optional(),
67
+ // Add more specific fields as needed, or use a catch-all for other properties
68
+ }).passthrough(), // Allow additional properties not explicitly defined
69
+ }),
70
+ });