@adminforth/agent 1.0.0

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 (66) hide show
  1. package/.woodpecker/buildRelease.sh +13 -0
  2. package/.woodpecker/buildSlackNotify.sh +46 -0
  3. package/.woodpecker/release.yml +57 -0
  4. package/agent/middleware/apiBasedTools.ts +109 -0
  5. package/agent/middleware/sequenceDebug.ts +302 -0
  6. package/agent/simpleAgent.ts +291 -0
  7. package/agent/skills/registry.ts +135 -0
  8. package/agent/systemPrompt.ts +69 -0
  9. package/agent/toolCallEvents.ts +17 -0
  10. package/agent/tools/apiTool.ts +99 -0
  11. package/agent/tools/fetchSkill.ts +58 -0
  12. package/agent/tools/fetchToolSchema.ts +50 -0
  13. package/agent/tools/index.ts +26 -0
  14. package/apiBasedTools.ts +625 -0
  15. package/build.log +30 -0
  16. package/custom/ChatSurface.vue +184 -0
  17. package/custom/ConversationArea.vue +175 -0
  18. package/custom/Message.vue +206 -0
  19. package/custom/SessionsHistory.vue +93 -0
  20. package/custom/ToolRenderer.vue +131 -0
  21. package/custom/ToolsGroup.vue +67 -0
  22. package/custom/incremark_code_renderers/IncremarkShikiCodeBlock.vue +301 -0
  23. package/custom/incremark_code_renderers/incremarkCodeHighlight.ts +285 -0
  24. package/custom/incremark_code_renderers/incremarkRenderer.ts +653 -0
  25. package/custom/incremark_code_renderers/renderIncremarkMarkdown.ts +118 -0
  26. package/custom/package.json +26 -0
  27. package/custom/pnpm-lock.yaml +1467 -0
  28. package/custom/skills/fetch_data/SKILL.md +15 -0
  29. package/custom/skills/mutate_data/SKILL.md +108 -0
  30. package/custom/tsconfig.json +16 -0
  31. package/custom/types.ts +34 -0
  32. package/custom/useAgentStore.ts +349 -0
  33. package/dist/agent/middleware/apiBasedTools.js +91 -0
  34. package/dist/agent/middleware/sequenceDebug.js +210 -0
  35. package/dist/agent/simpleAgent.js +173 -0
  36. package/dist/agent/skills/registry.js +108 -0
  37. package/dist/agent/systemPrompt.js +64 -0
  38. package/dist/agent/toolCallEvents.js +1 -0
  39. package/dist/agent/tools/apiTool.js +93 -0
  40. package/dist/agent/tools/fetchSkill.js +51 -0
  41. package/dist/agent/tools/fetchToolSchema.js +36 -0
  42. package/dist/agent/tools/index.js +28 -0
  43. package/dist/apiBasedTools.js +412 -0
  44. package/dist/custom/ChatSurface.vue +184 -0
  45. package/dist/custom/ConversationArea.vue +175 -0
  46. package/dist/custom/Message.vue +206 -0
  47. package/dist/custom/SessionsHistory.vue +93 -0
  48. package/dist/custom/ToolRenderer.vue +131 -0
  49. package/dist/custom/ToolsGroup.vue +67 -0
  50. package/dist/custom/incremark_code_renderers/IncremarkShikiCodeBlock.vue +301 -0
  51. package/dist/custom/incremark_code_renderers/incremarkCodeHighlight.ts +285 -0
  52. package/dist/custom/incremark_code_renderers/incremarkRenderer.ts +653 -0
  53. package/dist/custom/incremark_code_renderers/renderIncremarkMarkdown.ts +118 -0
  54. package/dist/custom/package.json +26 -0
  55. package/dist/custom/pnpm-lock.yaml +1467 -0
  56. package/dist/custom/skills/fetch_data/SKILL.md +15 -0
  57. package/dist/custom/skills/mutate_data/SKILL.md +108 -0
  58. package/dist/custom/tsconfig.json +16 -0
  59. package/dist/custom/types.ts +34 -0
  60. package/dist/custom/useAgentStore.ts +349 -0
  61. package/dist/index.js +415 -0
  62. package/dist/types.js +1 -0
  63. package/index.ts +457 -0
  64. package/package.json +58 -0
  65. package/tsconfig.json +13 -0
  66. package/types.ts +45 -0
