opc-agent 0.5.0 → 0.5.1
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 +6 -0
- package/dist/channels/web.js +146 -15
- package/dist/cli.js +274 -178
- package/dist/core/agent.d.ts +7 -1
- package/dist/core/agent.js +28 -3
- package/dist/core/runtime.d.ts +0 -3
- package/dist/core/runtime.js +23 -6
- package/dist/providers/index.d.ts +2 -5
- package/dist/providers/index.js +141 -18
- package/dist/schema/oad.d.ts +12 -12
- package/package.json +1 -1
- package/src/channels/web.ts +153 -20
- package/src/cli.ts +310 -202
- package/src/core/agent.ts +36 -3
- package/src/core/runtime.ts +24 -7
- package/src/providers/index.ts +161 -22
- package/tests/e2e.test.ts +134 -0
package/dist/channels/web.d.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { type Response } from 'express';
|
|
2
|
+
import type { Message } from '../core/types';
|
|
1
3
|
import { BaseChannel } from './index';
|
|
2
4
|
export declare class WebChannel extends BaseChannel {
|
|
3
5
|
readonly type = "web";
|
|
4
6
|
private app;
|
|
5
7
|
private server;
|
|
6
8
|
private port;
|
|
9
|
+
private streamHandler;
|
|
10
|
+
private agentName;
|
|
7
11
|
constructor(port?: number);
|
|
12
|
+
setAgentName(name: string): void;
|
|
13
|
+
onStreamMessage(handler: (msg: Message, res: Response) => Promise<void>): void;
|
|
8
14
|
private setupRoutes;
|
|
9
15
|
start(): Promise<void>;
|
|
10
16
|
stop(): Promise<void>;
|
package/dist/channels/web.js
CHANGED
|
@@ -6,11 +6,104 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.WebChannel = void 0;
|
|
7
7
|
const express_1 = __importDefault(require("express"));
|
|
8
8
|
const index_1 = require("./index");
|
|
9
|
+
const CHAT_HTML = `<!DOCTYPE html>
|
|
10
|
+
<html lang="en">
|
|
11
|
+
<head>
|
|
12
|
+
<meta charset="UTF-8">
|
|
13
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
14
|
+
<title>OPC Agent</title>
|
|
15
|
+
<style>
|
|
16
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
17
|
+
body{background:#0a0a0f;color:#e0e0e0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;height:100vh;display:flex;flex-direction:column}
|
|
18
|
+
header{background:#12121a;padding:16px 24px;border-bottom:1px solid #1e1e2e;display:flex;align-items:center;gap:12px}
|
|
19
|
+
header h1{font-size:18px;font-weight:600;color:#fff}
|
|
20
|
+
header .dot{width:8px;height:8px;border-radius:50%;background:#22c55e;animation:pulse 2s infinite}
|
|
21
|
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
|
|
22
|
+
#messages{flex:1;overflow-y:auto;padding:24px;display:flex;flex-direction:column;gap:16px}
|
|
23
|
+
.msg{max-width:720px;padding:12px 16px;border-radius:12px;line-height:1.6;font-size:14px;white-space:pre-wrap;word-break:break-word}
|
|
24
|
+
.msg.user{align-self:flex-end;background:#2563eb;color:#fff;border-bottom-right-radius:4px}
|
|
25
|
+
.msg.assistant{align-self:flex-start;background:#1e1e2e;color:#d4d4d8;border-bottom-left-radius:4px}
|
|
26
|
+
.msg.assistant .cursor{display:inline-block;width:2px;height:14px;background:#818cf8;animation:blink .6s infinite;vertical-align:text-bottom;margin-left:2px}
|
|
27
|
+
@keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
|
|
28
|
+
.msg.error{background:#7f1d1d;color:#fca5a5}
|
|
29
|
+
#input-area{background:#12121a;padding:16px 24px;border-top:1px solid #1e1e2e;display:flex;gap:12px}
|
|
30
|
+
#input{flex:1;background:#1e1e2e;border:1px solid #2e2e3e;border-radius:10px;padding:12px 16px;color:#fff;font-size:14px;outline:none;resize:none;max-height:120px;font-family:inherit}
|
|
31
|
+
#input:focus{border-color:#818cf8}
|
|
32
|
+
#send{background:#2563eb;color:#fff;border:none;border-radius:10px;padding:12px 20px;font-size:14px;cursor:pointer;font-weight:500;transition:background .2s}
|
|
33
|
+
#send:hover{background:#1d4ed8}
|
|
34
|
+
#send:disabled{background:#334155;cursor:not-allowed}
|
|
35
|
+
</style>
|
|
36
|
+
</head>
|
|
37
|
+
<body>
|
|
38
|
+
<header><div class="dot"></div><h1 id="title">OPC Agent</h1></header>
|
|
39
|
+
<div id="messages"></div>
|
|
40
|
+
<div id="input-area">
|
|
41
|
+
<textarea id="input" rows="1" placeholder="Type a message..." autocomplete="off"></textarea>
|
|
42
|
+
<button id="send">Send</button>
|
|
43
|
+
</div>
|
|
44
|
+
<script>
|
|
45
|
+
const msgs=document.getElementById('messages'),input=document.getElementById('input'),btn=document.getElementById('send');
|
|
46
|
+
let sessionId=crypto.randomUUID(),sending=false;
|
|
47
|
+
|
|
48
|
+
function addMsg(role,text){
|
|
49
|
+
const d=document.createElement('div');
|
|
50
|
+
d.className='msg '+role;
|
|
51
|
+
d.textContent=text;
|
|
52
|
+
msgs.appendChild(d);
|
|
53
|
+
msgs.scrollTop=msgs.scrollHeight;
|
|
54
|
+
return d;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
input.addEventListener('keydown',e=>{if(e.key==='Enter'&&!e.shiftKey){e.preventDefault();send()}});
|
|
58
|
+
input.addEventListener('input',()=>{input.style.height='auto';input.style.height=Math.min(input.scrollHeight,120)+'px'});
|
|
59
|
+
btn.addEventListener('click',send);
|
|
60
|
+
|
|
61
|
+
async function send(){
|
|
62
|
+
const text=input.value.trim();
|
|
63
|
+
if(!text||sending)return;
|
|
64
|
+
sending=true;btn.disabled=true;
|
|
65
|
+
input.value='';input.style.height='auto';
|
|
66
|
+
addMsg('user',text);
|
|
67
|
+
const el=addMsg('assistant','');
|
|
68
|
+
el.innerHTML='<span class="cursor"></span>';
|
|
69
|
+
try{
|
|
70
|
+
const res=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({message:text,sessionId})});
|
|
71
|
+
if(!res.ok)throw new Error('HTTP '+res.status);
|
|
72
|
+
const reader=res.body.getReader(),dec=new TextDecoder();
|
|
73
|
+
let full='';
|
|
74
|
+
while(true){
|
|
75
|
+
const{done,value}=await reader.read();
|
|
76
|
+
if(done)break;
|
|
77
|
+
const chunk=dec.decode(value,{stream:true});
|
|
78
|
+
const lines=chunk.split('\\n');
|
|
79
|
+
for(const line of lines){
|
|
80
|
+
if(!line.startsWith('data: '))continue;
|
|
81
|
+
const d=line.slice(6);
|
|
82
|
+
if(d==='[DONE]')continue;
|
|
83
|
+
try{const j=JSON.parse(d);if(j.content)full+=j.content;if(j.error)full='Error: '+j.error;}catch{}
|
|
84
|
+
}
|
|
85
|
+
el.textContent=full;
|
|
86
|
+
msgs.scrollTop=msgs.scrollHeight;
|
|
87
|
+
}
|
|
88
|
+
if(!full)el.textContent='(empty response)';
|
|
89
|
+
}catch(e){
|
|
90
|
+
el.className='msg error';el.textContent='Error: '+e.message;
|
|
91
|
+
}
|
|
92
|
+
sending=false;btn.disabled=false;input.focus();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Fetch agent info
|
|
96
|
+
fetch('/api/info').then(r=>r.json()).then(d=>{if(d.name)document.getElementById('title').textContent=d.name}).catch(()=>{});
|
|
97
|
+
</script>
|
|
98
|
+
</body>
|
|
99
|
+
</html>`;
|
|
9
100
|
class WebChannel extends index_1.BaseChannel {
|
|
10
101
|
type = 'web';
|
|
11
102
|
app;
|
|
12
103
|
server = null;
|
|
13
104
|
port;
|
|
105
|
+
streamHandler = null;
|
|
106
|
+
agentName = 'OPC Agent';
|
|
14
107
|
constructor(port = 3000) {
|
|
15
108
|
super();
|
|
16
109
|
this.port = port;
|
|
@@ -18,10 +111,61 @@ class WebChannel extends index_1.BaseChannel {
|
|
|
18
111
|
this.app.use(express_1.default.json());
|
|
19
112
|
this.setupRoutes();
|
|
20
113
|
}
|
|
114
|
+
setAgentName(name) {
|
|
115
|
+
this.agentName = name;
|
|
116
|
+
}
|
|
117
|
+
onStreamMessage(handler) {
|
|
118
|
+
this.streamHandler = handler;
|
|
119
|
+
}
|
|
21
120
|
setupRoutes() {
|
|
121
|
+
this.app.get('/', (_req, res) => {
|
|
122
|
+
res.type('html').send(CHAT_HTML);
|
|
123
|
+
});
|
|
22
124
|
this.app.get('/health', (_req, res) => {
|
|
23
125
|
res.json({ status: 'ok', timestamp: Date.now() });
|
|
24
126
|
});
|
|
127
|
+
this.app.get('/api/info', (_req, res) => {
|
|
128
|
+
res.json({ name: this.agentName });
|
|
129
|
+
});
|
|
130
|
+
// Streaming chat endpoint
|
|
131
|
+
this.app.post('/api/chat', async (req, res) => {
|
|
132
|
+
const { message, sessionId } = req.body;
|
|
133
|
+
if (!message) {
|
|
134
|
+
res.status(400).json({ error: 'message is required' });
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const msg = {
|
|
138
|
+
id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
|
139
|
+
role: 'user',
|
|
140
|
+
content: message,
|
|
141
|
+
timestamp: Date.now(),
|
|
142
|
+
metadata: { sessionId: sessionId ?? 'default' },
|
|
143
|
+
};
|
|
144
|
+
if (this.streamHandler) {
|
|
145
|
+
try {
|
|
146
|
+
await this.streamHandler(msg, res);
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
if (!res.headersSent) {
|
|
150
|
+
res.status(500).json({ error: 'Internal error' });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
// Fallback: non-streaming
|
|
156
|
+
if (!this.handler) {
|
|
157
|
+
res.status(503).json({ error: 'Agent not ready' });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const response = await this.handler(msg);
|
|
162
|
+
res.json({ response: response.content, id: response.id });
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
res.status(500).json({ error: 'Internal error' });
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
// Legacy endpoint
|
|
25
169
|
this.app.post('/chat', async (req, res) => {
|
|
26
170
|
if (!this.handler) {
|
|
27
171
|
res.status(503).json({ error: 'Agent not ready' });
|
|
@@ -43,28 +187,15 @@ class WebChannel extends index_1.BaseChannel {
|
|
|
43
187
|
const response = await this.handler(msg);
|
|
44
188
|
res.json({ response: response.content, id: response.id });
|
|
45
189
|
}
|
|
46
|
-
catch
|
|
190
|
+
catch {
|
|
47
191
|
res.status(500).json({ error: 'Internal error' });
|
|
48
192
|
}
|
|
49
193
|
});
|
|
50
|
-
// SSE streaming endpoint
|
|
51
|
-
this.app.get('/stream', (req, res) => {
|
|
52
|
-
res.writeHead(200, {
|
|
53
|
-
'Content-Type': 'text/event-stream',
|
|
54
|
-
'Cache-Control': 'no-cache',
|
|
55
|
-
'Connection': 'keep-alive',
|
|
56
|
-
});
|
|
57
|
-
res.write('data: {"type":"connected"}\n\n');
|
|
58
|
-
const interval = setInterval(() => {
|
|
59
|
-
res.write('data: {"type":"heartbeat"}\n\n');
|
|
60
|
-
}, 30000);
|
|
61
|
-
req.on('close', () => clearInterval(interval));
|
|
62
|
-
});
|
|
63
194
|
}
|
|
64
195
|
async start() {
|
|
65
196
|
return new Promise((resolve) => {
|
|
66
197
|
this.server = this.app.listen(this.port, () => {
|
|
67
|
-
console.log(`[WebChannel] Listening on
|
|
198
|
+
console.log(`[WebChannel] Listening on http://localhost:${this.port}`);
|
|
68
199
|
resolve();
|
|
69
200
|
});
|
|
70
201
|
});
|