openkbs 0.0.37 → 0.0.38
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/package.json +2 -2
- package/src/actions.js +30 -1
- package/templates/.openkbs/knowledge/examples/ai-copywriter-agent/scripts/run_job.js +26 -0
- package/templates/.openkbs/knowledge/examples/ai-copywriter-agent/scripts/utils/agent_client.js +169 -0
- package/templates/.openkbs/knowledge/examples/ai-copywriter-agent/src/Events/handler.js +1 -1
- package/templates/CLAUDE.md +103 -13
- package/templates/related-agents/README.md +6 -0
- package/templates/scripts/run_job.js +26 -0
- package/templates/scripts/utils/agent_client.js +169 -0
- package/templates/src/Events/handler.js +9 -3
- package/templates/.openkbs/knowledge/README.md +0 -57
- package/templates/.openkbs/knowledge/examples/ai-copywriter-agent/run_job.js +0 -124
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openkbs",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.38",
|
|
4
4
|
"description": "OpenKBS - Command Line Interface",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "pkg .",
|
|
8
|
-
"deploy": "npm run build && npm run deploy:linux && npm run deploy:macos && npm run deploy:win",
|
|
8
|
+
"deploy": "npm run build && npm run deploy:linux && npm run deploy:macos && npm run deploy:win && npm run deploy:templates",
|
|
9
9
|
"deploy:linux": "aws s3 cp build/openkbs-linux-x64 s3://openkbs-downloads/cli/linux/openkbs && aws s3 cp build/openkbs-linux-arm64 s3://openkbs-downloads/cli/linux/openkbs-arm64",
|
|
10
10
|
"deploy:macos": "aws s3 cp build/openkbs-macos-arm64 s3://openkbs-downloads/cli/macos/openkbs && aws s3 cp build/openkbs-macos-x64 s3://openkbs-downloads/cli/macos/openkbs-x64",
|
|
11
11
|
"deploy:win": "aws s3 cp build/openkbs-win-x64.exe s3://openkbs-downloads/cli/windows/openkbs.exe && aws s3 cp build/openkbs-win-arm64.exe s3://openkbs-downloads/cli/windows/openkbs-arm64.exe",
|
package/src/actions.js
CHANGED
|
@@ -542,6 +542,7 @@ async function updateKnowledgeAction() {
|
|
|
542
542
|
try {
|
|
543
543
|
const knowledgeDir = path.join(process.cwd(), '.openkbs', 'knowledge');
|
|
544
544
|
const metadataPath = path.join(knowledgeDir, 'metadata.json');
|
|
545
|
+
const claudeMdPath = path.join(process.cwd(), 'CLAUDE.md');
|
|
545
546
|
|
|
546
547
|
// Check if .openkbs/knowledge directory exists
|
|
547
548
|
if (!fs.existsSync(knowledgeDir)) {
|
|
@@ -593,7 +594,10 @@ async function updateKnowledgeAction() {
|
|
|
593
594
|
// Download updated knowledge files from S3
|
|
594
595
|
await downloadKnowledgeFromS3(knowledgeDir);
|
|
595
596
|
|
|
596
|
-
|
|
597
|
+
// Download CLAUDE.md file from S3
|
|
598
|
+
await downloadClaudeMdFromS3(claudeMdPath, s3Client, bucket);
|
|
599
|
+
|
|
600
|
+
console.green('Knowledge base and CLAUDE.md updated successfully!');
|
|
597
601
|
|
|
598
602
|
} catch (error) {
|
|
599
603
|
console.red('Error updating knowledge base:', error.message);
|
|
@@ -651,6 +655,31 @@ async function downloadKnowledgeFromS3(targetDir) {
|
|
|
651
655
|
}
|
|
652
656
|
}
|
|
653
657
|
|
|
658
|
+
async function downloadClaudeMdFromS3(claudeMdPath, s3Client, bucket) {
|
|
659
|
+
const claudeMdKey = 'templates/CLAUDE.md';
|
|
660
|
+
|
|
661
|
+
try {
|
|
662
|
+
// Download CLAUDE.md file from S3
|
|
663
|
+
const response = await s3Client.send(new GetObjectCommand({
|
|
664
|
+
Bucket: bucket,
|
|
665
|
+
Key: claudeMdKey
|
|
666
|
+
}));
|
|
667
|
+
|
|
668
|
+
const fileContent = await response.Body.transformToByteArray();
|
|
669
|
+
await fs.writeFile(claudeMdPath, fileContent);
|
|
670
|
+
|
|
671
|
+
// console.log('Downloaded: CLAUDE.md');
|
|
672
|
+
|
|
673
|
+
} catch (error) {
|
|
674
|
+
if (error.name === 'NoSuchKey') {
|
|
675
|
+
console.yellow('CLAUDE.md not found in remote repository, skipping...');
|
|
676
|
+
} else {
|
|
677
|
+
console.red('Error downloading CLAUDE.md from S3:', error.message);
|
|
678
|
+
throw error;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
654
683
|
module.exports = {
|
|
655
684
|
signAction,
|
|
656
685
|
loginAction,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// This boilerplate code is a starting point for development.
|
|
2
|
+
|
|
3
|
+
const OpenKBSAgentClient = require('./utils/agent_client');
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
const client = new OpenKBSAgentClient();
|
|
7
|
+
|
|
8
|
+
const message = `Today's Date: ${new Date().toLocaleDateString()}
|
|
9
|
+
|
|
10
|
+
PROCESS_PRODUCT:
|
|
11
|
+
Product Name: iPhone 14 Pro Max
|
|
12
|
+
Product Code: MQ9X3RX/A
|
|
13
|
+
ID: 97649
|
|
14
|
+
|
|
15
|
+
find at least 2 images and 2 videos
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const result = await client.runJob(message);
|
|
20
|
+
console.log('Job completed:', result);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error('Job failed:', error.message);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
main();
|
package/templates/.openkbs/knowledge/examples/ai-copywriter-agent/scripts/utils/agent_client.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
class OpenKBSAgentClient {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.settings = this.findSettings();
|
|
9
|
+
this.secretsPath = this.findSecretsPath();
|
|
10
|
+
this.apiKey = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
findSettings() {
|
|
14
|
+
let currentDir = __dirname;
|
|
15
|
+
|
|
16
|
+
while (currentDir !== path.parse(currentDir).root) {
|
|
17
|
+
const settingsPath = path.join(currentDir, 'app', 'settings.json');
|
|
18
|
+
if (fs.existsSync(settingsPath)) {
|
|
19
|
+
return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
currentDir = path.dirname(currentDir);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
throw new Error('Could not find app/settings.json in parent directories');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
findSecretsPath() {
|
|
28
|
+
let currentDir = __dirname;
|
|
29
|
+
|
|
30
|
+
while (currentDir !== path.parse(currentDir).root) {
|
|
31
|
+
const settingsPath = path.join(currentDir, 'app', 'settings.json');
|
|
32
|
+
if (fs.existsSync(settingsPath)) {
|
|
33
|
+
const secretsPath = path.join(currentDir, '.openkbs', 'secrets.json');
|
|
34
|
+
return secretsPath;
|
|
35
|
+
}
|
|
36
|
+
currentDir = path.dirname(currentDir);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
throw new Error('Could not find agent directory with app/settings.json');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async getApiKey() {
|
|
43
|
+
if (this.apiKey) return this.apiKey;
|
|
44
|
+
|
|
45
|
+
if (fs.existsSync(this.secretsPath)) {
|
|
46
|
+
const secrets = JSON.parse(fs.readFileSync(this.secretsPath, 'utf8'));
|
|
47
|
+
this.apiKey = secrets.apiKey;
|
|
48
|
+
return this.apiKey;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.apiKey = await this.promptForApiKey();
|
|
52
|
+
this.saveApiKey(this.apiKey);
|
|
53
|
+
return this.apiKey;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async promptForApiKey() {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
const rl = readline.createInterface({
|
|
59
|
+
input: process.stdin,
|
|
60
|
+
output: process.stdout
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
console.log(`Please generate an API key from: https://${this.settings.kbId}.apps.openkbs.com/?tab=access&createAPIKey=api-${+new Date()}`);
|
|
64
|
+
|
|
65
|
+
rl.question('Enter your API key: ', (key) => {
|
|
66
|
+
rl.close();
|
|
67
|
+
resolve(key);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
rl._writeToOutput = (str) => rl.output.write(rl.line ? "*" : str);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
saveApiKey(key) {
|
|
75
|
+
const secretsDir = path.dirname(this.secretsPath);
|
|
76
|
+
if (!fs.existsSync(secretsDir)) {
|
|
77
|
+
fs.mkdirSync(secretsDir, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
fs.writeFileSync(this.secretsPath, JSON.stringify({ apiKey: key }, null, 2));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async runJob(message, options = {}) {
|
|
83
|
+
const apiKey = await this.getApiKey();
|
|
84
|
+
|
|
85
|
+
if (!this.settings.kbId) {
|
|
86
|
+
throw new Error('First use: "openkbs push" to create the agent');
|
|
87
|
+
}
|
|
88
|
+
|
|
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;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async startJob(chatTitle, data, app) {
|
|
102
|
+
const response = await this.makeRequest('https://chat.openkbs.com/', {
|
|
103
|
+
...app,
|
|
104
|
+
chatTitle,
|
|
105
|
+
message: data
|
|
106
|
+
});
|
|
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
|
+
}
|
|
117
|
+
|
|
118
|
+
makeRequest(url, payload) {
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
const { hostname, pathname } = new URL(url);
|
|
121
|
+
const req = https.request({
|
|
122
|
+
hostname,
|
|
123
|
+
path: pathname,
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: { 'Content-Type': 'application/json' }
|
|
126
|
+
}, res => {
|
|
127
|
+
let data = '';
|
|
128
|
+
res.on('data', chunk => data += chunk);
|
|
129
|
+
res.on('end', () => resolve(data));
|
|
130
|
+
}).on('error', reject);
|
|
131
|
+
|
|
132
|
+
req.write(JSON.stringify(payload));
|
|
133
|
+
req.end();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
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);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = OpenKBSAgentClient;
|
|
@@ -13,12 +13,12 @@ export const backendHandler = async (event) => {
|
|
|
13
13
|
return acc;
|
|
14
14
|
}, []);
|
|
15
15
|
|
|
16
|
+
// For Coding Agents: avoid unnecessary LLM reactions after job is finished
|
|
16
17
|
const isJobFinished = /"JOB_COMPLETED"|"JOB_FAILED"/.test(lastMessage.content);
|
|
17
18
|
|
|
18
19
|
const meta = {
|
|
19
20
|
_meta_actions:
|
|
20
21
|
(
|
|
21
|
-
// disable post-response interactions if limit is exceeded or final job response
|
|
22
22
|
event?.payload?.messages?.length > maxSelfInvokeMessagesCount ||
|
|
23
23
|
isJobFinished && lastMessage.role === 'system'
|
|
24
24
|
)
|
package/templates/CLAUDE.md
CHANGED
|
@@ -1,19 +1,109 @@
|
|
|
1
1
|
# Claude Code Instructions
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
**CRITICAL**: Before taking ANY action, implementing ANY feature, or answering ANY question about this project:
|
|
3
|
+
# MANDATORY FIRST STEPS
|
|
4
|
+
**CRITICAL**: Before taking ANY action, implementing ANY feature, planing the implementation or answering ANY question about this project, you must perform the following steps:
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
**FIRST**: Update the knowledge base:
|
|
7
|
+
```bash
|
|
8
|
+
openkbs update knowledge
|
|
9
|
+
```
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
```
|
|
11
|
+
**SECOND**: Read every file in the examples folder using the Read tool:
|
|
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).
|
|
14
|
+
- You must read ALL files directly
|
|
16
15
|
|
|
17
|
-
**
|
|
16
|
+
**THIRD**: Read existing agent code using the Read tool:
|
|
17
|
+
- First discover all files in the `./app/`, `./src/`, and `./scripts/` folders.
|
|
18
|
+
- Then, use the Read tool to examine each file individually
|
|
18
19
|
|
|
19
|
-
|
|
20
|
+
# OpenKBS Agent Development Guidelines
|
|
21
|
+
|
|
22
|
+
## **Critical** Rules (**IMPORTANT**)
|
|
23
|
+
- Never skip reading examples
|
|
24
|
+
- Study the complete working examples to understand OpenKBS patterns
|
|
25
|
+
- Never guess framework methods, settings or variables — always reference the examples.
|
|
26
|
+
|
|
27
|
+
## Development Guidelines
|
|
28
|
+
- To add npm dependency to backend handlers, add it to onRequest.json and onResponse.json
|
|
29
|
+
- In src/Events and src/Frontend always use Imports (not Require)
|
|
30
|
+
- To add npm dependency to the frontend, add it to contentRender.json
|
|
31
|
+
- Valid values for the _meta_actions key are [] or ["REQUEST_CHAT_MODEL"]
|
|
32
|
+
- Add and use npm dependencies only if necessary, some of those shown in the examples are purely demonstrative
|
|
33
|
+
- If developing new agent, generate it's own ./scripts/run_job.js
|
|
34
|
+
- Before using third-party services in onRequest and onResponse handlers, ask the user for permission
|
|
35
|
+
|
|
36
|
+
## Architecture Overview
|
|
37
|
+
OpenKBS agents have **two execution environments**:
|
|
38
|
+
|
|
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`
|
|
43
|
+
|
|
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`
|
|
48
|
+
|
|
49
|
+
### Backend
|
|
50
|
+
The OpenKBS backend framework is for developing AI agents with custom tools, using Node.js.
|
|
51
|
+
It integrates with openkbs chat service via `onRequest` and `onResponse` handlers for custom actions and service integration.
|
|
52
|
+
|
|
53
|
+
#### Backend Handlers
|
|
54
|
+
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.
|
|
56
|
+
- **`onResponse` Handler:** Activated after the LLM generates a message, enabling command extraction, and action execution.
|
|
57
|
+
- **`onRequest` Handler:** Triggered on user message to allow the user to execute action
|
|
58
|
+
|
|
59
|
+
#### NPM Dependencies for onRequest.js or onResponse.js Backend Handlers
|
|
60
|
+
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
|
+
Example: If actions.js imports mysql2 and onResponse.js imports actions.js, then mysql2 must be in onResponse.json:
|
|
62
|
+
{
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"mysql2": "^3.14.2"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
Similarly, we need to create onRequest.json for onRequest.js as each handler have separate Node.js build with separate dependencies
|
|
69
|
+
|
|
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.
|
|
72
|
+
|
|
73
|
+
#### User-Run Scripts
|
|
74
|
+
**User-run scripts** are located in `./scripts/` folder and communicate with cloud agents via API calls.
|
|
75
|
+
|
|
76
|
+
**Key Components:**
|
|
77
|
+
- `scripts/run_job.js` - Main job runner for calling the cloud agent
|
|
78
|
+
- `scripts/utils/agent_client.js` - Shared utility using `OpenKBSAgentClient` class
|
|
79
|
+
- Custom workflow scripts for multi-agent orchestration
|
|
80
|
+
|
|
81
|
+
**Architecture:**
|
|
82
|
+
- Scripts use `OpenKBSAgentClient` to communicate with deployed cloud agents
|
|
83
|
+
- **Path Resolution**: Automatically finds `app/settings.json` and `.openkbs/secrets.json` by walking up directories
|
|
84
|
+
- **Usage**: `const client = new OpenKBSAgentClient(); await client.runJob(message);`
|
|
85
|
+
- **Multi-agent support**: Each agent (base or related) finds its own settings and secrets in its directory structure
|
|
86
|
+
|
|
87
|
+
#### NPM Dependencies for User-Run Scripts
|
|
88
|
+
Add needed NPM dependencies to `package.json`.
|
|
89
|
+
|
|
90
|
+
### Frontend Overview
|
|
91
|
+
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.
|
|
92
|
+
|
|
93
|
+
#### contentRender
|
|
94
|
+
The `contentRender.js` file is central to frontend customization, exporting key functions for interface adjustments.
|
|
95
|
+
- **`onRenderChatMessage(params)`:** function called every time a chat message is rendered.
|
|
96
|
+
|
|
97
|
+
#### OpenKBS commands
|
|
98
|
+
`openkbs push` - deploy the agent to openkbs cloud
|
|
99
|
+
`openkbs create my-agent` - creates a directory structure for a new agent
|
|
100
|
+
|
|
101
|
+
### Creating Related Agents
|
|
102
|
+
To create related agents that work alongside the main agent:
|
|
103
|
+
|
|
104
|
+
1. **Create in related-agents/ folder**: `cd related-agents && openkbs create agent-name`
|
|
105
|
+
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
|
|
107
|
+
4. **Multi-agent workflows**: Scripts can orchestrate multiple agents by creating separate client instances
|
|
108
|
+
|
|
109
|
+
Related agents are independent but can share the base agent's script utilities.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// This boilerplate code is a starting point for development.
|
|
2
|
+
|
|
3
|
+
const OpenKBSAgentClient = require('./utils/agent_client');
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
const client = new OpenKBSAgentClient();
|
|
7
|
+
|
|
8
|
+
const message = `Today's Date: ${new Date().toLocaleDateString()}
|
|
9
|
+
|
|
10
|
+
PROCESS_PRODUCT:
|
|
11
|
+
Product Name: iPhone 14 Pro Max
|
|
12
|
+
Product Code: MQ9X3RX/A
|
|
13
|
+
ID: 97649
|
|
14
|
+
|
|
15
|
+
find at least 2 images and 2 videos
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const result = await client.runJob(message);
|
|
20
|
+
console.log('Job completed:', result);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error('Job failed:', error.message);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
main();
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const https = require('https');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
class OpenKBSAgentClient {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.settings = this.findSettings();
|
|
9
|
+
this.secretsPath = this.findSecretsPath();
|
|
10
|
+
this.apiKey = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
findSettings() {
|
|
14
|
+
let currentDir = __dirname;
|
|
15
|
+
|
|
16
|
+
while (currentDir !== path.parse(currentDir).root) {
|
|
17
|
+
const settingsPath = path.join(currentDir, 'app', 'settings.json');
|
|
18
|
+
if (fs.existsSync(settingsPath)) {
|
|
19
|
+
return JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
20
|
+
}
|
|
21
|
+
currentDir = path.dirname(currentDir);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
throw new Error('Could not find app/settings.json in parent directories');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
findSecretsPath() {
|
|
28
|
+
let currentDir = __dirname;
|
|
29
|
+
|
|
30
|
+
while (currentDir !== path.parse(currentDir).root) {
|
|
31
|
+
const settingsPath = path.join(currentDir, 'app', 'settings.json');
|
|
32
|
+
if (fs.existsSync(settingsPath)) {
|
|
33
|
+
const secretsPath = path.join(currentDir, '.openkbs', 'secrets.json');
|
|
34
|
+
return secretsPath;
|
|
35
|
+
}
|
|
36
|
+
currentDir = path.dirname(currentDir);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
throw new Error('Could not find agent directory with app/settings.json');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async getApiKey() {
|
|
43
|
+
if (this.apiKey) return this.apiKey;
|
|
44
|
+
|
|
45
|
+
if (fs.existsSync(this.secretsPath)) {
|
|
46
|
+
const secrets = JSON.parse(fs.readFileSync(this.secretsPath, 'utf8'));
|
|
47
|
+
this.apiKey = secrets.apiKey;
|
|
48
|
+
return this.apiKey;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.apiKey = await this.promptForApiKey();
|
|
52
|
+
this.saveApiKey(this.apiKey);
|
|
53
|
+
return this.apiKey;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async promptForApiKey() {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
const rl = readline.createInterface({
|
|
59
|
+
input: process.stdin,
|
|
60
|
+
output: process.stdout
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
console.log(`Please generate an API key from: https://${this.settings.kbId}.apps.openkbs.com/?tab=access&createAPIKey=api-${+new Date()}`);
|
|
64
|
+
|
|
65
|
+
rl.question('Enter your API key: ', (key) => {
|
|
66
|
+
rl.close();
|
|
67
|
+
resolve(key);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
rl._writeToOutput = (str) => rl.output.write(rl.line ? "*" : str);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
saveApiKey(key) {
|
|
75
|
+
const secretsDir = path.dirname(this.secretsPath);
|
|
76
|
+
if (!fs.existsSync(secretsDir)) {
|
|
77
|
+
fs.mkdirSync(secretsDir, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
fs.writeFileSync(this.secretsPath, JSON.stringify({ apiKey: key }, null, 2));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async runJob(message, options = {}) {
|
|
83
|
+
const apiKey = await this.getApiKey();
|
|
84
|
+
|
|
85
|
+
if (!this.settings.kbId) {
|
|
86
|
+
throw new Error('First use: "openkbs push" to create the agent');
|
|
87
|
+
}
|
|
88
|
+
|
|
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;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async startJob(chatTitle, data, app) {
|
|
102
|
+
const response = await this.makeRequest('https://chat.openkbs.com/', {
|
|
103
|
+
...app,
|
|
104
|
+
chatTitle,
|
|
105
|
+
message: data
|
|
106
|
+
});
|
|
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
|
+
}
|
|
117
|
+
|
|
118
|
+
makeRequest(url, payload) {
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
const { hostname, pathname } = new URL(url);
|
|
121
|
+
const req = https.request({
|
|
122
|
+
hostname,
|
|
123
|
+
path: pathname,
|
|
124
|
+
method: 'POST',
|
|
125
|
+
headers: { 'Content-Type': 'application/json' }
|
|
126
|
+
}, res => {
|
|
127
|
+
let data = '';
|
|
128
|
+
res.on('data', chunk => data += chunk);
|
|
129
|
+
res.on('end', () => resolve(data));
|
|
130
|
+
}).on('error', reject);
|
|
131
|
+
|
|
132
|
+
req.write(JSON.stringify(payload));
|
|
133
|
+
req.end();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
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);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = OpenKBSAgentClient;
|
|
@@ -13,11 +13,17 @@ export const backendHandler = async (event) => {
|
|
|
13
13
|
return acc;
|
|
14
14
|
}, []);
|
|
15
15
|
|
|
16
|
+
// Avoid unnecessary LLM reactions if job is finished
|
|
17
|
+
const isJobFinished = /"JOB_COMPLETED"|"JOB_FAILED"/.test(lastMessage.content);
|
|
18
|
+
|
|
16
19
|
const meta = {
|
|
17
20
|
_meta_actions:
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
(
|
|
22
|
+
event?.payload?.messages?.length > maxSelfInvokeMessagesCount ||
|
|
23
|
+
isJobFinished && lastMessage.role === 'system'
|
|
24
|
+
)
|
|
25
|
+
? []
|
|
26
|
+
: ["REQUEST_CHAT_MODEL"]
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
if (matchingActions.length > 0) {
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# OpenKBS Agent Development Guidelines
|
|
2
|
-
|
|
3
|
-
## MANDATORY FIRST STEP
|
|
4
|
-
**READ EVERY SINGLE FILE IN THE EXAMPLES FOLDER**
|
|
5
|
-
|
|
6
|
-
Read the content of ALL files in `.openkbs/knowledge/examples/` directory and ALL subdirectories (without the icon.png)
|
|
7
|
-
|
|
8
|
-
## Development Flow
|
|
9
|
-
|
|
10
|
-
1. **Read ALL example files first** to get familiar with OpenKBS framework for building AI agents
|
|
11
|
-
2. **Read existing agent code:**
|
|
12
|
-
- `./app/` folder (settings, instructions, etc.)
|
|
13
|
-
- `./src/` folder (all Events and Frontend files)
|
|
14
|
-
- `./run_job.js` any files starting with "run"
|
|
15
|
-
3. **Implement requested features using knowledge from examples**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
## **Critical** Rules (**IMPORTANT**)
|
|
19
|
-
- Never skip reading examples
|
|
20
|
-
- Study the complete working examples to understand OpenKBS patterns
|
|
21
|
-
- Never guess framework methods, settings or variables — always reference the examples.
|
|
22
|
-
- Think hard before the implementation
|
|
23
|
-
|
|
24
|
-
## Development Guidelines
|
|
25
|
-
- If develop new agent from scratch, implement the run_job.js script as start point to backend-to-backend integrations
|
|
26
|
-
- To add npm dependency to backend handlers, add it to onRequest.json and onResponse.json
|
|
27
|
-
- To add npm dependency to the frontend, add it to contentRender.json
|
|
28
|
-
- Valid values for the _meta_actions key are [] or ["REQUEST_CHAT_MODEL"].
|
|
29
|
-
- Add and use npm dependencies only if necessary, some of those shown in the examples are purely demonstrative
|
|
30
|
-
|
|
31
|
-
### Backend
|
|
32
|
-
The OpenKBS backend framework is for developing AI agents with custom tools, using Node.js. It integrates with chat services via `onRequest` and `onResponse` handlers for custom actions and service integration.
|
|
33
|
-
|
|
34
|
-
#### Backend Handlers
|
|
35
|
-
The framework's core uses `onRequest` and `onResponse` handlers as middleware for message tool call parsing and execution.
|
|
36
|
-
- **`onResponse` Handler:** Activated after the LLM generates a message, enabling command extraction, and action execution.
|
|
37
|
-
- **`onRequest` Handler:** Triggered on user message to allow the user to execute action
|
|
38
|
-
|
|
39
|
-
#### NPM Dependencies for onRequest.js or onResponse.js Backend Handlers
|
|
40
|
-
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
|
|
41
|
-
Example: If actions.js imports mysql2 and onResponse.js imports actions.js, then mysql2 must be in onResponse.json:
|
|
42
|
-
{
|
|
43
|
-
"dependencies": {
|
|
44
|
-
"mysql2": "^3.14.2"
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
Similarly, we need to create onRequest.json for onRequest.js as each handler have separate Node.js build with separate dependencies
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
### Frontend Overview
|
|
52
|
-
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.
|
|
53
|
-
|
|
54
|
-
#### contentRender
|
|
55
|
-
The `contentRender.js` file is central to frontend customization, exporting key functions for interface adjustments.
|
|
56
|
-
- **`onRenderChatMessage(params)`:** function called every time a chat message is rendered.
|
|
57
|
-
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
// Start new agent job via API call to openkbs cloud service
|
|
2
|
-
|
|
3
|
-
const https = require('https');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const readline = require('readline');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
|
|
8
|
-
const settings = JSON.parse(fs.readFileSync('app/settings.json', 'utf8'));
|
|
9
|
-
const secretsPath = path.join('.openkbs', 'secrets.json');
|
|
10
|
-
let apiKey;
|
|
11
|
-
|
|
12
|
-
const message = `Today's Date: ${new Date().toLocaleDateString()}
|
|
13
|
-
|
|
14
|
-
PROCESS_PRODUCT:
|
|
15
|
-
Product Name: iPhone 14 Pro Max
|
|
16
|
-
Product Code: MQ9X3RX/A
|
|
17
|
-
ID: 97649
|
|
18
|
-
|
|
19
|
-
find at least 2 images and 2 videos
|
|
20
|
-
`;
|
|
21
|
-
|
|
22
|
-
if (!settings.kbId) {
|
|
23
|
-
console.log('First use: "openkbs push" To create the agent')
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function getApiKey() {
|
|
28
|
-
if (fs.existsSync(secretsPath)) {
|
|
29
|
-
const secrets = JSON.parse(fs.readFileSync(secretsPath, 'utf8'));
|
|
30
|
-
return secrets.apiKey;
|
|
31
|
-
}
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function promptForApiKey() {
|
|
36
|
-
return new Promise((resolve) => {
|
|
37
|
-
const rl = readline.createInterface({
|
|
38
|
-
input: process.stdin,
|
|
39
|
-
output: process.stdout
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
console.log(`Please generate an API key from: https://${settings.kbId}.apps.openkbs.com/?tab=access&createAPIKey=api-${+new Date()}`);
|
|
43
|
-
|
|
44
|
-
rl.question('Enter your API key: ', (key) => {
|
|
45
|
-
rl.close();
|
|
46
|
-
resolve(key);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
rl._writeToOutput = (str) => rl.output.write(rl.line ? "*" : str);
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function saveApiKey(key) {
|
|
54
|
-
if (!fs.existsSync('.openkbs')) {
|
|
55
|
-
fs.mkdirSync('.openkbs');
|
|
56
|
-
}
|
|
57
|
-
fs.writeFileSync(secretsPath, JSON.stringify({ apiKey: key }, null, 2));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function main() {
|
|
61
|
-
apiKey = getApiKey();
|
|
62
|
-
|
|
63
|
-
if (!apiKey) {
|
|
64
|
-
apiKey = await promptForApiKey();
|
|
65
|
-
console.log('\n')
|
|
66
|
-
saveApiKey(apiKey);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const app = { kbId: settings.kbId, apiKey };
|
|
70
|
-
|
|
71
|
-
const chatTitle = "Task " + new Date().getTime();
|
|
72
|
-
startJob(chatTitle, message, app).then(chatId => {
|
|
73
|
-
console.log(`Job ${chatId} created.\nWorking ...`);
|
|
74
|
-
pollForMessages(chatId, app);
|
|
75
|
-
}).catch(console.error);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function startJob(chatTitle, data, app) {
|
|
79
|
-
return makeRequest('https://chat.openkbs.com/', { ...app, chatTitle, message: data })
|
|
80
|
-
.then(res => {
|
|
81
|
-
try {
|
|
82
|
-
return JSON.parse(res)[0].createdChatId;
|
|
83
|
-
} catch (error) {
|
|
84
|
-
fs.unlinkSync(secretsPath);
|
|
85
|
-
throw 'Authentication failed.';
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function makeRequest(url, payload) {
|
|
91
|
-
return new Promise((resolve, reject) => {
|
|
92
|
-
const { hostname, pathname } = new URL(url);
|
|
93
|
-
const req = https.request({
|
|
94
|
-
hostname, path: pathname, method: 'POST',
|
|
95
|
-
headers: { 'Content-Type': 'application/json' }
|
|
96
|
-
}, res => {
|
|
97
|
-
let data = '';
|
|
98
|
-
res.on('data', chunk => data += chunk);
|
|
99
|
-
res.on('end', () => resolve(data));
|
|
100
|
-
}).on('error', reject);
|
|
101
|
-
|
|
102
|
-
req.write(JSON.stringify(payload));
|
|
103
|
-
req.end();
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function pollForMessages(chatId, app) {
|
|
108
|
-
const payload = { ...app, action: 'getChatMessages', chatId, decryptContent: true };
|
|
109
|
-
const interval = setInterval(() => {
|
|
110
|
-
makeRequest('https://chat.openkbs.com/', payload).then(jsonString => {
|
|
111
|
-
const messages = JSON.parse(jsonString)[0].data.messages;
|
|
112
|
-
for (const message of messages) {
|
|
113
|
-
if (message.role === 'system' && /{"type"\s*:\s*"(JOB_COMPLETED|JOB_FAILED)".*?}/s.test(message.content)) {
|
|
114
|
-
// return the final response found in the data results array generated by the backendHandler
|
|
115
|
-
console.log(JSON.parse(message.content)?.data?.find(item => item.type === 'JOB_COMPLETED' || item.type === 'JOB_FAILED'));
|
|
116
|
-
clearInterval(interval);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
}).catch(console.error);
|
|
121
|
-
}, 1000);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
main();
|