openkbs 0.0.53 → 0.0.59

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.
Files changed (28) hide show
  1. package/README.md +1490 -202
  2. package/package.json +2 -1
  3. package/src/actions.js +1282 -1
  4. package/src/index.js +77 -1
  5. package/src/utils.js +5 -2
  6. package/templates/.openkbs/knowledge/examples/ai-copywriter-agent/app/instructions.txt +44 -9
  7. package/templates/.openkbs/knowledge/examples/ai-copywriter-agent/src/Events/actions.js +43 -42
  8. package/templates/.openkbs/knowledge/examples/ai-copywriter-agent/src/Events/handler.js +14 -8
  9. package/templates/.openkbs/knowledge/examples/ai-copywriter-agent/src/Frontend/contentRender.js +95 -12
  10. package/templates/.openkbs/knowledge/examples/ai-marketing-agent/README.md +64 -0
  11. package/templates/.openkbs/knowledge/examples/ai-marketing-agent/app/instructions.txt +160 -0
  12. package/templates/.openkbs/knowledge/examples/ai-marketing-agent/app/settings.json +7 -0
  13. package/templates/.openkbs/knowledge/examples/ai-marketing-agent/src/Events/actions.js +258 -0
  14. package/templates/.openkbs/knowledge/examples/ai-marketing-agent/src/Events/onRequest.js +13 -0
  15. package/templates/.openkbs/knowledge/examples/ai-marketing-agent/src/Events/onRequest.json +3 -0
  16. package/templates/.openkbs/knowledge/examples/ai-marketing-agent/src/Events/onResponse.js +13 -0
  17. package/templates/.openkbs/knowledge/examples/ai-marketing-agent/src/Events/onResponse.json +3 -0
  18. package/templates/.openkbs/knowledge/examples/ai-marketing-agent/src/Frontend/contentRender.js +170 -0
  19. package/templates/.openkbs/knowledge/examples/ai-marketing-agent/src/Frontend/contentRender.json +3 -0
  20. package/templates/.openkbs/knowledge/metadata.json +1 -1
  21. package/templates/CLAUDE.md +593 -222
  22. package/templates/app/instructions.txt +13 -1
  23. package/templates/app/settings.json +5 -6
  24. package/templates/src/Events/actions.js +43 -9
  25. package/templates/src/Events/handler.js +24 -25
  26. package/templates/webpack.contentRender.config.js +8 -2
  27. package/version.json +3 -3
  28. package/MODIFY.md +0 -132
package/src/index.js CHANGED
@@ -13,7 +13,12 @@ const {
13
13
  deleteFileAction,
14
14
  describeAction, deployAction, createByTemplateAction, initByTemplateAction,
15
15
  logoutAction, installFrontendPackageAction, modifyAction, downloadModifyAction,
16
- updateKnowledgeAction, updateCliAction, publishAction, unpublishAction
16
+ updateKnowledgeAction, updateCliAction, publishAction, unpublishAction,
17
+ fnAction,
18
+ siteAction,
19
+ storageAction,
20
+ postgresAction,
21
+ pulseAction
17
22
  } = require('./actions');
18
23
 
19
24
 
@@ -197,4 +202,75 @@ Examples:
197
202
  This will unpublish your KB from the domain example.com
