openkbs 0.0.50 → 0.0.52
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/README.md +12 -11
- package/package.json +1 -1
- package/src/actions.js +3 -3
- package/templates/.openkbs/knowledge/examples/ai-copywriter-agent/app/settings.json +1 -1
- package/templates/.openkbs/knowledge/examples/ai-copywriter-agent/scripts/utils/agent_client.js +167 -71
- package/templates/.openkbs/knowledge/examples/ai-copywriter-agent/src/Events/actions.js +1 -11
- package/templates/.openkbs/knowledge/examples/ai-copywriter-agent/src/Events/handler.js +10 -14
- package/templates/.openkbs/knowledge/metadata.json +2 -2
- package/templates/CLAUDE.md +200 -25
- package/templates/README.md +4 -74
- package/templates/package.json +2 -4
- package/templates/scripts/run_job.js +3 -1
- package/templates/scripts/utils/agent_client.js +156 -70
- package/templates/src/Events/handler.js +1 -1
- package/version.json +3 -3
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# OpenKBS · [](https://github.com/open-kbs/openkbs-chat/blob/main/LICENSE) [](https://www.npmjs.com/package/openkbs)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
OpenKBS is an extendable AI service designed to build,
|
|
4
5
|
deploy and integrate AI agents and applications.
|
|
5
6
|
|
|
6
7
|
## Table of Contents
|
|
@@ -9,10 +10,10 @@ deploy and integrate AI agents and applications.
|
|
|
9
10
|
- [Create App](#create-app)
|
|
10
11
|
- [Deploy](#deploy)
|
|
11
12
|
- [Extend Frontend](#extend-frontend)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
- [Chat Render](#chat-render)
|
|
14
|
+
- [Setup Local Development](#setup-local-development)
|
|
15
|
+
- [Use Built-in MUI Components](#use-built-in-mui-components)
|
|
16
|
+
- [AI-Powered Generation](#ai-powered-frontend-generation)
|
|
16
17
|
- [Extend Backend](#extend-backend)
|
|
17
18
|
- [Mobile & Desktop App](#mobile--desktop-app)
|
|
18
19
|
- [Framework Documentation](#framework-documentation)
|
|
@@ -417,7 +418,7 @@ The `onPublicAPIRequest` handler serves as a bridge between publicly accessible
|
|
|
417
418
|
|
|
418
419
|
**How it works:**
|
|
419
420
|
|
|
420
|
-
1. **Public API Endpoint:** The `onPublicAPIRequest` handler is associated with a dedicated public API endpoint (e.g., `/publicAPIRequest`)
|
|
421
|
+
1. **Public API Endpoint:** The `onPublicAPIRequest` handler is associated with a dedicated public API endpoint (e.g., `/publicAPIRequest`)
|
|
421
422
|
2. **Payload** could be any JSON object
|
|
422
423
|
3. **Handler Logic:** Inside the `onPublicAPIRequest` handler, you receive this payload as an argument. Your code then processes the payload and performs the necessary actions using the OpenKBS SDK for example.
|
|
423
424
|
4. **Data Storage:** The `openkbs.items` function is typically used within this handler to create, update, or delete items in the OpenKBS NoSQL Items service. You can use encryption for sensitive data within this handler.
|
|
@@ -483,8 +484,8 @@ Remember to carefully consider security implications and implement necessary pre
|
|
|
483
484
|
|
|
484
485
|
#### onAddMessages Event Handler
|
|
485
486
|
|
|
486
|
-
The `onAddMessages` handler allows you to intercept and process messages *as they are added to the chat*.
|
|
487
|
-
This handler is triggered *after* the `onRequest` handler but *before* the message is sent to the LLM.
|
|
487
|
+
The `onAddMessages` handler allows you to intercept and process messages *as they are added to the chat*.
|
|
488
|
+
This handler is triggered *after* the `onRequest` handler but *before* the message is sent to the LLM.
|
|
488
489
|
It's particularly useful for scenarios where a third-party system or service sends messages directly to your OpenKBS app to perform an action.
|
|
489
490
|
Unlike `onPublicAPIRequest`, this handler requires an `apiKey`, which can be created in the 'Access' section of your OpenKBS app.
|
|
490
491
|
|
|
@@ -671,7 +672,7 @@ This file contains essential configuration settings for the AI agent.
|
|
|
671
672
|
|
|
672
673
|
#### LLM Instructions
|
|
673
674
|
`app/instructions.txt`
|
|
674
|
-
This file contains the instructions for the LLM, guiding its behavior and interaction with custom functionalities.
|
|
675
|
+
This file contains the instructions for the LLM, guiding its behavior and interaction with custom functionalities.
|
|
675
676
|
Clear and specific instructions ensure the LLM effectively utilizes provided actions and commands.
|
|
676
677
|
|
|
677
678
|
**Example Instructions:**
|
|
@@ -1247,7 +1248,7 @@ This endpoint allows adding messages to a specified chat, which is useful for in
|
|
|
1247
1248
|
#### createPublicChatToken
|
|
1248
1249
|
|
|
1249
1250
|
This endpoint allows third-party systems (like WordPress, e-commerce platforms, etc.) to generate signed JWT tokens for their users to interact with the OpenKBS agent. This enables secure, limited access to the agent while maintaining user context and authorization.
|
|
1250
|
-
|
|
1251
|
+
|
|
1251
1252
|
- **Endpoint:** `https://chat.openkbs.com/`
|
|
1252
1253
|
- **Method:** `POST`
|
|
1253
1254
|
- **Request Body (JSON):**
|
|
@@ -1370,4 +1371,4 @@ We welcome contributions from the community! Please feel free to submit issues,
|
|
|
1370
1371
|
|
|
1371
1372
|
## Contact
|
|
1372
1373
|
|
|
1373
|
-
For more information, visit our [official website](https://openkbs.com) or join our community discussions on [GitHub](https://github.com/open-kbs/openkbs/discussions).
|
|
1374
|
+
For more information, visit our [official website](https://openkbs.com) or join our community discussions on [GitHub](https://github.com/open-kbs/openkbs/discussions).
|
package/package.json
CHANGED
package/src/actions.js
CHANGED
|
@@ -138,10 +138,10 @@ function isModulePresent(moduleName) {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
async function deployAction(moduleName) {
|
|
141
|
-
const validModules = ['contentRender', 'onRequest', 'onResponse', 'onAddMessages', 'onPublicAPIRequest'];
|
|
141
|
+
const validModules = ['contentRender', 'onRequest', 'onResponse', 'onAddMessages', 'onPublicAPIRequest', 'onCronjob'];
|
|
142
142
|
|
|
143
143
|
if (moduleName && !validModules.includes(moduleName)) {
|
|
144
|
-
return console.error(`Invalid module name ${moduleName} (valid options: 'contentRender', 'onRequest', 'onResponse', 'onAddMessages', 'onPublicAPIRequest')`);
|
|
144
|
+
return console.error(`Invalid module name ${moduleName} (valid options: 'contentRender', 'onRequest', 'onResponse', 'onAddMessages', 'onPublicAPIRequest', 'onCronjob')`);
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
const modulesToDeploy = moduleName ? [moduleName] : validModules.filter(isModulePresent);
|
|
@@ -217,7 +217,7 @@ async function pushAction(location = 'origin', targetFile, options) {
|
|
|
217
217
|
await deployAction();
|
|
218
218
|
console.green(`KB update complete: All changes have been successfully uploaded to https://${kbId}.apps.openkbs.com`);
|
|
219
219
|
} else if (location === 'localstack') {
|
|
220
|
-
const modulesToDeploy = ['onRequest', 'onResponse', 'onAddMessages', 'onPublicAPIRequest'].filter(isModulePresent);
|
|
220
|
+
const modulesToDeploy = ['onRequest', 'onResponse', 'onAddMessages', 'onPublicAPIRequest', 'onCronjob'].filter(isModulePresent);
|
|
221
221
|
|
|
222
222
|
const originalDir = process.cwd();
|
|
223
223
|
for (const module of modulesToDeploy) {
|
package/templates/.openkbs/knowledge/examples/ai-copywriter-agent/scripts/utils/agent_client.js
CHANGED
|
@@ -2,6 +2,7 @@ const https = require('https');
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const readline = require('readline');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { URL } = require('url');
|
|
5
6
|
|
|
6
7
|
class OpenKBSAgentClient {
|
|
7
8
|
constructor() {
|
|
@@ -12,7 +13,7 @@ class OpenKBSAgentClient {
|
|
|
12
13
|
|
|
13
14
|
findSettings() {
|
|
14
15
|
let currentDir = __dirname;
|
|
15
|
-
|
|
16
|
+
|
|
16
17
|
while (currentDir !== path.parse(currentDir).root) {
|
|
17
18
|
const settingsPath = path.join(currentDir, 'app', 'settings.json');
|
|
18
19
|
if (fs.existsSync(settingsPath)) {
|
|
@@ -20,13 +21,13 @@ class OpenKBSAgentClient {
|
|
|
20
21
|
}
|
|
21
22
|
currentDir = path.dirname(currentDir);
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
+
|
|
24
25
|
throw new Error('Could not find app/settings.json in parent directories');
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
findSecretsPath() {
|
|
28
29
|
let currentDir = __dirname;
|
|
29
|
-
|
|
30
|
+
|
|
30
31
|
while (currentDir !== path.parse(currentDir).root) {
|
|
31
32
|
const settingsPath = path.join(currentDir, 'app', 'settings.json');
|
|
32
33
|
if (fs.existsSync(settingsPath)) {
|
|
@@ -35,24 +36,28 @@ class OpenKBSAgentClient {
|
|
|
35
36
|
}
|
|
36
37
|
currentDir = path.dirname(currentDir);
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
+
|
|
39
40
|
throw new Error('Could not find agent directory with app/settings.json');
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
async getApiKey() {
|
|
43
44
|
if (this.apiKey) return this.apiKey;
|
|
44
|
-
|
|
45
|
+
|
|
45
46
|
if (fs.existsSync(this.secretsPath)) {
|
|
46
47
|
const secrets = JSON.parse(fs.readFileSync(this.secretsPath, 'utf8'));
|
|
47
48
|
this.apiKey = secrets.apiKey;
|
|
48
49
|
return this.apiKey;
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
+
|
|
51
52
|
this.apiKey = await this.promptForApiKey();
|
|
52
53
|
this.saveApiKey(this.apiKey);
|
|
53
54
|
return this.apiKey;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
async init() {
|
|
58
|
+
await this.getApiKey();
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
async promptForApiKey() {
|
|
57
62
|
return new Promise((resolve) => {
|
|
58
63
|
const rl = readline.createInterface({
|
|
@@ -67,7 +72,17 @@ class OpenKBSAgentClient {
|
|
|
67
72
|
resolve(key);
|
|
68
73
|
});
|
|
69
74
|
|
|
70
|
-
rl._writeToOutput = (str) =>
|
|
75
|
+
rl._writeToOutput = (str) => {
|
|
76
|
+
if (str === '\n' || str === '\r\n') {
|
|
77
|
+
rl.output.write(str);
|
|
78
|
+
} else if (str.match(/[\x08\x7f]/)) {
|
|
79
|
+
rl.output.write(str);
|
|
80
|
+
} else if (rl.line && str.length === 1) {
|
|
81
|
+
rl.output.write('*');
|
|
82
|
+
} else {
|
|
83
|
+
rl.output.write(str);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
71
86
|
});
|
|
72
87
|
}
|
|
73
88
|
|
|
@@ -79,89 +94,170 @@ class OpenKBSAgentClient {
|
|
|
79
94
|
fs.writeFileSync(this.secretsPath, JSON.stringify({ apiKey: key }, null, 2));
|
|
80
95
|
}
|
|
81
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Run a job and get response from the agent
|
|
99
|
+
*
|
|
100
|
+
* @param {string|Array} message - Either:
|
|
101
|
+
* - String: "Do something ..."
|
|
102
|
+
* - Array: [
|
|
103
|
+
* {type:"text", text:"Process this invoice"},
|
|
104
|
+
* {type:"image_url", image_url:{url:"https://files.openkbs.com/invoice.png"}}
|
|
105
|
+
* ]
|
|
106
|
+
*
|
|
107
|
+
* @returns {Promise<Object>} Response structure:
|
|
108
|
+
* {
|
|
109
|
+
* data: {
|
|
110
|
+
* type: 'TEXT' | 'CUSTOM_TYPE',
|
|
111
|
+
* content: '...' | data: {...}
|
|
112
|
+
* },
|
|
113
|
+
* chatId: 'xxx-xxx',
|
|
114
|
+
* msgId: 'msg_xxx'
|
|
115
|
+
* }
|
|
116
|
+
*/
|
|
82
117
|
async runJob(message, options = {}) {
|
|
83
118
|
const apiKey = await this.getApiKey();
|
|
84
|
-
|
|
85
|
-
if (!this.settings.kbId) {
|
|
86
|
-
throw new Error('First use: "openkbs push" to create the agent');
|
|
87
|
-
}
|
|
119
|
+
if (!this.settings.kbId) throw new Error('First use: "openkbs push" to create the agent');
|
|
88
120
|
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
121
|
+
const payload = { message };
|
|
122
|
+
Object.keys(options).forEach(key => {
|
|
123
|
+
if (key === 'historyLimit') {
|
|
124
|
+
payload[key] = Math.min(Math.max(1, options[key]), 100);
|
|
125
|
+
} else if (options[key] !== undefined) {
|
|
126
|
+
payload[key] = options[key];
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const response = await this.request(
|
|
131
|
+
`https://${this.settings.kbId}.apps.openkbs.com/api`,
|
|
132
|
+
payload,
|
|
133
|
+
{ Authorization: `Bearer ${apiKey}` }
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (response.chatId) this.lastChatId = response.chatId;
|
|
137
|
+
return response;
|
|
99
138
|
}
|
|
100
139
|
|
|
101
|
-
async
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
140
|
+
async continueChat(message, chatId = null, options = {}) {
|
|
141
|
+
const targetChatId = chatId || this.lastChatId;
|
|
142
|
+
if (!targetChatId) throw new Error('No chatId provided and no previous chat to continue');
|
|
143
|
+
|
|
144
|
+
return this.runJob(message, {
|
|
145
|
+
...options,
|
|
146
|
+
chatId: targetChatId,
|
|
147
|
+
includeHistory: options.includeHistory !== false
|
|
106
148
|
});
|
|
107
|
-
|
|
108
|
-
try {
|
|
109
|
-
return JSON.parse(response)[0].createdChatId;
|
|
110
|
-
} catch (error) {
|
|
111
|
-
if (fs.existsSync(this.secretsPath)) {
|
|
112
|
-
fs.unlinkSync(this.secretsPath);
|
|
113
|
-
}
|
|
114
|
-
throw new Error('Authentication failed.');
|
|
115
|
-
}
|
|
116
149
|
}
|
|
117
150
|
|
|
118
|
-
|
|
151
|
+
async uploadFile(filePath, options = {}) {
|
|
152
|
+
const apiKey = await this.getApiKey();
|
|
153
|
+
if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
154
|
+
|
|
155
|
+
const fileContent = fs.readFileSync(filePath);
|
|
156
|
+
const ext = path.extname(filePath);
|
|
157
|
+
const fileName = options.fileName || `file-${Date.now()}-${Math.random().toString(36).substring(7)}${ext}`;
|
|
158
|
+
const fileType = options.fileType || this.getMimeType(filePath);
|
|
159
|
+
|
|
160
|
+
const presignedResponse = await this.request('https://kb.openkbs.com/', {
|
|
161
|
+
apiKey,
|
|
162
|
+
kbId: this.settings.kbId,
|
|
163
|
+
namespace: 'files',
|
|
164
|
+
presignedOperation: 'putObject',
|
|
165
|
+
action: 'createPresignedURL',
|
|
166
|
+
fileName,
|
|
167
|
+
fileType
|
|
168
|
+
}, { Origin: `https://${this.settings.kbId}.apps.openkbs.com` });
|
|
169
|
+
|
|
170
|
+
const presignedUrl = presignedResponse.presignedUrl || presignedResponse;
|
|
171
|
+
|
|
172
|
+
await this.requestRaw(presignedUrl, fileContent, {
|
|
173
|
+
'Content-Type': fileType,
|
|
174
|
+
'Content-Length': fileContent.length
|
|
175
|
+
}, 'PUT');
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
fileName,
|
|
179
|
+
uploaded: true,
|
|
180
|
+
url: `https://file.openkbs.com/files/${this.settings.kbId}/${fileName}`
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
getMimeType(filePath) {
|
|
185
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
186
|
+
const mimeTypes = {
|
|
187
|
+
'.pdf': 'application/pdf',
|
|
188
|
+
'.txt': 'text/plain',
|
|
189
|
+
'.json': 'application/json',
|
|
190
|
+
'.jpg': 'image/jpeg',
|
|
191
|
+
'.jpeg': 'image/jpeg',
|
|
192
|
+
'.png': 'image/png',
|
|
193
|
+
'.gif': 'image/gif',
|
|
194
|
+
'.doc': 'application/msword',
|
|
195
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
196
|
+
'.xls': 'application/vnd.ms-excel',
|
|
197
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
198
|
+
'.csv': 'text/csv',
|
|
199
|
+
'.html': 'text/html',
|
|
200
|
+
'.xml': 'application/xml',
|
|
201
|
+
'.zip': 'application/zip'
|
|
202
|
+
};
|
|
203
|
+
return mimeTypes[ext] || 'application/octet-stream';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Unified HTTP request helper
|
|
207
|
+
async request(url, payload, headers = {}, method = 'POST') {
|
|
119
208
|
return new Promise((resolve, reject) => {
|
|
120
|
-
const { hostname, pathname } = new URL(url);
|
|
209
|
+
const { hostname, pathname, search } = new URL(url);
|
|
121
210
|
const req = https.request({
|
|
122
|
-
hostname,
|
|
123
|
-
path: pathname,
|
|
124
|
-
method
|
|
125
|
-
headers: { 'Content-Type': 'application/json' }
|
|
211
|
+
hostname,
|
|
212
|
+
path: pathname + (search || ''),
|
|
213
|
+
method,
|
|
214
|
+
headers: { 'Content-Type': 'application/json', ...headers }
|
|
126
215
|
}, res => {
|
|
127
216
|
let data = '';
|
|
128
217
|
res.on('data', chunk => data += chunk);
|
|
129
|
-
res.on('end', () =>
|
|
218
|
+
res.on('end', () => {
|
|
219
|
+
try {
|
|
220
|
+
const result = JSON.parse(data);
|
|
221
|
+
if (res.statusCode === 401) {
|
|
222
|
+
if (fs.existsSync(this.secretsPath)) fs.unlinkSync(this.secretsPath);
|
|
223
|
+
reject(new Error('Authentication failed. Please run "node scripts/run_job.js init" to reconfigure.'));
|
|
224
|
+
} else if (res.statusCode !== 200) {
|
|
225
|
+
reject(new Error(`Request failed with status ${res.statusCode}: ${data}`));
|
|
226
|
+
} else {
|
|
227
|
+
resolve(result);
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
reject(new Error(`Failed to parse response: ${error.message}`));
|
|
231
|
+
}
|
|
232
|
+
});
|
|
130
233
|
}).on('error', reject);
|
|
131
234
|
|
|
132
|
-
req.write(JSON.stringify(payload));
|
|
235
|
+
req.write(typeof payload === 'string' ? payload : JSON.stringify(payload));
|
|
133
236
|
req.end();
|
|
134
237
|
});
|
|
135
238
|
}
|
|
136
239
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
resolve(result);
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
})
|
|
163
|
-
.catch(console.error);
|
|
164
|
-
}, 1000);
|
|
240
|
+
// Raw request for binary data
|
|
241
|
+
async requestRaw(url, data, headers = {}, method = 'PUT') {
|
|
242
|
+
return new Promise((resolve, reject) => {
|
|
243
|
+
const { hostname, pathname, search } = new URL(url);
|
|
244
|
+
const req = https.request({
|
|
245
|
+
hostname,
|
|
246
|
+
path: pathname + (search || ''),
|
|
247
|
+
method,
|
|
248
|
+
headers
|
|
249
|
+
}, res => {
|
|
250
|
+
if (res.statusCode === 200 || res.statusCode === 204) {
|
|
251
|
+
resolve();
|
|
252
|
+
} else {
|
|
253
|
+
let responseData = '';
|
|
254
|
+
res.on('data', chunk => responseData += chunk);
|
|
255
|
+
res.on('end', () => reject(new Error(`Request failed with status ${res.statusCode}: ${responseData}`)));
|
|
256
|
+
}
|
|
257
|
+
}).on('error', reject);
|
|
258
|
+
|
|
259
|
+
req.write(data);
|
|
260
|
+
req.end();
|
|
165
261
|
});
|
|
166
262
|
}
|
|
167
263
|
}
|
|
@@ -16,6 +16,7 @@ const extractJSONFromText = (text) => {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export const getActions = () => [
|
|
19
|
+
// IMPORTANT: Actions returning JOB_COMPLETED or JOB_FAILED stop agent execution and return final result
|
|
19
20
|
[/[\s\S]*"type"\s*:\s*"JOB_COMPLETED"[\s\S]*/, async (match, event) => {
|
|
20
21
|
const parsedData = extractJSONFromText(match[0]);
|
|
21
22
|
if (parsedData && parsedData.type === "JOB_COMPLETED") {
|
|
@@ -98,11 +99,6 @@ export const getActions = () => [
|
|
|
98
99
|
[/\/?webpageToText\("(.*)"\)/, async (match) => {
|
|
99
100
|
try {
|
|
100
101
|
let response = await openkbs.webpageToText(match[1]);
|
|
101
|
-
|
|
102
|
-
// limit output length
|
|
103
|
-
if (response?.content?.length > 5000) {
|
|
104
|
-
response.content = response.content.substring(0, 5000);
|
|
105
|
-
}
|
|
106
102
|
if(!response?.url) return { data: { error: "Unable to read website" } };
|
|
107
103
|
return { data: response };
|
|
108
104
|
} catch (e) {
|
|
@@ -113,12 +109,6 @@ export const getActions = () => [
|
|
|
113
109
|
[/\/?documentToText\("(.*)"\)/, async (match) => {
|
|
114
110
|
try {
|
|
115
111
|
let response = await openkbs.documentToText(match[1]);
|
|
116
|
-
|
|
117
|
-
// limit output length
|
|
118
|
-
if (response?.text?.length > 5000) {
|
|
119
|
-
response.text = response.text.substring(0, 5000);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
112
|
return { data: response };
|
|
123
113
|
} catch (e) {
|
|
124
114
|
return { error: e.response.data };
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {getActions} from './actions.js';
|
|
2
2
|
|
|
3
3
|
export const backendHandler = async (event) => {
|
|
4
|
-
const maxSelfInvokeMessagesCount = 60;
|
|
5
4
|
const lastMessage = event.payload.messages[event.payload.messages.length - 1];
|
|
6
5
|
const actions = getActions();
|
|
7
6
|
|
|
@@ -13,23 +12,20 @@ export const backendHandler = async (event) => {
|
|
|
13
12
|
return acc;
|
|
14
13
|
}, []);
|
|
15
14
|
|
|
16
|
-
|
|
17
|
-
const isJobFinished = /"JOB_COMPLETED"|"JOB_FAILED"/.test(lastMessage.content);
|
|
18
|
-
|
|
19
|
-
const meta = {
|
|
20
|
-
_meta_actions:
|
|
21
|
-
(
|
|
22
|
-
event?.payload?.messages?.length > maxSelfInvokeMessagesCount ||
|
|
23
|
-
isJobFinished && lastMessage.role === 'system'
|
|
24
|
-
)
|
|
25
|
-
? []
|
|
26
|
-
: ["REQUEST_CHAT_MODEL"]
|
|
27
|
-
}
|
|
15
|
+
const reachedMessageLimit = event?.payload?.messages?.length > 60;
|
|
28
16
|
|
|
29
17
|
if (matchingActions.length > 0) {
|
|
30
18
|
try {
|
|
31
19
|
const results = await Promise.all(matchingActions);
|
|
32
20
|
|
|
21
|
+
// IMPORTANT: Actions returning JOB_COMPLETED or JOB_FAILED stop agent execution and return final result
|
|
22
|
+
const isOnlyJobCompletion = results.length === 1 &&
|
|
23
|
+
(results[0]?.type === 'JOB_COMPLETED' || results[0]?.type === 'JOB_FAILED');
|
|
24
|
+
|
|
25
|
+
const meta = {
|
|
26
|
+
_meta_actions: (reachedMessageLimit || isOnlyJobCompletion) ? [] : ["REQUEST_CHAT_MODEL"]
|
|
27
|
+
};
|
|
28
|
+
|
|
33
29
|
if (results?.[0]?.data?.some?.(o => o?.type === 'image_url')) {
|
|
34
30
|
return {
|
|
35
31
|
...results[0],
|
|
@@ -46,7 +42,7 @@ export const backendHandler = async (event) => {
|
|
|
46
42
|
return {
|
|
47
43
|
type: 'ERROR',
|
|
48
44
|
error: error.message,
|
|
49
|
-
|
|
45
|
+
_meta_actions: reachedMessageLimit ? [] : ["REQUEST_CHAT_MODEL"]
|
|
50
46
|
};
|
|
51
47
|
}
|
|
52
48
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.0.
|
|
3
|
-
}
|
|
2
|
+
"version": "0.0.16"
|
|
3
|
+
}
|
package/templates/CLAUDE.md
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
**FIRST**: Update the knowledge base:
|
|
7
7
|
```bash
|
|
8
|
-
openkbs update
|
|
8
|
+
openkbs update
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
**SECOND**: Read every file in the examples folder using the Read tool:
|
|
11
|
+
**SECOND**: Read every single file in the examples folder using the Read tool:
|
|
12
12
|
- First discover all files in the `.openkbs/knowledge/examples/` directory and ALL subdirectories.
|
|
13
|
-
- Then, use the Read tool to examine the content of EACH file individually (skip only icon.png files).
|
|
13
|
+
- Then, use the Read tool to examine the content of EACH file individually (skip only icon.png files, src/Frontend/Presentational/* and src/Events/Helpers/*).
|
|
14
14
|
- You must read ALL files directly
|
|
15
15
|
|
|
16
16
|
**THIRD**: Read existing agent code using the Read tool:
|
|
@@ -24,6 +24,17 @@ openkbs update knowledge
|
|
|
24
24
|
- Study the complete working examples to understand OpenKBS patterns
|
|
25
25
|
- Never guess framework methods, settings or variables — always reference the examples.
|
|
26
26
|
|
|
27
|
+
## FIRST DECISION: Execution Context Analysis
|
|
28
|
+
|
|
29
|
+
** BEFORE writing ANY code, you MUST answer these questions IN ORDER:**
|
|
30
|
+
|
|
31
|
+
1. **Where will this code execute?** (Cloud or Local)
|
|
32
|
+
2. **What resources does it need to access?** (List each: databases, APIs, files, etc.)
|
|
33
|
+
3. **Where does each resource exist?** (Public internet, local network, specific machine)
|
|
34
|
+
4. **Can the execution environment reach each resource?** (Network path exists?)
|
|
35
|
+
|
|
36
|
+
**You MUST show your reasoning for these 4 questions before any code implementation.**
|
|
37
|
+
|
|
27
38
|
## Development Guidelines
|
|
28
39
|
- To add npm dependency to backend handlers, add it to onRequest.json and onResponse.json
|
|
29
40
|
- In src/Events and src/Frontend always use Imports (not Require)
|
|
@@ -32,46 +43,76 @@ openkbs update knowledge
|
|
|
32
43
|
- Add and use npm dependencies only if necessary, some of those shown in the examples are purely demonstrative
|
|
33
44
|
- If developing new agent, generate it's own ./scripts/run_job.js
|
|
34
45
|
- Before using third-party services in onRequest and onResponse handlers, ask the user for permission
|
|
46
|
+
- provide README.md
|
|
47
|
+
|
|
48
|
+
## Architecture Overview: Execution Environments Define Everything
|
|
49
|
+
|
|
50
|
+
OpenKBS provides **three distinct execution environments**, each with different capabilities and constraints:
|
|
51
|
+
|
|
52
|
+
### Execution Environment Reality Check
|
|
35
53
|
|
|
36
|
-
|
|
37
|
-
|
|
54
|
+
**Cloud Environment (`./src/Events/`):**
|
|
55
|
+
- Runs in serverless compute service for running code (stateless, ephemeral)
|
|
56
|
+
- No localhost, no local filesystem, no persistent state
|
|
57
|
+
- Can ONLY reach internet-accessible resources
|
|
58
|
+
- Each execution is isolated and temporary
|
|
38
59
|
|
|
39
|
-
|
|
40
|
-
-
|
|
41
|
-
-
|
|
42
|
-
-
|
|
60
|
+
**Browser Environment (`./src/Frontend/`):**
|
|
61
|
+
- Runs in user's browser when visiting https://[kbId].apps.openkbs.com
|
|
62
|
+
- React-based UI customization
|
|
63
|
+
- Subject to browser security constraints
|
|
43
64
|
|
|
44
|
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
65
|
+
**Local Environment (`./scripts/`) - Optional but Powerful:**
|
|
66
|
+
- Runs on YOUR machine with YOUR network context
|
|
67
|
+
- Can access YOUR localhost, files, and local services
|
|
68
|
+
- Enables advanced patterns: multi-agent orchestration, local resource integration
|
|
69
|
+
- Not required for simple agents, but unlocks complex workflows
|
|
70
|
+
|
|
71
|
+
### The Fundamental Rule
|
|
72
|
+
**Before writing ANY code, ask: Where does this run and what can it reach?**
|
|
48
73
|
|
|
49
74
|
### Backend
|
|
50
75
|
The OpenKBS backend framework is for developing AI agents with custom tools, using Node.js.
|
|
51
76
|
It integrates with openkbs chat service via `onRequest` and `onResponse` handlers for custom actions and service integration.
|
|
52
77
|
|
|
53
|
-
#### Backend Handlers
|
|
78
|
+
#### Backend Handlers (Cloud Environment)
|
|
54
79
|
The OpenKBS framework's core uses `onRequest` and `onResponse` handlers as middleware for message tool call parsing and execution.
|
|
55
|
-
|
|
80
|
+
These handlers run in the cloud environment.
|
|
56
81
|
- **`onResponse` Handler:** Activated after the LLM generates a message, enabling command extraction, and action execution.
|
|
57
82
|
- **`onRequest` Handler:** Triggered on user message to allow the user to execute action
|
|
58
83
|
|
|
59
|
-
#### NPM Dependencies for onRequest.js or onResponse.js Backend Handlers
|
|
84
|
+
#### NPM Dependencies for onRequest.js or onResponse.js Backend Handlers (Cloud → Public resources)
|
|
60
85
|
1. If a file imports an NPM dependency and is then imported by onRequest.js or onResponse.js, this dependency must be defined in the handler's corresponding json file
|
|
61
86
|
Example: If actions.js imports mysql2 and onResponse.js imports actions.js, then mysql2 must be in onResponse.json:
|
|
62
87
|
{
|
|
63
88
|
"dependencies": {
|
|
64
|
-
"mysql2": "
|
|
89
|
+
"mysql2": "latest"
|
|
65
90
|
}
|
|
66
91
|
}
|
|
67
92
|
|
|
68
93
|
Similarly, we need to create onRequest.json for onRequest.js as each handler have separate Node.js build with separate dependencies
|
|
69
94
|
|
|
70
|
-
#### Managing Secrets
|
|
71
|
-
|
|
95
|
+
#### Managing Secrets for Backend Handlers
|
|
96
|
+
Secrets securely store credentials that allow Backend Handlers to access external services like databases, APIs, etc.
|
|
97
|
+
Example:
|
|
98
|
+
`const key = "{{secrets.KEY}}"`
|
|
99
|
+
|
|
100
|
+
**Workflow**:
|
|
101
|
+
1. Write code using {{secrets.SECRET_NAME}} placeholders
|
|
102
|
+
2. Deploy agent with `openkbs push` (generates kbId in settings.json)
|
|
103
|
+
3. Prompt developer to define secrets: `Please set your credentials at: https://[kbId].apps.openkbs.com/?add_secrets=SECRET_NAME1,SECRET_NAME2`
|
|
104
|
+
|
|
105
|
+
**Example**: For MySQL connection requiring {{secrets.DB_HOST}}, {{secrets.DB_USER}}, {{secrets.DB_PASS}}:
|
|
106
|
+
`Please define your database credentials: https://[kbId].apps.openkbs.com/?add_secrets=DB_HOST,DB_USER,DB_PASS`
|
|
107
|
+
|
|
108
|
+
**Important**
|
|
109
|
+
Secrets syntax above is only applicable for all src/Events/* files, and NOT for User-Run Scripts
|
|
72
110
|
|
|
73
|
-
#### User-Run Scripts
|
|
74
|
-
|
|
111
|
+
#### User-Run Scripts (Local Environment)
|
|
112
|
+
User-run scripts are located in the `./scripts/` folder and communicate with cloud agents via API calls.
|
|
113
|
+
They execute locally, receiving the final result of the agent's flow as an API response.
|
|
114
|
+
This setup allows seamless interaction with local services, such as a MySQL database, directly on the user's machine.
|
|
115
|
+
To handle secrets in user-defined scripts, define them in a `.env` file and load them using the `dotenv` package.
|
|
75
116
|
|
|
76
117
|
**Key Components:**
|
|
77
118
|
- `scripts/run_job.js` - Main job runner for calling the cloud agent
|
|
@@ -84,8 +125,18 @@ To securely manage sensitive information like API keys or database passwords wit
|
|
|
84
125
|
- **Usage**: `const client = new OpenKBSAgentClient(); await client.runJob(message);`
|
|
85
126
|
- **Multi-agent support**: Each agent (base or related) finds its own settings and secrets in its directory structure
|
|
86
127
|
|
|
87
|
-
#### NPM Dependencies for User-Run Scripts
|
|
88
|
-
Add needed NPM dependencies to `package.json
|
|
128
|
+
#### NPM Dependencies for User-Run Scripts (Local → Public + Private resources)
|
|
129
|
+
Add needed NPM dependencies to `package.json`
|
|
130
|
+
Example: Script connecting to local MySQL:
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"dependencies": {
|
|
134
|
+
"mysql2": "latest",
|
|
135
|
+
"dotenv": "latest"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
Run `npm install` before executing scripts.
|
|
89
140
|
|
|
90
141
|
### Frontend Overview
|
|
91
142
|
The OpenKBS frontend framework, built with React and MUI, offers a flexible platform for custom chat interfaces. Developers can customize chat appearance and behavior via the `contentRender` module.
|
|
@@ -95,15 +146,139 @@ The `contentRender.js` file is central to frontend customization, exporting key
|
|
|
95
146
|
- **`onRenderChatMessage(params)`:** function called every time a chat message is rendered.
|
|
96
147
|
|
|
97
148
|
#### OpenKBS commands
|
|
98
|
-
`openkbs push` - after completing changes to your agent, use this command to deploy it to the OpenKBS cloud.
|
|
99
149
|
`openkbs create my-agent` - creates a directory structure for a new agent
|
|
150
|
+
`openkbs push` - after completing changes to your agent, use this command to deploy it to the OpenKBS cloud.
|
|
151
|
+
`node scripts/run_job.js init` - execute right after `openkbs push` to configure the API Key before running the job for the specific agent.
|
|
100
152
|
|
|
101
153
|
### Creating Related Agents
|
|
102
154
|
To create related agents that work alongside the main agent:
|
|
103
155
|
|
|
104
156
|
1. **Create in related-agents/ folder**: `cd related-agents && openkbs create agent-name`
|
|
105
157
|
2. **Each related agent gets**: Own `app/settings.json`, `src/` folder, and `.openkbs/secrets.json`
|
|
106
|
-
3. **Script usage**: Related agents use
|
|
158
|
+
3. **Script usage**: Related agents use their own `agent_client.js` located in their subdirectory, ensuring they access their own settings and secrets file.
|
|
107
159
|
4. **Multi-agent workflows**: Scripts can orchestrate multiple agents by creating separate client instances
|
|
108
160
|
|
|
109
161
|
Related agents are independent but can share the base agent's script utilities.
|
|
162
|
+
|
|
163
|
+
## OpenKBS Agent Architecture: From Single Agent to Complex Orchestration
|
|
164
|
+
|
|
165
|
+
### Core Architecture Philosophy
|
|
166
|
+
|
|
167
|
+
OpenKBS enables everything from simple single-agent automation to sophisticated multi-agent systems through a **dual-environment architecture** that seamlessly combines **cloud-based autonomous agents** with **local orchestration scripts**.
|
|
168
|
+
|
|
169
|
+
### Understanding Where Code Runs
|
|
170
|
+
|
|
171
|
+
**The fundamental principle**: Cloud agents operate in cloud infrastructure, local scripts run on your machine. This separation enables powerful patterns:
|
|
172
|
+
|
|
173
|
+
**Cloud agents (serverless functions)** can access:
|
|
174
|
+
- Internet-reachable services only
|
|
175
|
+
- Public APIs and databases
|
|
176
|
+
- Any service with a public endpoint
|
|
177
|
+
|
|
178
|
+
**Local scripts (on your machine)** can access:
|
|
179
|
+
- Local resources (localhost, 127.0.0.1, local files)
|
|
180
|
+
- Cloud agents via API calls
|
|
181
|
+
- Both local and internet resources
|
|
182
|
+
|
|
183
|
+
**Architecture emerges from execution context**: When resources exist only locally, the architecture naturally becomes: Local Script → Cloud Agent (processing) → JSON → Local Script → Local Resource
|
|
184
|
+
|
|
185
|
+
### Advanced Pattern: Tool Composition (When Needed)
|
|
186
|
+
|
|
187
|
+
Since cloud agent tools are code, you can create composite tools when facing repetitive multi-step operations.
|
|
188
|
+
This is useful when an agent would otherwise need many interaction cycles for a single logical operation.
|
|
189
|
+
|
|
190
|
+
**Use sparingly**: Only create composite tools when they significantly reduce agent interactions or when domain logic requires atomic operations.
|
|
191
|
+
|
|
192
|
+
### The Powerful Two-Environment System
|
|
193
|
+
|
|
194
|
+
#### 1. Cloud Environment (Autonomous Agent Execution)
|
|
195
|
+
**Location**: `./src/Events/` - Deployed via `openkbs push`
|
|
196
|
+
**Purpose**: Autonomous agent execution with intelligent decision-making
|
|
197
|
+
|
|
198
|
+
**Capabilities**:
|
|
199
|
+
- Autonomous multi-step workflows with decision branching
|
|
200
|
+
- Integration with ANY internet-accessible service (public or private)
|
|
201
|
+
- Secure credential management via {{secrets.KEY}} system
|
|
202
|
+
- Sequential tool call execution based on intermediate results
|
|
203
|
+
- Complex data extraction and structured JSON responses
|
|
204
|
+
- Access to cloud databases, public APIs, web services with proper credentials
|
|
205
|
+
|
|
206
|
+
**Agent Execution Flow**:
|
|
207
|
+
```
|
|
208
|
+
User Message → Agent Processes → Tool Call 1 → Analyze Result → Decision → Tool Call 2 → ... → Final JSON Response
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Key Insight**: Cloud agents can securely connect to APIs, and any service that have public IPs/URLs
|
|
212
|
+
|
|
213
|
+
#### 2. Local Environment (Orchestration & Local Services)
|
|
214
|
+
**Location**: `./scripts/` - Execute locally with `node`
|
|
215
|
+
**Purpose**: Agent orchestration and local infrastructure integration
|
|
216
|
+
|
|
217
|
+
**Capabilities**:
|
|
218
|
+
- Orchestrate multiple cloud agents in complex workflows
|
|
219
|
+
- Direct access to local services (localhost databases, file systems)
|
|
220
|
+
- Dynamic workflow creation based on agent responses
|
|
221
|
+
- Parallel and sequential agent coordination
|
|
222
|
+
- Local credential management via .env files
|
|
223
|
+
- Bridge between cloud intelligence and local infrastructure
|
|
224
|
+
|
|
225
|
+
### Why This Architecture Matters
|
|
226
|
+
|
|
227
|
+
#### Key Architectural Insights
|
|
228
|
+
- **Cloud agents** = Autonomous intelligence with internet access
|
|
229
|
+
- **Local scripts** = Infrastructure control and orchestration
|
|
230
|
+
- Scripts call agents via API, agents return JSON, scripts handle the rest
|
|
231
|
+
|
|
232
|
+
### Orchestration Patterns
|
|
233
|
+
|
|
234
|
+
Whether using a single agent or multiple agents, common patterns include:
|
|
235
|
+
|
|
236
|
+
1. **Single Agent**: Script → Agent → Process Result → Store/Act
|
|
237
|
+
2. **Hierarchical**: Script → Discovery Agent → N Results → Spawn N Detail Agents → Aggregate
|
|
238
|
+
3. **Pipeline**: Script → Agent A → Agent B (uses A's output) → Agent C → Final Result
|
|
239
|
+
4. **Event-Driven**: Database Change → Script Detects → Triggers Appropriate Agent(s)
|
|
240
|
+
5. **Parallel**: Script → [Agent A, Agent B, Agent C] simultaneously → Combine Results
|
|
241
|
+
6. **Database Integration**: Script reads local DB → Sends data to Agent → Agent processes → Returns JSON → Script stores in local DB
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
### Understanding the Architecture
|
|
245
|
+
|
|
246
|
+
#### Cloud Agents (The Intelligence Layer)
|
|
247
|
+
**This is where ALL agentic flow happens:**
|
|
248
|
+
- Agents execute autonomously in the cloud
|
|
249
|
+
- Users can login to the chat UI at `https://[kbId].apps.openkbs.com` to monitor execution
|
|
250
|
+
- Each message and tool call is visible in the chat interface
|
|
251
|
+
- Agents make decisions, call tools, and process data autonomously
|
|
252
|
+
- The agent IS the intelligence - it thinks, decides, and acts
|
|
253
|
+
|
|
254
|
+
#### Local Scripts (The Orchestration Layer)
|
|
255
|
+
**Scripts are API clients that:**
|
|
256
|
+
- Call cloud agents via API
|
|
257
|
+
- Receive the final JSON result after agent completes its autonomous flow
|
|
258
|
+
- Handle local infrastructure (databases, files)
|
|
259
|
+
- Orchestrate multiple agent calls
|
|
260
|
+
- Process and route results between agents
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
### Security & Best Practices
|
|
264
|
+
|
|
265
|
+
#### Cloud Security
|
|
266
|
+
- Use {{secrets.KEY}} for all sensitive credentials
|
|
267
|
+
- Secrets are encrypted and never exposed in code
|
|
268
|
+
- Each agent can have its own set of secrets
|
|
269
|
+
- Supports database passwords, API keys, OAuth tokens
|
|
270
|
+
|
|
271
|
+
#### Local Security
|
|
272
|
+
- Use .env files for local credentials
|
|
273
|
+
- Keep local scripts in private repositories
|
|
274
|
+
- Implement proper error handling and logging
|
|
275
|
+
- Use database transactions for consistency
|
|
276
|
+
|
|
277
|
+
### Summary
|
|
278
|
+
|
|
279
|
+
OpenKBS enables building sophisticated AI systems where:
|
|
280
|
+
- Cloud agents provide autonomous intelligence
|
|
281
|
+
- Local scripts orchestrate workflows and handle infrastructure
|
|
282
|
+
- You maintain full control while agents think and act independently
|
|
283
|
+
|
|
284
|
+
|
package/templates/README.md
CHANGED
|
@@ -1,84 +1,14 @@
|
|
|
1
|
-
# {{{openkbsAppName}}} · [](
|
|
1
|
+
# {{{openkbsAppName}}} · [](LICENSE)
|
|
2
|
+
|
|
2
3
|
<table>
|
|
3
4
|
<tr>
|
|
4
5
|
<td>
|
|
5
6
|
<img src="app/icon.png" alt="App Icon" style="width: 100px; margin-right: 10px;">
|
|
6
7
|
</td>
|
|
7
8
|
<td>
|
|
8
|
-
<strong>{{{
|
|
9
|
+
<strong>{{{openkbsAppName}}}</strong>
|
|
9
10
|
</td>
|
|
10
11
|
</tr>
|
|
11
12
|
</table>
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
<div style="display: flex; flex-direction: row;">
|
|
16
|
-
<img src="app/screenshots/1.png" alt="Screenshot 1" style="width: 23%; margin-right: 10px;">
|
|
17
|
-
<img src="app/screenshots/2.png" alt="Screenshot 2" style="width: 23%; margin-right: 10px;">
|
|
18
|
-
<img src="app/screenshots/3.png" alt="Screenshot 3" style="width: 23%; margin-right: 10px;">
|
|
19
|
-
<img src="app/screenshots/4.png" alt="Screenshot 4" style="width: 23%; margin-right: 10px;">
|
|
20
|
-
</div>
|
|
21
|
-
|
|
22
|
-
## Installation Guide
|
|
23
|
-
|
|
24
|
-
### Step 1: Install OpenKBS CLI and Login
|
|
25
|
-
|
|
26
|
-
You need to have the OpenKBS CLI installed on your system and Login:
|
|
27
|
-
|
|
28
|
-
```bash
|
|
29
|
-
npm install -g openkbs
|
|
30
|
-
openkbs login
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
If you do not have npm installed, you can check https://github.com/open-kbs/openkbs (Download Binary)
|
|
34
|
-
|
|
35
|
-
### Step 2: Clone the Repository
|
|
36
|
-
|
|
37
|
-
Clone the repository to your local machine:
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
git clone git@github.com:open-kbs/{{{openkbs.repo}}}.git
|
|
41
|
-
cd {{{openkbs.repo}}}
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### Step 3: Deploy the Application to the OpenKBS Cloud (to run our backend services locally - continue reading):
|
|
45
|
-
Deploy your application using the OpenKBS CLI:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
openkbs push
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
Once the deployment is complete, you will receive a URL for your app: `https://{kbId}.apps.openkbs.com`.
|
|
52
|
-
Login to your KB and have fun!
|
|
53
|
-
|
|
54
|
-
### Step 4: Running the Frontend Locally for development
|
|
55
|
-
|
|
56
|
-
Run the OpenKBS UI dev server locally:
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
npm i
|
|
60
|
-
npm start
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### Step 5: Running the Backend Locally
|
|
64
|
-
|
|
65
|
-
Run the Chat server locally:
|
|
66
|
-
|
|
67
|
-
```bash
|
|
68
|
-
npm run chat
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
- Enter your `OPENAI_KEY` when prompted. This key will be stored at `~/.openkbs/.env`.
|
|
72
|
-
- From OpenKBS UI change the Chat model to GPT-* On-premises models
|
|
73
|
-
|
|
74
|
-
### Step 6: Running the AI Services locally on your own GPU
|
|
75
|
-
To run this AI app on your own GPU with Llama 3.1 and Stable Diffusion 3, read more here [Install OpenKBS AI Server](https://github.com/open-kbs/openkbs?tab=readme-ov-file#installing-openkbs-ai-server-and-integrating-llama-31-and-stable-diffusion-3-locally)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
## Install via WEB
|
|
79
|
-
To install this app via our website visit [{{{openkbsAppName}}}](https://openkbs.com/apps/{{{openkbs.slug}}}/)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
## License
|
|
83
|
-
|
|
84
|
-
This project is licensed under the MIT License. For more details, please refer to the [LICENSE](https://github.com/open-kbs/{{{openkbs.repo}}}/blob/main/LICENSE) file.
|
|
14
|
+
## Setup and Usage
|
package/templates/package.json
CHANGED
|
@@ -7,10 +7,8 @@
|
|
|
7
7
|
"author": "your name",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"scripts": {
|
|
10
|
-
"postinstall": "npm install openkbs-ui@0
|
|
11
|
-
"start": "node index.js"
|
|
12
|
-
"chat": "cd node_modules/openkbs-chat && npm start",
|
|
13
|
-
"code": "KB_DIR=$(pwd) && cd node_modules/openkbs-code && KB_DIR=$KB_DIR npm start"
|
|
10
|
+
"postinstall": "npm install openkbs-ui@0",
|
|
11
|
+
"start": "node index.js"
|
|
14
12
|
},
|
|
15
13
|
"dependencies": {
|
|
16
14
|
"@emotion/react": "^11.10.6",
|
|
@@ -4,7 +4,9 @@ const OpenKBSAgentClient = require('./utils/agent_client');
|
|
|
4
4
|
|
|
5
5
|
async function main() {
|
|
6
6
|
const client = new OpenKBSAgentClient();
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
if (process.argv[2] === 'init') return await client.init();
|
|
9
|
+
|
|
8
10
|
const message = `Today's Date: ${new Date().toLocaleDateString()}
|
|
9
11
|
|
|
10
12
|
PROCESS_PRODUCT:
|
|
@@ -2,6 +2,7 @@ const https = require('https');
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const readline = require('readline');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { URL } = require('url');
|
|
5
6
|
|
|
6
7
|
class OpenKBSAgentClient {
|
|
7
8
|
constructor() {
|
|
@@ -12,7 +13,7 @@ class OpenKBSAgentClient {
|
|
|
12
13
|
|
|
13
14
|
findSettings() {
|
|
14
15
|
let currentDir = __dirname;
|
|
15
|
-
|
|
16
|
+
|
|
16
17
|
while (currentDir !== path.parse(currentDir).root) {
|
|
17
18
|
const settingsPath = path.join(currentDir, 'app', 'settings.json');
|
|
18
19
|
if (fs.existsSync(settingsPath)) {
|
|
@@ -20,13 +21,13 @@ class OpenKBSAgentClient {
|
|
|
20
21
|
}
|
|
21
22
|
currentDir = path.dirname(currentDir);
|
|
22
23
|
}
|
|
23
|
-
|
|
24
|
+
|
|
24
25
|
throw new Error('Could not find app/settings.json in parent directories');
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
findSecretsPath() {
|
|
28
29
|
let currentDir = __dirname;
|
|
29
|
-
|
|
30
|
+
|
|
30
31
|
while (currentDir !== path.parse(currentDir).root) {
|
|
31
32
|
const settingsPath = path.join(currentDir, 'app', 'settings.json');
|
|
32
33
|
if (fs.existsSync(settingsPath)) {
|
|
@@ -35,24 +36,28 @@ class OpenKBSAgentClient {
|
|
|
35
36
|
}
|
|
36
37
|
currentDir = path.dirname(currentDir);
|
|
37
38
|
}
|
|
38
|
-
|
|
39
|
+
|
|
39
40
|
throw new Error('Could not find agent directory with app/settings.json');
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
async getApiKey() {
|
|
43
44
|
if (this.apiKey) return this.apiKey;
|
|
44
|
-
|
|
45
|
+
|
|
45
46
|
if (fs.existsSync(this.secretsPath)) {
|
|
46
47
|
const secrets = JSON.parse(fs.readFileSync(this.secretsPath, 'utf8'));
|
|
47
48
|
this.apiKey = secrets.apiKey;
|
|
48
49
|
return this.apiKey;
|
|
49
50
|
}
|
|
50
|
-
|
|
51
|
+
|
|
51
52
|
this.apiKey = await this.promptForApiKey();
|
|
52
53
|
this.saveApiKey(this.apiKey);
|
|
53
54
|
return this.apiKey;
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
async init() {
|
|
58
|
+
await this.getApiKey();
|
|
59
|
+
}
|
|
60
|
+
|
|
56
61
|
async promptForApiKey() {
|
|
57
62
|
return new Promise((resolve) => {
|
|
58
63
|
const rl = readline.createInterface({
|
|
@@ -89,89 +94,170 @@ class OpenKBSAgentClient {
|
|
|
89
94
|
fs.writeFileSync(this.secretsPath, JSON.stringify({ apiKey: key }, null, 2));
|
|
90
95
|
}
|
|
91
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Run a job and get response from the agent
|
|
99
|
+
*
|
|
100
|
+
* @param {string|Array} message - Either:
|
|
101
|
+
* - String: "Do something ..."
|
|
102
|
+
* - Array: [
|
|
103
|
+
* {type:"text", text:"Process this invoice"},
|
|
104
|
+
* {type:"image_url", image_url:{url:"https://files.openkbs.com/invoice.png"}}
|
|
105
|
+
* ]
|
|
106
|
+
*
|
|
107
|
+
* @returns {Promise<Object>} Response structure:
|
|
108
|
+
* {
|
|
109
|
+
* data: {
|
|
110
|
+
* type: 'TEXT' | 'CUSTOM_TYPE',
|
|
111
|
+
* content: '...' | data: {...}
|
|
112
|
+
* },
|
|
113
|
+
* chatId: 'xxx-xxx',
|
|
114
|
+
* msgId: 'msg_xxx'
|
|
115
|
+
* }
|
|
116
|
+
*/
|
|
92
117
|
async runJob(message, options = {}) {
|
|
93
118
|
const apiKey = await this.getApiKey();
|
|
94
|
-
|
|
95
|
-
if (!this.settings.kbId) {
|
|
96
|
-
throw new Error('First use: "openkbs push" to create the agent');
|
|
97
|
-
}
|
|
119
|
+
if (!this.settings.kbId) throw new Error('First use: "openkbs push" to create the agent');
|
|
98
120
|
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
|
|
121
|
+
const payload = { message };
|
|
122
|
+
Object.keys(options).forEach(key => {
|
|
123
|
+
if (key === 'historyLimit') {
|
|
124
|
+
payload[key] = Math.min(Math.max(1, options[key]), 100);
|
|
125
|
+
} else if (options[key] !== undefined) {
|
|
126
|
+
payload[key] = options[key];
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const response = await this.request(
|
|
131
|
+
`https://${this.settings.kbId}.apps.openkbs.com/api`,
|
|
132
|
+
payload,
|
|
133
|
+
{ Authorization: `Bearer ${apiKey}` }
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (response.chatId) this.lastChatId = response.chatId;
|
|
137
|
+
return response;
|
|
109
138
|
}
|
|
110
139
|
|
|
111
|
-
async
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
140
|
+
async continueChat(message, chatId = null, options = {}) {
|
|
141
|
+
const targetChatId = chatId || this.lastChatId;
|
|
142
|
+
if (!targetChatId) throw new Error('No chatId provided and no previous chat to continue');
|
|
143
|
+
|
|
144
|
+
return this.runJob(message, {
|
|
145
|
+
...options,
|
|
146
|
+
chatId: targetChatId,
|
|
147
|
+
includeHistory: options.includeHistory !== false
|
|
116
148
|
});
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
return JSON.parse(response)[0].createdChatId;
|
|
120
|
-
} catch (error) {
|
|
121
|
-
if (fs.existsSync(this.secretsPath)) {
|
|
122
|
-
fs.unlinkSync(this.secretsPath);
|
|
123
|
-
}
|
|
124
|
-
throw new Error('Authentication failed.');
|
|
125
|
-
}
|
|
126
149
|
}
|
|
127
150
|
|
|
128
|
-
|
|
151
|
+
async uploadFile(filePath, options = {}) {
|
|
152
|
+
const apiKey = await this.getApiKey();
|
|
153
|
+
if (!fs.existsSync(filePath)) throw new Error(`File not found: ${filePath}`);
|
|
154
|
+
|
|
155
|
+
const fileContent = fs.readFileSync(filePath);
|
|
156
|
+
const ext = path.extname(filePath);
|
|
157
|
+
const fileName = options.fileName || `file-${Date.now()}-${Math.random().toString(36).substring(7)}${ext}`;
|
|
158
|
+
const fileType = options.fileType || this.getMimeType(filePath);
|
|
159
|
+
|
|
160
|
+
const presignedResponse = await this.request('https://kb.openkbs.com/', {
|
|
161
|
+
apiKey,
|
|
162
|
+
kbId: this.settings.kbId,
|
|
163
|
+
namespace: 'files',
|
|
164
|
+
presignedOperation: 'putObject',
|
|
165
|
+
action: 'createPresignedURL',
|
|
166
|
+
fileName,
|
|
167
|
+
fileType
|
|
168
|
+
}, { Origin: `https://${this.settings.kbId}.apps.openkbs.com` });
|
|
169
|
+
|
|
170
|
+
const presignedUrl = presignedResponse.presignedUrl || presignedResponse;
|
|
171
|
+
|
|
172
|
+
await this.requestRaw(presignedUrl, fileContent, {
|
|
173
|
+
'Content-Type': fileType,
|
|
174
|
+
'Content-Length': fileContent.length
|
|
175
|
+
}, 'PUT');
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
fileName,
|
|
179
|
+
uploaded: true,
|
|
180
|
+
url: `https://file.openkbs.com/files/${this.settings.kbId}/${fileName}`
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
getMimeType(filePath) {
|
|
185
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
186
|
+
const mimeTypes = {
|
|
187
|
+
'.pdf': 'application/pdf',
|
|
188
|
+
'.txt': 'text/plain',
|
|
189
|
+
'.json': 'application/json',
|
|
190
|
+
'.jpg': 'image/jpeg',
|
|
191
|
+
'.jpeg': 'image/jpeg',
|
|
192
|
+
'.png': 'image/png',
|
|
193
|
+
'.gif': 'image/gif',
|
|
194
|
+
'.doc': 'application/msword',
|
|
195
|
+
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
196
|
+
'.xls': 'application/vnd.ms-excel',
|
|
197
|
+
'.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
198
|
+
'.csv': 'text/csv',
|
|
199
|
+
'.html': 'text/html',
|
|
200
|
+
'.xml': 'application/xml',
|
|
201
|
+
'.zip': 'application/zip'
|
|
202
|
+
};
|
|
203
|
+
return mimeTypes[ext] || 'application/octet-stream';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Unified HTTP request helper
|
|
207
|
+
async request(url, payload, headers = {}, method = 'POST') {
|
|
129
208
|
return new Promise((resolve, reject) => {
|
|
130
|
-
const { hostname, pathname } = new URL(url);
|
|
209
|
+
const { hostname, pathname, search } = new URL(url);
|
|
131
210
|
const req = https.request({
|
|
132
|
-
hostname,
|
|
133
|
-
path: pathname,
|
|
134
|
-
method
|
|
135
|
-
headers: { 'Content-Type': 'application/json' }
|
|
211
|
+
hostname,
|
|
212
|
+
path: pathname + (search || ''),
|
|
213
|
+
method,
|
|
214
|
+
headers: { 'Content-Type': 'application/json', ...headers }
|
|
136
215
|
}, res => {
|
|
137
216
|
let data = '';
|
|
138
217
|
res.on('data', chunk => data += chunk);
|
|
139
|
-
res.on('end', () =>
|
|
218
|
+
res.on('end', () => {
|
|
219
|
+
try {
|
|
220
|
+
const result = JSON.parse(data);
|
|
221
|
+
if (res.statusCode === 401) {
|
|
222
|
+
if (fs.existsSync(this.secretsPath)) fs.unlinkSync(this.secretsPath);
|
|
223
|
+
reject(new Error('Authentication failed. Please run "node scripts/run_job.js init" to reconfigure.'));
|
|
224
|
+
} else if (res.statusCode !== 200) {
|
|
225
|
+
reject(new Error(`Request failed with status ${res.statusCode}: ${data}`));
|
|
226
|
+
} else {
|
|
227
|
+
resolve(result);
|
|
228
|
+
}
|
|
229
|
+
} catch (error) {
|
|
230
|
+
reject(new Error(`Failed to parse response: ${error.message}`));
|
|
231
|
+
}
|
|
232
|
+
});
|
|
140
233
|
}).on('error', reject);
|
|
141
234
|
|
|
142
|
-
req.write(JSON.stringify(payload));
|
|
235
|
+
req.write(typeof payload === 'string' ? payload : JSON.stringify(payload));
|
|
143
236
|
req.end();
|
|
144
237
|
});
|
|
145
238
|
}
|
|
146
239
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
resolve(result);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
})
|
|
173
|
-
.catch(console.error);
|
|
174
|
-
}, 1000);
|
|
240
|
+
// Raw request for binary data
|
|
241
|
+
async requestRaw(url, data, headers = {}, method = 'PUT') {
|
|
242
|
+
return new Promise((resolve, reject) => {
|
|
243
|
+
const { hostname, pathname, search } = new URL(url);
|
|
244
|
+
const req = https.request({
|
|
245
|
+
hostname,
|
|
246
|
+
path: pathname + (search || ''),
|
|
247
|
+
method,
|
|
248
|
+
headers
|
|
249
|
+
}, res => {
|
|
250
|
+
if (res.statusCode === 200 || res.statusCode === 204) {
|
|
251
|
+
resolve();
|
|
252
|
+
} else {
|
|
253
|
+
let responseData = '';
|
|
254
|
+
res.on('data', chunk => responseData += chunk);
|
|
255
|
+
res.on('end', () => reject(new Error(`Request failed with status ${res.statusCode}: ${responseData}`)));
|
|
256
|
+
}
|
|
257
|
+
}).on('error', reject);
|
|
258
|
+
|
|
259
|
+
req.write(data);
|
|
260
|
+
req.end();
|
|
175
261
|
});
|
|
176
262
|
}
|
|
177
263
|
}
|
|
@@ -13,7 +13,7 @@ export const backendHandler = async (event) => {
|
|
|
13
13
|
return acc;
|
|
14
14
|
}, []);
|
|
15
15
|
|
|
16
|
-
//
|
|
16
|
+
// IMPORTANT: Actions returning JOB_COMPLETED or JOB_FAILED stop agent execution and return final result
|
|
17
17
|
const isJobFinished = /"JOB_COMPLETED"|"JOB_FAILED"/.test(lastMessage.content);
|
|
18
18
|
|
|
19
19
|
const meta = {
|
package/version.json
CHANGED