@@ -0,0 +1,15 @@
1
+ name: fetch_data
2
+ description: Fetch one or more records. Use to find records entities. To use this skill you first need to call get_resource tool to know resource column names. This tool returns only atomic record(s) not capable with any aggregations.
3
+ ---
4
+
5
+ # Involved tools
6
+
7
+ You can use tool `get_resource_data` it returns one or more records and is capable of using filters
8
+
9
+ # Instructions
10
+
11
+ To find specific data record you should use filters. ILIKE filters are preferred when we are unsure the input is clear.You can combine filters with OR if you want to search multiple fields.If user queries one record you should try to fetch up to 5 records and if more then one returned return output them all to user and ask to select one. When you communicate about record with user, show its several most important fields.
12
+
13
+ For long texts show only several first words and add "..." at the end (only if user did not request this field specifically).
14
+
15
+ Also when you communicate with user about record, add related link to this record. For example /{BASE_URL}/resource/{resourceId}/show/{primary key}. Use _label from `get_resource_data` as anchor text for link (use markdown link).
@@ -0,0 +1,108 @@
1
+ name: mutate_data
2
+ description: Create/update/delete some record of resource or call actions on one or multiple records
3
+ ---
4
+
5
+ # General rules
6
+
7
+ - if there is a dedicated action for some routine (result of `get_resource` tool call, field actions), prefer this to manual updating of records, for example, if you want to approve some comment, prefer calling `approve` action instead of updating `approved` field of comment record (because in action there might be some additional logic like sending notification to user, updating some counters and so on)
8
+
9
+ ## Confirmation
10
+
11
+ Before performing any state mutation including action calls edit/delete please fetch record which is going to be edited/deleted and show user record in format field → value (show several most important fields which can help user to understand what exactly record he is going to edit or delete).
12
+
13
+ For field values with long texts show only several first words and add "..." at the end.
14
+ Also please add related link to record with will be changed. For example /{BASE_URL}/resource/{resourceId}/show/{primary key}. Use _label from `get_resource_data` as anchor text for link (use markdown link).
15
+ And in the same message ask user for final confirmation.
16
+
17
+ When creating new record, show user all data which you gona create and in same message ask for confirmation.
18
+
19
+ Accept any positive confirmation from user like "yes", "sure", "+", anything non-negative call to action, can be considered as confirmation.
20
+
21
+ # Calling actions
22
+
23
+ To call action on some record you can use `start_custom_action` tool, or `start_custom_bulk_action` if you need to perform action on several records at once.
24
+
25
+ Before calling any of this action you should understand whether this action is allowed. User result of `get_resource` tool call and check `action.allowed` - if this attribute is true or is not exists, assume action is allowed. If this attribute is false, action is not allowed, you should warn user that this action is not allowed for him.
26
+
27
+ ### Example
28
+
29
+ If you want to block some user you can confirm that this action by saying:
30
+
31
+ ```I am going to block user:
32
+ * Username: john_doe
33
+ * Email: john_doe@example.com
34
+ * IP Country: USA
35
+ * Currently blocked: No // show this field only if it exists in user record
36
+
37
+ View [John Doe](/resource/users/show/123)
38
+ Are you sure?
39
+ ```
40
+
41
+ ## Updating
42
+
43
+ You can use tool `update_record` tool it updates fields of record. To update `allowedActions.edit` should be set to true and
44
+ `updated` column `showIn.edit` should be true at the same time. If one of this condition is not met, explain to user that is
45
+ not allowed to edit
46
+
47
+ In addition to instructions above show user the table of edits (old value/new value)
48
+
49
+ ### Examples
50
+
51
+ For example if you gonna modify user record, in confirmation please share full user info (not only username but also email, ip country - anything which help adminto check that that is correct user). Message could look like this:
52
+
53
+ ```
54
+ I am going to update user:
55
+ * Username: john_doe
56
+ * Email: john_doe@example.com
57
+ * IP Country: USA
58
+ I am going to change email from john_doe@example.com to new_email@example.com
59
+
60
+ View [John Doe](/admin/resource/users/show/123)
61
+
62
+ Are you sure?
63
+ ```
64
+
65
+
66
+ ## Deleting
67
+
68
+ To delete some record you can use `delete_record` tool. To delete record `allowedActions.delete` should be set to true.
69
+
70
+ ### Example
71
+
72
+ If you gonna delete user record, in confirmation please share full user info (not only username but also email, ip country - anything which help adminto check that that is correct user). Message could look like this:
73
+
74
+ ```I am going to delete user:
75
+ * Username: john_doe
76
+ * Email: john_doe@example.com
77
+ * Signed up: 2024 Jan 1
78
+ * IP Country: USA
79
+
80
+ View [John Doe](/admin/resource/users/show/123)
81
+
82
+ Are you sure?
83
+ ```
84
+
85
+ ## Creating
86
+
87
+ To create new record you can use tool `create_record`. To create record `allowedActions.create` should be set to true.
88
+
89
+ When calling `create_record` tool pass only columns which have `showIn.create` set to true and `backendOnly` is not set to true.
90
+
91
+ For decimal fields please use string values with dot as decimal separator.
92
+
93
+ After creation of new record also show user a link to this record. If several records record were created, show links to all of them in list.
94
+
95
+ Omit any pictures or file paths, you are not capable of doing it. If they are not required all is good, if they are required, explain to user that you are not able to create record because of this reason.
96
+
97
+ ### Example
98
+
99
+
100
+ ```
101
+ I am going to create user:
102
+ * Username: john_doe
103
+ * Email: john_doe@example.com
104
+
105
+ View [John Doe](/admin/resource/users/show/421) # 421 is id of new created record
106
+
107
+ Are you sure?
108
+ ```
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".", // This should point to your project root
4
+ "paths": {
5
+ "@/*": [
6
+ "../node_modules/adminforth/dist/spa/src/*"
7
+ ],
8
+ "*": [
9
+ "../node_modules/adminforth/dist/spa/node_modules/*"
10
+ ],
11
+ "@@/*": [
12
+ "."
13
+ ]
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,34 @@
1
+ export interface IPartData {
2
+ toolCallId: string;
3
+ toolName: string;
4
+ phase: 'start' | 'end';
5
+ input?: any;
6
+ output?: any;
7
+ durationMs?: number;
8
+ }
9
+ export interface IPart {
10
+ type: string;
11
+ text?: string;
12
+ state?: 'started' | 'thinking' | 'processing' | 'streaming' | 'done';
13
+ data?: IPartData;
14
+ }
15
+
16
+ export interface IMessage {
17
+ id: string;
18
+ role: 'user' | 'assistant';
19
+ metadata?: any,
20
+ parts: IPart[];
21
+ }
22
+
23
+ export interface IAgentSession {
24
+ sessionId: string;
25
+ title: string;
26
+ timestamp: string;
27
+ messages: IMessage[];
28
+ }
29
+
30
+ export interface ISessionsListItem {
31
+ sessionId: string;
32
+ title: string;
33
+ timestamp: string;
34
+ }
@@ -0,0 +1,349 @@
1
+ import { defineStore } from 'pinia';
2
+ import { IAgentSession, ISessionsListItem, IMessage } from './types';
3
+ import { ref, nextTick, computed, watch, onMounted, shallowRef } from 'vue';
4
+ import { callAdminForthApi } from '@/utils';
5
+ import { useAdminforth } from '@/adminforth';
6
+ import { Chat } from "@ai-sdk/vue";
7
+ import { DefaultChatTransport } from 'ai';
8
+ import { useCoreStore } from '@/stores/core';
9
+
10
+ export const useAgentStore = defineStore('agent', () => {
11
+ const activeSessionId = ref<string | null>(null);
12
+ const currentSession = ref<IAgentSession | null>(null);
13
+ const sessionList = ref<ISessionsListItem[]>([]);
14
+ const sessions = ref<Record<string, IAgentSession>>({});
15
+ const adminforth = useAdminforth();
16
+ const isChatOpen = ref(false);
17
+ const isSessionHistoryOpen = ref(false);
18
+ const textInput = ref<HTMLInputElement | null>(null);
19
+ const userMessageInput = ref();
20
+ const trimmedUserMessage = computed(() => userMessageInput.value ? userMessageInput.value.trim() : '');
21
+ const lastMessage = ref('');
22
+ const isTeleportedToBody = ref(false);
23
+ const setIsTeleportedToBody = (isTeleported: boolean) => {
24
+ isTeleportedToBody.value = isTeleported;
25
+ }
26
+ const coreStore = useCoreStore();
27
+ const appRoot = ref<HTMLElement | null>(null);
28
+ const header = ref<HTMLElement | null>(null);
29
+ const chatWidth = ref(600);
30
+ function setLocalStorageItem(key: string, value: string) {
31
+ window.localStorage.setItem(`${coreStore.config.brandName || 'adminforth'}-${key}`, value);
32
+ }
33
+ function getLocalStorageItem(key: string) {
34
+ return window.localStorage.getItem(`${coreStore.config.brandName || 'adminforth'}-${key}`);
35
+ }
36
+ watch(isTeleportedToBody, (newVal: boolean) => {
37
+ setLocalStorageItem('isTeleportedToBody', newVal ? 'true' : 'false');
38
+ })
39
+ watch(isChatOpen, (newVal: boolean) => {
40
+ setLocalStorageItem('isChatOpen', newVal ? 'true' : 'false');
41
+ })
42
+ watch(chatWidth, (newVal: number) => {
43
+ setLocalStorageItem('chatWidth', newVal.toString());
44
+ })
45
+ onMounted(() => {
46
+ chatWidth.value = parseInt(getLocalStorageItem('chatWidth') || '600', 10);
47
+ isTeleportedToBody.value = getLocalStorageItem('isTeleportedToBody') === 'true';
48
+ if (isTeleportedToBody.value) {
49
+ isChatOpen.value = getLocalStorageItem('isChatOpen') === 'true';
50
+ }
51
+ if (coreStore.isMobile) {
52
+ chatWidth.value = window.innerWidth;
53
+ }
54
+ appRoot.value = document.getElementById('app');
55
+ header.value = document.getElementById('af-header-nav');
56
+ if (appRoot.value && header.value) {
57
+ nextTick(() => {
58
+ appRoot.value.style.transition = 'padding-right 200ms ease-in-out';
59
+ header.value.style.transition = 'padding-right 200ms ease-in-out';
60
+ });
61
+ }
62
+ })
63
+ function setChatWidth(width: number) {
64
+ if (appRoot.value && header.value) {
65
+ appRoot.value.style.transition = '';
66
+ header.value.style.transition = '';
67
+ }
68
+ chatWidth.value = width;
69
+
70
+ }
71
+ watch([isTeleportedToBody, isChatOpen, chatWidth], ([newIsTeleportedToBody, newIsChatOpen, newChatWidth]: [boolean, boolean, number]) => {
72
+ if (appRoot.value && header.value) {
73
+ if (newIsTeleportedToBody && newIsChatOpen) {
74
+ appRoot.value.style.paddingRight = `${chatWidth.value}px`;
75
+ header.value.style.paddingRight = `${chatWidth.value}px`;
76
+ } else {
77
+ appRoot.value.style.paddingRight = '';
78
+ header.value.style.paddingRight = '';
79
+ }
80
+ }
81
+ })
82
+ const chats = new Map<string, Chat>();
83
+ const currentChat = shallowRef<Chat>(null);
84
+ function setCurrentChat(sessionId: string) {
85
+ if (chats.has(sessionId)) {
86
+ currentChat.value = chats.get(sessionId) || null;
87
+ } else {
88
+ const newChat = new Chat({
89
+ transport: new DefaultChatTransport({
90
+ api: `${import.meta.env.VITE_ADMINFORTH_PUBLIC_PATH || ''}/adminapi/v1/agent/response`,
91
+ credentials: 'include',
92
+ prepareSendMessagesRequest({ messages }: any) {
93
+ const message = lastMessage.value;
94
+ const body = {
95
+ message,
96
+ sessionId,
97
+ timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
98
+ };
99
+
100
+ return {
101
+ headers: {
102
+ Accept: 'text/event-stream',
103
+ 'x-vercel-ai-ui-message-stream': 'v1',
104
+ },
105
+ body
106
+ };
107
+ }
108
+ }),
109
+ onError(error: unknown) {
110
+ console.error("Chat error:", error);
111
+ },
112
+ });
113
+ chats.set(sessionId, newChat);
114
+ currentChat.value = newChat;
115
+ }
116
+
117
+ }
118
+ const isResponseInProgress = computed( () => {
119
+ return currentChat.value?.status === 'streaming';
120
+ });
121
+ const blockCloseOfChat = ref(false);
122
+
123
+ async function sendMessage() {
124
+ const message = trimmedUserMessage.value;
125
+ if (!message || isResponseInProgress.value) {
126
+ return;
127
+ }
128
+ if (!currentSession.value || currentSession.value.sessionId === 'pre-session') {
129
+ await createNewSession(message);
130
+ }
131
+ lastMessage.value = message;
132
+ currentChat.value?.sendMessage({
133
+ text: message,
134
+ });
135
+ userMessageInput.value = '';
136
+ }
137
+
138
+ function closeChat() {
139
+ if (blockCloseOfChat.value) {
140
+ return;
141
+ }
142
+ isChatOpen.value = false;
143
+ isSessionHistoryOpen.value = false;
144
+ }
145
+
146
+ function openChat() {
147
+ isChatOpen.value = true;
148
+ nextTick(() => {
149
+ textInput.value?.focus();
150
+ });
151
+ }
152
+
153
+ function setIsChatOpen(isOpen: boolean) {
154
+ isOpen ? openChat() : closeChat();
155
+ }
156
+
157
+ function setSessionHistoryOpen(isOpen: boolean) {
158
+ isSessionHistoryOpen.value = isOpen;
159
+ }
160
+ function regisrerTextInput(el: HTMLInputElement | null) {
161
+ textInput.value = el;
162
+ }
163
+
164
+
165
+ //create a pre-session, until user will type something, so we can save session
166
+ async function createPreSession() {
167
+ saveCurrentSessionInCache();
168
+ if (sessionList.value.some((s: ISessionsListItem) => s.sessionId === 'pre-session')) {
169
+ return;
170
+ }
171
+ sessionList.value.unshift({
172
+ sessionId: 'pre-session',
173
+ title: 'New Session',
174
+ timestamp: new Date().toISOString(),
175
+ })
176
+ activeSessionId.value = 'pre-session';
177
+ currentSession.value = {
178
+ sessionId: 'pre-session',
179
+ title: 'New Session',
180
+ timestamp: new Date().toISOString(),
181
+ messages: [],
182
+ };
183
+ sessions.value['pre-session'] = currentSession.value;
184
+ setCurrentChat('pre-session');
185
+ }
186
+
187
+ async function deletePreSession() {
188
+ sessionList.value = sessionList.value.filter((s: ISessionsListItem) => s.sessionId !== 'pre-session');
189
+ if (activeSessionId.value === 'pre-session') {
190
+ activeSessionId.value = null;
191
+ currentSession.value = null;
192
+ }
193
+ }
194
+
195
+ async function createNewSession(triggerMessage?: string) {
196
+ try {
197
+ const res = await callAdminForthApi({
198
+ method: 'POST',
199
+ path: '/agent/create-session',
200
+ body: {
201
+ triggerMessage
202
+ },
203
+ });
204
+ if (res.error) {
205
+ console.error('Error creating new session:', res.error);
206
+ return;
207
+ }
208
+ deletePreSession();
209
+ sessions.value[res.sessionId] = res;
210
+ sessionList.value.unshift({
211
+ sessionId: res.sessionId,
212
+ title: res.title,
213
+ timestamp: new Date().toISOString(),
214
+ });
215
+ setActiveSession(res.sessionId);
216
+ } catch (error) {
217
+ console.error('Error creating new session', error);
218
+ }
219
+ }
220
+
221
+ async function deleteSession(sessionId: string) {
222
+ if (sessionId === 'pre-session') {
223
+ deletePreSession();
224
+ return;
225
+ }
226
+ blockCloseOfChat.value = true;
227
+ const isConfirmed = await adminforth.confirm({message: 'Are you sure, that you want to delete this session?', yes: 'Yes', no: 'No'})
228
+ blockCloseOfChat.value = false;
229
+ if (!isConfirmed) {
230
+ return;
231
+ }
232
+ try {
233
+ const res = await callAdminForthApi({
234
+ method: 'POST',
235
+ path: '/agent/delete-session',
236
+ body: { sessionId },
237
+ });
238
+ if (res.error) {
239
+ console.error('Error deleting session:', res.error);
240
+ return;
241
+ }
242
+ delete sessions.value[sessionId];
243
+ sessionList.value = sessionList.value.filter((s: ISessionsListItem) => s.sessionId !== sessionId);
244
+ if (activeSessionId.value === sessionId) {
245
+ activeSessionId.value = null;
246
+ currentSession.value = null;
247
+ }
248
+ } catch (error) {
249
+ console.error('Error deleting session', error);
250
+ }
251
+ if(sessionId === activeSessionId.value) {
252
+ activeSessionId.value = sessionList.value.length > 0 ? sessionList.value[0].sessionId : null;
253
+ if (activeSessionId.value) {
254
+ currentSession.value = sessions.value[activeSessionId.value] || null;
255
+ } else {
256
+ currentSession.value = null;
257
+ }
258
+ }
259
+ }
260
+
261
+ async function fetchSession(sessionId: string) {
262
+ try {
263
+ const res = await callAdminForthApi({
264
+ method: 'POST',
265
+ path: '/agent/get-session-info',
266
+ body: { sessionId },
267
+ });
268
+ if (res.error) {
269
+ console.error('Error fetching session:', res.error);
270
+ return;
271
+ }
272
+ sessions.value[sessionId] = res.session;
273
+ setCurrentChat(sessionId);
274
+ } catch (error) {
275
+ console.error('Error fetching session', error);
276
+ }
277
+ }
278
+
279
+ function saveCurrentSessionInCache() {
280
+ if (currentSession.value) {
281
+ currentSession.value.messages = currentChat.value?.messages.map((m: any) => ({
282
+ role: m.role,
283
+ text: m.parts.map((p: any) => p.text).join(' '),
284
+ })) || [];
285
+ sessions.value[currentSession.value.sessionId] = currentSession.value;
286
+ }
287
+ }
288
+
289
+ async function setActiveSession(sessionId: string) {
290
+ activeSessionId.value = sessionId;
291
+ saveCurrentSessionInCache();
292
+ if (!sessions.value[sessionId]) {
293
+ await fetchSession(sessionId);
294
+ }
295
+ currentSession.value = sessions.value[sessionId];
296
+ setCurrentChat(sessionId);
297
+ currentChat.value.messages = currentSession.value?.messages.map((m: any) => ({
298
+ role: m.role,
299
+ parts:[{
300
+ type: 'text',
301
+ text: m.text,
302
+ state: 'done',
303
+ }]
304
+ }));
305
+ }
306
+
307
+ async function fetchSessionsList() {
308
+ try {
309
+ const res = await callAdminForthApi({
310
+ method: 'POST',
311
+ path: '/agent/get-sessions',
312
+ });
313
+ if (res.error) {
314
+ console.error('Error fetching sessions list:', res.error);
315
+ return;
316
+ }
317
+ sessionList.value = res.sessions;
318
+ } catch (error) {
319
+ console.error('Error fetching sessions list', error);
320
+ }
321
+ }
322
+
323
+ return {
324
+ //_________-Sessions management-_____________
325
+ activeSessionId,
326
+ currentSession,
327
+ sessions,
328
+ sessionList,
329
+ setActiveSession,
330
+ fetchSessionsList,
331
+ deleteSession,
332
+ createPreSession,
333
+ //____________________________________________
334
+ regisrerTextInput,
335
+ isChatOpen,
336
+ setIsChatOpen,
337
+ isSessionHistoryOpen,
338
+ setSessionHistoryOpen,
339
+ sendMessage,
340
+ userMessageInput,
341
+ chatMessages: computed(() => currentChat.value?.messages || []),
342
+ trimmedUserMessage,
343
+ isResponseInProgress,
344
+ isTeleportedToBody,
345
+ setIsTeleportedToBody,
346
+ chatWidth,
347
+ setChatWidth,
348
+ }
349
+ })