198
203
  `);
199
204
 
205
+ program
206
+ .command('fn [subCommand] [args...]')
207
+ .description('Manage Elastic Functions (serverless Lambda functions)')
208
+ .allowUnknownOption()
209
+ .action((subCommand, args) => fnAction(subCommand, args))
210
+ .addHelpText('after', `
211
+ Examples:
212
+ $ openkbs fn list List all functions
213
+ $ openkbs fn deploy hello --region us-east-1 Deploy function from ./functions/hello/
214
+ $ openkbs fn delete hello Delete a function
215
+ $ openkbs fn logs hello View function logs
216
+ $ openkbs fn env hello View environment variables
217
+ $ openkbs fn env hello API_KEY=secret Set environment variable
218
+ $ openkbs fn invoke hello '{"test": true}' Invoke a function
219
+ `);
220
+
221
+ program
222
+ .command('site [subCommand] [args...]')
223
+ .description('Manage static site files for whitelabel domains')
224
+ .action((subCommand, args) => siteAction(subCommand, args))
225
+ .addHelpText('after', `
226
+ Examples:
227
+ $ openkbs site deploy Deploy all files to S3
228
+
229
+ Run from a directory containing settings.json with kbId.
230
+ Files are uploaded to the whitelabel domain's files bucket.
231
+ `);
232
+
233
+ program
234
+ .command('storage [subCommand] [args...]')
235
+ .description('Manage Elastic Storage (S3 buckets for persistent file storage)')
236
+ .action((subCommand, args) => storageAction(subCommand, args))
237
+ .addHelpText('after', `
238
+ Examples:
239
+ $ openkbs storage enable Enable storage for current KB
240
+ $ openkbs storage status Show storage status
241
+ $ openkbs storage ls [prefix] List objects in bucket
242
+ $ openkbs storage put <file> <key> Upload a file
243
+ $ openkbs storage get <key> <file> Download a file
244
+ $ openkbs storage rm <key> Delete an object
245
+ $ openkbs storage disable Disable storage (delete bucket)
246
+ $ openkbs storage cloudfront media Add storage to CloudFront at /media/*
247
+ $ openkbs storage cloudfront remove media Remove storage from CloudFront
248
+ `);
249
+
250
+ program
251
+ .command('postgres [subCommand]')
252
+ .description('Manage Elastic Postgres (Neon PostgreSQL database)')
253
+ .action((subCommand) => postgresAction(subCommand))
254
+ .addHelpText('after', `
255
+ Examples:
256
+ $ openkbs postgres enable Enable Postgres for current KB
257
+ $ openkbs postgres status Show Postgres status
258
+ $ openkbs postgres connection Show connection string
259
+ $ openkbs postgres disable Disable Postgres (delete database)
260
+ `);
261
+
262
+ program
263
+ .command('pulse [subCommand] [args...]')
264
+ .description('Manage Elastic Pulse (real-time WebSocket pub/sub)')
265
+ .action((subCommand, args) => pulseAction(subCommand, args))
266
+ .addHelpText('after', `
267
+ Examples:
268
+ $ openkbs pulse enable Enable Pulse for current KB
269
+ $ openkbs pulse status Show Pulse status and endpoint
270
+ $ openkbs pulse channels List active channels
271
+ $ openkbs pulse presence chat Show clients connected to 'chat' channel
272
+ $ openkbs pulse publish chat "Hello!" Send message to 'chat' channel
273
+ $ openkbs pulse disable Disable Pulse
274
+ `);
275
+
200
276
  program.parse(process.argv);
package/src/utils.js CHANGED
@@ -164,8 +164,11 @@ function makePostRequest(url, data) {
164
164
  resolve(data);
165
165
  } else {
166
166
  try {
167
- if (JSON.parse(body).error) {
168
- console.red(JSON.parse(body).error);
167
+ const parsed = JSON.parse(body);
168
+ if (parsed.error) {
169
+ console.red(parsed.error);
170
+ } else if (parsed.message) {
171
+ console.red(parsed.message);
169
172
  } else {
170
173
  console.red(`Invalid Request`);
171
174
  }
@@ -38,24 +38,59 @@ Description: """
38
38
  Output this JSON format if you can't extract the required data
39
39
  """
40
40
 
41
- List of API commands you can use to accomplish the Task:
41
+ LIST OF AVAILABLE COMMANDS:
42
+ To execute a command, output it as text and wait for system response.
42
43
 
43
- /googleSearch("query")
44
+ <googleSearch>
45
+ {
46
+ "query": "search query"
47
+ }
48
+ </googleSearch>
44
49
  Description: """
45
50
  Get results from Google Search API.
46
51
  """
47
52
 
48
- /youtubeSearch("query")
53
+ <youtubeSearch>
54
+ {
55
+ "query": "search query"
56
+ }
57
+ </youtubeSearch>
49
58
  Description: """
50
- Get results from youtube Search API.
59
+ Get results from YouTube Search API.
51
60
  """
52
61
 
53
- /googleImageSearch("query")
62
+ <googleImageSearch>
63
+ {
64
+ "query": "search query"
65
+ }
66
+ </googleImageSearch>
54
67
  Description: """
55
- Get results from google Image Search
68
+ Get results from Google Image Search.
56
69
  """
57
70
 
58
- /webpageToText("URL")
71
+ <webpageToText>
72
+ {
73
+ "url": "https://example.com/page"
74
+ }
75
+ </webpageToText>
59
76
  Description: """
60
- Use this API to open/read a web pages like product pages.
61
- """
77
+ Use this API to open/read web pages like product pages.
78
+ """
79
+
80
+ <documentToText>
81
+ {
82
+ "url": "https://example.com/document.pdf"
83
+ }
84
+ </documentToText>
85
+ Description: """
86
+ Extract text from documents (PDF, DOC, etc.).
87
+ """
88
+
89
+ <imageToText>
90
+ {
91
+ "url": "https://example.com/image.png"
92
+ }
93
+ </imageToText>
94
+ Description: """
95
+ OCR - Extract text from images.
96
+ """
@@ -15,7 +15,7 @@ const extractJSONFromText = (text) => {
15
15
  return null;
16
16
  }
17
17
 
18
- export const getActions = () => [
18
+ export const getActions = (meta) => [
19
19
  // IMPORTANT: Actions returning JOB_COMPLETED or JOB_FAILED stop agent execution and return final result
20
20
  [/[\s\S]*"type"\s*:\s*"JOB_COMPLETED"[\s\S]*/, async (match, event) => {
21
21
  const parsedData = extractJSONFromText(match[0]);
@@ -31,7 +31,6 @@ export const getActions = () => [
31
31
  }
32
32
  }],
