opc-agent 0.6.0 → 0.8.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/email.d.ts +69 -0
- package/dist/channels/email.js +118 -0
- package/dist/channels/slack.d.ts +62 -0
- package/dist/channels/slack.js +107 -0
- package/dist/channels/web.d.ts +7 -1
- package/dist/channels/web.js +165 -4
- package/dist/channels/wechat.d.ts +62 -0
- package/dist/channels/wechat.js +104 -0
- package/dist/core/auth.d.ts +13 -0
- package/dist/core/auth.js +41 -0
- package/dist/core/compose.d.ts +35 -0
- package/dist/core/compose.js +49 -0
- package/dist/core/orchestrator.d.ts +68 -0
- package/dist/core/orchestrator.js +145 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +34 -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/dist/tools/calculator.d.ts +7 -0
- package/dist/tools/calculator.js +70 -0
- package/dist/tools/datetime.d.ts +7 -0
- package/dist/tools/datetime.js +159 -0
- package/dist/tools/json-transform.d.ts +7 -0
- package/dist/tools/json-transform.js +184 -0
- package/dist/tools/text-analysis.d.ts +8 -0
- package/dist/tools/text-analysis.js +113 -0
- package/package.json +1 -1
- package/src/channels/email.ts +177 -0
- package/src/channels/slack.ts +160 -0
- package/src/channels/web.ts +175 -4
- package/src/channels/wechat.ts +149 -0
- package/src/core/auth.ts +57 -0
- package/src/core/compose.ts +77 -0
- package/src/core/orchestrator.ts +215 -0
- package/src/index.ts +27 -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/src/tools/calculator.ts +73 -0
- package/src/tools/datetime.ts +149 -0
- package/src/tools/json-transform.ts +187 -0
- package/src/tools/text-analysis.ts +116 -0
- package/tests/v070.test.ts +76 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import type { AgentContext, Message } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Multi-Agent Orchestrator — v0.8.0
|
|
6
|
+
* Routes messages to specialized sub-agents, supports parallel execution and handoffs.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface AgentNode {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
description: string;
|
|
13
|
+
/** Patterns or intents this agent handles */
|
|
14
|
+
routes: string[];
|
|
15
|
+
/** Function that processes a message and returns a response */
|
|
16
|
+
handler: (context: AgentContext, message: Message) => Promise<Message>;
|
|
17
|
+
/** Priority for routing conflicts (higher wins) */
|
|
18
|
+
priority?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface OrchestratorWorkflow {
|
|
22
|
+
name: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
/** Ordered list of agent IDs for sequential execution */
|
|
25
|
+
steps?: string[];
|
|
26
|
+
/** List of agent IDs for parallel execution */
|
|
27
|
+
parallel?: string[];
|
|
28
|
+
/** Router config: auto-route based on message content */
|
|
29
|
+
router?: {
|
|
30
|
+
agents: string[];
|
|
31
|
+
fallback?: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface HandoffRequest {
|
|
36
|
+
fromAgent: string;
|
|
37
|
+
toAgent: string;
|
|
38
|
+
context: AgentContext;
|
|
39
|
+
reason: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface OrchestratorConfig {
|
|
43
|
+
agents: AgentNode[];
|
|
44
|
+
workflows?: OrchestratorWorkflow[];
|
|
45
|
+
defaultWorkflow?: string;
|
|
46
|
+
maxParallel?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class Orchestrator extends EventEmitter {
|
|
50
|
+
private agents: Map<string, AgentNode> = new Map();
|
|
51
|
+
private workflows: Map<string, OrchestratorWorkflow> = new Map();
|
|
52
|
+
private defaultWorkflow?: string;
|
|
53
|
+
private maxParallel: number;
|
|
54
|
+
|
|
55
|
+
constructor(config: OrchestratorConfig) {
|
|
56
|
+
super();
|
|
57
|
+
this.maxParallel = config.maxParallel ?? 5;
|
|
58
|
+
this.defaultWorkflow = config.defaultWorkflow;
|
|
59
|
+
|
|
60
|
+
for (const agent of config.agents) {
|
|
61
|
+
this.agents.set(agent.id, agent);
|
|
62
|
+
}
|
|
63
|
+
for (const wf of config.workflows ?? []) {
|
|
64
|
+
this.workflows.set(wf.name, wf);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Register a new agent node */
|
|
69
|
+
registerAgent(agent: AgentNode): void {
|
|
70
|
+
this.agents.set(agent.id, agent);
|
|
71
|
+
this.emit('agent:registered', agent.id);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Unregister an agent */
|
|
75
|
+
unregisterAgent(id: string): void {
|
|
76
|
+
this.agents.delete(id);
|
|
77
|
+
this.emit('agent:unregistered', id);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Route a message to the best-matching agent */
|
|
81
|
+
route(message: Message): AgentNode | undefined {
|
|
82
|
+
const content = message.content.toLowerCase();
|
|
83
|
+
let bestMatch: AgentNode | undefined;
|
|
84
|
+
let bestPriority = -1;
|
|
85
|
+
|
|
86
|
+
for (const agent of this.agents.values()) {
|
|
87
|
+
for (const route of agent.routes) {
|
|
88
|
+
if (content.includes(route.toLowerCase()) || new RegExp(route, 'i').test(content)) {
|
|
89
|
+
const priority = agent.priority ?? 0;
|
|
90
|
+
if (priority > bestPriority) {
|
|
91
|
+
bestMatch = agent;
|
|
92
|
+
bestPriority = priority;
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return bestMatch;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Execute a single agent */
|
|
102
|
+
async executeAgent(agentId: string, context: AgentContext, message: Message): Promise<Message> {
|
|
103
|
+
const agent = this.agents.get(agentId);
|
|
104
|
+
if (!agent) throw new Error(`Agent not found: ${agentId}`);
|
|
105
|
+
this.emit('agent:execute', agentId, message);
|
|
106
|
+
const result = await agent.handler(context, message);
|
|
107
|
+
this.emit('agent:complete', agentId, result);
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Run multiple agents in parallel */
|
|
112
|
+
async executeParallel(
|
|
113
|
+
agentIds: string[],
|
|
114
|
+
context: AgentContext,
|
|
115
|
+
message: Message
|
|
116
|
+
): Promise<Map<string, Message>> {
|
|
117
|
+
const results = new Map<string, Message>();
|
|
118
|
+
const batches: string[][] = [];
|
|
119
|
+
|
|
120
|
+
// Batch by maxParallel
|
|
121
|
+
for (let i = 0; i < agentIds.length; i += this.maxParallel) {
|
|
122
|
+
batches.push(agentIds.slice(i, i + this.maxParallel));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const batch of batches) {
|
|
126
|
+
const promises = batch.map(async (id) => {
|
|
127
|
+
const result = await this.executeAgent(id, context, message);
|
|
128
|
+
results.set(id, result);
|
|
129
|
+
});
|
|
130
|
+
await Promise.all(promises);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return results;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Execute a named workflow */
|
|
137
|
+
async executeWorkflow(
|
|
138
|
+
workflowName: string,
|
|
139
|
+
context: AgentContext,
|
|
140
|
+
message: Message
|
|
141
|
+
): Promise<Message[]> {
|
|
142
|
+
const wf = this.workflows.get(workflowName);
|
|
143
|
+
if (!wf) throw new Error(`Workflow not found: ${workflowName}`);
|
|
144
|
+
|
|
145
|
+
const results: Message[] = [];
|
|
146
|
+
|
|
147
|
+
// Sequential steps
|
|
148
|
+
if (wf.steps) {
|
|
149
|
+
let currentMessage = message;
|
|
150
|
+
for (const agentId of wf.steps) {
|
|
151
|
+
const result = await this.executeAgent(agentId, context, currentMessage);
|
|
152
|
+
results.push(result);
|
|
153
|
+
currentMessage = result; // chain output → next input
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Parallel execution
|
|
158
|
+
if (wf.parallel) {
|
|
159
|
+
const parallelResults = await this.executeParallel(wf.parallel, context, message);
|
|
160
|
+
results.push(...parallelResults.values());
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Router-based
|
|
164
|
+
if (wf.router) {
|
|
165
|
+
const matched = this.route(message);
|
|
166
|
+
const targetId = matched && wf.router.agents.includes(matched.id)
|
|
167
|
+
? matched.id
|
|
168
|
+
: wf.router.fallback;
|
|
169
|
+
if (targetId) {
|
|
170
|
+
const result = await this.executeAgent(targetId, context, message);
|
|
171
|
+
results.push(result);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return results;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Hand off conversation from one agent to another */
|
|
179
|
+
async handoff(request: HandoffRequest): Promise<Message> {
|
|
180
|
+
this.emit('agent:handoff', request);
|
|
181
|
+
const { toAgent, context } = request;
|
|
182
|
+
const lastMessage = context.messages[context.messages.length - 1];
|
|
183
|
+
if (!lastMessage) throw new Error('No message in context for handoff');
|
|
184
|
+
return this.executeAgent(toAgent, context, lastMessage);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Process an incoming message using the default workflow or routing */
|
|
188
|
+
async process(context: AgentContext, message: Message): Promise<Message[]> {
|
|
189
|
+
if (this.defaultWorkflow) {
|
|
190
|
+
return this.executeWorkflow(this.defaultWorkflow, context, message);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Fallback: route to single agent
|
|
194
|
+
const agent = this.route(message);
|
|
195
|
+
if (agent) {
|
|
196
|
+
const result = await this.executeAgent(agent.id, context, message);
|
|
197
|
+
return [result];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return [{
|
|
201
|
+
id: `orch-${Date.now()}`,
|
|
202
|
+
role: 'assistant',
|
|
203
|
+
content: 'No agent available to handle this request.',
|
|
204
|
+
timestamp: Date.now(),
|
|
205
|
+
}];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
getAgents(): AgentNode[] {
|
|
209
|
+
return Array.from(this.agents.values());
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
getWorkflows(): OrchestratorWorkflow[] {
|
|
213
|
+
return Array.from(this.workflows.values());
|
|
214
|
+
}
|
|
215
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -54,3 +54,30 @@ 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
|
+
// v0.8.0 modules
|
|
62
|
+
export { Orchestrator } from './core/orchestrator';
|
|
63
|
+
export type { AgentNode, OrchestratorWorkflow, OrchestratorConfig, HandoffRequest } from './core/orchestrator';
|
|
64
|
+
export { AgentPipeline, compose } from './core/compose';
|
|
65
|
+
export type { ComposableAgent, ComposeOptions } from './core/compose';
|
|
66
|
+
export { EmailChannel } from './channels/email';
|
|
67
|
+
export type { EmailChannelConfig, EmailMessage } from './channels/email';
|
|
68
|
+
export { SlackChannel } from './channels/slack';
|
|
69
|
+
export type { SlackChannelConfig, SlashCommandConfig, SlashCommandPayload } from './channels/slack';
|
|
70
|
+
export { WeChatChannel } from './channels/wechat';
|
|
71
|
+
export type { WeChatChannelConfig, WeChatMessage, TemplateMessageData } from './channels/wechat';
|
|
72
|
+
export { CalculatorTool } from './tools/calculator';
|
|
73
|
+
export { DateTimeTool } from './tools/datetime';
|
|
74
|
+
export { JsonTransformTool } from './tools/json-transform';
|
|
75
|
+
export { TextAnalysisTool } from './tools/text-analysis';
|
|
76
|
+
|
|
77
|
+
export { HttpSkill } from './skills/http';
|
|
78
|
+
export { WebhookTriggerSkill } from './skills/webhook-trigger';
|
|
79
|
+
export type { WebhookTarget } from './skills/webhook-trigger';
|
|
80
|
+
export { SchedulerSkill } from './skills/scheduler';
|
|
81
|
+
export type { ScheduledTask } from './skills/scheduler';
|
|
82
|
+
export { DocumentSkill } from './skills/document';
|
|
83
|
+
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
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { BaseSkill } from './base';
|
|
2
|
+
import type { AgentContext, Message, SkillResult } from '../core/types';
|
|
3
|
+
|
|
4
|
+
export interface ScheduledTask {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
cronExpr: string;
|
|
8
|
+
action: string;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
lastRun?: number;
|
|
11
|
+
nextRun?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class SchedulerSkill extends BaseSkill {
|
|
15
|
+
name = 'scheduler';
|
|
16
|
+
description = 'Schedule recurring tasks. Usage: schedule list | schedule add <name> <cron> <action> | schedule remove <id>';
|
|
17
|
+
private tasks: Map<string, ScheduledTask> = new Map();
|
|
18
|
+
private timers: Map<string, NodeJS.Timeout> = new Map();
|
|
19
|
+
|
|
20
|
+
async execute(context: AgentContext, message: Message): Promise<SkillResult> {
|
|
21
|
+
const text = message.content.trim();
|
|
22
|
+
|
|
23
|
+
if (/^schedule\s+list$/i.test(text)) {
|
|
24
|
+
if (this.tasks.size === 0) return this.match('No scheduled tasks.');
|
|
25
|
+
const lines = Array.from(this.tasks.values()).map(t =>
|
|
26
|
+
`• ${t.name} (${t.id}) — ${t.cronExpr} — ${t.enabled ? '✅' : '❌'} — Last: ${t.lastRun ? new Date(t.lastRun).toISOString() : 'never'}`
|
|
27
|
+
);
|
|
28
|
+
return this.match(`Scheduled tasks:\n${lines.join('\n')}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const addMatch = text.match(/^schedule\s+add\s+(\S+)\s+(".*?"|\S+)\s+(.+)$/i);
|
|
32
|
+
if (addMatch) {
|
|
33
|
+
const [, name, cronExpr, action] = addMatch;
|
|
34
|
+
const id = `task_${Date.now().toString(36)}`;
|
|
35
|
+
const task: ScheduledTask = {
|
|
36
|
+
id, name, cronExpr: cronExpr.replace(/"/g, ''), action, enabled: true,
|
|
37
|
+
};
|
|
38
|
+
this.tasks.set(id, task);
|
|
39
|
+
// Simple interval-based scheduling (parse cron for interval in minutes)
|
|
40
|
+
const intervalMs = this.parseCronToInterval(task.cronExpr);
|
|
41
|
+
if (intervalMs > 0) {
|
|
42
|
+
const timer = setInterval(() => {
|
|
43
|
+
task.lastRun = Date.now();
|
|
44
|
+
}, intervalMs);
|
|
45
|
+
this.timers.set(id, timer);
|
|
46
|
+
}
|
|
47
|
+
return this.match(`Task scheduled: ${name} (${id}) — ${task.cronExpr} → "${action}"`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const rmMatch = text.match(/^schedule\s+remove\s+(\S+)$/i);
|
|
51
|
+
if (rmMatch) {
|
|
52
|
+
const id = rmMatch[1];
|
|
53
|
+
const timer = this.timers.get(id);
|
|
54
|
+
if (timer) clearInterval(timer);
|
|
55
|
+
this.timers.delete(id);
|
|
56
|
+
const removed = this.tasks.delete(id);
|
|
57
|
+
return this.match(removed ? `Task ${id} removed.` : `Task ${id} not found.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return this.noMatch();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private parseCronToInterval(expr: string): number {
|
|
64
|
+
// Simple: support "every Xm" or "every Xh" or basic intervals
|
|
65
|
+
const m = expr.match(/every\s+(\d+)\s*(m|min|h|hr|s|sec)/i);
|
|
66
|
+
if (m) {
|
|
67
|
+
const val = parseInt(m[1]);
|
|
68
|
+
const unit = m[2].toLowerCase();
|
|
69
|
+
if (unit.startsWith('h')) return val * 3600_000;
|
|
70
|
+
if (unit.startsWith('m')) return val * 60_000;
|
|
71
|
+
if (unit.startsWith('s')) return val * 1000;
|
|
72
|
+
}
|
|
73
|
+
return 0; // Unknown cron format, no auto-schedule
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
destroy(): void {
|
|
77
|
+
for (const timer of this.timers.values()) clearInterval(timer);
|
|
78
|
+
this.timers.clear();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { BaseSkill } from './base';
|
|
2
|
+
import type { AgentContext, Message, SkillResult } from '../core/types';
|
|
3
|
+
|
|
4
|
+
export interface WebhookTarget {
|
|
5
|
+
name: string;
|
|
6
|
+
url: string;
|
|
7
|
+
method?: string;
|
|
8
|
+
headers?: Record<string, string>;
|
|
9
|
+
secret?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class WebhookTriggerSkill extends BaseSkill {
|
|
13
|
+
name = 'webhook-trigger';
|
|
14
|
+
description = 'Trigger external webhooks. Usage: webhook <name> [payload JSON]';
|
|
15
|
+
private targets: Map<string, WebhookTarget> = new Map();
|
|
16
|
+
|
|
17
|
+
registerTarget(target: WebhookTarget): void {
|
|
18
|
+
this.targets.set(target.name, target);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async execute(context: AgentContext, message: Message): Promise<SkillResult> {
|
|
22
|
+
const match = message.content.trim().match(/^webhook\s+(\S+)(?:\s+(.+))?$/is);
|
|
23
|
+
if (!match) return this.noMatch();
|
|
24
|
+
|
|
25
|
+
const [, name, payloadStr] = match;
|
|
26
|
+
const target = this.targets.get(name);
|
|
27
|
+
if (!target) {
|
|
28
|
+
const available = Array.from(this.targets.keys()).join(', ') || 'none';
|
|
29
|
+
return this.match(`Unknown webhook "${name}". Available: ${available}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
const headers: Record<string, string> = {
|
|
34
|
+
'Content-Type': 'application/json',
|
|
35
|
+
'User-Agent': 'OPC-Agent/0.7.0',
|
|
36
|
+
...target.headers,
|
|
37
|
+
};
|
|
38
|
+
if (target.secret) {
|
|
39
|
+
headers['X-Webhook-Secret'] = target.secret;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const body = payloadStr ?? JSON.stringify({
|
|
43
|
+
agent: context.agentName,
|
|
44
|
+
timestamp: Date.now(),
|
|
45
|
+
trigger: 'manual',
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const res = await fetch(target.url, {
|
|
49
|
+
method: target.method ?? 'POST',
|
|
50
|
+
headers,
|
|
51
|
+
body,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return this.match(`Webhook "${name}" triggered → ${res.status} ${res.statusText}`);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
return this.match(`Webhook error: ${err instanceof Error ? err.message : String(err)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { MCPTool, MCPToolResult } from './mcp';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Calculator Tool — v0.8.0
|
|
5
|
+
* Safe math expression evaluation as an LLM function tool.
|
|
6
|
+
*/
|
|
7
|
+
export const CalculatorTool: MCPTool = {
|
|
8
|
+
name: 'calculator',
|
|
9
|
+
description: 'Evaluate a mathematical expression. Supports basic arithmetic, powers, sqrt, abs, min, max, round, ceil, floor, PI, E.',
|
|
10
|
+
inputSchema: {
|
|
11
|
+
type: 'object',
|
|
12
|
+
properties: {
|
|
13
|
+
expression: {
|
|
14
|
+
type: 'string',
|
|
15
|
+
description: 'Mathematical expression to evaluate, e.g. "2 + 3 * 4" or "sqrt(144) + PI"',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
required: ['expression'],
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
async execute(input: Record<string, unknown>): Promise<MCPToolResult> {
|
|
22
|
+
const expr = String(input.expression ?? '');
|
|
23
|
+
try {
|
|
24
|
+
const result = safeEval(expr);
|
|
25
|
+
return { content: String(result) };
|
|
26
|
+
} catch (err) {
|
|
27
|
+
return { content: `Error: ${(err as Error).message}`, isError: true };
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Safe math evaluator — no eval(), no arbitrary code */
|
|
33
|
+
function safeEval(expr: string): number {
|
|
34
|
+
// Whitelist: digits, operators, parens, dots, commas, spaces, and known functions
|
|
35
|
+
const sanitized = expr.replace(/\s+/g, '');
|
|
36
|
+
const allowed = /^[0-9+\-*/().,%^a-zA-Z_]+$/;
|
|
37
|
+
if (!allowed.test(sanitized)) {
|
|
38
|
+
throw new Error('Invalid characters in expression');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Replace known math functions/constants
|
|
42
|
+
const prepared = sanitized
|
|
43
|
+
.replace(/\bPI\b/gi, String(Math.PI))
|
|
44
|
+
.replace(/\bE\b/g, String(Math.E))
|
|
45
|
+
.replace(/\bsqrt\b/gi, 'Math.sqrt')
|
|
46
|
+
.replace(/\babs\b/gi, 'Math.abs')
|
|
47
|
+
.replace(/\bmin\b/gi, 'Math.min')
|
|
48
|
+
.replace(/\bmax\b/gi, 'Math.max')
|
|
49
|
+
.replace(/\bround\b/gi, 'Math.round')
|
|
50
|
+
.replace(/\bceil\b/gi, 'Math.ceil')
|
|
51
|
+
.replace(/\bfloor\b/gi, 'Math.floor')
|
|
52
|
+
.replace(/\bpow\b/gi, 'Math.pow')
|
|
53
|
+
.replace(/\blog\b/gi, 'Math.log')
|
|
54
|
+
.replace(/\blog10\b/gi, 'Math.log10')
|
|
55
|
+
.replace(/\bsin\b/gi, 'Math.sin')
|
|
56
|
+
.replace(/\bcos\b/gi, 'Math.cos')
|
|
57
|
+
.replace(/\btan\b/gi, 'Math.tan')
|
|
58
|
+
.replace(/\^/g, '**');
|
|
59
|
+
|
|
60
|
+
// Block anything that isn't math
|
|
61
|
+
if (/[a-zA-Z_]/.test(prepared.replace(/Math\.\w+/g, ''))) {
|
|
62
|
+
throw new Error('Unsupported function or variable in expression');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Use Function constructor with restricted scope
|
|
66
|
+
const fn = new Function('Math', `"use strict"; return (${prepared});`);
|
|
67
|
+
const result = fn(Math);
|
|
68
|
+
|
|
69
|
+
if (typeof result !== 'number' || !isFinite(result)) {
|
|
70
|
+
throw new Error('Expression did not evaluate to a finite number');
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|