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.
- package/README.md +266 -211
- package/bin/morpheus.js +30 -0
- package/dist/channels/telegram.js +11 -3
- package/dist/cli/commands/config.js +18 -3
- package/dist/cli/commands/init.js +3 -3
- package/dist/cli/index.js +4 -0
- package/dist/config/mcp-loader.js +42 -0
- package/dist/config/schemas.js +16 -0
- package/dist/http/__tests__/auth.test.js +53 -0
- package/dist/http/api.js +12 -0
- package/dist/http/middleware/auth.js +23 -0
- package/dist/http/server.js +2 -1
- package/dist/runtime/__tests__/agent.test.js +8 -4
- package/dist/runtime/__tests__/agent_memory_limit.test.js +61 -0
- package/dist/runtime/__tests__/agent_persistence.test.js +1 -1
- package/dist/runtime/__tests__/manual_start_verify.js +4 -0
- package/dist/runtime/agent.js +84 -9
- package/dist/runtime/audio-agent.js +11 -1
- package/dist/runtime/display.js +12 -0
- package/dist/runtime/memory/sqlite.js +142 -9
- package/dist/runtime/providers/factory.js +50 -6
- package/dist/runtime/scaffold.js +5 -0
- package/dist/runtime/tools/__tests__/factory.test.js +42 -0
- package/dist/runtime/tools/__tests__/tools.test.js +127 -0
- package/dist/runtime/tools/analytics-tools.js +73 -0
- package/dist/runtime/tools/config-tools.js +70 -0
- package/dist/runtime/tools/diagnostic-tools.js +124 -0
- package/dist/runtime/tools/factory.js +78 -0
- package/dist/runtime/tools/index.js +4 -0
- package/dist/types/auth.js +4 -0
- package/dist/types/config.js +4 -0
- package/dist/types/mcp.js +11 -0
- package/dist/types/tools.js +2 -0
- package/dist/types/usage.js +1 -0
- package/dist/ui/assets/index-4kQpg2wK.js +50 -0
- package/dist/ui/assets/index-CwL7mn36.css +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +5 -2
- package/dist/ui/assets/index-Az60Fu0M.js +0 -50
- 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
|
-
|
|
101
|
-
const
|
|
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
|
-
|
|
150
|
+
msg = new HumanMessage(row.content);
|
|
151
|
+
break;
|
|
106
152
|
case "ai":
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.";
|
package/dist/runtime/scaffold.js
CHANGED
|
@@ -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
|
+
});
|