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 CHANGED
@@ -1,6 +1,7 @@
1
1
  # OpenKBS · [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/open-kbs/openkbs-chat/blob/main/LICENSE) [![npm version](https://img.shields.io/badge/npm-v0.0.20-orange.svg)](https://www.npmjs.com/package/openkbs)
2
2
 
3
- OpenKBS is an extendable AI service designed to build,
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
- - [Chat Render](#chat-render)
13
- - [Setup Local Development](#setup-local-development)
14
- - [Use Built-in MUI Components](#use-built-in-mui-components)
15
- - [AI-Powered Generation](#ai-powered-frontend-generation)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openkbs",
3
- "version": "0.0.50",
3
+ "version": "0.0.52",
4
4
  "description": "OpenKBS - Command Line Interface",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
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) {
@@ -2,5 +2,5 @@
2
2
  "chatVendor": "anthropic",
3
3
  "kbDescription": "An autonomous AI powerhouse for content generation",
4
4
  "kbTitle": "Copywriter",
5
- "model": "claude-3-5-haiku-20241022"
5
+ "model": "claude-sonnet-4-20250514"
6
6
  }
@@ -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) => rl.output.write(rl.line ? "*" : 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 chatTitle = options.chatTitle || `Task ${new Date().getTime()}`;
90
- const chatId = await this.startJob(chatTitle, message, { kbId: this.settings.kbId, apiKey });
91
-
92
- console.log(`Job ${chatId} created.\nWorking ...`);
93
-
94
- if (options.poll !== false) {
95
- return this.pollForMessages(chatId, { kbId: this.settings.kbId, apiKey });
96
- }
97
-
98
- return chatId;
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 startJob(chatTitle, data, app) {
102
- const response = await this.makeRequest('https://chat.openkbs.com/', {
103
- ...app,
104
- chatTitle,
105
- message: data
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
- makeRequest(url, payload) {
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: 'POST',
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', () => resolve(data));
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
- async pollForMessages(chatId, app) {
138
- const payload = {
139
- ...app,
140
- action: 'getChatMessages',
141
- chatId,
142
- decryptContent: true
143
- };
144
-
145
- return new Promise((resolve) => {
146
- const interval = setInterval(() => {
147
- this.makeRequest('https://chat.openkbs.com/', payload)
148
- .then(jsonString => {
149
- const messages = JSON.parse(jsonString)[0].data.messages;
150
- for (const message of messages) {
151
- if (message.role === 'system' &&
152
- /{"type"\s*:\s*"(JOB_COMPLETED|JOB_FAILED)".*?}/s.test(message.content)) {
153
-
154
- const result = JSON.parse(message.content)?.data?.find(item =>
155
- item.type === 'JOB_COMPLETED' || item.type === 'JOB_FAILED'
156
- );
157
- clearInterval(interval);
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
- // For Coding Agents: avoid unnecessary LLM reactions after job is finished
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
- ...meta
45
+ _meta_actions: reachedMessageLimit ? [] : ["REQUEST_CHAT_MODEL"]
50
46
  };
51
47
  }
52
48
  }
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.0.8"
3
- }
2
+ "version": "0.0.16"
3
+ }
@@ -5,12 +5,12 @@
5
5
 
6
6
  **FIRST**: Update the knowledge base:
7
7
  ```bash
8
- openkbs update knowledge
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
- ## Architecture Overview
37
- OpenKBS agents have **two execution environments**:
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
- ### 1. Cloud Environment (`./src/`)
40
- - **Event handlers** (`onRequest.js`, `onResponse.js`) run on OpenKBS cloud platform
41
- - **Purpose**: Process user messages, execute AI actions, return responses
42
- - **Deployment**: Code is deployed via `openkbs push`
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
- ### 2. Local Environment (`./scripts/`)
45
- - **User-run scripts** execute locally on user's machine
46
- - **Purpose**: Call cloud agents via API, orchestrate multi-agent workflows, integrate with external systems
47
- - **Execution**: Run directly with `node scripts/script-name.js`
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
- All these event handlers are executed on-demand (upon API request) by the OpenKBS cloud platform, where user production agents are deployed.
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": "^3.14.2"
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
- To securely manage sensitive information like API keys or database passwords within your backend event handlers (onRequest, onResponse, etc.), use the {{secrets.your_secret_name}} syntax.
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
- **User-run scripts** are located in `./scripts/` folder and communicate with cloud agents via API calls.
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 same `OpenKBSAgentClient` - it automatically finds their settings and secrets
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
+
@@ -1,84 +1,14 @@
1
- # {{{openkbsAppName}}} &middot; [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://github.com/open-kbs/{{{openkbs.repo}}}/blob/main/LICENSE)
1
+ # {{{openkbsAppName}}} &middot; [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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>{{{openkbs.kbDescription}}}</strong>
9
+ <strong>{{{openkbsAppName}}}</strong>
9
10
  </td>
10
11
  </tr>
11
12
  </table>
12
13
 
13
- <br />
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
@@ -7,10 +7,8 @@
7
7
  "author": "your name",
8
8
  "license": "MIT",
9
9
  "scripts": {
10
- "postinstall": "npm install openkbs-ui@0 && npm install openkbs-chat@0 && npm install openkbs-ai-server@0 && npm install openkbs-code@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 chatTitle = options.chatTitle || `Task ${new Date().getTime()}`;
100
- const chatId = await this.startJob(chatTitle, message, { kbId: this.settings.kbId, apiKey });
101
-
102
- console.log(`Job ${chatId} created.\nWorking ...`);
103
-
104
- if (options.poll !== false) {
105
- return this.pollForMessages(chatId, { kbId: this.settings.kbId, apiKey });
106
- }
107
-
108
- return chatId;
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 startJob(chatTitle, data, app) {
112
- const response = await this.makeRequest('https://chat.openkbs.com/', {
113
- ...app,
114
- chatTitle,
115
- message: data
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
- makeRequest(url, payload) {
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: 'POST',
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', () => resolve(data));
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
- async pollForMessages(chatId, app) {
148
- const payload = {
149
- ...app,
150
- action: 'getChatMessages',
151
- chatId,
152
- decryptContent: true
153
- };
154
-
155
- return new Promise((resolve) => {
156
- const interval = setInterval(() => {
157
- this.makeRequest('https://chat.openkbs.com/', payload)
158
- .then(jsonString => {
159
- const messages = JSON.parse(jsonString)[0].data.messages;
160
- for (const message of messages) {
161
- if (message.role === 'system' &&
162
- /{"type"\s*:\s*"(JOB_COMPLETED|JOB_FAILED)".*?}/s.test(message.content)) {
163
-
164
- const result = JSON.parse(message.content)?.data?.find(item =>
165
- item.type === 'JOB_COMPLETED' || item.type === 'JOB_FAILED'
166
- );
167
- clearInterval(interval);
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
- // Avoid unnecessary LLM reactions if job is finished
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
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.0.50",
3
- "releaseDate": "2025-08-01",
4
- "releaseNotes": "OpenKBS CLI version 0.0.50"
2
+ "version": "0.0.52",
3
+ "releaseDate": "2025-08-27",
4
+ "releaseNotes": "OpenKBS CLI version 0.0.52"
5
5
  }