33
33
 
34
-
35
34
  [/[\s\S]*"type"\s*:\s*"JOB_FAILED"[\s\S]*/, async (match, event) => {
36
35
  const parsedData = extractJSONFromText(match[0]);
37
36
  if (parsedData && parsedData.type === "JOB_FAILED") {
@@ -45,24 +44,26 @@ export const getActions = () => [
45
44
  }
46
45
  }],
47
46
 
48
- [/\/?googleSearch\("(.*?)"\)/, async (match) => {
49
- const q = match[1];
47
+ // Google Search with XML+JSON format
48
+ [/<googleSearch>([\s\S]*?)<\/googleSearch>/s, async (match) => {
50
49
  try {
51
- const response = await openkbs.googleSearch(q, {});
52
- const data = response?.map(({ title, link, snippet, pagemap }) => ({
50
+ const data = JSON.parse(match[1].trim());
51
+ const response = await openkbs.googleSearch(data.query);
52
+ const results = response?.map(({ title, link, snippet, pagemap }) => ({
53
53
  title, link, snippet, image: pagemap?.metatags?.[0]?.["og:image"]
54
54
  }));
55
- return { data };
55
+ return { data: results, ...meta };
56
56
  } catch (e) {
57
- return { error: e.message };
57
+ return { error: e.message, ...meta };
58
58
  }
59
59
  }],
60
60
 
61
- [/\/?youtubeSearch\("(.*?)"\)/, async (match) => {
62
- const q = match[1];
61
+ // YouTube Search with XML+JSON format
62
+ [/<youtubeSearch>([\s\S]*?)<\/youtubeSearch>/s, async (match) => {
63
63
  try {
64
- const response = await openkbs.googleSearch(q + ' site:youtube.com', { videoOnly: true });
65
- const data = response?.map(({ title, link, snippet, pagemap }) => ({
64
+ const data = JSON.parse(match[1].trim());
65
+ const response = await openkbs.googleSearch(data.query + ' site:youtube.com', { videoOnly: true });
66
+ const results = response?.map(({ title, link, snippet, pagemap }) => ({
66
67
  title,
67
68
  link: link.replace('www.youtube.com/watch?v=', 'youtu.be/'),
68
69
  snippet,
@@ -70,62 +71,62 @@ export const getActions = () => [
70
71
  duration: pagemap?.videoobject?.[0]?.duration,
71
72
  channel: pagemap?.metatags?.[0]?.["og:site_name"],
72
73
  })).filter(item => item.link.includes('youtu'));
73
- return { data };
74
+ return { data: results, ...meta };
74
75
  } catch (e) {
75
- return { error: e.message };
76
+ return { error: e.message, ...meta };
76
77
  }
77
78
  }],
78
79
 
79
- [/\/?googleImageSearch\("(.*?)"\)/, async (match) => {
80
- const q = match[1];
80
+ // Google Image Search with XML+JSON format
81
+ [/<googleImageSearch>([\s\S]*?)<\/googleImageSearch>/s, async (match) => {
81
82
  try {
82
- const response = await openkbs.googleSearch(q, { searchType: 'image' });
83
- const data = response?.map(({ title, link, snippet, pagemap }) => {
83
+ const data = JSON.parse(match[1].trim());
84
+ const response = await openkbs.googleSearch(data.query, { searchType: 'image' });
85
+ const results = response?.map(({ title, link, snippet, pagemap }) => {
84
86
  const imageObj = pagemap?.cse_image?.[0];
85
87
  const thumbnail = imageObj?.src || pagemap?.metatags?.[0]?.["og:image"] || link;
86
- return {
87
- title,
88
- link: link,
89
- snippet,
90
- image: thumbnail
91
- };
88
+ return { title, link, snippet, image: thumbnail };
92
89
  });
93
- return { data };
90
+ return { data: results, ...meta };
94
91
  } catch (e) {
95
- return { error: e.message };
92
+ return { error: e.message, ...meta };
96
93
  }
97
94
  }],
98
95
 
99
- [/\/?webpageToText\("(.*)"\)/, async (match) => {
96
+ // Webpage to Text with XML+JSON format
97
+ [/<webpageToText>([\s\S]*?)<\/webpageToText>/s, async (match) => {
100
98
  try {
101
- let response = await openkbs.webpageToText(match[1]);
102
- if(!response?.url) return { data: { error: "Unable to read website" } };
103
- return { data: response };
99
+ const data = JSON.parse(match[1].trim());
100
+ let response = await openkbs.webpageToText(data.url);
101
+ if (!response?.url) return { data: { error: "Unable to read website" }, ...meta };
102
+ return { data: response, ...meta };
104
103
  } catch (e) {
105
- return { error: e.response?.data || e };
104
+ return { error: e.response?.data || e.message, ...meta };
106
105
  }
107
106
  }],
108
107
 
109
- [/\/?documentToText\("(.*)"\)/, async (match) => {
108
+ // Document to Text with XML+JSON format
109
+ [/<documentToText>([\s\S]*?)<\/documentToText>/s, async (match) => {
110
110
  try {
111
- let response = await openkbs.documentToText(match[1]);
112
- return { data: response };
111
+ const data = JSON.parse(match[1].trim());
112
+ let response = await openkbs.documentToText(data.url);
113
+ return { data: response, ...meta };
113
114
  } catch (e) {
114
- return { error: e.response.data };
115
+ return { error: e.response?.data || e.message, ...meta };
115
116
  }
116
117
  }],
117
118
 
118
- [/\/?imageToText\("(.*)"\)/, async (match) => {
119
+ // Image to Text (OCR) with XML+JSON format
120
+ [/<imageToText>([\s\S]*?)<\/imageToText>/s, async (match) => {
119
121
  try {
120
- let response = await openkbs.imageToText(match[1]);
121
-
122
+ const data = JSON.parse(match[1].trim());
123
+ let response = await openkbs.imageToText(data.url);
122
124
  if (response?.detections?.[0]?.txt) {
123
125
  response = { detections: response?.detections?.[0]?.txt };
124
126
  }
125
-
126
- return { data: response };
127
+ return { data: response, ...meta };
127
128
  } catch (e) {
128
- return { error: e.response.data };
129
+ return { error: e.response?.data || e.message, ...meta };
129
130
  }
130
131
  }],
131
- ];
132
+ ];
@@ -2,7 +2,14 @@ import {getActions} from './actions.js';
2
2
 
3
3
  export const backendHandler = async (event) => {
4
4
  const lastMessage = event.payload.messages[event.payload.messages.length - 1];
5
- const actions = getActions();
5
+ const reachedMessageLimit = event?.payload?.messages?.length > 60;
6
+
7
+ // Meta for continuing chat model requests
8
+ const meta = {
9
+ _meta_actions: reachedMessageLimit ? [] : ["REQUEST_CHAT_MODEL"]
10
+ };
11
+
12
+ const actions = getActions(meta);
6
13
 
7
14
  const matchingActions = actions.reduce((acc, [regex, action]) => {
8
15
  const matches = [...lastMessage.content.matchAll(new RegExp(regex, 'g'))];
@@ -12,31 +19,30 @@ export const backendHandler = async (event) => {
12
19
  return acc;
13
20
  }, []);
14
21
 
15
- const reachedMessageLimit = event?.payload?.messages?.length > 60;
16
-
17
22
  if (matchingActions.length > 0) {
18
23
  try {
19
24
  const results = await Promise.all(matchingActions);
20
25
 
21
26
  // IMPORTANT: Actions returning JOB_COMPLETED or JOB_FAILED stop agent execution and return final result
22
- const isOnlyJobCompletion = results.length === 1 &&
27
+ const isOnlyJobCompletion = results.length === 1 &&
23
28
  (results[0]?.type === 'JOB_COMPLETED' || results[0]?.type === 'JOB_FAILED');
24
-
25
- const meta = {
29
+
30
+ // Override meta for job completion
31
+ const finalMeta = {
26
32
  _meta_actions: (reachedMessageLimit || isOnlyJobCompletion) ? [] : ["REQUEST_CHAT_MODEL"]
27
33
  };
28
34
 
29
35
  if (results?.[0]?.data?.some?.(o => o?.type === 'image_url')) {
30
36
  return {
31
37
  ...results[0],
32
- ...meta
38
+ ...finalMeta
33
39
  };
34
40
  }
35
41
 
36
42
  return {
37
43
  type: 'RESPONSE',
38
44
  data: results,
39
- ...meta
45
+ ...finalMeta
40
46
  };
41
47
  } catch (error) {
42
48
  return {
@@ -1,6 +1,11 @@
1
1
  import React, { useEffect } from "react";
2
2
  import JsonView from '@uiw/react-json-view';
3
- import { Chip } from '@mui/material';
3
+ import { Box, Tooltip, Chip } from '@mui/material';
4
+ import SearchIcon from '@mui/icons-material/Search';
5
+ import LanguageIcon from '@mui/icons-material/Language';
6
+ import ImageIcon from '@mui/icons-material/Image';
7
+ import YouTubeIcon from '@mui/icons-material/YouTube';
8
+ import ArticleIcon from '@mui/icons-material/Article';
4
9
 
5
10
  const extractJSONFromText = (text) => {
6
11
  let braceCount = 0, startIndex = text.indexOf('{');
@@ -19,24 +24,102 @@ const extractJSONFromText = (text) => {
19
24
  return null;
20
25
  }
21
26
 
22
- const parseCommands = (text) => {
23
- return text.match(/\/\w+\("([^"]+)"\)/g) || [];
24
- }
27
+ // Command patterns for XML+JSON format
28
+ const COMMAND_PATTERNS = [
29
+ /<googleSearch>[\s\S]*?<\/googleSearch>/,
30
+ /<youtubeSearch>[\s\S]*?<\/youtubeSearch>/,
31
+ /<googleImageSearch>[\s\S]*?<\/googleImageSearch>/,
32
+ /<webpageToText>[\s\S]*?<\/webpageToText>/,
33
+ /<documentToText>[\s\S]*?<\/documentToText>/,
34
+ /<imageToText>[\s\S]*?<\/imageToText>/
35
+ ];
36
+
37
+ // Icon mapping for commands
38
+ const commandIcons = {
39
+ googleSearch: SearchIcon,
40
+ youtubeSearch: YouTubeIcon,
41
+ googleImageSearch: ImageIcon,
42
+ webpageToText: LanguageIcon,
43
+ documentToText: ArticleIcon,
44
+ imageToText: ImageIcon
45
+ };
46
+
47
+ // Parse commands from content
48
+ const parseCommands = (content) => {
49
+ const commands = [];
50
+ const regex = /<(\w+)>([\s\S]*?)<\/\1>/g;
51
+ let match;
52
+ while ((match = regex.exec(content)) !== null) {
53
+ try {
54
+ commands.push({
55
+ name: match[1],
56
+ data: JSON.parse(match[2].trim())
57
+ });
58
+ } catch (e) {
59
+ commands.push({ name: match[1], data: match[2].trim() });
60
+ }
61
+ }
62
+ return commands;
63
+ };
64
+
65
+ // Render command as icon with tooltip
66
+ const CommandIcon = ({ command }) => {
67
+ const Icon = commandIcons[command.name] || SearchIcon;
68
+ return (
69
+ <Tooltip
70
+ title={
71
+ <Box sx={{ p: 1 }}>
72
+ <Box sx={{ fontWeight: 'bold', color: '#4CAF50', mb: 0.5 }}>{command.name}</Box>
73
+ <pre style={{ margin: 0, fontSize: '10px' }}>
74
+ {typeof command.data === 'object'
75
+ ? JSON.stringify(command.data, null, 2)
76
+ : command.data}
77
+ </pre>
78
+ </Box>
79
+ }
80
+ arrow
81
+ >
82
+ <Box sx={{
83
+ display: 'inline-flex',
84
+ width: 32, height: 32,
85
+ borderRadius: '50%',
86
+ backgroundColor: 'rgba(76, 175, 80, 0.1)',
87
+ border: '2px solid rgba(76, 175, 80, 0.3)',
88
+ alignItems: 'center',
89
+ justifyContent: 'center',
90
+ mx: 0.5,
91
+ cursor: 'pointer',
92
+ '&:hover': {
93
+ backgroundColor: 'rgba(76, 175, 80, 0.2)',
94
+ transform: 'scale(1.1)'
95
+ },
96
+ transition: 'all 0.2s'
97
+ }}>
98
+ <Icon sx={{ fontSize: 16, color: '#4CAF50' }} />
99
+ </Box>
100
+ </Tooltip>
101
+ );
102
+ };
25
103
 
26
104
  // do NOT useState() directly in this function, it is not a React component
27
105
  const onRenderChatMessage = async (params) => {
28
106
  const { content } = params.messages[params.msgIndex];
29
107
  const JSONData = extractJSONFromText(content);
30
- const commands = parseCommands(content);
31
-
108
+
109
+ // Render JOB_COMPLETED as JSON view
32
110
  if (JSONData?.type === 'JOB_COMPLETED') {
33
111
  return <JsonView value={JSONData} />
34
112
  }
35
-
36
- if (commands.length > 0) {
37
- return commands.map((cmd, i) => (
38
- <Chip key={i} label={cmd} size="small" />
39
- ));
113
+
114
+ // Check for commands in content
115
+ const hasCommand = COMMAND_PATTERNS.some(p => p.test(content));
116
+ if (hasCommand) {
117
+ const commands = parseCommands(content);
118
+ return (
119
+ <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
120
+ {commands.map((cmd, i) => <CommandIcon key={i} command={cmd} />)}
121
+ </Box>
122
+ );
40
123
  }
41
124
 
42
125
  // return undefined to use default message render
@@ -61,4 +144,4 @@ const Header = ({ setRenderSettings }) => {
61
144
 
62
145
  const exports = { onRenderChatMessage, Header };
63
146
  window.contentRender = exports;
64
- export default exports;
147
+ export default exports;
@@ -0,0 +1,64 @@
1
+ # AI Marketing Agent
2
+
3
+ A comprehensive AI marketing assistant that can create content, generate images and videos, search the web, send emails, and schedule tasks.
4
+
5
+ ## Features
6
+
7
+ - **AI Image Generation**: Gemini 2.5 Flash Image and GPT-Image-1 models
8
+ - **AI Video Generation**: Sora 2 and Sora 2 Pro models
9
+ - **Web Search**: Google Search and Image Search
10
+ - **Email Marketing**: Send emails directly from chat
11
+ - **Task Scheduling**: Schedule future reminders and tasks
12
+ - **Web Publishing**: Create and publish HTML landing pages
13
+ - **Memory System**: Persistent storage for user preferences and content
14
+
15
+ ## Command Format
16
+
17
+ All commands use XML tags with JSON content:
18
+
19
+ ```xml
20
+ <commandName>
21
+ {
22
+ "param1": "value1",
23
+ "param2": "value2"
24
+ }
25
+ </commandName>
26
+ ```
27
+
28
+ ## Available Commands
29
+
30
+ ### Content Creation
31
+ - `<createAIImage>` - Generate images with AI
32
+ - `<createAIVideo>` - Generate videos with Sora 2
33
+ - `<publishWebPage>` - Publish HTML landing pages
34
+
35
+ ### Search & Research
36
+ - `<googleSearch>` - Web search
37
+ - `<googleImageSearch>` - Image search
38
+ - `<webpageToText>` - Extract text from webpages
39
+ - `<viewImage>` - View image in context
40
+
41
+ ### Communication
42
+ - `<sendMail>` - Send emails
43
+
44
+ ### Task Management
45
+ - `<scheduleTask>` - Schedule future tasks
46
+ - `<getScheduledTasks/>` - List scheduled tasks
47
+
48
+ ### Memory
49
+ - `<setMemory>` - Save to memory
50
+ - `<deleteItem>` - Delete from memory
51
+
52
+ ## Architecture
53
+
54
+ - `src/Events/actions.js` - Command implementations
55
+ - `src/Events/onRequest.js` - User message handler
56
+ - `src/Events/onResponse.js` - LLM response handler
57
+ - `src/Frontend/contentRender.js` - UI customization
58
+ - `app/instructions.txt` - LLM instructions
59
+
60
+ ## Deployment
61
+
62
+ ```bash
63
+ openkbs push
64
+ ```