opc-agent 1.3.1 → 1.4.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/.github/ISSUE_TEMPLATE/bug_report.md +20 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +14 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +13 -0
- package/CHANGELOG.md +23 -57
- package/CONTRIBUTING.md +36 -75
- package/dist/channels/slack.js +93 -10
- package/dist/channels/web.d.ts +10 -0
- package/dist/channels/web.js +33 -2
- package/dist/cli.js +137 -26
- package/dist/core/runtime.d.ts +4 -0
- package/dist/core/runtime.js +27 -0
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.js +7 -1
- package/examples/README.md +22 -0
- package/examples/basic-agent.ts +90 -0
- package/examples/brain-integration.ts +71 -0
- package/examples/multi-channel.ts +74 -0
- package/package.json +1 -1
- package/src/channels/slack.ts +67 -10
- package/src/channels/web.ts +38 -2
- package/src/cli.ts +158 -26
- package/src/core/runtime.ts +31 -0
- package/src/providers/index.ts +9 -1
- package/test-agent/Dockerfile +9 -0
- package/test-agent/README.md +50 -0
- package/test-agent/agent.yaml +23 -0
- package/test-agent/docker-compose.yml +11 -0
- package/test-agent/oad.yaml +31 -0
- package/test-agent/package-lock.json +1492 -0
- package/test-agent/package.json +18 -0
- package/test-agent/src/index.ts +24 -0
- package/test-agent/src/skills/echo.ts +15 -0
- package/test-agent/tsconfig.json +25 -0
package/src/channels/slack.ts
CHANGED
|
@@ -86,9 +86,57 @@ export class SlackChannel extends BaseChannel {
|
|
|
86
86
|
|
|
87
87
|
/** Start Events API HTTP server */
|
|
88
88
|
private async startEventsAPI(): Promise<void> {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
const http = await import('http');
|
|
90
|
+
const port = this.config.port ?? 3001;
|
|
91
|
+
|
|
92
|
+
const server = http.createServer(async (req, res) => {
|
|
93
|
+
if (req.method !== 'POST') {
|
|
94
|
+
res.writeHead(404);
|
|
95
|
+
res.end();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const chunks: Buffer[] = [];
|
|
100
|
+
for await (const chunk of req) chunks.push(chunk as Buffer);
|
|
101
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
102
|
+
|
|
103
|
+
// URL verification challenge
|
|
104
|
+
if (body.type === 'url_verification') {
|
|
105
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
106
|
+
res.end(JSON.stringify({ challenge: body.challenge }));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Event callback
|
|
111
|
+
if (body.type === 'event_callback' && body.event) {
|
|
112
|
+
const event = body.event as SlackMessageEvent;
|
|
113
|
+
if (event.type === 'message' || event.type === 'app_mention') {
|
|
114
|
+
// Don't block the HTTP response
|
|
115
|
+
this.handleMessage(event).catch(() => {});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Slash commands (form-urlencoded, but we handle JSON for simplicity)
|
|
120
|
+
if (req.url === '/slack/commands' && body.command) {
|
|
121
|
+
const reply = await this.handleSlashCommand({
|
|
122
|
+
command: body.command,
|
|
123
|
+
text: body.text ?? '',
|
|
124
|
+
userId: body.user_id,
|
|
125
|
+
channelId: body.channel_id,
|
|
126
|
+
triggerId: body.trigger_id,
|
|
127
|
+
});
|
|
128
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
129
|
+
res.end(JSON.stringify({ response_type: 'ephemeral', text: reply }));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
res.writeHead(200);
|
|
134
|
+
res.end('ok');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
server.listen(port, () => {
|
|
138
|
+
console.log(`[SlackChannel] Events API listening on port ${port}`);
|
|
139
|
+
});
|
|
92
140
|
}
|
|
93
141
|
|
|
94
142
|
/** Handle incoming Slack message */
|
|
@@ -149,12 +197,21 @@ export class SlackChannel extends BaseChannel {
|
|
|
149
197
|
|
|
150
198
|
/** Send a message to a Slack channel */
|
|
151
199
|
async sendMessage(channel: string, text: string, threadTs?: string): Promise<void> {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
200
|
+
const body: Record<string, string> = { channel, text };
|
|
201
|
+
if (threadTs) body.thread_ts = threadTs;
|
|
202
|
+
|
|
203
|
+
const res = await fetch('https://slack.com/api/chat.postMessage', {
|
|
204
|
+
method: 'POST',
|
|
205
|
+
headers: {
|
|
206
|
+
'Authorization': `Bearer ${this.config.botToken}`,
|
|
207
|
+
'Content-Type': 'application/json',
|
|
208
|
+
},
|
|
209
|
+
body: JSON.stringify(body),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const data = await res.json() as { ok: boolean; error?: string };
|
|
213
|
+
if (!data.ok) {
|
|
214
|
+
console.error(`[SlackChannel] chat.postMessage failed: ${data.error}`);
|
|
215
|
+
}
|
|
159
216
|
}
|
|
160
217
|
}
|
package/src/channels/web.ts
CHANGED
|
@@ -288,6 +288,11 @@ export class WebChannel extends BaseChannel {
|
|
|
288
288
|
private port: number;
|
|
289
289
|
private streamHandler: ((msg: Message, res: Response) => Promise<void>) | null = null;
|
|
290
290
|
private agentName: string = 'OPC Agent';
|
|
291
|
+
private agentVersion: string = '1.0.0';
|
|
292
|
+
private memoryType: string = 'in-memory';
|
|
293
|
+
private skillNames: string[] = [];
|
|
294
|
+
private channelNames: string[] = ['web'];
|
|
295
|
+
private analyticsProvider: (() => any) | null = null;
|
|
291
296
|
private currentProvider: string = 'openai';
|
|
292
297
|
private stats = { sessions: 0, messages: 0, totalResponseMs: 0, tokenUsage: 0, knowledgeFiles: 0, startedAt: Date.now(), errors: 0 };
|
|
293
298
|
private eventHandlers: Map<string, Function[]> = new Map();
|
|
@@ -335,6 +340,26 @@ export class WebChannel extends BaseChannel {
|
|
|
335
340
|
this.agentName = name;
|
|
336
341
|
}
|
|
337
342
|
|
|
343
|
+
setAgentVersion(version: string): void {
|
|
344
|
+
this.agentVersion = version;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
setMemoryType(type: string): void {
|
|
348
|
+
this.memoryType = type;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
setSkillNames(names: string[]): void {
|
|
352
|
+
this.skillNames = names;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
setChannelNames(names: string[]): void {
|
|
356
|
+
this.channelNames = names;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
setAnalyticsProvider(fn: () => any): void {
|
|
360
|
+
this.analyticsProvider = fn;
|
|
361
|
+
}
|
|
362
|
+
|
|
338
363
|
onStreamMessage(handler: (msg: Message, res: Response) => Promise<void>): void {
|
|
339
364
|
this.streamHandler = handler;
|
|
340
365
|
}
|
|
@@ -345,7 +370,17 @@ export class WebChannel extends BaseChannel {
|
|
|
345
370
|
});
|
|
346
371
|
|
|
347
372
|
this.app.get('/health', (_req: Request, res: Response) => {
|
|
348
|
-
res.json({
|
|
373
|
+
res.json({
|
|
374
|
+
status: 'ok',
|
|
375
|
+
agent: this.agentName,
|
|
376
|
+
version: this.agentVersion,
|
|
377
|
+
uptime: Date.now() - this.stats.startedAt,
|
|
378
|
+
memory: this.memoryType,
|
|
379
|
+
skills: this.skillNames,
|
|
380
|
+
channels: this.channelNames,
|
|
381
|
+
analytics: this.analyticsProvider ? this.analyticsProvider() : null,
|
|
382
|
+
timestamp: Date.now(),
|
|
383
|
+
});
|
|
349
384
|
});
|
|
350
385
|
|
|
351
386
|
this.app.get('/api/info', (_req: Request, res: Response) => {
|
|
@@ -426,7 +461,8 @@ export class WebChannel extends BaseChannel {
|
|
|
426
461
|
});
|
|
427
462
|
|
|
428
463
|
this.app.get('/api/dashboard', (_req: Request, res: Response) => {
|
|
429
|
-
|
|
464
|
+
const analytics = this.analyticsProvider ? this.analyticsProvider() : null;
|
|
465
|
+
res.json({ ...this.stats, analytics });
|
|
430
466
|
});
|
|
431
467
|
|
|
432
468
|
// --- Knowledge Base Upload ---
|
package/src/cli.ts
CHANGED
|
@@ -94,7 +94,7 @@ async function select(question: string, options: { value: string; label: string
|
|
|
94
94
|
program
|
|
95
95
|
.name('opc')
|
|
96
96
|
.description('OPC Agent - Open Agent Framework for business workstations')
|
|
97
|
-
.version('1.
|
|
97
|
+
.version('1.4.0');
|
|
98
98
|
|
|
99
99
|
// ── Init command ─────────────────────────────────────────────
|
|
100
100
|
|
|
@@ -119,6 +119,8 @@ program
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
fs.mkdirSync(dir, { recursive: true });
|
|
122
|
+
fs.mkdirSync(path.join(dir, 'src', 'skills'), { recursive: true });
|
|
123
|
+
|
|
122
124
|
const factory = TEMPLATES[template]?.factory ?? createCustomerServiceConfig;
|
|
123
125
|
const config = factory();
|
|
124
126
|
config.metadata.name = name;
|
|
@@ -130,6 +132,113 @@ program
|
|
|
130
132
|
|
|
131
133
|
fs.writeFileSync(path.join(dir, 'oad.yaml'), yaml.dump(config, { lineWidth: 120 }));
|
|
132
134
|
|
|
135
|
+
// agent.yaml — standalone OAD config for runtime usage
|
|
136
|
+
fs.writeFileSync(
|
|
137
|
+
path.join(dir, 'agent.yaml'),
|
|
138
|
+
`apiVersion: opc/v1
|
|
139
|
+
kind: Agent
|
|
140
|
+
metadata:
|
|
141
|
+
name: ${name}
|
|
142
|
+
version: 1.0.0
|
|
143
|
+
description: My AI Agent
|
|
144
|
+
spec:
|
|
145
|
+
model: qwen2.5
|
|
146
|
+
provider:
|
|
147
|
+
default: ollama
|
|
148
|
+
systemPrompt: |
|
|
149
|
+
You are a helpful AI assistant named ${name}.
|
|
150
|
+
Be concise, helpful, and friendly.
|
|
151
|
+
channels:
|
|
152
|
+
- type: web
|
|
153
|
+
port: 3000
|
|
154
|
+
memory:
|
|
155
|
+
shortTerm: true
|
|
156
|
+
longTerm:
|
|
157
|
+
provider: deepbrain
|
|
158
|
+
skills:
|
|
159
|
+
- name: echo
|
|
160
|
+
description: Echo test skill
|
|
161
|
+
`,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// src/index.ts — entry point
|
|
165
|
+
fs.writeFileSync(
|
|
166
|
+
path.join(dir, 'src', 'index.ts'),
|
|
167
|
+
`import { AgentRuntime } from 'opc-agent';
|
|
168
|
+
import { EchoSkill } from './skills/echo';
|
|
169
|
+
|
|
170
|
+
async function main() {
|
|
171
|
+
const runtime = new AgentRuntime();
|
|
172
|
+
|
|
173
|
+
// Load OAD config
|
|
174
|
+
await runtime.loadConfig('./agent.yaml');
|
|
175
|
+
|
|
176
|
+
// Initialize agent with channels, memory, etc.
|
|
177
|
+
const agent = await runtime.initialize();
|
|
178
|
+
|
|
179
|
+
// Register custom skills
|
|
180
|
+
runtime.registerSkill(new EchoSkill());
|
|
181
|
+
|
|
182
|
+
// Start serving
|
|
183
|
+
await runtime.start();
|
|
184
|
+
|
|
185
|
+
console.log('🤖 Agent is running!');
|
|
186
|
+
console.log(' Web UI: http://localhost:3000');
|
|
187
|
+
console.log(' Press Ctrl+C to stop');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
main().catch(console.error);
|
|
191
|
+
`,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// src/skills/echo.ts — example skill
|
|
195
|
+
fs.writeFileSync(
|
|
196
|
+
path.join(dir, 'src', 'skills', 'echo.ts'),
|
|
197
|
+
`import { BaseSkill } from 'opc-agent';
|
|
198
|
+
import type { AgentContext, Message, SkillResult } from 'opc-agent';
|
|
199
|
+
|
|
200
|
+
export class EchoSkill extends BaseSkill {
|
|
201
|
+
name = 'echo';
|
|
202
|
+
description = 'Echo back the message (test skill)';
|
|
203
|
+
|
|
204
|
+
async execute(context: AgentContext, message: Message): Promise<SkillResult> {
|
|
205
|
+
if (message.content.toLowerCase().startsWith('/echo ')) {
|
|
206
|
+
const text = message.content.slice(6);
|
|
207
|
+
return this.match(\`🔊 Echo: \${text}\`);
|
|
208
|
+
}
|
|
209
|
+
return this.noMatch();
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
`,
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
// tsconfig.json
|
|
216
|
+
fs.writeFileSync(
|
|
217
|
+
path.join(dir, 'tsconfig.json'),
|
|
218
|
+
JSON.stringify(
|
|
219
|
+
{
|
|
220
|
+
compilerOptions: {
|
|
221
|
+
target: 'ES2022',
|
|
222
|
+
module: 'commonjs',
|
|
223
|
+
lib: ['ES2022'],
|
|
224
|
+
outDir: 'dist',
|
|
225
|
+
rootDir: 'src',
|
|
226
|
+
strict: true,
|
|
227
|
+
esModuleInterop: true,
|
|
228
|
+
skipLibCheck: true,
|
|
229
|
+
forceConsistentCasingInFileNames: true,
|
|
230
|
+
resolveJsonModule: true,
|
|
231
|
+
declaration: true,
|
|
232
|
+
sourceMap: true,
|
|
233
|
+
},
|
|
234
|
+
include: ['src/**/*'],
|
|
235
|
+
exclude: ['node_modules', 'dist'],
|
|
236
|
+
},
|
|
237
|
+
null,
|
|
238
|
+
2,
|
|
239
|
+
),
|
|
240
|
+
);
|
|
241
|
+
|
|
133
242
|
// .env.example
|
|
134
243
|
fs.writeFileSync(
|
|
135
244
|
path.join(dir, '.env.example'),
|
|
@@ -142,9 +251,9 @@ OPC_LLM_MODEL=gpt-4o-mini
|
|
|
142
251
|
# OPC_LLM_BASE_URL=https://api.deepseek.com/v1
|
|
143
252
|
# OPC_LLM_MODEL=deepseek-chat
|
|
144
253
|
|
|
145
|
-
# For local Ollama:
|
|
254
|
+
# For local Ollama (default in agent.yaml):
|
|
146
255
|
# OPC_LLM_BASE_URL=http://localhost:11434/v1
|
|
147
|
-
# OPC_LLM_MODEL=
|
|
256
|
+
# OPC_LLM_MODEL=qwen2.5
|
|
148
257
|
`,
|
|
149
258
|
);
|
|
150
259
|
|
|
@@ -167,10 +276,16 @@ OPC_LLM_MODEL=gpt-4o-mini
|
|
|
167
276
|
private: true,
|
|
168
277
|
scripts: {
|
|
169
278
|
start: 'opc run',
|
|
279
|
+
dev: 'opc dev',
|
|
170
280
|
chat: 'opc chat',
|
|
281
|
+
build: 'tsc',
|
|
171
282
|
},
|
|
172
283
|
dependencies: {
|
|
173
|
-
'opc-agent': '^
|
|
284
|
+
'opc-agent': '^1.3.0',
|
|
285
|
+
},
|
|
286
|
+
devDependencies: {
|
|
287
|
+
typescript: '^5.5.0',
|
|
288
|
+
tsx: '^4.0.0',
|
|
174
289
|
},
|
|
175
290
|
},
|
|
176
291
|
null,
|
|
@@ -179,7 +294,7 @@ OPC_LLM_MODEL=gpt-4o-mini
|
|
|
179
294
|
);
|
|
180
295
|
|
|
181
296
|
// .gitignore
|
|
182
|
-
fs.writeFileSync(path.join(dir, '.gitignore'), 'node_modules\n.env\n.opc-knowledge.json\ndata/\n');
|
|
297
|
+
fs.writeFileSync(path.join(dir, '.gitignore'), 'node_modules\ndist\n.env\n.opc-knowledge.json\ndata/\n');
|
|
183
298
|
|
|
184
299
|
// Dockerfile
|
|
185
300
|
fs.writeFileSync(
|
|
@@ -188,7 +303,8 @@ OPC_LLM_MODEL=gpt-4o-mini
|
|
|
188
303
|
WORKDIR /app
|
|
189
304
|
COPY package.json package-lock.json* ./
|
|
190
305
|
RUN npm ci --production 2>/dev/null || npm install --production
|
|
191
|
-
COPY oad.yaml .env* ./
|
|
306
|
+
COPY oad.yaml agent.yaml .env* ./
|
|
307
|
+
COPY src/ ./src/
|
|
192
308
|
COPY prompts/ ./prompts/ 2>/dev/null || true
|
|
193
309
|
EXPOSE 3000
|
|
194
310
|
CMD ["npx", "opc", "run"]
|
|
@@ -207,7 +323,7 @@ services:
|
|
|
207
323
|
env_file:
|
|
208
324
|
- .env
|
|
209
325
|
volumes:
|
|
210
|
-
- ./
|
|
326
|
+
- ./agent.yaml:/app/agent.yaml:ro
|
|
211
327
|
restart: unless-stopped
|
|
212
328
|
`,
|
|
213
329
|
);
|
|
@@ -221,53 +337,69 @@ Created with [OPC Agent](https://github.com/Deepleaper/opc-agent) using the \`${
|
|
|
221
337
|
|
|
222
338
|
## Quick Start
|
|
223
339
|
|
|
224
|
-
1. **
|
|
340
|
+
1. **Install dependencies:**
|
|
225
341
|
\`\`\`bash
|
|
226
|
-
|
|
227
|
-
cp .env.example .env
|
|
228
|
-
# Then edit .env with your actual key
|
|
342
|
+
npm install
|
|
229
343
|
\`\`\`
|
|
230
344
|
|
|
231
|
-
2. **
|
|
345
|
+
2. **Run with Ollama (default):**
|
|
232
346
|
\`\`\`bash
|
|
233
|
-
|
|
347
|
+
# Make sure Ollama is running with qwen2.5 model
|
|
348
|
+
ollama pull qwen2.5
|
|
349
|
+
npx tsx src/index.ts
|
|
234
350
|
\`\`\`
|
|
235
351
|
|
|
236
|
-
3. **
|
|
352
|
+
3. **Or use OpenAI/other providers:**
|
|
237
353
|
\`\`\`bash
|
|
354
|
+
# Edit .env and set your API key
|
|
238
355
|
npx opc run
|
|
239
356
|
\`\`\`
|
|
240
357
|
|
|
241
358
|
4. **Open browser:** [http://localhost:3000](http://localhost:3000)
|
|
242
359
|
|
|
243
|
-
##
|
|
360
|
+
## Development
|
|
244
361
|
|
|
245
362
|
\`\`\`bash
|
|
246
|
-
npx opc
|
|
363
|
+
npx opc dev # Hot-reload mode
|
|
364
|
+
npx opc chat # CLI chat
|
|
365
|
+
\`\`\`
|
|
366
|
+
|
|
367
|
+
## Project Structure
|
|
368
|
+
|
|
369
|
+
\`\`\`
|
|
370
|
+
${name}/
|
|
371
|
+
├── agent.yaml # OAD agent config (used by src/index.ts)
|
|
372
|
+
├── oad.yaml # OAD config (used by opc CLI)
|
|
373
|
+
├── src/
|
|
374
|
+
│ ├── index.ts # Entry point
|
|
375
|
+
│ └── skills/
|
|
376
|
+
│ └── echo.ts # Example skill
|
|
377
|
+
├── package.json
|
|
378
|
+
└── tsconfig.json
|
|
247
379
|
\`\`\`
|
|
248
380
|
|
|
249
381
|
## Configuration
|
|
250
382
|
|
|
251
|
-
Edit \`
|
|
383
|
+
Edit \`agent.yaml\` to customize your agent's personality, skills, and behavior.
|
|
252
384
|
`,
|
|
253
385
|
);
|
|
254
386
|
|
|
255
387
|
console.log(`\n${icon.success} Created agent project: ${color.bold(name + '/')}`);
|
|
256
|
-
console.log(` ${icon.file}
|
|
257
|
-
console.log(` ${icon.file}
|
|
258
|
-
console.log(` ${icon.file} .
|
|
259
|
-
console.log(` ${icon.file} .
|
|
388
|
+
console.log(` ${icon.file} agent.yaml - Agent definition (OAD)`);
|
|
389
|
+
console.log(` ${icon.file} src/index.ts - Entry point`);
|
|
390
|
+
console.log(` ${icon.file} src/skills/echo.ts - Example skill`);
|
|
391
|
+
console.log(` ${icon.file} package.json - Dependencies`);
|
|
392
|
+
console.log(` ${icon.file} tsconfig.json - TypeScript config`);
|
|
393
|
+
console.log(` ${icon.file} .env.example - Environment template`);
|
|
260
394
|
console.log(` ${icon.file} .gitignore`);
|
|
261
395
|
console.log(` ${icon.file} Dockerfile`);
|
|
262
|
-
console.log(` ${icon.file} docker-compose.yml`);
|
|
263
396
|
console.log(` ${icon.file} README.md`);
|
|
264
397
|
console.log(`\n Template: ${color.cyan(template)}`);
|
|
265
398
|
console.log(`\n${color.bold('Next steps:')}`);
|
|
266
399
|
console.log(` 1. cd ${name}`);
|
|
267
|
-
console.log(` 2.
|
|
268
|
-
console.log(` 3.
|
|
269
|
-
console.log(` 4.
|
|
270
|
-
console.log(` 5. Open http://localhost:3000\n`);
|
|
400
|
+
console.log(` 2. npm install`);
|
|
401
|
+
console.log(` 3. npx tsx src/index.ts ${color.dim('# or: npx opc run')}`);
|
|
402
|
+
console.log(` 4. Open http://localhost:3000\n`);
|
|
271
403
|
});
|
|
272
404
|
|
|
273
405
|
// ── Chat command ─────────────────────────────────────────────
|
package/src/core/runtime.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { WebChannel } from '../channels/web';
|
|
|
5
5
|
import { TelegramChannel } from '../channels/telegram';
|
|
6
6
|
import { WebSocketChannel } from '../channels/websocket';
|
|
7
7
|
import { DeepBrainMemoryStore } from '../memory/deepbrain';
|
|
8
|
+
import { Analytics } from '../analytics';
|
|
8
9
|
import type { OADDocument } from '../schema/oad';
|
|
9
10
|
import type { ISkill, MemoryStore, Message } from './types';
|
|
10
11
|
import type { Response } from 'express';
|
|
@@ -25,6 +26,7 @@ export class AgentRuntime {
|
|
|
25
26
|
private historyLimit: number = DEFAULT_HISTORY_LIMIT;
|
|
26
27
|
private shutdownHandlers: (() => Promise<void>)[] = [];
|
|
27
28
|
private isShuttingDown = false;
|
|
29
|
+
private analytics: Analytics = new Analytics();
|
|
28
30
|
|
|
29
31
|
async loadConfig(filePath: string): Promise<OADDocument> {
|
|
30
32
|
this.config = loadOAD(filePath);
|
|
@@ -64,6 +66,12 @@ export class AgentRuntime {
|
|
|
64
66
|
const port = ch.port ?? 3000;
|
|
65
67
|
const webChannel = new WebChannel(port);
|
|
66
68
|
webChannel.setAgentName(cfg.metadata.name);
|
|
69
|
+
webChannel.setAgentVersion(cfg.metadata.version);
|
|
70
|
+
webChannel.setAnalyticsProvider(() => this.analytics.getSnapshot());
|
|
71
|
+
webChannel.setChannelNames(cfg.spec.channels.map((c: any) => c.type));
|
|
72
|
+
webChannel.setSkillNames(cfg.spec.skills.map((s: any) => s.name));
|
|
73
|
+
const memType = memCfg && typeof memCfg.longTerm === 'object' && memCfg.longTerm.provider === 'deepbrain' ? 'deepbrain' : 'in-memory';
|
|
74
|
+
webChannel.setMemoryType(memType);
|
|
67
75
|
// Wire streaming
|
|
68
76
|
webChannel.onStreamMessage(async (msg: Message, res: Response) => {
|
|
69
77
|
res.writeHead(200, {
|
|
@@ -72,14 +80,17 @@ export class AgentRuntime {
|
|
|
72
80
|
Connection: 'keep-alive',
|
|
73
81
|
'Access-Control-Allow-Origin': '*',
|
|
74
82
|
});
|
|
83
|
+
const startTime = Date.now();
|
|
75
84
|
try {
|
|
76
85
|
for await (const chunk of this.agent!.handleMessageStream(msg)) {
|
|
77
86
|
res.write(`data: ${JSON.stringify({ content: chunk })}\n\n`);
|
|
78
87
|
}
|
|
79
88
|
res.write('data: [DONE]\n\n');
|
|
89
|
+
this.analytics.recordMessage(Date.now() - startTime);
|
|
80
90
|
} catch (err) {
|
|
81
91
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
82
92
|
res.write(`data: ${JSON.stringify({ error: errMsg })}\n\n`);
|
|
93
|
+
this.analytics.recordError();
|
|
83
94
|
}
|
|
84
95
|
res.end();
|
|
85
96
|
});
|
|
@@ -98,6 +109,18 @@ export class AgentRuntime {
|
|
|
98
109
|
}
|
|
99
110
|
|
|
100
111
|
await this.agent.init();
|
|
112
|
+
|
|
113
|
+
// Wire analytics to agent events
|
|
114
|
+
this.agent.on('message:out', () => {
|
|
115
|
+
// responseTime is approximated; real timing is done via skill/llm events
|
|
116
|
+
});
|
|
117
|
+
this.agent.on('skill:execute', (skillName: string) => {
|
|
118
|
+
this.analytics.recordSkillUsage(skillName);
|
|
119
|
+
});
|
|
120
|
+
this.agent.on('error', () => {
|
|
121
|
+
this.analytics.recordError();
|
|
122
|
+
});
|
|
123
|
+
|
|
101
124
|
this.logger.info('Agent initialized', { name: cfg.metadata.name });
|
|
102
125
|
return this.agent;
|
|
103
126
|
}
|
|
@@ -149,4 +172,12 @@ export class AgentRuntime {
|
|
|
149
172
|
getAgent(): BaseAgent | null {
|
|
150
173
|
return this.agent;
|
|
151
174
|
}
|
|
175
|
+
|
|
176
|
+
getAnalytics(): Analytics {
|
|
177
|
+
return this.analytics;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
getConfig(): OADDocument | null {
|
|
181
|
+
return this.config;
|
|
182
|
+
}
|
|
152
183
|
}
|
package/src/providers/index.ts
CHANGED
|
@@ -309,6 +309,14 @@ function isGeminiNative(): boolean {
|
|
|
309
309
|
|
|
310
310
|
export function createProvider(name: string = 'openai', model?: string, baseUrl?: string, apiKey?: string): LLMProvider {
|
|
311
311
|
const finalModel = model || process.env.OPC_LLM_MODEL || 'gpt-4o-mini';
|
|
312
|
+
|
|
313
|
+
// Auto-detect ollama: use localhost:11434/v1 and dummy apiKey
|
|
314
|
+
if (name === 'ollama') {
|
|
315
|
+
const ollamaBase = baseUrl || process.env.OPC_LLM_BASE_URL || 'http://localhost:11434/v1';
|
|
316
|
+
const ollamaKey = apiKey || process.env.OPC_LLM_API_KEY || 'ollama';
|
|
317
|
+
return new OpenAICompatibleProvider('ollama', finalModel, ollamaBase, ollamaKey);
|
|
318
|
+
}
|
|
319
|
+
|
|
312
320
|
const finalKey = apiKey || getApiKey();
|
|
313
321
|
const finalBaseUrl = baseUrl || getBaseUrl();
|
|
314
322
|
|
|
@@ -328,4 +336,4 @@ export function createProvider(name: string = 'openai', model?: string, baseUrl?
|
|
|
328
336
|
return new OpenAICompatibleProvider(resolvedName, finalModel, baseUrl, apiKey);
|
|
329
337
|
}
|
|
330
338
|
|
|
331
|
-
export const SUPPORTED_PROVIDERS = ['openai', 'deepseek', 'qwen', 'gemini', 'dashscope', 'zhipu', 'moonshot'] as const;
|
|
339
|
+
export const SUPPORTED_PROVIDERS = ['openai', 'ollama', 'deepseek', 'qwen', 'gemini', 'dashscope', 'zhipu', 'moonshot'] as const;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
FROM node:22-alpine
|
|
2
|
+
WORKDIR /app
|
|
3
|
+
COPY package.json package-lock.json* ./
|
|
4
|
+
RUN npm ci --production 2>/dev/null || npm install --production
|
|
5
|
+
COPY oad.yaml agent.yaml .env* ./
|
|
6
|
+
COPY src/ ./src/
|
|
7
|
+
COPY prompts/ ./prompts/ 2>/dev/null || true
|
|
8
|
+
EXPOSE 3000
|
|
9
|
+
CMD ["npx", "opc", "run"]
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# test-agent
|
|
2
|
+
|
|
3
|
+
Created with [OPC Agent](https://github.com/Deepleaper/opc-agent) using the `customer-service` template.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
1. **Install dependencies:**
|
|
8
|
+
```bash
|
|
9
|
+
npm install
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
2. **Run with Ollama (default):**
|
|
13
|
+
```bash
|
|
14
|
+
# Make sure Ollama is running with qwen2.5 model
|
|
15
|
+
ollama pull qwen2.5
|
|
16
|
+
npx tsx src/index.ts
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
3. **Or use OpenAI/other providers:**
|
|
20
|
+
```bash
|
|
21
|
+
# Edit .env and set your API key
|
|
22
|
+
npx opc run
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
4. **Open browser:** [http://localhost:3000](http://localhost:3000)
|
|
26
|
+
|
|
27
|
+
## Development
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx opc dev # Hot-reload mode
|
|
31
|
+
npx opc chat # CLI chat
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Project Structure
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
test-agent/
|
|
38
|
+
├── agent.yaml # OAD agent config (used by src/index.ts)
|
|
39
|
+
├── oad.yaml # OAD config (used by opc CLI)
|
|
40
|
+
├── src/
|
|
41
|
+
│ ├── index.ts # Entry point
|
|
42
|
+
│ └── skills/
|
|
43
|
+
│ └── echo.ts # Example skill
|
|
44
|
+
├── package.json
|
|
45
|
+
└── tsconfig.json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Configuration
|
|
49
|
+
|
|
50
|
+
Edit `agent.yaml` to customize your agent's personality, skills, and behavior.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
apiVersion: opc/v1
|
|
2
|
+
kind: Agent
|
|
3
|
+
metadata:
|
|
4
|
+
name: test-agent
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
description: My AI Agent
|
|
7
|
+
spec:
|
|
8
|
+
model: qwen2.5
|
|
9
|
+
provider:
|
|
10
|
+
default: ollama
|
|
11
|
+
systemPrompt: |
|
|
12
|
+
You are a helpful AI assistant named test-agent.
|
|
13
|
+
Be concise, helpful, and friendly.
|
|
14
|
+
channels:
|
|
15
|
+
- type: web
|
|
16
|
+
port: 3000
|
|
17
|
+
memory:
|
|
18
|
+
shortTerm: true
|
|
19
|
+
longTerm:
|
|
20
|
+
provider: deepbrain
|
|
21
|
+
skills:
|
|
22
|
+
- name: echo
|
|
23
|
+
description: Echo test skill
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
apiVersion: opc/v1
|
|
2
|
+
kind: Agent
|
|
3
|
+
metadata:
|
|
4
|
+
name: test-agent
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
description: Customer service agent with FAQ and human handoff
|
|
7
|
+
author: OPC Agent
|
|
8
|
+
license: Apache-2.0
|
|
9
|
+
spec:
|
|
10
|
+
provider:
|
|
11
|
+
default: deepseek
|
|
12
|
+
allowed:
|
|
13
|
+
- openai
|
|
14
|
+
- deepseek
|
|
15
|
+
- qwen
|
|
16
|
+
model: deepseek-chat
|
|
17
|
+
systemPrompt: |-
|
|
18
|
+
You are a friendly and professional customer service agent.
|
|
19
|
+
You help customers with their questions about products, orders, shipping, and returns.
|
|
20
|
+
Be concise, helpful, and empathetic. If you're unsure, offer to connect them with a human agent.
|
|
21
|
+
skills:
|
|
22
|
+
- name: faq-lookup
|
|
23
|
+
description: Look up FAQ answers
|
|
24
|
+
- name: human-handoff
|
|
25
|
+
description: Hand off to human agent
|
|
26
|
+
channels:
|
|
27
|
+
- type: web
|
|
28
|
+
port: 3000
|
|
29
|
+
memory:
|
|
30
|
+
shortTerm: true
|
|
31
|
+
longTerm: false
|