@adminforth/agent 1.6.0 → 1.8.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.
@@ -170,21 +170,9 @@ function createAgentLlmMetricsLogger() {
170
170
  return new AgentLlmMetricsLogger();
171
171
  }
172
172
 
173
- function normalizeReasoning(reasoning: AgentReasoning) {
174
- if (reasoning === "none") {
175
- return undefined;
176
- }
177
-
178
- return {
179
- effort: reasoning as "minimal" | "low" | "medium" | "high" | "xhigh",
180
- summary: "auto" as const,
181
- };
182
- }
183
-
184
173
  export function createAgentChatModel(params: {
185
174
  adapter: CompletionAdapter;
186
175
  maxTokens: number;
187
- reasoning: AgentReasoning;
188
176
  modelName?: string;
189
177
  }) {
190
178
  const adapter = params.adapter as OpenAIBackedCompletionAdapter;
@@ -198,7 +186,7 @@ export function createAgentChatModel(params: {
198
186
 
199
187
  const model = params.modelName ?? options.model ?? "gpt-5-nano";
200
188
  const baseURL = options.baseURL ?? options.baseUrl;
201
- const reasoning = normalizeReasoning(params.reasoning);
189
+ const reasoning = options.extraRequestBodyParameters?.reasoning;
202
190
 
203
191
  // @ts-ignore
204
192
  return new ChatOpenAI({
@@ -57,9 +57,10 @@ export async function buildAgentSystemPrompt(adminforth: IAdminForth) {
57
57
  listBundledSkillManifests(),
58
58
  ]);
59
59
  const alwaysAvailableTools = ALWAYS_AVAILABLE_API_TOOL_NAMES.join(", ");
60
+ const adminBasePath = adminforth.config.baseUrlSlashed;
60
61
  const sections = [
61
62
  DEFAULT_AGENT_SYSTEM_PROMPT,
62
- `BASE_URL: ${adminforth.config.baseUrl}`,
63
+ `ADMIN_BASE_PATH: ${adminBasePath}`,
63
64
  `List of resources:\n${formatResources(adminforth.config.resources)}`,
64
65
  `You have always-available base tools: ${alwaysAvailableTools}.`,
65
66
  primarySkills.length > 0
@@ -76,6 +77,8 @@ export async function buildAgentSystemPrompt(adminforth: IAdminForth) {
76
77
  "If a fetched skill lists a non-base tool you need, call fetch_tool_schema for it immediately instead of telling the user the tool is unavailable.",
77
78
  "For example: for record creation load mutate_data, read its tool list, call fetch_tool_schema for create_record, and then use create_record after confirmation.",
78
79
  "When fetch_tool_schema succeeds, that tool becomes available on the next step.",
80
+ "All admin links must be relative paths and must start with ADMIN_BASE_PATH.",
81
+ "Build record links as ADMIN_BASE_PATH + resource/{resourceId}/show/{primary key}. Do not prepend any extra slash before resource.",
79
82
  "Try to call as many tools as possible in parallel in one step.",
80
83
  ];
81
84
 
package/build.log CHANGED
@@ -29,5 +29,5 @@ custom/skills/fetch_data/SKILL.md
29
29
  custom/skills/mutate_data/
30
30
  custom/skills/mutate_data/SKILL.md
31
31
 
32
- sent 170,361 bytes received 413 bytes 341,548.00 bytes/sec
33
- total size is 168,687 speedup is 0.99
32
+ sent 174,726 bytes received 413 bytes 350,278.00 bytes/sec
33
+ total size is 173,048 speedup is 0.99
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div
3
- class="relative w-6 h-6 cursor-pointer mr-6 mt-1
3
+ class="relative w-6 h-6 cursor-pointer mr-1 mt-1
4
4
  text-lightNavbarIcons hover:text-lightNavbarIcons/80
5
5
  dark:text-darkNavbarIcons hover:text-darkNavbarIcons/80
6
6
  hover:scale-110 transition-colors duration-200"
@@ -78,13 +78,49 @@
78
78
  v-model="agentStore.userMessageInput"
79
79
  ref="textInput"
80
80
  @input="autoResize"
81
- class="min-h-12 p-4 pr-12 w-full resize-none overflow-hidden border text-lightInputText dark:text-darkInputText rounded-md bg-transparent text-sm bg-gray-50 dark:bg-gray-700 dark:border-gray-600 focus:outline-none"
81
+ :class="[
82
+ 'min-h-12 w-full resize-none overflow-hidden border text-lightInputText dark:text-darkInputText rounded-md bg-transparent text-sm bg-gray-50 dark:bg-gray-700 dark:border-gray-600 focus:outline-none',
83
+ agentStore.availableModes.length > 1 ? 'p-4 pr-12 pb-12' : 'p-4 pr-12',
84
+ ]"
82
85
  placeholder="Type a message..."
83
- @keydown.enter.exact.prevent="async () => {await agentStore.sendMessage(); autoResize();}"
86
+ @keydown.enter.exact.prevent="sendMessage"
84
87
  />
88
+ <div
89
+ v-if="agentStore.availableModes.length > 1"
90
+ ref="modeMenu"
91
+ class="absolute bottom-2 left-4"
92
+ >
93
+ <button
94
+ aria-label="Select mode"
95
+ class="flex h-8 w-8 items-center justify-center rounded-md border border-gray-200 bg-white text-lightNavbarIcons transition-colors duration-200 hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-darkNavbarIcons dark:hover:bg-gray-700"
96
+ :class="isModeMenuOpen ? 'bg-gray-100 dark:bg-gray-700' : ''"
97
+ :disabled="agentStore.isResponseInProgress"
98
+ title="Select mode"
99
+ type="button"
100
+ @click="toggleModeMenu"
101
+ >
102
+ <IconBrainOutline class="h-4 w-4" />
103
+ </button>
104
+
105
+ <div
106
+ v-if="isModeMenuOpen"
107
+ class="absolute bottom-full left-0 mb-2 min-w-40 overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg dark:border-gray-600 dark:bg-gray-800"
108
+ >
109
+ <button
110
+ v-for="mode in agentStore.availableModes"
111
+ :key="mode.name"
112
+ class="block w-full px-3 py-2 text-left text-sm text-lightInputText transition-colors duration-150 hover:bg-gray-100 dark:text-darkInputText dark:hover:bg-gray-700"
113
+ :class="mode.name === agentStore.activeModeName ? 'bg-gray-100 dark:bg-gray-700' : ''"
114
+ type="button"
115
+ @click="selectMode(mode.name)"
116
+ >
117
+ {{ mode.name }}
118
+ </button>
119
+ </div>
120
+ </div>
85
121
  <Button
86
122
  class="absolute right-4 bottom-2 !p-0 h-[34px] w-[34px]"
87
- @click="async () => {await agentStore.sendMessage(); autoResize();}"
123
+ @click="sendMessage"
88
124
  :disabled="!agentStore.trimmedUserMessage || agentStore.isResponseInProgress"
89
125
  >
90
126
  <IconArrowUpOutline
@@ -101,8 +137,8 @@
101
137
 
102
138
  <script setup lang="ts">
103
139
  import { IconChatBubbleLeft20Solid, IconSparklesSolid } from '@iconify-prerendered/vue-heroicons';
104
- import { IconCloseOutline, IconBarsOutline, IconArrowUpOutline, IconCloseSidebarSolid, IconOpenSidebarSolid } from '@iconify-prerendered/vue-flowbite';
105
- import { useTemplateRef, onMounted, ref, computed } from 'vue';
140
+ import { IconCloseOutline, IconBarsOutline, IconArrowUpOutline, IconCloseSidebarSolid, IconOpenSidebarSolid, IconBrainOutline } from '@iconify-prerendered/vue-flowbite';
141
+ import { useTemplateRef, onMounted, ref } from 'vue';
106
142
  import { onClickOutside } from '@vueuse/core'
107
143
  import ConversationArea from './ConversationArea.vue';
108
144
  import { useAgentStore } from './useAgentStore';
@@ -112,13 +148,19 @@ import { useCoreStore } from '@/stores/core';
112
148
  const props = defineProps<{
113
149
  meta: {
114
150
  pluginInstanceId: string;
151
+ modes: Array<{
152
+ name: string;
153
+ }>;
154
+ defaultModeName: string | null;
115
155
  }
116
156
  }>();
117
157
 
118
158
  const chatSurface = useTemplateRef('chatSurface');
119
159
  const textInput = useTemplateRef('textInput');
160
+ const modeMenu = useTemplateRef('modeMenu');
120
161
  const agentStore = useAgentStore();
121
162
  const coreStore = useCoreStore();
163
+ const isModeMenuOpen = ref(false);
122
164
 
123
165
  const MAX_WIDTH = 800;
124
166
  const MIN_WIDTH = 382; //w-96
@@ -158,8 +200,10 @@ const stopResize = () => {
158
200
  }
159
201
 
160
202
  onClickOutside(chatSurface, () => {if (!agentStore.isTeleportedToBody) agentStore.setIsChatOpen(false);});
203
+ onClickOutside(modeMenu, () => { isModeMenuOpen.value = false; });
161
204
 
162
205
  onMounted(async () => {
206
+ agentStore.setAvailableModes(props.meta.modes, props.meta.defaultModeName);
163
207
  agentStore.regisrerTextInput(textInput.value);
164
208
  textInput.value?.focus();
165
209
  await agentStore.fetchSessionsList();
@@ -181,4 +225,19 @@ function autoResize() {
181
225
  }
182
226
  }
183
227
 
184
- </script>
228
+ function toggleModeMenu() {
229
+ isModeMenuOpen.value = !isModeMenuOpen.value;
230
+ }
231
+
232
+ function selectMode(modeName: string) {
233
+ agentStore.setActiveMode(modeName);
234
+ isModeMenuOpen.value = false;
235
+ }
236
+
237
+ async function sendMessage() {
238
+ isModeMenuOpen.value = false;
239
+ await agentStore.sendMessage();
240
+ autoResize();
241
+ }
242
+
243
+ </script>
@@ -207,4 +207,44 @@
207
207
  max-height: 144px;
208
208
  }
209
209
 
210
+ </style>
211
+
212
+ <style lang="scss">
213
+ .incremark a.incremark-link,
214
+ .incremark a.incremark-link:visited {
215
+ display: inline-block;
216
+ text-decoration: underline;
217
+ text-underline-offset: 4px;
218
+ text-decoration-style:dotted;
219
+ color: rgb(0, 0, 0);
220
+ transition: color 0.2s ease;
221
+ }
222
+
223
+ .incremark a.incremark-link:hover {
224
+ color: rgb(74, 74, 255);
225
+ text-decoration: underline;
226
+ text-underline-offset: 4px;
227
+ text-decoration-style:dotted;
228
+ }
229
+
230
+ html[data-theme="dark"] .incremark a.incremark-link,
231
+ html[data-theme="dark"] .incremark a.incremark-link:visited {
232
+ color: rgb(220, 220, 220);
233
+ }
234
+
235
+ html[data-theme="dark"] .incremark a.incremark-link:hover {
236
+ color: rgb(147, 147, 255);
237
+ }
238
+
239
+ a.incremark-link::after {
240
+ content: "";
241
+ display: inline-block;
242
+ width: 16px;
243
+ height: 16px;
244
+ vertical-align: middle;
245
+ rotate: -45deg;
246
+ background-color: currentColor;
247
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M13 12H2m12 0l-4 4m4-4l-4-4'/%3E%3C/svg%3E") no-repeat center;
248
+ mask-size: contain;
249
+ }
210
250
  </style>
@@ -255,6 +255,7 @@ function clearVega() {
255
255
  background:
256
256
  linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(248, 250, 252, 0.96));
257
257
  box-shadow: 0 14px 30px -24px rgba(15, 23, 42, 0.45);
258
+ margin: 16px 0 ;
258
259
  }
259
260
 
260
261
  .dark .incremark-shiki-code {
@@ -12,4 +12,4 @@ To find specific data record you should use filters. ILIKE filters are preferred
12
12
 
13
13
  For long texts show only several first words and add "..." at the end (only if user did not request this field specifically).
14
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). Links shoudl be always relative path, starting with slash.
15
+ Also when you communicate with user about record, add related link to this record. Build it as `{ADMIN_BASE_PATH}resource/{resourceId}/show/{primary key}`. Use _label from `get_resource_data` as anchor text for link (use markdown link). Links should always be relative paths and must start with `ADMIN_BASE_PATH`. Do not add an extra slash after `ADMIN_BASE_PATH`.
@@ -21,7 +21,7 @@ Use `start_custom_action` and `start_custom_bulk_action` for resource actions.
21
21
  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).
22
22
 
23
23
  For field values with long texts show only several first words and add "..." at the end.
24
- 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). Links shoudl be always relative path, starting with slash.
24
+ Also please add related link to record with will be changed. Build it as `{ADMIN_BASE_PATH}resource/{resourceId}/show/{primary key}`. Use _label from `get_resource_data` as anchor text for link (use markdown link). Links should always be relative paths and must start with `ADMIN_BASE_PATH`. Do not add an extra slash after `ADMIN_BASE_PATH`.
25
25
 
26
26
  And in the same message ask user for final confirmation.
27
27
 
@@ -62,7 +62,7 @@ If you want to block some user you can confirm that this action by saying:
62
62
  * IP Country: USA
63
63
  * Currently blocked: No // show this field only if it exists in user record
64
64
 
65
- View [John Doe](/resource/users/show/123)
65
+ View [John Doe]({ADMIN_BASE_PATH}resource/users/show/123)
66
66
  Are you sure?
67
67
  ```
68
68
 
@@ -85,7 +85,7 @@ I am going to update user:
85
85
  * IP Country: USA
86
86
  I am going to change email from john_doe@example.com to new_email@example.com
87
87
 
88
- View [John Doe](/admin/resource/users/show/123)
88
+ View [John Doe]({ADMIN_BASE_PATH}resource/users/show/123)
89
89
 
90
90
  Are you sure?
91
91
  ```
@@ -105,7 +105,7 @@ If you gonna delete user record, in confirmation please share full user info (no
105
105
  * Signed up: 2024 Jan 1
106
106
  * IP Country: USA
107
107
 
108
- View [John Doe](/admin/resource/users/show/123)
108
+ View [John Doe]({ADMIN_BASE_PATH}resource/users/show/123)
109
109
 
110
110
  Are you sure?
111
111
  ```
@@ -130,7 +130,7 @@ I am going to create user:
130
130
  * Username: john_doe
131
131
  * Email: john_doe@example.com
132
132
 
133
- View [John Doe](/admin/resource/users/show/421) # 421 is id of new created record
133
+ View [John Doe]({ADMIN_BASE_PATH}resource/users/show/421) # 421 is id of new created record
134
134
 
135
135
  Are you sure?
136
136
  ```
@@ -7,6 +7,10 @@ import { Chat } from './chat';
7
7
  import { DefaultChatTransport } from 'ai';
8
8
  import { useCoreStore } from '@/stores/core';
9
9
 
10
+ type AgentMode = {
11
+ name: string;
12
+ };
13
+
10
14
  export const useAgentStore = defineStore('agent', () => {
11
15
  const activeSessionId = ref<string | null>(null);
12
16
  const currentSession = ref<IAgentSession | null>(null);
@@ -28,6 +32,8 @@ export const useAgentStore = defineStore('agent', () => {
28
32
  const header = ref<HTMLElement | null>(null);
29
33
  const lastSessionId = ref<string | null>(null);
30
34
  const chatWidth = ref(600);
35
+ const availableModes = ref<AgentMode[]>([]);
36
+ const activeModeName = ref<string | null>(null);
31
37
  function setLocalStorageItem(key: string, value: string) {
32
38
  window.localStorage.setItem(`${coreStore.config.brandName || 'adminforth'}-${key}`, value);
33
39
  }
@@ -91,6 +97,24 @@ export const useAgentStore = defineStore('agent', () => {
91
97
  })
92
98
  const chats = new Map<string, Chat<any>>();
93
99
  const currentChat = shallowRef<Chat<any>>();
100
+
101
+ function setAvailableModes(modes: AgentMode[], defaultModeName?: string | null) {
102
+ availableModes.value = modes;
103
+ activeModeName.value =
104
+ modes.find((mode) => mode.name === activeModeName.value)?.name
105
+ ?? defaultModeName
106
+ ?? modes[0]?.name
107
+ ?? null;
108
+ }
109
+
110
+ function setActiveMode(modeName: string) {
111
+ if (!availableModes.value.some((mode) => mode.name === modeName)) {
112
+ return;
113
+ }
114
+
115
+ activeModeName.value = modeName;
116
+ }
117
+
94
118
  function setCurrentChat(sessionId: string) {
95
119
  if (chats.has(sessionId)) {
96
120
  currentChat.value = chats.get(sessionId) || null;
@@ -105,6 +129,7 @@ export const useAgentStore = defineStore('agent', () => {
105
129
  message,
106
130
  sessionId,
107
131
  timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
132
+ mode: activeModeName.value,
108
133
  };
109
134
 
110
135
  return {
@@ -368,6 +393,10 @@ export const useAgentStore = defineStore('agent', () => {
368
393
  setIsTeleportedToBody,
369
394
  chatWidth,
370
395
  setChatWidth,
371
- focusTextInput
396
+ focusTextInput,
397
+ availableModes,
398
+ activeModeName,
399
+ setAvailableModes,
400
+ setActiveMode,
372
401
  }
373
- })
402
+ })
@@ -103,17 +103,8 @@ class AgentLlmMetricsLogger extends BaseCallbackHandler {
103
103
  function createAgentLlmMetricsLogger() {
104
104
  return new AgentLlmMetricsLogger();
105
105
  }
106
- function normalizeReasoning(reasoning) {
107
- if (reasoning === "none") {
108
- return undefined;
109
- }
110
- return {
111
- effort: reasoning,
112
- summary: "auto",
113
- };
114
- }
115
106
  export function createAgentChatModel(params) {
116
- var _a, _b, _c, _d;
107
+ var _a, _b, _c, _d, _e;
117
108
  const adapter = params.adapter;
118
109
  const options = (_a = adapter.options) !== null && _a !== void 0 ? _a : {};
119
110
  if (!options.openAiApiKey) {
@@ -121,7 +112,7 @@ export function createAgentChatModel(params) {
121
112
  }
122
113
  const model = (_c = (_b = params.modelName) !== null && _b !== void 0 ? _b : options.model) !== null && _c !== void 0 ? _c : "gpt-5-nano";
123
114
  const baseURL = (_d = options.baseURL) !== null && _d !== void 0 ? _d : options.baseUrl;
124
- const reasoning = normalizeReasoning(params.reasoning);
115
+ const reasoning = (_e = options.extraRequestBodyParameters) === null || _e === void 0 ? void 0 : _e.reasoning;
125
116
  // @ts-ignore
126
117
  return new ChatOpenAI(Object.assign(Object.assign(Object.assign({ apiKey: options.openAiApiKey, model, maxTokens: params.maxTokens, useResponsesApi: true, outputVersion: "v1", promptCacheKey: `adminforth-agent:${model}:system-v1:tools-v1`, promptCacheRetention: "in_memory" }, (reasoning ? { reasoning } : {})), (typeof options.timeoutMs === "number"
127
118
  ? { timeout: options.timeoutMs }
@@ -52,9 +52,10 @@ export function buildAgentSystemPrompt(adminforth) {
52
52
  listBundledSkillManifests(),
53
53
  ]);
54
54
  const alwaysAvailableTools = ALWAYS_AVAILABLE_API_TOOL_NAMES.join(", ");
55
+ const adminBasePath = adminforth.config.baseUrlSlashed;
55
56
  const sections = [
56
57
  DEFAULT_AGENT_SYSTEM_PROMPT,
57
- `BASE_URL: ${adminforth.config.baseUrl}`,
58
+ `ADMIN_BASE_PATH: ${adminBasePath}`,
58
59
  `List of resources:\n${formatResources(adminforth.config.resources)}`,
59
60
  `You have always-available base tools: ${alwaysAvailableTools}.`,
60
61
  primarySkills.length > 0
@@ -71,6 +72,8 @@ export function buildAgentSystemPrompt(adminforth) {
71
72
  "If a fetched skill lists a non-base tool you need, call fetch_tool_schema for it immediately instead of telling the user the tool is unavailable.",
72
73
  "For example: for record creation load mutate_data, read its tool list, call fetch_tool_schema for create_record, and then use create_record after confirmation.",
73
74
  "When fetch_tool_schema succeeds, that tool becomes available on the next step.",
75
+ "All admin links must be relative paths and must start with ADMIN_BASE_PATH.",
76
+ "Build record links as ADMIN_BASE_PATH + resource/{resourceId}/show/{primary key}. Do not prepend any extra slash before resource.",
74
77
  "Try to call as many tools as possible in parallel in one step.",
75
78
  ];
76
79
  return sections.filter(Boolean).join("\n\n");
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div
3
- class="relative w-6 h-6 cursor-pointer mr-6 mt-1
3
+ class="relative w-6 h-6 cursor-pointer mr-1 mt-1
4
4
  text-lightNavbarIcons hover:text-lightNavbarIcons/80
5
5
  dark:text-darkNavbarIcons hover:text-darkNavbarIcons/80
6
6
  hover:scale-110 transition-colors duration-200"
@@ -78,13 +78,49 @@
78
78
  v-model="agentStore.userMessageInput"
79
79
  ref="textInput"
80
80
  @input="autoResize"
81
- class="min-h-12 p-4 pr-12 w-full resize-none overflow-hidden border text-lightInputText dark:text-darkInputText rounded-md bg-transparent text-sm bg-gray-50 dark:bg-gray-700 dark:border-gray-600 focus:outline-none"
81
+ :class="[
82
+ 'min-h-12 w-full resize-none overflow-hidden border text-lightInputText dark:text-darkInputText rounded-md bg-transparent text-sm bg-gray-50 dark:bg-gray-700 dark:border-gray-600 focus:outline-none',
83
+ agentStore.availableModes.length > 1 ? 'p-4 pr-12 pb-12' : 'p-4 pr-12',
84
+ ]"
82
85
  placeholder="Type a message..."
83
- @keydown.enter.exact.prevent="async () => {await agentStore.sendMessage(); autoResize();}"
86
+ @keydown.enter.exact.prevent="sendMessage"
84
87
  />
88
+ <div
89
+ v-if="agentStore.availableModes.length > 1"
90
+ ref="modeMenu"
91
+ class="absolute bottom-2 left-4"
92
+ >
93
+ <button
94
+ aria-label="Select mode"
95
+ class="flex h-8 w-8 items-center justify-center rounded-md border border-gray-200 bg-white text-lightNavbarIcons transition-colors duration-200 hover:bg-gray-100 dark:border-gray-600 dark:bg-gray-800 dark:text-darkNavbarIcons dark:hover:bg-gray-700"
96
+ :class="isModeMenuOpen ? 'bg-gray-100 dark:bg-gray-700' : ''"
97
+ :disabled="agentStore.isResponseInProgress"
98
+ title="Select mode"
99
+ type="button"
100
+ @click="toggleModeMenu"
101
+ >
102
+ <IconBrainOutline class="h-4 w-4" />
103
+ </button>
104
+
105
+ <div
106
+ v-if="isModeMenuOpen"
107
+ class="absolute bottom-full left-0 mb-2 min-w-40 overflow-hidden rounded-md border border-gray-200 bg-white shadow-lg dark:border-gray-600 dark:bg-gray-800"
108
+ >
109
+ <button
110
+ v-for="mode in agentStore.availableModes"
111
+ :key="mode.name"
112
+ class="block w-full px-3 py-2 text-left text-sm text-lightInputText transition-colors duration-150 hover:bg-gray-100 dark:text-darkInputText dark:hover:bg-gray-700"
113
+ :class="mode.name === agentStore.activeModeName ? 'bg-gray-100 dark:bg-gray-700' : ''"
114
+ type="button"
115
+ @click="selectMode(mode.name)"
116
+ >
117
+ {{ mode.name }}
118
+ </button>
119
+ </div>
120
+ </div>
85
121
  <Button
86
122
  class="absolute right-4 bottom-2 !p-0 h-[34px] w-[34px]"
87
- @click="async () => {await agentStore.sendMessage(); autoResize();}"
123
+ @click="sendMessage"
88
124
  :disabled="!agentStore.trimmedUserMessage || agentStore.isResponseInProgress"
89
125
  >
90
126
  <IconArrowUpOutline
@@ -101,8 +137,8 @@
101
137
 
102
138
  <script setup lang="ts">
103
139
  import { IconChatBubbleLeft20Solid, IconSparklesSolid } from '@iconify-prerendered/vue-heroicons';
104
- import { IconCloseOutline, IconBarsOutline, IconArrowUpOutline, IconCloseSidebarSolid, IconOpenSidebarSolid } from '@iconify-prerendered/vue-flowbite';
105
- import { useTemplateRef, onMounted, ref, computed } from 'vue';
140
+ import { IconCloseOutline, IconBarsOutline, IconArrowUpOutline, IconCloseSidebarSolid, IconOpenSidebarSolid, IconBrainOutline } from '@iconify-prerendered/vue-flowbite';
141
+ import { useTemplateRef, onMounted, ref } from 'vue';
106
142
  import { onClickOutside } from '@vueuse/core'
107
143
  import ConversationArea from './ConversationArea.vue';
108
144
  import { useAgentStore } from './useAgentStore';
@@ -112,13 +148,19 @@ import { useCoreStore } from '@/stores/core';
112
148
  const props = defineProps<{
113
149
  meta: {
114
150
  pluginInstanceId: string;
151
+ modes: Array<{
152
+ name: string;
153
+ }>;
154
+ defaultModeName: string | null;
115
155
  }
116
156
  }>();
117
157
 
118
158
  const chatSurface = useTemplateRef('chatSurface');
119
159
  const textInput = useTemplateRef('textInput');
160
+ const modeMenu = useTemplateRef('modeMenu');
120
161
  const agentStore = useAgentStore();
121
162
  const coreStore = useCoreStore();
163
+ const isModeMenuOpen = ref(false);
122
164
 
123
165
  const MAX_WIDTH = 800;
124
166
  const MIN_WIDTH = 382; //w-96
@@ -158,8 +200,10 @@ const stopResize = () => {
158
200
  }
159
201
 
160
202
  onClickOutside(chatSurface, () => {if (!agentStore.isTeleportedToBody) agentStore.setIsChatOpen(false);});
203
+ onClickOutside(modeMenu, () => { isModeMenuOpen.value = false; });
161
204
 
162
205
  onMounted(async () => {
206
+ agentStore.setAvailableModes(props.meta.modes, props.meta.defaultModeName);
163
207
  agentStore.regisrerTextInput(textInput.value);
164
208
  textInput.value?.focus();
165
209
  await agentStore.fetchSessionsList();
@@ -181,4 +225,19 @@ function autoResize() {
181
225
  }
182
226
  }
183
227
 
184
- </script>
228
+ function toggleModeMenu() {
229
+ isModeMenuOpen.value = !isModeMenuOpen.value;
230
+ }
231
+
232
+ function selectMode(modeName: string) {
233
+ agentStore.setActiveMode(modeName);
234
+ isModeMenuOpen.value = false;
235
+ }
236
+
237
+ async function sendMessage() {
238
+ isModeMenuOpen.value = false;
239
+ await agentStore.sendMessage();
240
+ autoResize();
241
+ }
242
+
243
+ </script>
@@ -207,4 +207,44 @@
207
207
  max-height: 144px;
208
208
  }
209
209
 
210
+ </style>
211
+
212
+ <style lang="scss">
213
+ .incremark a.incremark-link,
214
+ .incremark a.incremark-link:visited {
215
+ display: inline-block;
216
+ text-decoration: underline;
217
+ text-underline-offset: 4px;
218
+ text-decoration-style:dotted;
219
+ color: rgb(0, 0, 0);
220
+ transition: color 0.2s ease;
221
+ }
222
+
223
+ .incremark a.incremark-link:hover {
224
+ color: rgb(74, 74, 255);
225
+ text-decoration: underline;
226
+ text-underline-offset: 4px;
227
+ text-decoration-style:dotted;
228
+ }
229
+
230
+ html[data-theme="dark"] .incremark a.incremark-link,
231
+ html[data-theme="dark"] .incremark a.incremark-link:visited {
232
+ color: rgb(220, 220, 220);
233
+ }
234
+
235
+ html[data-theme="dark"] .incremark a.incremark-link:hover {
236
+ color: rgb(147, 147, 255);
237
+ }
238
+
239
+ a.incremark-link::after {
240
+ content: "";
241
+ display: inline-block;
242
+ width: 16px;
243
+ height: 16px;
244
+ vertical-align: middle;
245
+ rotate: -45deg;
246
+ background-color: currentColor;
247
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M13 12H2m12 0l-4 4m4-4l-4-4'/%3E%3C/svg%3E") no-repeat center;
248
+ mask-size: contain;
249
+ }
210
250
  </style>
@@ -255,6 +255,7 @@ function clearVega() {
255
255
  background:
256
256
  linear-gradient(180deg, rgba(255, 255, 255, 0.94), rgba(248, 250, 252, 0.96));
257
257
  box-shadow: 0 14px 30px -24px rgba(15, 23, 42, 0.45);
258
+ margin: 16px 0 ;
258
259
  }
259
260
 
260
261
  .dark .incremark-shiki-code {
@@ -12,4 +12,4 @@ To find specific data record you should use filters. ILIKE filters are preferred
12
12
 
13
13
  For long texts show only several first words and add "..." at the end (only if user did not request this field specifically).
14
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). Links shoudl be always relative path, starting with slash.
15
+ Also when you communicate with user about record, add related link to this record. Build it as `{ADMIN_BASE_PATH}resource/{resourceId}/show/{primary key}`. Use _label from `get_resource_data` as anchor text for link (use markdown link). Links should always be relative paths and must start with `ADMIN_BASE_PATH`. Do not add an extra slash after `ADMIN_BASE_PATH`.
@@ -21,7 +21,7 @@ Use `start_custom_action` and `start_custom_bulk_action` for resource actions.
21
21
  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).
22
22
 
23
23
  For field values with long texts show only several first words and add "..." at the end.
24
- 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). Links shoudl be always relative path, starting with slash.
24
+ Also please add related link to record with will be changed. Build it as `{ADMIN_BASE_PATH}resource/{resourceId}/show/{primary key}`. Use _label from `get_resource_data` as anchor text for link (use markdown link). Links should always be relative paths and must start with `ADMIN_BASE_PATH`. Do not add an extra slash after `ADMIN_BASE_PATH`.
25
25
 
26
26
  And in the same message ask user for final confirmation.
27
27
 
@@ -62,7 +62,7 @@ If you want to block some user you can confirm that this action by saying:
62
62
  * IP Country: USA
63
63
  * Currently blocked: No // show this field only if it exists in user record
64
64
 
65
- View [John Doe](/resource/users/show/123)
65
+ View [John Doe]({ADMIN_BASE_PATH}resource/users/show/123)
66
66
  Are you sure?
67
67
  ```
68
68
 
@@ -85,7 +85,7 @@ I am going to update user:
85
85
  * IP Country: USA
86
86
  I am going to change email from john_doe@example.com to new_email@example.com
87
87
 
88
- View [John Doe](/admin/resource/users/show/123)
88
+ View [John Doe]({ADMIN_BASE_PATH}resource/users/show/123)
89
89
 
90
90
  Are you sure?
91
91
  ```
@@ -105,7 +105,7 @@ If you gonna delete user record, in confirmation please share full user info (no
105
105
  * Signed up: 2024 Jan 1
106
106
  * IP Country: USA
107
107
 
108
- View [John Doe](/admin/resource/users/show/123)
108
+ View [John Doe]({ADMIN_BASE_PATH}resource/users/show/123)
109
109
 
110
110
  Are you sure?
111
111
  ```
@@ -130,7 +130,7 @@ I am going to create user:
130
130
  * Username: john_doe
131
131
  * Email: john_doe@example.com
132
132
 
133
- View [John Doe](/admin/resource/users/show/421) # 421 is id of new created record
133
+ View [John Doe]({ADMIN_BASE_PATH}resource/users/show/421) # 421 is id of new created record
134
134
 
135
135
  Are you sure?
136
136
  ```
@@ -7,6 +7,10 @@ import { Chat } from './chat';
7
7
  import { DefaultChatTransport } from 'ai';
8
8
  import { useCoreStore } from '@/stores/core';
9
9
 
10
+ type AgentMode = {
11
+ name: string;
12
+ };
13
+
10
14
  export const useAgentStore = defineStore('agent', () => {
11
15
  const activeSessionId = ref<string | null>(null);
12
16
  const currentSession = ref<IAgentSession | null>(null);
@@ -28,6 +32,8 @@ export const useAgentStore = defineStore('agent', () => {
28
32
  const header = ref<HTMLElement | null>(null);
29
33
  const lastSessionId = ref<string | null>(null);
30
34
  const chatWidth = ref(600);
35
+ const availableModes = ref<AgentMode[]>([]);
36
+ const activeModeName = ref<string | null>(null);
31
37
  function setLocalStorageItem(key: string, value: string) {
32
38
  window.localStorage.setItem(`${coreStore.config.brandName || 'adminforth'}-${key}`, value);
33
39
  }
@@ -91,6 +97,24 @@ export const useAgentStore = defineStore('agent', () => {
91
97
  })
92
98
  const chats = new Map<string, Chat<any>>();
93
99
  const currentChat = shallowRef<Chat<any>>();
100
+
101
+ function setAvailableModes(modes: AgentMode[], defaultModeName?: string | null) {
102
+ availableModes.value = modes;
103
+ activeModeName.value =
104
+ modes.find((mode) => mode.name === activeModeName.value)?.name
105
+ ?? defaultModeName
106
+ ?? modes[0]?.name
107
+ ?? null;
108
+ }
109
+
110
+ function setActiveMode(modeName: string) {
111
+ if (!availableModes.value.some((mode) => mode.name === modeName)) {
112
+ return;
113
+ }
114
+
115
+ activeModeName.value = modeName;
116
+ }
117
+
94
118
  function setCurrentChat(sessionId: string) {
95
119
  if (chats.has(sessionId)) {
96
120
  currentChat.value = chats.get(sessionId) || null;
@@ -105,6 +129,7 @@ export const useAgentStore = defineStore('agent', () => {
105
129
  message,
106
130
  sessionId,
107
131
  timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
132
+ mode: activeModeName.value,
108
133
  };
109
134
 
110
135
  return {
@@ -368,6 +393,10 @@ export const useAgentStore = defineStore('agent', () => {
368
393
  setIsTeleportedToBody,
369
394
  chatWidth,
370
395
  setChatWidth,
371
- focusTextInput
396
+ focusTextInput,
397
+ availableModes,
398
+ activeModeName,
399
+ setAvailableModes,
400
+ setActiveMode,
372
401
  }
373
- })
402
+ })
package/dist/index.js CHANGED
@@ -100,7 +100,11 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
100
100
  modifyResourceConfig: { get: () => super.modifyResourceConfig }
101
101
  });
102
102
  return __awaiter(this, void 0, void 0, function* () {
103
+ var _a;
103
104
  _super.modifyResourceConfig.call(this, adminforth, resourceConfig);
105
+ if (!((_a = this.options.modes) === null || _a === void 0 ? void 0 : _a.length)) {
106
+ throw new Error("modes is required for AdminForthAgentPlugin");
107
+ }
104
108
  if (!this.adminforth.config.customization.globalInjections.header) {
105
109
  this.adminforth.config.customization.globalInjections.header = [];
106
110
  }
@@ -108,11 +112,10 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
108
112
  file: this.componentPath("ChatSurface.vue"),
109
113
  meta: {
110
114
  pluginInstanceId: this.pluginInstanceId,
115
+ modes: this.options.modes.map((mode) => ({ name: mode.name })),
116
+ defaultModeName: this.options.modes[0].name,
111
117
  }
112
118
  });
113
- if (!this.pluginOptions.completionAdapter) {
114
- throw new Error("CompletionAdapter is required for AdminForthAgentPlugin");
115
- }
116
119
  if (!this.pluginOptions.sessionResource) {
117
120
  throw new Error("sessionResource is required for AdminForthAgentPlugin");
118
121
  }
@@ -210,17 +213,14 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
210
213
  messageId,
211
214
  });
212
215
  const maxTokens = (_f = this.options.maxTokens) !== null && _f !== void 0 ? _f : 10000;
213
- const reasoning = (_g = this.options.reasoning) !== null && _g !== void 0 ? _g : 'low';
214
- const summaryReasoning = 'low';
216
+ const selectedMode = (_g = this.options.modes.find((mode) => mode.name === body.mode)) !== null && _g !== void 0 ? _g : this.options.modes[0];
215
217
  const model = createAgentChatModel({
216
- adapter: this.options.completionAdapter,
218
+ adapter: selectedMode.completionAdapter,
217
219
  maxTokens,
218
- reasoning,
219
220
  });
220
221
  const summaryModel = createAgentChatModel({
221
- adapter: this.options.completionAdapter,
222
+ adapter: selectedMode.completionAdapter,
222
223
  maxTokens,
223
- reasoning: summaryReasoning,
224
224
  });
225
225
  const systemPrompt = yield this.agentSystemPromptPromise;
226
226
  const stream = yield callAgent({
package/index.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type {
2
2
  AdminForthResource,
3
- AdminUser,
4
3
  IAdminForth,
5
4
  IHttpServer
6
5
  } from "adminforth";
@@ -115,6 +114,9 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
115
114
 
116
115
  async modifyResourceConfig(adminforth: IAdminForth, resourceConfig: AdminForthResource) {
117
116
  super.modifyResourceConfig(adminforth, resourceConfig);
117
+ if (!this.options.modes?.length) {
118
+ throw new Error("modes is required for AdminForthAgentPlugin");
119
+ }
118
120
  if (!this.adminforth.config.customization.globalInjections.header) {
119
121
  this.adminforth.config.customization.globalInjections.header = [];
120
122
  }
@@ -122,12 +124,10 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
122
124
  file: this.componentPath("ChatSurface.vue"),
123
125
  meta: {
124
126
  pluginInstanceId: this.pluginInstanceId,
127
+ modes: this.options.modes.map((mode) => ({ name: mode.name })),
128
+ defaultModeName: this.options.modes[0].name,
125
129
  }
126
130
  });
127
-
128
- if (!this.pluginOptions.completionAdapter) {
129
- throw new Error("CompletionAdapter is required for AdminForthAgentPlugin");
130
- }
131
131
  if (!this.pluginOptions.sessionResource) {
132
132
  throw new Error("sessionResource is required for AdminForthAgentPlugin");
133
133
  }
@@ -244,19 +244,15 @@ export default class AdminForthAgentPlugin extends AdminForthPlugin {
244
244
  });
245
245
 
246
246
  const maxTokens = this.options.maxTokens ?? 10000;
247
- const reasoning = this.options.reasoning ?? 'low';
248
- const summaryReasoning = 'low';
249
-
247
+ const selectedMode = this.options.modes.find((mode) => mode.name === body.mode) ?? this.options.modes[0];
250
248
  const model = createAgentChatModel({
251
- adapter: this.options.completionAdapter,
249
+ adapter: selectedMode.completionAdapter,
252
250
  maxTokens,
253
- reasoning,
254
251
  });
255
252
 
256
253
  const summaryModel = createAgentChatModel({
257
- adapter: this.options.completionAdapter,
254
+ adapter: selectedMode.completionAdapter,
258
255
  maxTokens,
259
- reasoning: summaryReasoning,
260
256
  });
261
257
  const systemPrompt = await this.agentSystemPromptPromise;
262
258
  const stream = await callAgent({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adminforth/agent",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
package/types.ts CHANGED
@@ -1,5 +1,4 @@
1
- import {type PluginsCommonOptions } from "adminforth";
2
- import { type CompletionAdapter } from "adminforth";
1
+ import { type PluginsCommonOptions, type CompletionAdapter } from "adminforth";
3
2
 
4
3
  interface ISessionResource {
5
4
  resourceId: string;
@@ -22,11 +21,14 @@ interface ITurnResource {
22
21
 
23
22
  export interface PluginOptions extends PluginsCommonOptions {
24
23
  /**
25
- * Adapter instance that will be used to generate responses.
26
- * You can use any adapter that implements the CompletionAdapter interface, for example the OpenAIAdapter included in adminforth,
27
- * or create your own that fetches responses from your custom backend.
24
+ * Modes for the plugin.
25
+ * Each mode can have its own configuration.
26
+ * Each mode uses its own completion adapter instance.
28
27
  */
29
- completionAdapter: CompletionAdapter;
28
+ modes: {
29
+ name: string;
30
+ completionAdapter: CompletionAdapter;
31
+ }[];
30
32
 
31
33
  /**
32
34
  * Max tokens for the generation.
@@ -40,6 +42,13 @@ export interface PluginOptions extends PluginsCommonOptions {
40
42
  */
41
43
  reasoning?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
42
44
 
45
+ /**
46
+ * Resource configuration for sessions.
47
+ */
43
48
  sessionResource: ISessionResource;
49
+
50
+ /**
51
+ * Resource configuration for turns.
52
+ */
44
53
  turnResource: ITurnResource;
45
54
  }