morpheus-cli 0.1.5 → 0.1.8
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 +273 -213
- package/bin/morpheus.js +30 -0
- package/dist/channels/telegram.js +7 -2
- package/dist/cli/commands/config.js +18 -3
- package/dist/cli/commands/init.js +3 -3
- package/dist/cli/index.js +8 -0
- package/dist/config/mcp-loader.js +42 -0
- package/dist/config/schemas.js +22 -0
- package/dist/http/__tests__/auth.test.js +53 -0
- package/dist/http/api.js +23 -0
- package/dist/http/middleware/auth.js +23 -0
- package/dist/http/server.js +2 -1
- package/dist/runtime/__tests__/agent_persistence.test.js +39 -33
- package/dist/runtime/__tests__/manual_start_verify.js +1 -0
- package/dist/runtime/agent.js +93 -8
- package/dist/runtime/audio-agent.js +11 -1
- package/dist/runtime/display.js +12 -0
- package/dist/runtime/memory/sqlite.js +186 -9
- package/dist/runtime/providers/factory.js +28 -2
- package/dist/runtime/scaffold.js +5 -0
- package/dist/runtime/tools/__tests__/tools.test.js +127 -0
- package/dist/runtime/tools/analytics-tools.js +103 -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 +62 -18
- package/dist/runtime/tools/index.js +4 -0
- package/dist/types/auth.js +4 -0
- package/dist/types/config.js +1 -0
- package/dist/types/mcp.js +11 -0
- package/dist/types/stats.js +1 -0
- package/dist/types/tools.js +2 -0
- package/dist/types/usage.js +1 -0
- package/dist/ui/assets/index-CMGsLiNG.css +1 -0
- package/dist/ui/assets/index-DpbRiL07.js +50 -0
- package/dist/ui/index.html +2 -2
- package/package.json +5 -1
- package/dist/ui/assets/index-D1kvj0eG.css +0 -1
- package/dist/ui/assets/index-DTh8waF7.js +0 -50
|
@@ -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";
|
|
@@ -82,37 +82,129 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
82
82
|
session_id TEXT NOT NULL,
|
|
83
83
|
type TEXT NOT NULL,
|
|
84
84
|
content TEXT NOT NULL,
|
|
85
|
-
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,
|
|
90
|
+
provider TEXT,
|
|
91
|
+
model TEXT
|
|
86
92
|
);
|
|
87
93
|
|
|
88
94
|
CREATE INDEX IF NOT EXISTS idx_messages_session_id
|
|
89
95
|
ON messages(session_id);
|
|
90
96
|
`);
|
|
97
|
+
this.migrateTable();
|
|
91
98
|
}
|
|
92
99
|
catch (error) {
|
|
93
100
|
throw new Error(`Failed to create messages table: ${error}`);
|
|
94
101
|
}
|
|
95
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Checks for missing columns and adds them if necessary.
|
|
105
|
+
*/
|
|
106
|
+
migrateTable() {
|
|
107
|
+
try {
|
|
108
|
+
const tableInfo = this.db.pragma('table_info(messages)');
|
|
109
|
+
const columns = new Set(tableInfo.map(c => c.name));
|
|
110
|
+
const newColumns = [
|
|
111
|
+
'input_tokens',
|
|
112
|
+
'output_tokens',
|
|
113
|
+
'total_tokens',
|
|
114
|
+
'cache_read_tokens',
|
|
115
|
+
'provider',
|
|
116
|
+
'model'
|
|
117
|
+
];
|
|
118
|
+
const integerColumns = new Set(['input_tokens', 'output_tokens', 'total_tokens', 'cache_read_tokens']);
|
|
119
|
+
for (const col of newColumns) {
|
|
120
|
+
if (!columns.has(col)) {
|
|
121
|
+
try {
|
|
122
|
+
const type = integerColumns.has(col) ? 'INTEGER' : 'TEXT';
|
|
123
|
+
this.db.exec(`ALTER TABLE messages ADD COLUMN ${col} ${type}`);
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
// Ignore error if column already exists (race condition or check failed)
|
|
127
|
+
console.warn(`[SQLite] Failed to add column ${col}: ${e}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
console.warn(`[SQLite] Migration check failed: ${error}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
96
136
|
/**
|
|
97
137
|
* Retrieves all messages for the current session from the database.
|
|
98
138
|
* @returns Promise resolving to an array of BaseMessage objects
|
|
99
139
|
*/
|
|
100
140
|
async getMessages() {
|
|
101
141
|
try {
|
|
102
|
-
//
|
|
103
|
-
const stmt = this.db.prepare("SELECT type, content FROM messages WHERE session_id = ? ORDER BY id ASC LIMIT ?");
|
|
142
|
+
// Fetch new columns
|
|
143
|
+
const stmt = this.db.prepare("SELECT type, content, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model FROM messages WHERE session_id = ? ORDER BY id ASC LIMIT ?");
|
|
104
144
|
const rows = stmt.all(this.sessionId, this.limit);
|
|
105
145
|
return rows.map((row) => {
|
|
146
|
+
let msg;
|
|
147
|
+
// Reconstruct usage metadata if present
|
|
148
|
+
const usage_metadata = row.total_tokens != null ? {
|
|
149
|
+
input_tokens: row.input_tokens || 0,
|
|
150
|
+
output_tokens: row.output_tokens || 0,
|
|
151
|
+
total_tokens: row.total_tokens || 0,
|
|
152
|
+
input_token_details: row.cache_read_tokens ? { cache_read: row.cache_read_tokens } : undefined
|
|
153
|
+
} : undefined;
|
|
154
|
+
// Reconstruct provider metadata
|
|
155
|
+
const provider_metadata = row.provider ? {
|
|
156
|
+
provider: row.provider,
|
|
157
|
+
model: row.model || "unknown"
|
|
158
|
+
} : undefined;
|
|
106
159
|
switch (row.type) {
|
|
107
160
|
case "human":
|
|
108
|
-
|
|
161
|
+
msg = new HumanMessage(row.content);
|
|
162
|
+
break;
|
|
109
163
|
case "ai":
|
|
110
|
-
|
|
164
|
+
try {
|
|
165
|
+
// Attempt to parse structured content (for tool calls)
|
|
166
|
+
const parsed = JSON.parse(row.content);
|
|
167
|
+
if (parsed && typeof parsed === 'object' && Array.isArray(parsed.tool_calls)) {
|
|
168
|
+
msg = new AIMessage({
|
|
169
|
+
content: parsed.text || "",
|
|
170
|
+
tool_calls: parsed.tool_calls
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
msg = new AIMessage(row.content);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Fallback for legacy text-only messages
|
|
179
|
+
msg = new AIMessage(row.content);
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
111
182
|
case "system":
|
|
112
|
-
|
|
183
|
+
msg = new SystemMessage(row.content);
|
|
184
|
+
break;
|
|
185
|
+
case "tool":
|
|
186
|
+
try {
|
|
187
|
+
const parsed = JSON.parse(row.content);
|
|
188
|
+
msg = new ToolMessage({
|
|
189
|
+
content: parsed.content,
|
|
190
|
+
tool_call_id: parsed.tool_call_id || 'unknown',
|
|
191
|
+
name: parsed.name
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
msg = new ToolMessage({ content: row.content, tool_call_id: 'unknown' });
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
113
198
|
default:
|
|
114
199
|
throw new Error(`Unknown message type: ${row.type}`);
|
|
115
200
|
}
|
|
201
|
+
if (usage_metadata) {
|
|
202
|
+
msg.usage_metadata = usage_metadata;
|
|
203
|
+
}
|
|
204
|
+
if (provider_metadata) {
|
|
205
|
+
msg.provider_metadata = provider_metadata;
|
|
206
|
+
}
|
|
207
|
+
return msg;
|
|
116
208
|
});
|
|
117
209
|
}
|
|
118
210
|
catch (error) {
|
|
@@ -139,14 +231,53 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
139
231
|
else if (message instanceof SystemMessage) {
|
|
140
232
|
type = "system";
|
|
141
233
|
}
|
|
234
|
+
else if (message instanceof ToolMessage) {
|
|
235
|
+
type = "tool";
|
|
236
|
+
}
|
|
142
237
|
else {
|
|
143
238
|
throw new Error(`Unsupported message type: ${message.constructor.name}`);
|
|
144
239
|
}
|
|
145
240
|
const content = typeof message.content === "string"
|
|
146
241
|
? message.content
|
|
147
242
|
: JSON.stringify(message.content);
|
|
148
|
-
|
|
149
|
-
|
|
243
|
+
// Extract usage metadata
|
|
244
|
+
// 1. Try generic usage_metadata (LangChain standard)
|
|
245
|
+
// 2. Try extraUsage (passed via some adapters) - attached to additional_kwargs usually, but we might pass it differently
|
|
246
|
+
// The Spec says we might pass it to chat(), but addMessage receives a BaseMessage.
|
|
247
|
+
// So we should expect usage to be on the message object properties.
|
|
248
|
+
const anyMsg = message;
|
|
249
|
+
const usage = anyMsg.usage_metadata || anyMsg.response_metadata?.usage || anyMsg.response_metadata?.tokenUsage || anyMsg.usage;
|
|
250
|
+
const inputTokens = usage?.input_tokens ?? null;
|
|
251
|
+
const outputTokens = usage?.output_tokens ?? null;
|
|
252
|
+
const totalTokens = usage?.total_tokens ?? null;
|
|
253
|
+
const cacheReadTokens = usage?.input_token_details?.cache_read ?? usage?.cache_read_tokens ?? null;
|
|
254
|
+
// Extract provider metadata
|
|
255
|
+
const provider = anyMsg.provider_metadata?.provider ?? null;
|
|
256
|
+
const model = anyMsg.provider_metadata?.model ?? null;
|
|
257
|
+
// Handle special content serialization for Tools
|
|
258
|
+
let finalContent = "";
|
|
259
|
+
if (type === 'ai' && (message.tool_calls?.length ?? 0) > 0) {
|
|
260
|
+
// Serialize tool calls with content
|
|
261
|
+
finalContent = JSON.stringify({
|
|
262
|
+
text: message.content,
|
|
263
|
+
tool_calls: message.tool_calls
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
else if (type === 'tool') {
|
|
267
|
+
const tm = message;
|
|
268
|
+
finalContent = JSON.stringify({
|
|
269
|
+
content: tm.content,
|
|
270
|
+
tool_call_id: tm.tool_call_id,
|
|
271
|
+
name: tm.name
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
finalContent = typeof message.content === "string"
|
|
276
|
+
? message.content
|
|
277
|
+
: JSON.stringify(message.content);
|
|
278
|
+
}
|
|
279
|
+
const stmt = this.db.prepare("INSERT INTO messages (session_id, type, content, created_at, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
|
|
280
|
+
stmt.run(this.sessionId, type, finalContent, Date.now(), inputTokens, outputTokens, totalTokens, cacheReadTokens, provider, model);
|
|
150
281
|
}
|
|
151
282
|
catch (error) {
|
|
152
283
|
// Check for specific SQLite errors
|
|
@@ -164,6 +295,52 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
|
|
|
164
295
|
throw new Error(`Failed to add message: ${error}`);
|
|
165
296
|
}
|
|
166
297
|
}
|
|
298
|
+
/**
|
|
299
|
+
* Retrieves aggregated usage statistics for all messages in the database.
|
|
300
|
+
*/
|
|
301
|
+
async getGlobalUsageStats() {
|
|
302
|
+
try {
|
|
303
|
+
const stmt = this.db.prepare("SELECT SUM(input_tokens) as totalInput, SUM(output_tokens) as totalOutput FROM messages");
|
|
304
|
+
const row = stmt.get();
|
|
305
|
+
return {
|
|
306
|
+
totalInputTokens: row.totalInput || 0,
|
|
307
|
+
totalOutputTokens: row.totalOutput || 0
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
throw new Error(`Failed to get usage stats: ${error}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Retrieves aggregated usage statistics grouped by provider and model.
|
|
316
|
+
*/
|
|
317
|
+
async getUsageStatsByProviderAndModel() {
|
|
318
|
+
try {
|
|
319
|
+
const stmt = this.db.prepare(`SELECT
|
|
320
|
+
provider,
|
|
321
|
+
COALESCE(model, 'unknown') as model,
|
|
322
|
+
SUM(input_tokens) as totalInputTokens,
|
|
323
|
+
SUM(output_tokens) as totalOutputTokens,
|
|
324
|
+
SUM(total_tokens) as totalTokens,
|
|
325
|
+
COUNT(*) as messageCount
|
|
326
|
+
FROM messages
|
|
327
|
+
WHERE provider IS NOT NULL
|
|
328
|
+
GROUP BY provider, COALESCE(model, 'unknown')
|
|
329
|
+
ORDER BY provider, model`);
|
|
330
|
+
const rows = stmt.all();
|
|
331
|
+
return rows.map((row) => ({
|
|
332
|
+
provider: row.provider,
|
|
333
|
+
model: row.model,
|
|
334
|
+
totalInputTokens: row.totalInputTokens || 0,
|
|
335
|
+
totalOutputTokens: row.totalOutputTokens || 0,
|
|
336
|
+
totalTokens: row.totalTokens || 0,
|
|
337
|
+
messageCount: row.messageCount || 0
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
throw new Error(`Failed to get grouped usage stats: ${error}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
167
344
|
/**
|
|
168
345
|
* Clears all messages for the current session from the database.
|
|
169
346
|
*/
|
|
@@ -3,13 +3,30 @@ 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";
|
|
6
|
+
import { createAgent, createMiddleware } from "langchain";
|
|
7
7
|
// import { MultiServerMCPClient, } from "@langchain/mcp-adapters"; // REMOVED
|
|
8
8
|
import { z } from "zod";
|
|
9
9
|
import { DisplayManager } from "../display.js";
|
|
10
|
+
import { ConfigQueryTool, ConfigUpdateTool, DiagnosticTool, MessageCountTool, TokenUsageTool, ProviderModelUsageTool } from "../tools/index.js";
|
|
10
11
|
export class ProviderFactory {
|
|
11
12
|
static async create(config, tools = []) {
|
|
12
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
|
+
});
|
|
13
30
|
let model;
|
|
14
31
|
const responseSchema = z.object({
|
|
15
32
|
content: z.string().describe("The main response content from the agent"),
|
|
@@ -49,10 +66,19 @@ export class ProviderFactory {
|
|
|
49
66
|
default:
|
|
50
67
|
throw new Error(`Unsupported provider: ${config.provider}`);
|
|
51
68
|
}
|
|
52
|
-
const toolsForAgent =
|
|
69
|
+
const toolsForAgent = [
|
|
70
|
+
...tools,
|
|
71
|
+
ConfigQueryTool,
|
|
72
|
+
ConfigUpdateTool,
|
|
73
|
+
DiagnosticTool,
|
|
74
|
+
MessageCountTool,
|
|
75
|
+
TokenUsageTool,
|
|
76
|
+
ProviderModelUsageTool
|
|
77
|
+
];
|
|
53
78
|
return createAgent({
|
|
54
79
|
model: model,
|
|
55
80
|
tools: toolsForAgent,
|
|
81
|
+
middleware: [toolMonitoringMiddleware]
|
|
56
82
|
});
|
|
57
83
|
}
|
|
58
84
|
catch (error) {
|
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,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,103 @@
|
|
|
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 grouped by provider and model
|
|
38
|
+
export const ProviderModelUsageTool = tool(async () => {
|
|
39
|
+
try {
|
|
40
|
+
const db = new Database(dbPath);
|
|
41
|
+
const query = `
|
|
42
|
+
SELECT
|
|
43
|
+
provider,
|
|
44
|
+
COALESCE(model, 'unknown') as model,
|
|
45
|
+
SUM(input_tokens) as totalInputTokens,
|
|
46
|
+
SUM(output_tokens) as totalOutputTokens,
|
|
47
|
+
SUM(total_tokens) as totalTokens,
|
|
48
|
+
COUNT(*) as messageCount
|
|
49
|
+
FROM messages
|
|
50
|
+
WHERE provider IS NOT NULL
|
|
51
|
+
GROUP BY provider, COALESCE(model, 'unknown')
|
|
52
|
+
ORDER BY provider, model
|
|
53
|
+
`;
|
|
54
|
+
const results = db.prepare(query).all();
|
|
55
|
+
db.close();
|
|
56
|
+
return JSON.stringify(results);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.error("Error in ProviderModelUsageTool:", error);
|
|
60
|
+
return JSON.stringify({ error: `Failed to get provider usage stats: ${error.message}` });
|
|
61
|
+
}
|
|
62
|
+
}, {
|
|
63
|
+
name: "provider_model_usage",
|
|
64
|
+
description: "Returns token usage statistics grouped by provider and model.",
|
|
65
|
+
schema: z.object({}),
|
|
66
|
+
});
|
|
67
|
+
// Tool for querying token usage statistics from the database
|
|
68
|
+
export const TokenUsageTool = tool(async ({ timeRange }) => {
|
|
69
|
+
try {
|
|
70
|
+
// Connect to database
|
|
71
|
+
const db = new Database(dbPath);
|
|
72
|
+
let query = "SELECT SUM(input_tokens) as inputTokens, SUM(output_tokens) as outputTokens, SUM(input_tokens + output_tokens) as totalTokens FROM messages";
|
|
73
|
+
const params = [];
|
|
74
|
+
if (timeRange) {
|
|
75
|
+
query += " WHERE timestamp BETWEEN ? AND ?";
|
|
76
|
+
params.push(timeRange.start);
|
|
77
|
+
params.push(timeRange.end);
|
|
78
|
+
}
|
|
79
|
+
const result = db.prepare(query).get(params);
|
|
80
|
+
db.close();
|
|
81
|
+
// Handle potential null values
|
|
82
|
+
const tokenStats = {
|
|
83
|
+
totalTokens: result.totalTokens || 0,
|
|
84
|
+
inputTokens: result.inputTokens || 0,
|
|
85
|
+
outputTokens: result.outputTokens || 0,
|
|
86
|
+
timestamp: new Date().toISOString()
|
|
87
|
+
};
|
|
88
|
+
return JSON.stringify(tokenStats);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error("Error in TokenUsageTool:", error);
|
|
92
|
+
return JSON.stringify({ error: `Failed to get token usage: ${error.message}` });
|
|
93
|
+
}
|
|
94
|
+
}, {
|
|
95
|
+
name: "token_usage",
|
|
96
|
+
description: "Returns token usage statistics. Accepts an optional 'timeRange' parameter with start and end timestamps for filtering.",
|
|
97
|
+
schema: z.object({
|
|
98
|
+
timeRange: z.object({
|
|
99
|
+
start: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/, "ISO date string"),
|
|
100
|
+
end: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z?$/, "ISO date string"),
|
|
101
|
+
}).optional(),
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
@@ -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
|
+
});
|