opc-agent 0.8.0 → 0.9.0
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/dist/cli.js +45 -17
- package/dist/core/analytics-engine.d.ts +51 -0
- package/dist/core/analytics-engine.js +186 -0
- package/dist/core/cache.d.ts +47 -0
- package/dist/core/cache.js +156 -0
- package/dist/core/rate-limiter.d.ts +47 -0
- package/dist/core/rate-limiter.js +92 -0
- package/dist/i18n/index.d.ts +6 -1
- package/dist/i18n/index.js +86 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +18 -1
- package/dist/templates/data-analyst.d.ts +53 -0
- package/dist/templates/data-analyst.js +70 -0
- package/dist/templates/teacher.d.ts +58 -0
- package/dist/templates/teacher.js +78 -0
- package/dist/testing/index.d.ts +37 -0
- package/dist/testing/index.js +176 -0
- package/docs/.vitepress/config.ts +92 -0
- package/docs/api/cli.md +48 -0
- package/docs/api/sdk.md +80 -0
- package/docs/guide/configuration.md +79 -0
- package/docs/guide/deployment.md +42 -0
- package/docs/guide/testing.md +84 -0
- package/docs/index.md +27 -0
- package/docs/zh/api/oad-schema.md +3 -0
- package/docs/zh/guide/concepts.md +28 -0
- package/docs/zh/guide/configuration.md +39 -0
- package/docs/zh/guide/deployment.md +3 -0
- package/docs/zh/guide/getting-started.md +58 -0
- package/docs/zh/guide/templates.md +22 -0
- package/docs/zh/guide/testing.md +18 -0
- package/docs/zh/index.md +27 -0
- package/package.json +7 -3
- package/src/cli.ts +45 -19
- package/src/core/analytics-engine.ts +186 -0
- package/src/core/cache.ts +141 -0
- package/src/core/rate-limiter.ts +128 -0
- package/src/i18n/index.ts +87 -1
- package/src/index.ts +12 -0
- package/src/templates/data-analyst.ts +70 -0
- package/src/templates/teacher.ts +79 -0
- package/src/testing/index.ts +181 -0
package/dist/cli.js
CHANGED
|
@@ -50,7 +50,11 @@ const content_writer_1 = require("./templates/content-writer");
|
|
|
50
50
|
const legal_assistant_1 = require("./templates/legal-assistant");
|
|
51
51
|
const financial_advisor_1 = require("./templates/financial-advisor");
|
|
52
52
|
const executive_assistant_1 = require("./templates/executive-assistant");
|
|
53
|
+
const data_analyst_1 = require("./templates/data-analyst");
|
|
54
|
+
const teacher_1 = require("./templates/teacher");
|
|
53
55
|
const analytics_1 = require("./analytics");
|
|
56
|
+
const analytics_engine_1 = require("./core/analytics-engine");
|
|
57
|
+
const testing_1 = require("./testing");
|
|
54
58
|
const openclaw_1 = require("./deploy/openclaw");
|
|
55
59
|
const hermes_1 = require("./deploy/hermes");
|
|
56
60
|
const workflow_1 = require("./core/workflow");
|
|
@@ -90,6 +94,8 @@ const TEMPLATES = {
|
|
|
90
94
|
'legal-assistant': { label: 'Legal Assistant - contract review + compliance + legal research', factory: legal_assistant_1.createLegalAssistantConfig },
|
|
91
95
|
'financial-advisor': { label: 'Financial Advisor - budget analysis + expense tracking + planning', factory: financial_advisor_1.createFinancialAdvisorConfig },
|
|
92
96
|
'executive-assistant': { label: 'Executive Assistant - calendar + email drafting + meeting prep', factory: executive_assistant_1.createExecutiveAssistantConfig },
|
|
97
|
+
'data-analyst': { label: 'Data Analyst - data querying + visualization + insights', factory: data_analyst_1.createDataAnalystConfig },
|
|
98
|
+
'teacher': { label: 'Teacher - lesson planning + quizzes + concept explanation', factory: teacher_1.createTeacherConfig },
|
|
93
99
|
};
|
|
94
100
|
async function promptUser(question, defaultValue) {
|
|
95
101
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -113,7 +119,7 @@ async function select(question, options) {
|
|
|
113
119
|
program
|
|
114
120
|
.name('opc')
|
|
115
121
|
.description('OPC Agent - Open Agent Framework for business workstations')
|
|
116
|
-
.version('0.
|
|
122
|
+
.version('0.9.0');
|
|
117
123
|
// ── Init command ─────────────────────────────────────────────
|
|
118
124
|
program
|
|
119
125
|
.command('init')
|
|
@@ -396,25 +402,47 @@ program
|
|
|
396
402
|
// ── Test command ─────────────────────────────────────────────
|
|
397
403
|
program
|
|
398
404
|
.command('test')
|
|
399
|
-
.description('Run agent in
|
|
405
|
+
.description('Run agent tests defined in OAD or tests.yaml')
|
|
400
406
|
.option('-f, --file <file>', 'OAD file', 'oad.yaml')
|
|
407
|
+
.option('--json', 'Output as JSON')
|
|
401
408
|
.action(async (opts) => {
|
|
402
409
|
loadDotEnv();
|
|
403
|
-
console.log(`\n${icon.gear} Running agent
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
410
|
+
console.log(`\n${icon.gear} Running agent tests...\n`);
|
|
411
|
+
try {
|
|
412
|
+
const report = await (0, testing_1.runTests)(opts.file);
|
|
413
|
+
if (opts.json) {
|
|
414
|
+
console.log(JSON.stringify(report, null, 2));
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
console.log((0, testing_1.formatReport)(report));
|
|
418
|
+
}
|
|
419
|
+
process.exit(report.failed > 0 ? 1 : 0);
|
|
420
|
+
}
|
|
421
|
+
catch (err) {
|
|
422
|
+
console.error(`${icon.error} Test failed:`, err instanceof Error ? err.message : err);
|
|
423
|
+
process.exit(1);
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
// ── Analytics command ────────────────────────────────────────
|
|
427
|
+
program
|
|
428
|
+
.command('analytics')
|
|
429
|
+
.description('Show agent analytics and usage stats')
|
|
430
|
+
.option('--json', 'Output as JSON')
|
|
431
|
+
.option('--clear', 'Clear analytics data')
|
|
432
|
+
.action(async (opts) => {
|
|
433
|
+
const engine = new analytics_engine_1.AnalyticsEngine('.');
|
|
434
|
+
if (opts.clear) {
|
|
435
|
+
engine.clear();
|
|
436
|
+
console.log(`${icon.success} Analytics data cleared.`);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
const stats = engine.getStats();
|
|
440
|
+
if (opts.json) {
|
|
441
|
+
console.log(JSON.stringify(stats, null, 2));
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
console.log(analytics_engine_1.AnalyticsEngine.formatStats(stats));
|
|
445
|
+
}
|
|
418
446
|
});
|
|
419
447
|
// ── Dev command ──────────────────────────────────────────────
|
|
420
448
|
program
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface AnalyticsEvent {
|
|
2
|
+
type: 'message' | 'llm_call' | 'tool_use' | 'error';
|
|
3
|
+
timestamp: number;
|
|
4
|
+
data: Record<string, any>;
|
|
5
|
+
}
|
|
6
|
+
export interface AnalyticsStats {
|
|
7
|
+
totalMessages: number;
|
|
8
|
+
totalLLMCalls: number;
|
|
9
|
+
totalToolUses: number;
|
|
10
|
+
totalErrors: number;
|
|
11
|
+
avgResponseTimeMs: number;
|
|
12
|
+
totalTokens: {
|
|
13
|
+
input: number;
|
|
14
|
+
output: number;
|
|
15
|
+
total: number;
|
|
16
|
+
};
|
|
17
|
+
topSkills: {
|
|
18
|
+
name: string;
|
|
19
|
+
count: number;
|
|
20
|
+
}[];
|
|
21
|
+
topErrors: {
|
|
22
|
+
message: string;
|
|
23
|
+
count: number;
|
|
24
|
+
}[];
|
|
25
|
+
messagesPerDay: Record<string, number>;
|
|
26
|
+
period: {
|
|
27
|
+
from: number;
|
|
28
|
+
to: number;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
export declare class AnalyticsEngine {
|
|
32
|
+
private dataDir;
|
|
33
|
+
private eventsFile;
|
|
34
|
+
private events;
|
|
35
|
+
constructor(dataDir?: string);
|
|
36
|
+
private load;
|
|
37
|
+
private save;
|
|
38
|
+
track(type: AnalyticsEvent['type'], data: Record<string, any>): void;
|
|
39
|
+
trackMessage(userId: string, responseTimeMs: number, tokensIn: number, tokensOut: number): void;
|
|
40
|
+
trackLLMCall(provider: string, model: string, tokensIn: number, tokensOut: number, latencyMs: number): void;
|
|
41
|
+
trackToolUse(toolName: string, success: boolean, latencyMs: number): void;
|
|
42
|
+
trackError(error: string, context?: string): void;
|
|
43
|
+
getStats(fromTs?: number, toTs?: number): AnalyticsStats;
|
|
44
|
+
getRecentEvents(limit?: number): AnalyticsEvent[];
|
|
45
|
+
clear(): void;
|
|
46
|
+
/**
|
|
47
|
+
* Format stats for CLI display.
|
|
48
|
+
*/
|
|
49
|
+
static formatStats(stats: AnalyticsStats): string;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=analytics-engine.d.ts.map
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.AnalyticsEngine = void 0;
|
|
37
|
+
/**
|
|
38
|
+
* Analytics Engine - Persistent analytics with JSON file storage.
|
|
39
|
+
* Tracks every message, LLM call, tool use, and error with timestamps.
|
|
40
|
+
*/
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
class AnalyticsEngine {
|
|
44
|
+
dataDir;
|
|
45
|
+
eventsFile;
|
|
46
|
+
events = [];
|
|
47
|
+
constructor(dataDir = '.') {
|
|
48
|
+
this.dataDir = path.resolve(dataDir, 'data');
|
|
49
|
+
this.eventsFile = path.join(this.dataDir, 'analytics.json');
|
|
50
|
+
this.load();
|
|
51
|
+
}
|
|
52
|
+
load() {
|
|
53
|
+
try {
|
|
54
|
+
if (fs.existsSync(this.eventsFile)) {
|
|
55
|
+
const raw = fs.readFileSync(this.eventsFile, 'utf-8');
|
|
56
|
+
this.events = JSON.parse(raw);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
this.events = [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
save() {
|
|
64
|
+
if (!fs.existsSync(this.dataDir)) {
|
|
65
|
+
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
66
|
+
}
|
|
67
|
+
// Keep last 10000 events to prevent unbounded growth
|
|
68
|
+
if (this.events.length > 10000) {
|
|
69
|
+
this.events = this.events.slice(-10000);
|
|
70
|
+
}
|
|
71
|
+
fs.writeFileSync(this.eventsFile, JSON.stringify(this.events, null, 2));
|
|
72
|
+
}
|
|
73
|
+
track(type, data) {
|
|
74
|
+
this.events.push({ type, timestamp: Date.now(), data });
|
|
75
|
+
this.save();
|
|
76
|
+
}
|
|
77
|
+
trackMessage(userId, responseTimeMs, tokensIn, tokensOut) {
|
|
78
|
+
this.track('message', { userId, responseTimeMs, tokensIn, tokensOut });
|
|
79
|
+
}
|
|
80
|
+
trackLLMCall(provider, model, tokensIn, tokensOut, latencyMs) {
|
|
81
|
+
this.track('llm_call', { provider, model, tokensIn, tokensOut, latencyMs });
|
|
82
|
+
}
|
|
83
|
+
trackToolUse(toolName, success, latencyMs) {
|
|
84
|
+
this.track('tool_use', { toolName, success, latencyMs });
|
|
85
|
+
}
|
|
86
|
+
trackError(error, context) {
|
|
87
|
+
this.track('error', { error, context });
|
|
88
|
+
}
|
|
89
|
+
getStats(fromTs, toTs) {
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
const from = fromTs ?? 0;
|
|
92
|
+
const to = toTs ?? now;
|
|
93
|
+
const filtered = this.events.filter(e => e.timestamp >= from && e.timestamp <= to);
|
|
94
|
+
const messages = filtered.filter(e => e.type === 'message');
|
|
95
|
+
const llmCalls = filtered.filter(e => e.type === 'llm_call');
|
|
96
|
+
const toolUses = filtered.filter(e => e.type === 'tool_use');
|
|
97
|
+
const errors = filtered.filter(e => e.type === 'error');
|
|
98
|
+
// Avg response time
|
|
99
|
+
const totalResponseTime = messages.reduce((sum, e) => sum + (e.data.responseTimeMs ?? 0), 0);
|
|
100
|
+
const avgResponseTimeMs = messages.length > 0 ? Math.round(totalResponseTime / messages.length) : 0;
|
|
101
|
+
// Total tokens
|
|
102
|
+
const totalTokensIn = llmCalls.reduce((sum, e) => sum + (e.data.tokensIn ?? 0), 0);
|
|
103
|
+
const totalTokensOut = llmCalls.reduce((sum, e) => sum + (e.data.tokensOut ?? 0), 0);
|
|
104
|
+
// Top skills (from tool_use)
|
|
105
|
+
const skillCounts = {};
|
|
106
|
+
for (const e of toolUses) {
|
|
107
|
+
const name = e.data.toolName ?? 'unknown';
|
|
108
|
+
skillCounts[name] = (skillCounts[name] ?? 0) + 1;
|
|
109
|
+
}
|
|
110
|
+
const topSkills = Object.entries(skillCounts)
|
|
111
|
+
.sort((a, b) => b[1] - a[1])
|
|
112
|
+
.slice(0, 10)
|
|
113
|
+
.map(([name, count]) => ({ name, count }));
|
|
114
|
+
// Top errors
|
|
115
|
+
const errorCounts = {};
|
|
116
|
+
for (const e of errors) {
|
|
117
|
+
const msg = e.data.error ?? 'unknown';
|
|
118
|
+
errorCounts[msg] = (errorCounts[msg] ?? 0) + 1;
|
|
119
|
+
}
|
|
120
|
+
const topErrors = Object.entries(errorCounts)
|
|
121
|
+
.sort((a, b) => b[1] - a[1])
|
|
122
|
+
.slice(0, 10)
|
|
123
|
+
.map(([message, count]) => ({ message, count }));
|
|
124
|
+
// Messages per day
|
|
125
|
+
const messagesPerDay = {};
|
|
126
|
+
for (const e of messages) {
|
|
127
|
+
const day = new Date(e.timestamp).toISOString().slice(0, 10);
|
|
128
|
+
messagesPerDay[day] = (messagesPerDay[day] ?? 0) + 1;
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
totalMessages: messages.length,
|
|
132
|
+
totalLLMCalls: llmCalls.length,
|
|
133
|
+
totalToolUses: toolUses.length,
|
|
134
|
+
totalErrors: errors.length,
|
|
135
|
+
avgResponseTimeMs,
|
|
136
|
+
totalTokens: { input: totalTokensIn, output: totalTokensOut, total: totalTokensIn + totalTokensOut },
|
|
137
|
+
topSkills,
|
|
138
|
+
topErrors,
|
|
139
|
+
messagesPerDay,
|
|
140
|
+
period: { from, to },
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
getRecentEvents(limit = 50) {
|
|
144
|
+
return this.events.slice(-limit);
|
|
145
|
+
}
|
|
146
|
+
clear() {
|
|
147
|
+
this.events = [];
|
|
148
|
+
this.save();
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Format stats for CLI display.
|
|
152
|
+
*/
|
|
153
|
+
static formatStats(stats) {
|
|
154
|
+
const lines = [];
|
|
155
|
+
lines.push('');
|
|
156
|
+
lines.push('══════════════════════════════════════════');
|
|
157
|
+
lines.push(' OPC Agent Analytics');
|
|
158
|
+
lines.push('══════════════════════════════════════════');
|
|
159
|
+
lines.push('');
|
|
160
|
+
lines.push(` 📨 Messages: ${stats.totalMessages}`);
|
|
161
|
+
lines.push(` 🤖 LLM Calls: ${stats.totalLLMCalls}`);
|
|
162
|
+
lines.push(` 🔧 Tool Uses: ${stats.totalToolUses}`);
|
|
163
|
+
lines.push(` ❌ Errors: ${stats.totalErrors}`);
|
|
164
|
+
lines.push(` ⏱ Avg Response: ${stats.avgResponseTimeMs}ms`);
|
|
165
|
+
lines.push(` 🪙 Tokens: ${stats.totalTokens.total} (in: ${stats.totalTokens.input}, out: ${stats.totalTokens.output})`);
|
|
166
|
+
lines.push('');
|
|
167
|
+
if (stats.topSkills.length > 0) {
|
|
168
|
+
lines.push(' Top Skills:');
|
|
169
|
+
for (const s of stats.topSkills.slice(0, 5)) {
|
|
170
|
+
lines.push(` • ${s.name}: ${s.count}`);
|
|
171
|
+
}
|
|
172
|
+
lines.push('');
|
|
173
|
+
}
|
|
174
|
+
if (stats.topErrors.length > 0) {
|
|
175
|
+
lines.push(' Top Errors:');
|
|
176
|
+
for (const e of stats.topErrors.slice(0, 3)) {
|
|
177
|
+
lines.push(` • ${e.message}: ${e.count}`);
|
|
178
|
+
}
|
|
179
|
+
lines.push('');
|
|
180
|
+
}
|
|
181
|
+
lines.push('──────────────────────────────────────────');
|
|
182
|
+
return lines.join('\n');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
exports.AnalyticsEngine = AnalyticsEngine;
|
|
186
|
+
//# sourceMappingURL=analytics-engine.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export interface CacheEntry {
|
|
2
|
+
key: string;
|
|
3
|
+
value: string;
|
|
4
|
+
createdAt: number;
|
|
5
|
+
ttlMs: number;
|
|
6
|
+
hits: number;
|
|
7
|
+
}
|
|
8
|
+
export interface CacheConfig {
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
ttlMs: number;
|
|
11
|
+
maxEntries: number;
|
|
12
|
+
dataDir: string;
|
|
13
|
+
}
|
|
14
|
+
export declare class LLMCache {
|
|
15
|
+
private cache;
|
|
16
|
+
private config;
|
|
17
|
+
private filePath;
|
|
18
|
+
private stats;
|
|
19
|
+
constructor(config?: Partial<CacheConfig>);
|
|
20
|
+
private load;
|
|
21
|
+
private save;
|
|
22
|
+
private isExpired;
|
|
23
|
+
/**
|
|
24
|
+
* Generate a cache key from messages and system prompt.
|
|
25
|
+
*/
|
|
26
|
+
static makeKey(messages: Array<{
|
|
27
|
+
role: string;
|
|
28
|
+
content: string;
|
|
29
|
+
}>, systemPrompt?: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Get a cached response. Returns null if not found or expired.
|
|
32
|
+
*/
|
|
33
|
+
get(key: string): string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Set a cached response.
|
|
36
|
+
*/
|
|
37
|
+
set(key: string, value: string, ttlMs?: number): void;
|
|
38
|
+
getStats(): {
|
|
39
|
+
hits: number;
|
|
40
|
+
misses: number;
|
|
41
|
+
evictions: number;
|
|
42
|
+
size: number;
|
|
43
|
+
hitRate: string;
|
|
44
|
+
};
|
|
45
|
+
clear(): void;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.LLMCache = void 0;
|
|
37
|
+
/**
|
|
38
|
+
* Caching Layer - Cache LLM responses with configurable TTL.
|
|
39
|
+
* Hash-based key from input messages + system prompt.
|
|
40
|
+
*/
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const crypto = __importStar(require("crypto"));
|
|
44
|
+
class LLMCache {
|
|
45
|
+
cache = new Map();
|
|
46
|
+
config;
|
|
47
|
+
filePath;
|
|
48
|
+
stats = { hits: 0, misses: 0, evictions: 0 };
|
|
49
|
+
constructor(config) {
|
|
50
|
+
this.config = {
|
|
51
|
+
enabled: config?.enabled ?? true,
|
|
52
|
+
ttlMs: config?.ttlMs ?? 3600_000, // 1 hour default
|
|
53
|
+
maxEntries: config?.maxEntries ?? 1000,
|
|
54
|
+
dataDir: config?.dataDir ?? '.',
|
|
55
|
+
};
|
|
56
|
+
this.filePath = path.join(this.config.dataDir, 'data', 'cache.json');
|
|
57
|
+
this.load();
|
|
58
|
+
}
|
|
59
|
+
load() {
|
|
60
|
+
try {
|
|
61
|
+
if (fs.existsSync(this.filePath)) {
|
|
62
|
+
const raw = fs.readFileSync(this.filePath, 'utf-8');
|
|
63
|
+
const entries = JSON.parse(raw);
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
if (!this.isExpired(entry)) {
|
|
66
|
+
this.cache.set(entry.key, entry);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// ignore
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
save() {
|
|
76
|
+
const dir = path.dirname(this.filePath);
|
|
77
|
+
if (!fs.existsSync(dir))
|
|
78
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
79
|
+
const entries = Array.from(this.cache.values());
|
|
80
|
+
fs.writeFileSync(this.filePath, JSON.stringify(entries));
|
|
81
|
+
}
|
|
82
|
+
isExpired(entry) {
|
|
83
|
+
return Date.now() - entry.createdAt > entry.ttlMs;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Generate a cache key from messages and system prompt.
|
|
87
|
+
*/
|
|
88
|
+
static makeKey(messages, systemPrompt) {
|
|
89
|
+
const payload = JSON.stringify({ systemPrompt, messages: messages.map(m => ({ role: m.role, content: m.content })) });
|
|
90
|
+
return crypto.createHash('sha256').update(payload).digest('hex').slice(0, 16);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get a cached response. Returns null if not found or expired.
|
|
94
|
+
*/
|
|
95
|
+
get(key) {
|
|
96
|
+
if (!this.config.enabled)
|
|
97
|
+
return null;
|
|
98
|
+
const entry = this.cache.get(key);
|
|
99
|
+
if (!entry || this.isExpired(entry)) {
|
|
100
|
+
if (entry) {
|
|
101
|
+
this.cache.delete(key);
|
|
102
|
+
this.stats.evictions++;
|
|
103
|
+
}
|
|
104
|
+
this.stats.misses++;
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
entry.hits++;
|
|
108
|
+
this.stats.hits++;
|
|
109
|
+
return entry.value;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Set a cached response.
|
|
113
|
+
*/
|
|
114
|
+
set(key, value, ttlMs) {
|
|
115
|
+
if (!this.config.enabled)
|
|
116
|
+
return;
|
|
117
|
+
// Evict oldest if at capacity
|
|
118
|
+
if (this.cache.size >= this.config.maxEntries) {
|
|
119
|
+
let oldestKey = null;
|
|
120
|
+
let oldestTime = Infinity;
|
|
121
|
+
for (const [k, v] of this.cache) {
|
|
122
|
+
if (v.createdAt < oldestTime) {
|
|
123
|
+
oldestTime = v.createdAt;
|
|
124
|
+
oldestKey = k;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (oldestKey) {
|
|
128
|
+
this.cache.delete(oldestKey);
|
|
129
|
+
this.stats.evictions++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
this.cache.set(key, {
|
|
133
|
+
key,
|
|
134
|
+
value,
|
|
135
|
+
createdAt: Date.now(),
|
|
136
|
+
ttlMs: ttlMs ?? this.config.ttlMs,
|
|
137
|
+
hits: 0,
|
|
138
|
+
});
|
|
139
|
+
this.save();
|
|
140
|
+
}
|
|
141
|
+
getStats() {
|
|
142
|
+
const total = this.stats.hits + this.stats.misses;
|
|
143
|
+
return {
|
|
144
|
+
...this.stats,
|
|
145
|
+
size: this.cache.size,
|
|
146
|
+
hitRate: total > 0 ? `${((this.stats.hits / total) * 100).toFixed(1)}%` : '0%',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
clear() {
|
|
150
|
+
this.cache.clear();
|
|
151
|
+
this.stats = { hits: 0, misses: 0, evictions: 0 };
|
|
152
|
+
this.save();
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
exports.LLMCache = LLMCache;
|
|
156
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiter - Per-user and per-provider rate limiting with queuing.
|
|
3
|
+
*/
|
|
4
|
+
interface RateLimitConfig {
|
|
5
|
+
maxRequests: number;
|
|
6
|
+
windowMs: number;
|
|
7
|
+
}
|
|
8
|
+
export declare class RateLimiter {
|
|
9
|
+
private userLimits;
|
|
10
|
+
private providerLimits;
|
|
11
|
+
private userConfig;
|
|
12
|
+
private providerConfig;
|
|
13
|
+
private maxQueueSize;
|
|
14
|
+
constructor(opts?: {
|
|
15
|
+
userLimit?: RateLimitConfig;
|
|
16
|
+
providerLimit?: RateLimitConfig;
|
|
17
|
+
maxQueueSize?: number;
|
|
18
|
+
});
|
|
19
|
+
/**
|
|
20
|
+
* Check if a request is allowed. If not, queues it with backpressure.
|
|
21
|
+
* Returns a promise that resolves when the request can proceed.
|
|
22
|
+
*/
|
|
23
|
+
acquire(userId: string, provider: string): Promise<void>;
|
|
24
|
+
private checkLimit;
|
|
25
|
+
private processQueue;
|
|
26
|
+
/**
|
|
27
|
+
* Get current usage stats.
|
|
28
|
+
*/
|
|
29
|
+
getUsage(userId?: string, provider?: string): {
|
|
30
|
+
user?: {
|
|
31
|
+
used: number;
|
|
32
|
+
limit: number;
|
|
33
|
+
windowMs: number;
|
|
34
|
+
};
|
|
35
|
+
provider?: {
|
|
36
|
+
used: number;
|
|
37
|
+
limit: number;
|
|
38
|
+
windowMs: number;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Reset all limits.
|
|
43
|
+
*/
|
|
44
|
+
reset(): void;
|
|
45
|
+
}
|
|
46
|
+
export {};
|
|
47
|
+
//# sourceMappingURL=rate-limiter.d.ts.map
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Rate Limiter - Per-user and per-provider rate limiting with queuing.
|
|
4
|
+
*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RateLimiter = void 0;
|
|
7
|
+
class RateLimiter {
|
|
8
|
+
userLimits = new Map();
|
|
9
|
+
providerLimits = new Map();
|
|
10
|
+
userConfig;
|
|
11
|
+
providerConfig;
|
|
12
|
+
maxQueueSize;
|
|
13
|
+
constructor(opts) {
|
|
14
|
+
this.userConfig = opts?.userLimit ?? { maxRequests: 60, windowMs: 60_000 };
|
|
15
|
+
this.providerConfig = opts?.providerLimit ?? { maxRequests: 100, windowMs: 60_000 };
|
|
16
|
+
this.maxQueueSize = opts?.maxQueueSize ?? 50;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Check if a request is allowed. If not, queues it with backpressure.
|
|
20
|
+
* Returns a promise that resolves when the request can proceed.
|
|
21
|
+
*/
|
|
22
|
+
async acquire(userId, provider) {
|
|
23
|
+
await this.checkLimit(userId, this.userLimits, this.userConfig, 'user');
|
|
24
|
+
await this.checkLimit(provider, this.providerLimits, this.providerConfig, 'provider');
|
|
25
|
+
}
|
|
26
|
+
async checkLimit(key, limits, config, type) {
|
|
27
|
+
let entry = limits.get(key);
|
|
28
|
+
if (!entry) {
|
|
29
|
+
entry = { timestamps: [], queue: [] };
|
|
30
|
+
limits.set(key, entry);
|
|
31
|
+
}
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
// Prune old timestamps
|
|
34
|
+
entry.timestamps = entry.timestamps.filter(t => t > now - config.windowMs);
|
|
35
|
+
if (entry.timestamps.length < config.maxRequests) {
|
|
36
|
+
entry.timestamps.push(now);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
// Rate limited - queue with backpressure
|
|
40
|
+
if (entry.queue.length >= this.maxQueueSize) {
|
|
41
|
+
throw new Error(`Rate limit exceeded for ${type} "${key}" and queue is full (${this.maxQueueSize})`);
|
|
42
|
+
}
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
entry.queue.push({ resolve, reject });
|
|
45
|
+
// Set timeout to process queue when window expires
|
|
46
|
+
const oldestTs = entry.timestamps[0];
|
|
47
|
+
const waitMs = oldestTs + config.windowMs - now + 10;
|
|
48
|
+
setTimeout(() => {
|
|
49
|
+
this.processQueue(key, limits, config);
|
|
50
|
+
}, Math.max(waitMs, 100));
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
processQueue(key, limits, config) {
|
|
54
|
+
const entry = limits.get(key);
|
|
55
|
+
if (!entry || entry.queue.length === 0)
|
|
56
|
+
return;
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
entry.timestamps = entry.timestamps.filter(t => t > now - config.windowMs);
|
|
59
|
+
while (entry.queue.length > 0 && entry.timestamps.length < config.maxRequests) {
|
|
60
|
+
entry.timestamps.push(now);
|
|
61
|
+
const item = entry.queue.shift();
|
|
62
|
+
item.resolve();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Get current usage stats.
|
|
67
|
+
*/
|
|
68
|
+
getUsage(userId, provider) {
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
const result = {};
|
|
71
|
+
if (userId) {
|
|
72
|
+
const entry = this.userLimits.get(userId);
|
|
73
|
+
const used = entry ? entry.timestamps.filter(t => t > now - this.userConfig.windowMs).length : 0;
|
|
74
|
+
result.user = { used, limit: this.userConfig.maxRequests, windowMs: this.userConfig.windowMs };
|
|
75
|
+
}
|
|
76
|
+
if (provider) {
|
|
77
|
+
const entry = this.providerLimits.get(provider);
|
|
78
|
+
const used = entry ? entry.timestamps.filter(t => t > now - this.providerConfig.windowMs).length : 0;
|
|
79
|
+
result.provider = { used, limit: this.providerConfig.maxRequests, windowMs: this.providerConfig.windowMs };
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Reset all limits.
|
|
85
|
+
*/
|
|
86
|
+
reset() {
|
|
87
|
+
this.userLimits.clear();
|
|
88
|
+
this.providerLimits.clear();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
exports.RateLimiter = RateLimiter;
|
|
92
|
+
//# sourceMappingURL=rate-limiter.js.map
|