opc-agent 0.6.0 → 0.7.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/channels/web.d.ts +7 -1
- package/dist/channels/web.js +165 -4
- package/dist/core/auth.d.ts +13 -0
- package/dist/core/auth.js +41 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +13 -1
- package/dist/schema/oad.d.ts +69 -0
- package/dist/schema/oad.js +7 -1
- package/dist/skills/document.d.ts +27 -0
- package/dist/skills/document.js +80 -0
- package/dist/skills/http.d.ts +8 -0
- package/dist/skills/http.js +34 -0
- package/dist/skills/scheduler.d.ts +21 -0
- package/dist/skills/scheduler.js +70 -0
- package/dist/skills/webhook-trigger.d.ts +17 -0
- package/dist/skills/webhook-trigger.js +49 -0
- package/package.json +1 -1
- package/src/channels/web.ts +175 -4
- package/src/core/auth.ts +57 -0
- package/src/index.ts +11 -0
- package/src/schema/oad.ts +7 -0
- package/src/skills/document.ts +100 -0
- package/src/skills/http.ts +35 -0
- package/src/skills/scheduler.ts +80 -0
- package/src/skills/webhook-trigger.ts +59 -0
- package/tests/v070.test.ts +76 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { BaseSkill } from './base';
|
|
2
|
+
import type { AgentContext, Message, SkillResult } from '../core/types';
|
|
3
|
+
export interface ScheduledTask {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
cronExpr: string;
|
|
7
|
+
action: string;
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
lastRun?: number;
|
|
10
|
+
nextRun?: number;
|
|
11
|
+
}
|
|
12
|
+
export declare class SchedulerSkill extends BaseSkill {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
private tasks;
|
|
16
|
+
private timers;
|
|
17
|
+
execute(context: AgentContext, message: Message): Promise<SkillResult>;
|
|
18
|
+
private parseCronToInterval;
|
|
19
|
+
destroy(): void;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=scheduler.d.ts.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SchedulerSkill = void 0;
|
|
4
|
+
const base_1 = require("./base");
|
|
5
|
+
class SchedulerSkill extends base_1.BaseSkill {
|
|
6
|
+
name = 'scheduler';
|
|
7
|
+
description = 'Schedule recurring tasks. Usage: schedule list | schedule add <name> <cron> <action> | schedule remove <id>';
|
|
8
|
+
tasks = new Map();
|
|
9
|
+
timers = new Map();
|
|
10
|
+
async execute(context, message) {
|
|
11
|
+
const text = message.content.trim();
|
|
12
|
+
if (/^schedule\s+list$/i.test(text)) {
|
|
13
|
+
if (this.tasks.size === 0)
|
|
14
|
+
return this.match('No scheduled tasks.');
|
|
15
|
+
const lines = Array.from(this.tasks.values()).map(t => `• ${t.name} (${t.id}) — ${t.cronExpr} — ${t.enabled ? '✅' : '❌'} — Last: ${t.lastRun ? new Date(t.lastRun).toISOString() : 'never'}`);
|
|
16
|
+
return this.match(`Scheduled tasks:\n${lines.join('\n')}`);
|
|
17
|
+
}
|
|
18
|
+
const addMatch = text.match(/^schedule\s+add\s+(\S+)\s+(".*?"|\S+)\s+(.+)$/i);
|
|
19
|
+
if (addMatch) {
|
|
20
|
+
const [, name, cronExpr, action] = addMatch;
|
|
21
|
+
const id = `task_${Date.now().toString(36)}`;
|
|
22
|
+
const task = {
|
|
23
|
+
id, name, cronExpr: cronExpr.replace(/"/g, ''), action, enabled: true,
|
|
24
|
+
};
|
|
25
|
+
this.tasks.set(id, task);
|
|
26
|
+
// Simple interval-based scheduling (parse cron for interval in minutes)
|
|
27
|
+
const intervalMs = this.parseCronToInterval(task.cronExpr);
|
|
28
|
+
if (intervalMs > 0) {
|
|
29
|
+
const timer = setInterval(() => {
|
|
30
|
+
task.lastRun = Date.now();
|
|
31
|
+
}, intervalMs);
|
|
32
|
+
this.timers.set(id, timer);
|
|
33
|
+
}
|
|
34
|
+
return this.match(`Task scheduled: ${name} (${id}) — ${task.cronExpr} → "${action}"`);
|
|
35
|
+
}
|
|
36
|
+
const rmMatch = text.match(/^schedule\s+remove\s+(\S+)$/i);
|
|
37
|
+
if (rmMatch) {
|
|
38
|
+
const id = rmMatch[1];
|
|
39
|
+
const timer = this.timers.get(id);
|
|
40
|
+
if (timer)
|
|
41
|
+
clearInterval(timer);
|
|
42
|
+
this.timers.delete(id);
|
|
43
|
+
const removed = this.tasks.delete(id);
|
|
44
|
+
return this.match(removed ? `Task ${id} removed.` : `Task ${id} not found.`);
|
|
45
|
+
}
|
|
46
|
+
return this.noMatch();
|
|
47
|
+
}
|
|
48
|
+
parseCronToInterval(expr) {
|
|
49
|
+
// Simple: support "every Xm" or "every Xh" or basic intervals
|
|
50
|
+
const m = expr.match(/every\s+(\d+)\s*(m|min|h|hr|s|sec)/i);
|
|
51
|
+
if (m) {
|
|
52
|
+
const val = parseInt(m[1]);
|
|
53
|
+
const unit = m[2].toLowerCase();
|
|
54
|
+
if (unit.startsWith('h'))
|
|
55
|
+
return val * 3600_000;
|
|
56
|
+
if (unit.startsWith('m'))
|
|
57
|
+
return val * 60_000;
|
|
58
|
+
if (unit.startsWith('s'))
|
|
59
|
+
return val * 1000;
|
|
60
|
+
}
|
|
61
|
+
return 0; // Unknown cron format, no auto-schedule
|
|
62
|
+
}
|
|
63
|
+
destroy() {
|
|
64
|
+
for (const timer of this.timers.values())
|
|
65
|
+
clearInterval(timer);
|
|
66
|
+
this.timers.clear();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
exports.SchedulerSkill = SchedulerSkill;
|
|
70
|
+
//# sourceMappingURL=scheduler.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { BaseSkill } from './base';
|
|
2
|
+
import type { AgentContext, Message, SkillResult } from '../core/types';
|
|
3
|
+
export interface WebhookTarget {
|
|
4
|
+
name: string;
|
|
5
|
+
url: string;
|
|
6
|
+
method?: string;
|
|
7
|
+
headers?: Record<string, string>;
|
|
8
|
+
secret?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare class WebhookTriggerSkill extends BaseSkill {
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
private targets;
|
|
14
|
+
registerTarget(target: WebhookTarget): void;
|
|
15
|
+
execute(context: AgentContext, message: Message): Promise<SkillResult>;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=webhook-trigger.d.ts.map
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebhookTriggerSkill = void 0;
|
|
4
|
+
const base_1 = require("./base");
|
|
5
|
+
class WebhookTriggerSkill extends base_1.BaseSkill {
|
|
6
|
+
name = 'webhook-trigger';
|
|
7
|
+
description = 'Trigger external webhooks. Usage: webhook <name> [payload JSON]';
|
|
8
|
+
targets = new Map();
|
|
9
|
+
registerTarget(target) {
|
|
10
|
+
this.targets.set(target.name, target);
|
|
11
|
+
}
|
|
12
|
+
async execute(context, message) {
|
|
13
|
+
const match = message.content.trim().match(/^webhook\s+(\S+)(?:\s+(.+))?$/is);
|
|
14
|
+
if (!match)
|
|
15
|
+
return this.noMatch();
|
|
16
|
+
const [, name, payloadStr] = match;
|
|
17
|
+
const target = this.targets.get(name);
|
|
18
|
+
if (!target) {
|
|
19
|
+
const available = Array.from(this.targets.keys()).join(', ') || 'none';
|
|
20
|
+
return this.match(`Unknown webhook "${name}". Available: ${available}`);
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const headers = {
|
|
24
|
+
'Content-Type': 'application/json',
|
|
25
|
+
'User-Agent': 'OPC-Agent/0.7.0',
|
|
26
|
+
...target.headers,
|
|
27
|
+
};
|
|
28
|
+
if (target.secret) {
|
|
29
|
+
headers['X-Webhook-Secret'] = target.secret;
|
|
30
|
+
}
|
|
31
|
+
const body = payloadStr ?? JSON.stringify({
|
|
32
|
+
agent: context.agentName,
|
|
33
|
+
timestamp: Date.now(),
|
|
34
|
+
trigger: 'manual',
|
|
35
|
+
});
|
|
36
|
+
const res = await fetch(target.url, {
|
|
37
|
+
method: target.method ?? 'POST',
|
|
38
|
+
headers,
|
|
39
|
+
body,
|
|
40
|
+
});
|
|
41
|
+
return this.match(`Webhook "${name}" triggered → ${res.status} ${res.statusText}`);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
return this.match(`Webhook error: ${err instanceof Error ? err.message : String(err)}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.WebhookTriggerSkill = WebhookTriggerSkill;
|
|
49
|
+
//# sourceMappingURL=webhook-trigger.js.map
|
package/package.json
CHANGED
package/src/channels/web.ts
CHANGED
|
@@ -4,6 +4,59 @@ import type { Message } from '../core/types';
|
|
|
4
4
|
import { BaseChannel } from './index';
|
|
5
5
|
import { KnowledgeBase } from '../core/knowledge';
|
|
6
6
|
import { createProvider, type LLMProvider } from '../providers';
|
|
7
|
+
import { createAuthMiddleware, type AuthConfig } from '../core/auth';
|
|
8
|
+
|
|
9
|
+
const AGENT_TEMPLATES = [
|
|
10
|
+
{ id: 'customer-service', name: 'Customer Service', description: 'Handle support tickets, FAQs, and customer inquiries', icon: '🎧', category: 'Business' },
|
|
11
|
+
{ id: 'code-reviewer', name: 'Code Reviewer', description: 'Review PRs, suggest improvements, check for bugs', icon: '🔍', category: 'Engineering' },
|
|
12
|
+
{ id: 'content-writer', name: 'Content Writer', description: 'Write blogs, social media posts, and marketing copy', icon: '✍️', category: 'Marketing' },
|
|
13
|
+
{ id: 'executive-assistant', name: 'Executive Assistant', description: 'Schedule management, email drafting, meeting prep', icon: '📋', category: 'Business' },
|
|
14
|
+
{ id: 'knowledge-base', name: 'Knowledge Base', description: 'RAG-powered Q&A over your documents', icon: '📚', category: 'Knowledge' },
|
|
15
|
+
{ id: 'project-manager', name: 'Project Manager', description: 'Track tasks, milestones, and team coordination', icon: '📊', category: 'Business' },
|
|
16
|
+
{ id: 'sales-assistant', name: 'Sales Assistant', description: 'Lead qualification, outreach drafting, CRM updates', icon: '💼', category: 'Sales' },
|
|
17
|
+
{ id: 'financial-advisor', name: 'Financial Advisor', description: 'Budget analysis, financial planning, cost optimization', icon: '💰', category: 'Finance' },
|
|
18
|
+
{ id: 'hr-recruiter', name: 'HR Recruiter', description: 'Resume screening, interview scheduling, candidate comms', icon: '👥', category: 'HR' },
|
|
19
|
+
{ id: 'legal-assistant', name: 'Legal Assistant', description: 'Contract review, compliance checks, legal research', icon: '⚖️', category: 'Legal' },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const TEMPLATES_HTML = `<!DOCTYPE html>
|
|
23
|
+
<html lang="en">
|
|
24
|
+
<head>
|
|
25
|
+
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
26
|
+
<title>Agent Templates</title>
|
|
27
|
+
<style>
|
|
28
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
29
|
+
body{background:#0a0a0f;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:24px}
|
|
30
|
+
h1{font-size:28px;margin-bottom:8px;color:#fff}
|
|
31
|
+
.sub{color:#888;margin-bottom:32px;font-size:14px}
|
|
32
|
+
nav{margin-bottom:24px}
|
|
33
|
+
nav a{color:#818cf8;text-decoration:none;margin-right:16px;font-size:14px}
|
|
34
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px}
|
|
35
|
+
.card{background:#12121a;border:1px solid #1e1e2e;border-radius:12px;padding:24px;cursor:pointer;transition:all .2s}
|
|
36
|
+
.card:hover{border-color:#818cf8;transform:translateY(-2px)}
|
|
37
|
+
.card .icon{font-size:32px;margin-bottom:12px}
|
|
38
|
+
.card h3{font-size:16px;color:#fff;margin-bottom:8px}
|
|
39
|
+
.card p{font-size:13px;color:#888;line-height:1.5}
|
|
40
|
+
.card .cat{font-size:11px;color:#818cf8;text-transform:uppercase;letter-spacing:1px;margin-top:12px}
|
|
41
|
+
.btn{display:inline-block;background:#2563eb;color:#fff;border:none;border-radius:8px;padding:8px 16px;font-size:13px;cursor:pointer;margin-top:12px}
|
|
42
|
+
.btn:hover{background:#1d4ed8}
|
|
43
|
+
</style>
|
|
44
|
+
</head>
|
|
45
|
+
<body>
|
|
46
|
+
<nav><a href="/">← Chat</a><a href="/dashboard">Dashboard</a><a href="/templates">Templates</a></nav>
|
|
47
|
+
<h1>🧩 Agent Templates</h1>
|
|
48
|
+
<p class="sub">Create a new agent from a pre-built template in one click.</p>
|
|
49
|
+
<div class="grid" id="grid"></div>
|
|
50
|
+
<script>
|
|
51
|
+
fetch('/api/templates').then(r=>r.json()).then(d=>{
|
|
52
|
+
const g=document.getElementById('grid');
|
|
53
|
+
d.templates.forEach(t=>{
|
|
54
|
+
g.innerHTML+=\`<div class="card"><div class="icon">\${t.icon}</div><h3>\${t.name}</h3><p>\${t.description}</p><div class="cat">\${t.category}</div><button class="btn" onclick="alert('Creating agent from template: '+'\${t.id}'+'\\\\nRun: opc init --template \${t.id}')">Use Template</button></div>\`;
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
</script>
|
|
58
|
+
</body>
|
|
59
|
+
</html>`;
|
|
7
60
|
|
|
8
61
|
const CHAT_HTML = `<!DOCTYPE html>
|
|
9
62
|
<html lang="en">
|
|
@@ -157,8 +210,12 @@ export class WebChannel extends BaseChannel {
|
|
|
157
210
|
private streamHandler: ((msg: Message, res: Response) => Promise<void>) | null = null;
|
|
158
211
|
private agentName: string = 'OPC Agent';
|
|
159
212
|
private currentProvider: string = 'openai';
|
|
160
|
-
private stats = { sessions: 0, messages: 0, totalResponseMs: 0, tokenUsage: 0, knowledgeFiles: 0, startedAt: Date.now() };
|
|
213
|
+
private stats = { sessions: 0, messages: 0, totalResponseMs: 0, tokenUsage: 0, knowledgeFiles: 0, startedAt: Date.now(), errors: 0 };
|
|
161
214
|
private eventHandlers: Map<string, Function[]> = new Map();
|
|
215
|
+
private conversations: Map<string, Message[]> = new Map();
|
|
216
|
+
private requestCount = 0;
|
|
217
|
+
private llmLatencySum = 0;
|
|
218
|
+
private llmCalls = 0;
|
|
162
219
|
|
|
163
220
|
private emit(event: string, data: any): void {
|
|
164
221
|
const handlers = this.eventHandlers.get(event) ?? [];
|
|
@@ -175,15 +232,23 @@ export class WebChannel extends BaseChannel {
|
|
|
175
232
|
this.stats.messages++;
|
|
176
233
|
this.stats.totalResponseMs += responseMs;
|
|
177
234
|
this.stats.tokenUsage += tokens;
|
|
235
|
+
this.requestCount++;
|
|
236
|
+
this.llmLatencySum += responseMs;
|
|
237
|
+
this.llmCalls++;
|
|
178
238
|
}
|
|
179
239
|
|
|
240
|
+
trackError(): void { this.stats.errors++; }
|
|
241
|
+
|
|
180
242
|
trackSession(): void { this.stats.sessions++; }
|
|
181
243
|
|
|
182
|
-
constructor(port: number = 3000) {
|
|
244
|
+
constructor(port: number = 3000, authConfig?: AuthConfig) {
|
|
183
245
|
super();
|
|
184
246
|
this.port = port;
|
|
185
247
|
this.app = express();
|
|
186
|
-
this.app.use(express.json());
|
|
248
|
+
this.app.use(express.json({ limit: '10mb' }));
|
|
249
|
+
if (authConfig && authConfig.apiKeys.length > 0) {
|
|
250
|
+
this.app.use(createAuthMiddleware(authConfig));
|
|
251
|
+
}
|
|
187
252
|
this.setupRoutes();
|
|
188
253
|
}
|
|
189
254
|
|
|
@@ -211,6 +276,7 @@ export class WebChannel extends BaseChannel {
|
|
|
211
276
|
// Streaming chat endpoint
|
|
212
277
|
this.app.post('/api/chat', async (req: Request, res: Response) => {
|
|
213
278
|
const { message, sessionId } = req.body;
|
|
279
|
+
const sid = sessionId ?? 'default';
|
|
214
280
|
if (!message) {
|
|
215
281
|
res.status(400).json({ error: 'message is required' });
|
|
216
282
|
return;
|
|
@@ -221,9 +287,13 @@ export class WebChannel extends BaseChannel {
|
|
|
221
287
|
role: 'user',
|
|
222
288
|
content: message,
|
|
223
289
|
timestamp: Date.now(),
|
|
224
|
-
metadata: { sessionId:
|
|
290
|
+
metadata: { sessionId: sid },
|
|
225
291
|
};
|
|
226
292
|
|
|
293
|
+
// Track conversation
|
|
294
|
+
if (!this.conversations.has(sid)) this.conversations.set(sid, []);
|
|
295
|
+
this.conversations.get(sid)!.push(msg);
|
|
296
|
+
|
|
227
297
|
if (this.streamHandler) {
|
|
228
298
|
try {
|
|
229
299
|
await this.streamHandler(msg, res);
|
|
@@ -301,6 +371,107 @@ export class WebChannel extends BaseChannel {
|
|
|
301
371
|
} catch { res.json({ totalEntries: 0, sources: [] }); }
|
|
302
372
|
});
|
|
303
373
|
|
|
374
|
+
// --- Health Check (detailed) ---
|
|
375
|
+
this.app.get('/api/health', (_req: Request, res: Response) => {
|
|
376
|
+
const uptimeMs = Date.now() - this.stats.startedAt;
|
|
377
|
+
res.json({
|
|
378
|
+
status: 'ok',
|
|
379
|
+
timestamp: Date.now(),
|
|
380
|
+
uptime: uptimeMs,
|
|
381
|
+
uptimeHuman: `${Math.floor(uptimeMs / 3600000)}h ${Math.floor((uptimeMs % 3600000) / 60000)}m`,
|
|
382
|
+
version: '0.7.0',
|
|
383
|
+
agent: this.agentName,
|
|
384
|
+
stats: {
|
|
385
|
+
sessions: this.stats.sessions,
|
|
386
|
+
messages: this.stats.messages,
|
|
387
|
+
errors: this.stats.errors,
|
|
388
|
+
avgResponseMs: this.stats.messages > 0 ? Math.round(this.stats.totalResponseMs / this.stats.messages) : 0,
|
|
389
|
+
},
|
|
390
|
+
memory: {
|
|
391
|
+
rss: process.memoryUsage().rss,
|
|
392
|
+
heapUsed: process.memoryUsage().heapUsed,
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// --- Prometheus Metrics ---
|
|
398
|
+
this.app.get('/api/metrics', (_req: Request, res: Response) => {
|
|
399
|
+
const uptimeMs = Date.now() - this.stats.startedAt;
|
|
400
|
+
const avgLatency = this.llmCalls > 0 ? this.llmLatencySum / this.llmCalls : 0;
|
|
401
|
+
const mem = process.memoryUsage();
|
|
402
|
+
res.type('text/plain').send(
|
|
403
|
+
`# HELP opc_uptime_seconds Agent uptime in seconds\n` +
|
|
404
|
+
`# TYPE opc_uptime_seconds gauge\n` +
|
|
405
|
+
`opc_uptime_seconds ${(uptimeMs / 1000).toFixed(1)}\n` +
|
|
406
|
+
`# HELP opc_requests_total Total requests\n` +
|
|
407
|
+
`# TYPE opc_requests_total counter\n` +
|
|
408
|
+
`opc_requests_total ${this.requestCount}\n` +
|
|
409
|
+
`# HELP opc_messages_total Total messages processed\n` +
|
|
410
|
+
`# TYPE opc_messages_total counter\n` +
|
|
411
|
+
`opc_messages_total ${this.stats.messages}\n` +
|
|
412
|
+
`# HELP opc_errors_total Total errors\n` +
|
|
413
|
+
`# TYPE opc_errors_total counter\n` +
|
|
414
|
+
`opc_errors_total ${this.stats.errors}\n` +
|
|
415
|
+
`# HELP opc_llm_latency_avg_ms Average LLM response latency\n` +
|
|
416
|
+
`# TYPE opc_llm_latency_avg_ms gauge\n` +
|
|
417
|
+
`opc_llm_latency_avg_ms ${avgLatency.toFixed(1)}\n` +
|
|
418
|
+
`# HELP opc_sessions_total Total sessions\n` +
|
|
419
|
+
`# TYPE opc_sessions_total counter\n` +
|
|
420
|
+
`opc_sessions_total ${this.stats.sessions}\n` +
|
|
421
|
+
`# HELP opc_token_usage_total Total token usage\n` +
|
|
422
|
+
`# TYPE opc_token_usage_total counter\n` +
|
|
423
|
+
`opc_token_usage_total ${this.stats.tokenUsage}\n` +
|
|
424
|
+
`# HELP process_resident_memory_bytes Resident memory size\n` +
|
|
425
|
+
`# TYPE process_resident_memory_bytes gauge\n` +
|
|
426
|
+
`process_resident_memory_bytes ${mem.rss}\n`
|
|
427
|
+
);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
// --- Conversation tracking & export ---
|
|
431
|
+
this.app.get('/api/conversations/export', (req: Request, res: Response) => {
|
|
432
|
+
const sessionId = req.query.sessionId as string;
|
|
433
|
+
const format = (req.query.format as string) ?? 'json';
|
|
434
|
+
|
|
435
|
+
const messages = sessionId ? (this.conversations.get(sessionId) ?? []) : Array.from(this.conversations.values()).flat();
|
|
436
|
+
|
|
437
|
+
if (format === 'markdown') {
|
|
438
|
+
const md = messages.map(m => `**${m.role}** (${new Date(m.timestamp).toISOString()}):\n${m.content}`).join('\n\n---\n\n');
|
|
439
|
+
res.type('text/markdown').send(md);
|
|
440
|
+
} else if (format === 'csv') {
|
|
441
|
+
const header = 'id,role,content,timestamp\n';
|
|
442
|
+
const rows = messages.map(m => `"${m.id}","${m.role}","${m.content.replace(/"/g, '""')}",${m.timestamp}`).join('\n');
|
|
443
|
+
res.type('text/csv').send(header + rows);
|
|
444
|
+
} else {
|
|
445
|
+
res.json({ sessionId: sessionId ?? 'all', messages, count: messages.length });
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
// --- Document Upload ---
|
|
450
|
+
this.app.post('/api/documents/upload', async (req: Request, res: Response) => {
|
|
451
|
+
try {
|
|
452
|
+
const { content, filename, mimeType } = req.body;
|
|
453
|
+
if (!content || !filename) {
|
|
454
|
+
res.status(400).json({ error: 'content and filename are required' });
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
const kb = new KnowledgeBase('.');
|
|
458
|
+
const result = await kb.addText(content, filename);
|
|
459
|
+
this.stats.knowledgeFiles++;
|
|
460
|
+
res.json({ ok: true, filename, chunks: result.chunks, chars: content.length });
|
|
461
|
+
} catch (err) {
|
|
462
|
+
res.status(500).json({ error: err instanceof Error ? err.message : 'Upload failed' });
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// --- Agent Templates Gallery ---
|
|
467
|
+
this.app.get('/api/templates', (_req: Request, res: Response) => {
|
|
468
|
+
res.json({ templates: AGENT_TEMPLATES });
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
this.app.get('/templates', (_req: Request, res: Response) => {
|
|
472
|
+
res.type('html').send(TEMPLATES_HTML);
|
|
473
|
+
});
|
|
474
|
+
|
|
304
475
|
// Legacy endpoint
|
|
305
476
|
this.app.post('/chat', async (req: Request, res: Response) => {
|
|
306
477
|
if (!this.handler) {
|
package/src/core/auth.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
2
|
+
|
|
3
|
+
export interface AuthConfig {
|
|
4
|
+
apiKeys: string[];
|
|
5
|
+
sessionIsolation?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface AuthSession {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
userId: string;
|
|
11
|
+
createdAt: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const sessions = new Map<string, AuthSession>();
|
|
15
|
+
|
|
16
|
+
export function createAuthMiddleware(config: AuthConfig) {
|
|
17
|
+
return (req: Request, res: Response, next: NextFunction): void => {
|
|
18
|
+
// Skip auth for non-API routes and health/metrics
|
|
19
|
+
if (!req.path.startsWith('/api/') || req.path === '/api/health' || req.path === '/api/metrics') {
|
|
20
|
+
next();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const apiKey = req.headers['x-api-key'] as string
|
|
25
|
+
?? req.headers['authorization']?.replace(/^Bearer\s+/i, '')
|
|
26
|
+
?? (req.query as any).apiKey;
|
|
27
|
+
|
|
28
|
+
if (!apiKey || !config.apiKeys.includes(apiKey)) {
|
|
29
|
+
res.status(401).json({ error: 'Unauthorized. Provide a valid API key via X-API-Key header, Bearer token, or ?apiKey query.' });
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Derive userId from API key for session isolation
|
|
34
|
+
const userId = `user_${hashKey(apiKey)}`;
|
|
35
|
+
if (!sessions.has(apiKey)) {
|
|
36
|
+
sessions.set(apiKey, { apiKey, userId, createdAt: Date.now() });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Attach user info to request
|
|
40
|
+
(req as any).userId = userId;
|
|
41
|
+
(req as any).sessionPrefix = config.sessionIsolation ? `${userId}:` : '';
|
|
42
|
+
|
|
43
|
+
next();
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function hashKey(key: string): string {
|
|
48
|
+
let h = 0;
|
|
49
|
+
for (let i = 0; i < key.length; i++) {
|
|
50
|
+
h = ((h << 5) - h + key.charCodeAt(i)) | 0;
|
|
51
|
+
}
|
|
52
|
+
return Math.abs(h).toString(36);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getActiveSessions(): AuthSession[] {
|
|
56
|
+
return Array.from(sessions.values());
|
|
57
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -54,3 +54,14 @@ export { deployToHermes } from './deploy/hermes';
|
|
|
54
54
|
export type { HermesDeployOptions, HermesDeployResult } from './deploy/hermes';
|
|
55
55
|
export { publishAgent, installAgent } from './marketplace';
|
|
56
56
|
export type { AgentManifest, PublishOptions, InstallOptions } from './marketplace';
|
|
57
|
+
|
|
58
|
+
// v0.7.0 modules
|
|
59
|
+
export { createAuthMiddleware, getActiveSessions } from './core/auth';
|
|
60
|
+
export type { AuthConfig, AuthSession } from './core/auth';
|
|
61
|
+
export { HttpSkill } from './skills/http';
|
|
62
|
+
export { WebhookTriggerSkill } from './skills/webhook-trigger';
|
|
63
|
+
export type { WebhookTarget } from './skills/webhook-trigger';
|
|
64
|
+
export { SchedulerSkill } from './skills/scheduler';
|
|
65
|
+
export type { ScheduledTask } from './skills/scheduler';
|
|
66
|
+
export { DocumentSkill } from './skills/document';
|
|
67
|
+
export type { DocumentChunk } from './skills/document';
|
package/src/schema/oad.ts
CHANGED
|
@@ -48,6 +48,12 @@ export const HITLSchema = z.object({
|
|
|
48
48
|
defaultAction: z.enum(['approve', 'deny']).default('deny'),
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
export const AuthSchema = z.object({
|
|
52
|
+
enabled: z.boolean().default(false),
|
|
53
|
+
apiKeys: z.array(z.string()).default([]),
|
|
54
|
+
sessionIsolation: z.boolean().default(true),
|
|
55
|
+
});
|
|
56
|
+
|
|
51
57
|
export const ChannelSchema = z.object({
|
|
52
58
|
type: z.enum(['web', 'websocket', 'telegram', 'cli', 'voice', 'webhook']),
|
|
53
59
|
port: z.number().optional(),
|
|
@@ -124,6 +130,7 @@ export const SpecSchema = z.object({
|
|
|
124
130
|
voice: VoiceSchema.optional(),
|
|
125
131
|
webhook: WebhookSchema.optional(),
|
|
126
132
|
hitl: HITLSchema.optional(),
|
|
133
|
+
auth: AuthSchema.optional(),
|
|
127
134
|
});
|
|
128
135
|
|
|
129
136
|
export const OADSchema = z.object({
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { BaseSkill } from './base';
|
|
2
|
+
import type { AgentContext, Message, SkillResult } from '../core/types';
|
|
3
|
+
import { KnowledgeBase } from '../core/knowledge';
|
|
4
|
+
|
|
5
|
+
export interface DocumentChunk {
|
|
6
|
+
content: string;
|
|
7
|
+
metadata: {
|
|
8
|
+
filename: string;
|
|
9
|
+
mimeType: string;
|
|
10
|
+
chunkIndex: number;
|
|
11
|
+
totalChunks: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class DocumentSkill extends BaseSkill {
|
|
16
|
+
name = 'document';
|
|
17
|
+
description = 'Process uploaded documents (PDF, TXT, MD, DOCX). Chunks content and adds to knowledge base.';
|
|
18
|
+
private knowledgeBase: KnowledgeBase;
|
|
19
|
+
|
|
20
|
+
constructor(kbPath: string = '.') {
|
|
21
|
+
super();
|
|
22
|
+
this.knowledgeBase = new KnowledgeBase(kbPath);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async execute(context: AgentContext, message: Message): Promise<SkillResult> {
|
|
26
|
+
// Check if message has document attachment metadata
|
|
27
|
+
const meta = message.metadata;
|
|
28
|
+
if (!meta?.document) return this.noMatch();
|
|
29
|
+
|
|
30
|
+
const { content, filename, mimeType } = meta.document as {
|
|
31
|
+
content: string; filename: string; mimeType: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const text = this.extractText(content, mimeType ?? this.guessMime(filename));
|
|
36
|
+
const chunks = this.chunk(text, 1000, 100);
|
|
37
|
+
|
|
38
|
+
const result = await this.knowledgeBase.addText(
|
|
39
|
+
chunks.map(c => c.content).join('\n\n---\n\n'),
|
|
40
|
+
filename
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return this.match(
|
|
44
|
+
`📄 Processed "${filename}": ${chunks.length} chunks, ${text.length} chars → added to knowledge base (${result.chunks} KB entries)`
|
|
45
|
+
);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
return this.match(`Document processing error: ${err instanceof Error ? err.message : String(err)}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
extractText(content: string, mimeType: string): string {
|
|
52
|
+
// For plain text formats, content is already text
|
|
53
|
+
if (mimeType.includes('text/') || mimeType.includes('markdown')) {
|
|
54
|
+
return content;
|
|
55
|
+
}
|
|
56
|
+
// For other formats, assume base64-encoded or pre-extracted text
|
|
57
|
+
// In a real implementation, you'd use pdf-parse, mammoth, etc.
|
|
58
|
+
return content;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
chunk(text: string, size: number = 1000, overlap: number = 100): DocumentChunk[] {
|
|
62
|
+
const chunks: DocumentChunk[] = [];
|
|
63
|
+
let start = 0;
|
|
64
|
+
while (start < text.length) {
|
|
65
|
+
const end = Math.min(start + size, text.length);
|
|
66
|
+
chunks.push({
|
|
67
|
+
content: text.slice(start, end),
|
|
68
|
+
metadata: {
|
|
69
|
+
filename: '',
|
|
70
|
+
mimeType: '',
|
|
71
|
+
chunkIndex: chunks.length,
|
|
72
|
+
totalChunks: 0, // filled after
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
start = end - overlap;
|
|
76
|
+
if (start >= text.length) break;
|
|
77
|
+
}
|
|
78
|
+
// Fill totalChunks
|
|
79
|
+
for (const c of chunks) c.metadata.totalChunks = chunks.length;
|
|
80
|
+
return chunks;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private guessMime(filename: string): string {
|
|
84
|
+
const ext = filename.split('.').pop()?.toLowerCase();
|
|
85
|
+
switch (ext) {
|
|
86
|
+
case 'pdf': return 'application/pdf';
|
|
87
|
+
case 'txt': return 'text/plain';
|
|
88
|
+
case 'md': return 'text/markdown';
|
|
89
|
+
case 'docx': return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
|
90
|
+
default: return 'text/plain';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Process raw text content directly (for API uploads) */
|
|
95
|
+
async processText(content: string, filename: string): Promise<{ chunks: number; chars: number }> {
|
|
96
|
+
const chunks = this.chunk(content);
|
|
97
|
+
const result = await this.knowledgeBase.addText(content, filename);
|
|
98
|
+
return { chunks: chunks.length, chars: content.length };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { BaseSkill } from './base';
|
|
2
|
+
import type { AgentContext, Message, SkillResult } from '../core/types';
|
|
3
|
+
|
|
4
|
+
export class HttpSkill extends BaseSkill {
|
|
5
|
+
name = 'http';
|
|
6
|
+
description = 'Make HTTP requests to external APIs. Usage: http GET|POST|PUT|DELETE <url> [body]';
|
|
7
|
+
|
|
8
|
+
async execute(context: AgentContext, message: Message): Promise<SkillResult> {
|
|
9
|
+
const text = message.content.trim();
|
|
10
|
+
const match = text.match(/^http\s+(GET|POST|PUT|PATCH|DELETE)\s+(\S+)(?:\s+(.+))?$/is);
|
|
11
|
+
if (!match) return this.noMatch();
|
|
12
|
+
|
|
13
|
+
const [, method, url, bodyStr] = match;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const opts: RequestInit = {
|
|
17
|
+
method: method.toUpperCase(),
|
|
18
|
+
headers: { 'Content-Type': 'application/json', 'User-Agent': 'OPC-Agent/0.7.0' },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
if (bodyStr && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {
|
|
22
|
+
opts.body = bodyStr;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const res = await fetch(url, opts);
|
|
26
|
+
const contentType = res.headers.get('content-type') ?? '';
|
|
27
|
+
const body = contentType.includes('json') ? JSON.stringify(await res.json(), null, 2) : await res.text();
|
|
28
|
+
|
|
29
|
+
const truncated = body.length > 4000 ? body.slice(0, 4000) + '\n...[truncated]' : body;
|
|
30
|
+
return this.match(`HTTP ${res.status} ${res.statusText}\n\n${truncated}`);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
return this.match(`HTTP Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|