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.
Files changed (38) hide show
  1. package/README.md +273 -213
  2. package/bin/morpheus.js +30 -0
  3. package/dist/channels/telegram.js +7 -2
  4. package/dist/cli/commands/config.js +18 -3
  5. package/dist/cli/commands/init.js +3 -3
  6. package/dist/cli/index.js +8 -0
  7. package/dist/config/mcp-loader.js +42 -0
  8. package/dist/config/schemas.js +22 -0
  9. package/dist/http/__tests__/auth.test.js +53 -0
  10. package/dist/http/api.js +23 -0
  11. package/dist/http/middleware/auth.js +23 -0
  12. package/dist/http/server.js +2 -1
  13. package/dist/runtime/__tests__/agent_persistence.test.js +39 -33
  14. package/dist/runtime/__tests__/manual_start_verify.js +1 -0
  15. package/dist/runtime/agent.js +93 -8
  16. package/dist/runtime/audio-agent.js +11 -1
  17. package/dist/runtime/display.js +12 -0
  18. package/dist/runtime/memory/sqlite.js +186 -9
  19. package/dist/runtime/providers/factory.js +28 -2
  20. package/dist/runtime/scaffold.js +5 -0
  21. package/dist/runtime/tools/__tests__/tools.test.js +127 -0
  22. package/dist/runtime/tools/analytics-tools.js +103 -0
  23. package/dist/runtime/tools/config-tools.js +70 -0
  24. package/dist/runtime/tools/diagnostic-tools.js +124 -0
  25. package/dist/runtime/tools/factory.js +62 -18
  26. package/dist/runtime/tools/index.js +4 -0
  27. package/dist/types/auth.js +4 -0
  28. package/dist/types/config.js +1 -0
  29. package/dist/types/mcp.js +11 -0
  30. package/dist/types/stats.js +1 -0
  31. package/dist/types/tools.js +2 -0
  32. package/dist/types/usage.js +1 -0
  33. package/dist/ui/assets/index-CMGsLiNG.css +1 -0
  34. package/dist/ui/assets/index-DpbRiL07.js +50 -0
  35. package/dist/ui/index.html +2 -2
  36. package/package.json +5 -1
  37. package/dist/ui/assets/index-D1kvj0eG.css +0 -1
  38. 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
- // 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 ?");
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
- return new HumanMessage(row.content);
161
+ msg = new HumanMessage(row.content);
162
+ break;
109
163
  case "ai":
110
- return new AIMessage(row.content);
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
- return new SystemMessage(row.content);
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
- const stmt = this.db.prepare("INSERT INTO messages (session_id, type, content, created_at) VALUES (?, ?, ?, ?)");
149
- stmt.run(this.sessionId, type, content, Date.now());
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 = tools;
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) {
@@ -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
+ });