@adminforth/agent 1.51.1 → 1.52.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.
- package/agent/middleware/apiBasedTools.ts +31 -25
- package/agent/runtime/AgentRuntime.ts +24 -3
- package/agent/systemPrompt.ts +3 -6
- package/agent/turn/TurnLifecycleService.ts +18 -0
- package/agent/turn/TurnStreamConsumer.ts +11 -3
- package/agent/turn/turnTypes.ts +9 -1
- package/agentEvents.ts +5 -0
- package/agentTurnService.ts +158 -12
- package/apiBasedTools.ts +7 -0
- package/build.log +3 -2
- package/custom/ChatFooter.vue +3 -2
- package/custom/composables/agentStore/useAgentChat.ts +169 -6
- package/custom/composables/agentStore/useAgentSessions.ts +3 -1
- package/custom/composables/useAgentStore.ts +87 -0
- package/custom/conversation_area/MessageRenderer.vue +6 -1
- package/custom/conversation_area/ToolApprovalRenderer.vue +98 -0
- package/custom/skills/mutate_data/SKILL.md +10 -36
- package/custom/types.ts +4 -1
- package/dist/agent/middleware/apiBasedTools.js +26 -25
- package/dist/agent/runtime/AgentRuntime.d.ts +1 -1
- package/dist/agent/runtime/AgentRuntime.js +18 -3
- package/dist/agent/systemPrompt.js +3 -6
- package/dist/agent/turn/TurnLifecycleService.d.ts +8 -1
- package/dist/agent/turn/TurnLifecycleService.js +17 -1
- package/dist/agent/turn/TurnStreamConsumer.d.ts +2 -1
- package/dist/agent/turn/TurnStreamConsumer.js +14 -8
- package/dist/agent/turn/turnTypes.d.ts +14 -1
- package/dist/agentEvents.d.ts +4 -0
- package/dist/agentTurnService.d.ts +1 -0
- package/dist/agentTurnService.js +132 -14
- package/dist/apiBasedTools.d.ts +5 -0
- package/dist/apiBasedTools.js +1 -0
- package/dist/custom/ChatFooter.vue +3 -2
- package/dist/custom/composables/agentStore/useAgentChat.ts +169 -6
- package/dist/custom/composables/agentStore/useAgentSessions.ts +3 -1
- package/dist/custom/composables/useAgentStore.ts +87 -0
- package/dist/custom/conversation_area/MessageRenderer.vue +6 -1
- package/dist/custom/conversation_area/ToolApprovalRenderer.vue +98 -0
- package/dist/custom/skills/mutate_data/SKILL.md +10 -36
- package/dist/custom/types.ts +4 -1
- package/dist/endpoints/core.js +28 -0
- package/dist/index.js +1 -1
- package/dist/sessionStore.d.ts +1 -0
- package/dist/sessionStore.js +6 -0
- package/dist/surfaces/web-sse/createSseEventEmitter.js +13 -0
- package/endpoints/core.ts +30 -0
- package/index.ts +1 -1
- package/package.json +3 -6
- package/sessionStore.ts +11 -0
- package/surfaces/web-sse/createSseEventEmitter.ts +14 -0
|
@@ -14,15 +14,159 @@ type CreateAgentChatManagerOptions = {
|
|
|
14
14
|
lastMessage: Ref<string>;
|
|
15
15
|
activeModeName: Ref<string | null>;
|
|
16
16
|
onOpenPage: (targetPath: string) => void;
|
|
17
|
+
onToolApprovalRequest: (sessionId: string, interrupt: unknown) => void;
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
export function createAgentChatManager({
|
|
20
21
|
lastMessage,
|
|
21
22
|
activeModeName,
|
|
22
23
|
onOpenPage,
|
|
24
|
+
onToolApprovalRequest,
|
|
23
25
|
}: CreateAgentChatManagerOptions) {
|
|
24
26
|
const chats = new Map<string, Chat<any>>();
|
|
25
27
|
const currentChat = shallowRef<Chat<any> | null>();
|
|
28
|
+
const agentApiBase = `${(import.meta as AgentImportMeta).env.VITE_ADMINFORTH_PUBLIC_PATH || ''}/adminapi/v1/agent`;
|
|
29
|
+
|
|
30
|
+
function replaceLastMessage(message: any) {
|
|
31
|
+
const chat = currentChat.value;
|
|
32
|
+
|
|
33
|
+
if (!chat) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
chat.messages.splice(chat.messages.length - 1, 1, message);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getOrCreateAssistantMessage() {
|
|
41
|
+
const chat = currentChat.value;
|
|
42
|
+
const lastChatMessage = chat?.lastMessage;
|
|
43
|
+
|
|
44
|
+
if (lastChatMessage?.role === 'assistant') {
|
|
45
|
+
return lastChatMessage;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const assistantMessage = {
|
|
49
|
+
role: 'assistant',
|
|
50
|
+
parts: [],
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
chat?.messages.push(assistantMessage);
|
|
54
|
+
return assistantMessage;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function appendTextDelta(delta: string) {
|
|
58
|
+
const assistantMessage = getOrCreateAssistantMessage();
|
|
59
|
+
const lastPart = assistantMessage.parts.at(-1);
|
|
60
|
+
|
|
61
|
+
if (lastPart?.type === 'text') {
|
|
62
|
+
lastPart.text = `${lastPart.text ?? ''}${delta}`;
|
|
63
|
+
lastPart.state = 'streaming';
|
|
64
|
+
} else {
|
|
65
|
+
assistantMessage.parts.push({
|
|
66
|
+
type: 'text',
|
|
67
|
+
text: delta,
|
|
68
|
+
state: 'streaming',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
replaceLastMessage(assistantMessage);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function finishTextPart() {
|
|
76
|
+
const assistantMessage = currentChat.value?.lastMessage;
|
|
77
|
+
const lastPart = assistantMessage?.parts.at(-1);
|
|
78
|
+
|
|
79
|
+
if (assistantMessage?.role === 'assistant' && lastPart?.type === 'text') {
|
|
80
|
+
lastPart.state = 'done';
|
|
81
|
+
replaceLastMessage(assistantMessage);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function appendDataPart(type: string, data: unknown) {
|
|
86
|
+
const assistantMessage = getOrCreateAssistantMessage();
|
|
87
|
+
|
|
88
|
+
assistantMessage.parts.push({ type, data });
|
|
89
|
+
replaceLastMessage(assistantMessage);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function handleRealtimeChatData(dataPart: any) {
|
|
93
|
+
if (dataPart?.type === 'data-open-page' && typeof dataPart.data?.targetPath === 'string') {
|
|
94
|
+
onOpenPage(dataPart.data.targetPath);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (dataPart?.type === 'data-interrupt' && typeof dataPart.data?.sessionId === 'string') {
|
|
99
|
+
onToolApprovalRequest(dataPart.data.sessionId, dataPart.data.interrupt);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function handleManualApprovalStreamPart(dataPart: any) {
|
|
104
|
+
if (dataPart?.type === 'text-delta' && typeof dataPart.delta === 'string') {
|
|
105
|
+
appendTextDelta(dataPart.delta);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (dataPart?.type === 'text-end') {
|
|
110
|
+
finishTextPart();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (dataPart?.type === 'data-tool-call') {
|
|
115
|
+
appendDataPart('data-tool-call', dataPart.data);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (dataPart?.type === 'data-rendering') {
|
|
120
|
+
appendDataPart('data-rendering', dataPart.data);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (dataPart?.type === 'data-open-page' && typeof dataPart.data?.targetPath === 'string') {
|
|
125
|
+
onOpenPage(dataPart.data.targetPath);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (dataPart?.type === 'data-interrupt' && typeof dataPart.data?.sessionId === 'string') {
|
|
130
|
+
onToolApprovalRequest(dataPart.data.sessionId, dataPart.data.interrupt);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function consumeAgentStream(response: Response) {
|
|
135
|
+
const reader = response.body?.getReader();
|
|
136
|
+
|
|
137
|
+
if (!reader) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const decoder = new TextDecoder();
|
|
142
|
+
let buffer = '';
|
|
143
|
+
|
|
144
|
+
while (true) {
|
|
145
|
+
const { value, done } = await reader.read();
|
|
146
|
+
|
|
147
|
+
if (done) {
|
|
148
|
+
break;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
buffer += decoder.decode(value, { stream: true });
|
|
152
|
+
const events = buffer.split('\n\n');
|
|
153
|
+
buffer = events.pop() ?? '';
|
|
154
|
+
|
|
155
|
+
for (const event of events) {
|
|
156
|
+
const data = event
|
|
157
|
+
.split('\n')
|
|
158
|
+
.filter(line => line.startsWith('data:'))
|
|
159
|
+
.map(line => line.slice(5).trim())
|
|
160
|
+
.join('\n');
|
|
161
|
+
|
|
162
|
+
if (!data || data === '[DONE]') {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
handleManualApprovalStreamPart(JSON.parse(data));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
26
170
|
|
|
27
171
|
function setCurrentChat(sessionId: string) {
|
|
28
172
|
if (chats.has(sessionId)) {
|
|
@@ -30,7 +174,7 @@ export function createAgentChatManager({
|
|
|
30
174
|
} else {
|
|
31
175
|
const newChat = new Chat({
|
|
32
176
|
transport: new DefaultChatTransport({
|
|
33
|
-
api: `${
|
|
177
|
+
api: `${agentApiBase}/response`,
|
|
34
178
|
credentials: 'include',
|
|
35
179
|
prepareSendMessagesRequest({ messages }: any) {
|
|
36
180
|
const message = lastMessage.value;
|
|
@@ -54,11 +198,7 @@ export function createAgentChatManager({
|
|
|
54
198
|
onError(error: unknown) {
|
|
55
199
|
console.error('Chat error:', error);
|
|
56
200
|
},
|
|
57
|
-
onData
|
|
58
|
-
if (dataPart?.type === 'data-open-page' && typeof dataPart.data?.targetPath === 'string') {
|
|
59
|
-
onOpenPage(dataPart.data.targetPath);
|
|
60
|
-
}
|
|
61
|
-
},
|
|
201
|
+
onData: handleRealtimeChatData,
|
|
62
202
|
});
|
|
63
203
|
chats.set(sessionId, newChat);
|
|
64
204
|
currentChat.value = newChat;
|
|
@@ -69,9 +209,32 @@ export function createAgentChatManager({
|
|
|
69
209
|
currentChat.value?.stop();
|
|
70
210
|
}
|
|
71
211
|
|
|
212
|
+
async function submitToolApproval(sessionId: string, decision: 'approve' | 'reject') {
|
|
213
|
+
const response = await fetch(`${agentApiBase}/approval`, {
|
|
214
|
+
method: 'POST',
|
|
215
|
+
credentials: 'include',
|
|
216
|
+
headers: {
|
|
217
|
+
Accept: 'text/event-stream',
|
|
218
|
+
'Content-Type': 'application/json',
|
|
219
|
+
'x-vercel-ai-ui-message-stream': 'v1',
|
|
220
|
+
},
|
|
221
|
+
body: JSON.stringify({
|
|
222
|
+
sessionId,
|
|
223
|
+
decision,
|
|
224
|
+
}),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (!response.ok) {
|
|
228
|
+
throw new Error(`Agent approval failed with status ${response.status}`);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
await consumeAgentStream(response);
|
|
232
|
+
}
|
|
233
|
+
|
|
72
234
|
return {
|
|
73
235
|
currentChat,
|
|
74
236
|
setCurrentChat,
|
|
75
237
|
abortCurrentChatRequest,
|
|
238
|
+
submitToolApproval,
|
|
76
239
|
};
|
|
77
240
|
}
|
|
@@ -17,6 +17,7 @@ type CreateAgentSessionManagerOptions = {
|
|
|
17
17
|
currentChat: ShallowRef<Chat<any> | null | undefined>;
|
|
18
18
|
trimmedUserMessage: ComputedRef<string>;
|
|
19
19
|
isResponseInProgress: ComputedRef<boolean>;
|
|
20
|
+
isMessageInputBlocked: ComputedRef<boolean>;
|
|
20
21
|
userMessageInput: Ref<any>;
|
|
21
22
|
lastMessage: Ref<string>;
|
|
22
23
|
blockCloseOfChat: Ref<boolean>;
|
|
@@ -32,6 +33,7 @@ export function createAgentSessionManager({
|
|
|
32
33
|
currentChat,
|
|
33
34
|
trimmedUserMessage,
|
|
34
35
|
isResponseInProgress,
|
|
36
|
+
isMessageInputBlocked,
|
|
35
37
|
userMessageInput,
|
|
36
38
|
lastMessage,
|
|
37
39
|
blockCloseOfChat,
|
|
@@ -130,7 +132,7 @@ export function createAgentSessionManager({
|
|
|
130
132
|
|
|
131
133
|
async function sendMessage() {
|
|
132
134
|
const message = trimmedUserMessage.value;
|
|
133
|
-
if (!message ||
|
|
135
|
+
if (!message || isMessageInputBlocked.value) {
|
|
134
136
|
return;
|
|
135
137
|
}
|
|
136
138
|
if (!currentSession.value || currentSession.value.sessionId === PRE_SESSION_ID) {
|
|
@@ -53,10 +53,12 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
53
53
|
currentChat,
|
|
54
54
|
setCurrentChat,
|
|
55
55
|
abortCurrentChatRequest,
|
|
56
|
+
submitToolApproval: submitToolApprovalResponse,
|
|
56
57
|
} = createAgentChatManager({
|
|
57
58
|
lastMessage,
|
|
58
59
|
activeModeName,
|
|
59
60
|
onOpenPage: openAgentPage,
|
|
61
|
+
onToolApprovalRequest: addToolApprovalMessage,
|
|
60
62
|
});
|
|
61
63
|
const {
|
|
62
64
|
userMessagePlaceholder,
|
|
@@ -95,6 +97,75 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
95
97
|
isAudioChatMode.value = isAudioChat;
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
function getToolApprovalMessages(interrupt: unknown): string[] {
|
|
101
|
+
const interrupts = Array.isArray(interrupt) ? interrupt : [interrupt];
|
|
102
|
+
|
|
103
|
+
return interrupts.flatMap((item: any) => {
|
|
104
|
+
const value = item?.value ?? item;
|
|
105
|
+
const actionRequests = Array.isArray(value?.actionRequests) ? value.actionRequests : [];
|
|
106
|
+
|
|
107
|
+
return actionRequests.map((actionRequest: any) => (
|
|
108
|
+
typeof actionRequest?.description === 'string'
|
|
109
|
+
? actionRequest.description
|
|
110
|
+
: String(actionRequest?.name ?? 'Tool execution pending approval')
|
|
111
|
+
));
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function addToolApprovalMessage(sessionId: string, interrupt: unknown) {
|
|
116
|
+
const approvalPart = {
|
|
117
|
+
type: 'data-tool-approval' as const,
|
|
118
|
+
data: {
|
|
119
|
+
sessionId,
|
|
120
|
+
status: 'pending' as const,
|
|
121
|
+
messages: getToolApprovalMessages(interrupt),
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
const lastChatMessage = currentChat.value?.lastMessage;
|
|
125
|
+
|
|
126
|
+
if (lastChatMessage?.role === 'assistant') {
|
|
127
|
+
lastChatMessage.parts.push(approvalPart);
|
|
128
|
+
currentChat.value?.messages.splice(currentChat.value.messages.length - 1, 1, lastChatMessage);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
currentChat.value?.messages.push({
|
|
133
|
+
role: 'assistant',
|
|
134
|
+
parts: [approvalPart],
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function submitToolApproval(sessionId: string, decision: 'approve' | 'reject') {
|
|
139
|
+
const message = currentChat.value?.messages
|
|
140
|
+
.findLast(candidate => candidate.role === 'assistant' && candidate.parts.some(part => {
|
|
141
|
+
return part.type === 'data-tool-approval'
|
|
142
|
+
&& part.data?.sessionId === sessionId
|
|
143
|
+
&& part.data?.status === 'pending';
|
|
144
|
+
}));
|
|
145
|
+
const approvalPart = message?.parts.find(part => {
|
|
146
|
+
return part.type === 'data-tool-approval'
|
|
147
|
+
&& part.data?.sessionId === sessionId
|
|
148
|
+
&& part.data?.status === 'pending';
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
if (approvalPart?.data) {
|
|
152
|
+
approvalPart.data.status = 'processing';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
await submitToolApprovalResponse(sessionId, decision);
|
|
157
|
+
|
|
158
|
+
if (approvalPart?.data) {
|
|
159
|
+
approvalPart.data.status = decision === 'approve' ? 'approved' : 'rejected';
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (approvalPart?.data) {
|
|
163
|
+
approvalPart.data.status = 'pending';
|
|
164
|
+
}
|
|
165
|
+
console.error('Error submitting tool approval', error);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
98
169
|
watch(isAudioChatMode, (newVal: boolean) => {
|
|
99
170
|
if (newVal) {
|
|
100
171
|
addSystemMessage(RESERVED_SYSTEM_MESSAGE_CONTENT.START_AUDIO_CHAT);
|
|
@@ -107,6 +178,18 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
107
178
|
const isResponseInProgress = computed( () => {
|
|
108
179
|
return currentChat.value?.status === 'streaming';
|
|
109
180
|
});
|
|
181
|
+
const hasPendingToolApproval = computed(() => (
|
|
182
|
+
currentChat.value?.messages.some((message: any) => (
|
|
183
|
+
message.role === 'assistant'
|
|
184
|
+
&& message.parts.some((part: any) => (
|
|
185
|
+
part.type === 'data-tool-approval'
|
|
186
|
+
&& (part.data?.status === 'pending' || part.data?.status === 'processing')
|
|
187
|
+
))
|
|
188
|
+
)) ?? false
|
|
189
|
+
));
|
|
190
|
+
const isMessageInputBlocked = computed(() => (
|
|
191
|
+
isResponseInProgress.value || hasPendingToolApproval.value
|
|
192
|
+
));
|
|
110
193
|
const blockCloseOfChat = ref(false);
|
|
111
194
|
const {
|
|
112
195
|
sendMessage,
|
|
@@ -129,6 +212,7 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
129
212
|
currentChat,
|
|
130
213
|
trimmedUserMessage,
|
|
131
214
|
isResponseInProgress,
|
|
215
|
+
isMessageInputBlocked,
|
|
132
216
|
userMessageInput,
|
|
133
217
|
lastMessage,
|
|
134
218
|
blockCloseOfChat,
|
|
@@ -385,11 +469,14 @@ export const useAgentStore = defineStore('agent', () => {
|
|
|
385
469
|
isSessionHistoryOpen,
|
|
386
470
|
setSessionHistoryOpen,
|
|
387
471
|
sendMessage,
|
|
472
|
+
submitToolApproval,
|
|
388
473
|
userMessageInput,
|
|
389
474
|
userMessagePlaceholder,
|
|
390
475
|
chatMessages: computed(() => currentChat.value?.messages || []),
|
|
391
476
|
trimmedUserMessage,
|
|
392
477
|
isResponseInProgress,
|
|
478
|
+
hasPendingToolApproval,
|
|
479
|
+
isMessageInputBlocked,
|
|
393
480
|
isTeleportedToBody,
|
|
394
481
|
setIsTeleportedToBody,
|
|
395
482
|
chatWidth,
|
|
@@ -13,8 +13,12 @@
|
|
|
13
13
|
:role="props.message.role"
|
|
14
14
|
:state="part.state ?? (props.message.role === 'user' ? 'done' : undefined)"
|
|
15
15
|
/>
|
|
16
|
+
<ToolApprovalRenderer
|
|
17
|
+
v-else-if="part.type === 'data-tool-approval'"
|
|
18
|
+
:data="part.data ?? {}"
|
|
19
|
+
/>
|
|
16
20
|
<SystemMessageRenderer
|
|
17
|
-
v-else
|
|
21
|
+
v-else-if="part.type === 'text'"
|
|
18
22
|
:message="part.text"
|
|
19
23
|
/>
|
|
20
24
|
</template>
|
|
@@ -30,6 +34,7 @@
|
|
|
30
34
|
import { getMessageParts } from '../utils';
|
|
31
35
|
import ProcessingTimeline from './ProcessingTimeline.vue';
|
|
32
36
|
import SystemMessageRenderer from './SystemMessageRenderer.vue';
|
|
37
|
+
import ToolApprovalRenderer from './ToolApprovalRenderer.vue';
|
|
33
38
|
import { RESERVED_SYSTEM_MESSAGE_CONTENT } from '../composables/agentStore/constants';
|
|
34
39
|
|
|
35
40
|
const props = defineProps<{
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="mx-4 my-3 max-w-[min(34rem,calc(100%-2rem))] rounded-lg border border-lightPrimary/30 bg-lightNavbar p-4 text-lightListTableHeadingText shadow-sm dark:border-darkPrimary/40 dark:bg-darkNavbar dark:text-darkListTableHeadingText">
|
|
3
|
+
<div class="flex items-start gap-3">
|
|
4
|
+
<div class="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-lightPrimary/10 text-lightPrimary dark:bg-darkPrimary/15 dark:text-darkPrimary">
|
|
5
|
+
<IconExclamationCircleOutline class="h-5 w-5" />
|
|
6
|
+
</div>
|
|
7
|
+
<div class="min-w-0 flex-1">
|
|
8
|
+
<h3 class="text-sm font-semibold leading-5">{{ $t('Approval required') }}</h3>
|
|
9
|
+
<p class="mt-1 text-sm leading-5 text-lightListTableText dark:text-darkListTableText">
|
|
10
|
+
{{ $t('Review the agent message before continuing.') }}
|
|
11
|
+
</p>
|
|
12
|
+
<button
|
|
13
|
+
v-if="data.messages?.length"
|
|
14
|
+
type="button"
|
|
15
|
+
class="mt-3 inline-flex items-center gap-2 text-sm font-medium text-lightListTableHeadingText transition hover:opacity-80 dark:text-darkListTableHeadingText"
|
|
16
|
+
@click="isExpanded = !isExpanded"
|
|
17
|
+
>
|
|
18
|
+
<IconChevronDownOutline
|
|
19
|
+
class="h-4 w-4 transition-transform"
|
|
20
|
+
:class="isExpanded ? 'rotate-180' : ''"
|
|
21
|
+
/>
|
|
22
|
+
{{ isExpanded ? $t('Hide details') : $t('Show details') }}
|
|
23
|
+
<span class="rounded-full bg-lightListTableText/10 px-2 py-0.5 text-xs text-lightListTableHeadingText dark:bg-darkListTableText/10 dark:text-darkListTableHeadingText">
|
|
24
|
+
{{ data.messages.length }}
|
|
25
|
+
</span>
|
|
26
|
+
</button>
|
|
27
|
+
<ul
|
|
28
|
+
v-if="isExpanded && data.messages?.length"
|
|
29
|
+
class="mt-3 space-y-1 text-sm leading-5 text-lightListTableHeadingText dark:text-darkListTableHeadingText"
|
|
30
|
+
>
|
|
31
|
+
<li
|
|
32
|
+
v-for="message in data.messages"
|
|
33
|
+
:key="message"
|
|
34
|
+
class="flex gap-2"
|
|
35
|
+
>
|
|
36
|
+
<span class="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-lightListTableHeadingText dark:bg-darkListTableHeadingText" />
|
|
37
|
+
<span>{{ message }}</span>
|
|
38
|
+
</li>
|
|
39
|
+
</ul>
|
|
40
|
+
<div class="mt-4 flex flex-wrap gap-2">
|
|
41
|
+
<template v-if="data.status === 'pending'">
|
|
42
|
+
<button
|
|
43
|
+
type="button"
|
|
44
|
+
class="inline-flex h-9 items-center gap-2 rounded-md border border-lightButtonsBorder bg-lightButtonsBackground px-3 text-sm font-medium text-lightButtonsText transition hover:bg-lightButtonsHover dark:border-darkButtonsBorder dark:bg-darkButtonsBackground dark:text-darkButtonsText dark:hover:bg-darkButtonsHover"
|
|
45
|
+
@click="submit('approve')"
|
|
46
|
+
>
|
|
47
|
+
<IconCheckOutline class="h-4 w-4" />
|
|
48
|
+
{{ $t('Approve') }}
|
|
49
|
+
</button>
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
class="inline-flex h-9 items-center gap-2 rounded-md border border-lightListTableText/20 bg-transparent px-3 text-sm font-medium text-lightListTableHeadingText transition hover:bg-lightListTableText/10 dark:border-darkListTableText/20 dark:text-darkListTableHeadingText dark:hover:bg-darkListTableText/10"
|
|
53
|
+
@click="submit('reject')"
|
|
54
|
+
>
|
|
55
|
+
<IconCloseOutline class="h-4 w-4" />
|
|
56
|
+
{{ $t('Reject') }}
|
|
57
|
+
</button>
|
|
58
|
+
</template>
|
|
59
|
+
<span
|
|
60
|
+
v-else
|
|
61
|
+
class="inline-flex h-8 items-center gap-2 rounded-md border px-2.5 text-sm font-medium"
|
|
62
|
+
:class="data.status === 'processing'
|
|
63
|
+
? 'border-lightListTableText/20 bg-lightListTableText/10 text-lightListTableHeadingText dark:border-darkListTableText/20 dark:bg-darkListTableText/10 dark:text-darkListTableHeadingText'
|
|
64
|
+
: data.status === 'approved'
|
|
65
|
+
? 'border-lightListTableText/20 bg-lightListTableText/10 text-lightListTableHeadingText dark:border-darkListTableText/20 dark:bg-darkListTableText/10 dark:text-darkListTableHeadingText'
|
|
66
|
+
: 'border-lightListTableText/20 bg-lightListTableText/10 text-lightListTableHeadingText dark:border-darkListTableText/20 dark:bg-darkListTableText/10 dark:text-darkListTableHeadingText'"
|
|
67
|
+
>
|
|
68
|
+
<IconCheckOutline v-if="data.status === 'approved'" class="h-4 w-4" />
|
|
69
|
+
<IconCloseOutline v-else-if="data.status === 'rejected'" class="h-4 w-4" />
|
|
70
|
+
{{ data.status === 'processing' ? $t('Processing') : data.status === 'approved' ? $t('Approved') : $t('Rejected') }}
|
|
71
|
+
</span>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</template>
|
|
77
|
+
|
|
78
|
+
<script setup lang="ts">
|
|
79
|
+
import type { IPartData } from '../types';
|
|
80
|
+
import { useAgentStore } from '../composables/useAgentStore';
|
|
81
|
+
import { ref } from 'vue';
|
|
82
|
+
import { IconCheckOutline, IconChevronDownOutline, IconCloseOutline, IconExclamationCircleOutline } from '@iconify-prerendered/vue-flowbite';
|
|
83
|
+
|
|
84
|
+
const props = defineProps<{
|
|
85
|
+
data: IPartData;
|
|
86
|
+
}>();
|
|
87
|
+
|
|
88
|
+
const agentStore = useAgentStore();
|
|
89
|
+
const isExpanded = ref(false);
|
|
90
|
+
|
|
91
|
+
function submit(decision: 'approve' | 'reject') {
|
|
92
|
+
if (!props.data.sessionId || props.data.status !== 'pending') {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
agentStore.submitToolApproval(props.data.sessionId, decision);
|
|
97
|
+
}
|
|
98
|
+
</script>
|
|
@@ -16,41 +16,22 @@ Use `start_custom_action` and `start_custom_bulk_action` for resource actions.
|
|
|
16
16
|
|
|
17
17
|
- 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)
|
|
18
18
|
|
|
19
|
-
##
|
|
19
|
+
## Mutation context
|
|
20
20
|
|
|
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).
|
|
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
|
-
Every
|
|
23
|
+
Every mutation description must describe one exact fetched row. Never combine `_label`, primary key, link, or field values from different rows or different resources in one description.
|
|
24
24
|
|
|
25
25
|
For field values with long texts show only several first words and add "..." at the end.
|
|
26
26
|
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`.
|
|
27
27
|
|
|
28
|
-
Before
|
|
28
|
+
Before calling the mutation tool, verify that the `resourceId`, `{primary key}`, `_label`, and all shown fields come from the same exact fetched row.
|
|
29
29
|
|
|
30
|
-
Never show information about more than 10 records in one message. If a mutation plan affects more than 10 records, show only the 10 most important examples plus the total count if known
|
|
30
|
+
Never show information about more than 10 records in one message. If a mutation plan affects more than 10 records, show only the 10 most important examples plus the total count if known.
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
When creating new record, show user all data which you gonna create.
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
Accept any positive confirmation from user like "yes", "sure", "+", anything non-negative call to action, can be considered as confirmation.
|
|
37
|
-
|
|
38
|
-
A confirmation is valid only for the clearly described mutation plan from the immediately previous assistant message.
|
|
39
|
-
|
|
40
|
-
Never reuse an older confirmation for a later mutation.
|
|
41
|
-
|
|
42
|
-
One confirmation may cover:
|
|
43
|
-
- one single mutation
|
|
44
|
-
- one explicitly described batch
|
|
45
|
-
- one short sequence of related mutations that together implement the same user request
|
|
46
|
-
|
|
47
|
-
If the confirmed plan contains several related mutation steps, execute that whole confirmed plan without asking again between those steps.
|
|
48
|
-
|
|
49
|
-
Ask for confirmation again if the plan changes in any way: different record, different fields, different values, different number of records, different action, or any extra mutation that was not listed in the confirmation message.
|
|
50
|
-
|
|
51
|
-
If you are creating or deleting multiple records in one batch, you may ask once for that exact batch, but list the whole batch explicitly in the confirmation message. Any extra record outside that described batch requires a new confirmation.
|
|
52
|
-
|
|
53
|
-
After the confirmed plan is finished, do not treat that confirmation as still active for later requests.
|
|
34
|
+
Do not ask user for textual confirmation. Dangerous tools are approved by the runtime approval UI.
|
|
54
35
|
|
|
55
36
|
# Calling actions
|
|
56
37
|
|
|
@@ -60,7 +41,7 @@ Before calling any of this action you should understand whether this action is a
|
|
|
60
41
|
|
|
61
42
|
### Example
|
|
62
43
|
|
|
63
|
-
If you want to block some user you can
|
|
44
|
+
If you want to block some user you can describe the action by saying:
|
|
64
45
|
|
|
65
46
|
```I am going to block user:
|
|
66
47
|
* Username: john_doe
|
|
@@ -69,7 +50,6 @@ If you want to block some user you can confirm that this action by saying:
|
|
|
69
50
|
* Currently blocked: No // show this field only if it exists in user record
|
|
70
51
|
|
|
71
52
|
View [John Doe]({ADMIN_BASE_PATH}resource/users/show/123)
|
|
72
|
-
Are you sure?
|
|
73
53
|
```
|
|
74
54
|
|
|
75
55
|
## Updating
|
|
@@ -81,7 +61,7 @@ In addition to instructions above show user the table of edits (old value/new va
|
|
|
81
61
|
|
|
82
62
|
### Examples
|
|
83
63
|
|
|
84
|
-
For example if you gonna modify user record,
|
|
64
|
+
For example if you gonna modify user record, 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:
|
|
85
65
|
|
|
86
66
|
```
|
|
87
67
|
I am going to update user:
|
|
@@ -91,8 +71,6 @@ I am going to update user:
|
|
|
91
71
|
I am going to change email from john_doe@example.com to new_email@example.com
|
|
92
72
|
|
|
93
73
|
View [John Doe]({ADMIN_BASE_PATH}resource/users/show/123)
|
|
94
|
-
|
|
95
|
-
Are you sure?
|
|
96
74
|
```
|
|
97
75
|
|
|
98
76
|
|
|
@@ -102,7 +80,7 @@ To delete some record you can use `delete_record` tool. To delete record `allowe
|
|
|
102
80
|
|
|
103
81
|
### Example
|
|
104
82
|
|
|
105
|
-
If you gonna delete user record,
|
|
83
|
+
If you gonna delete user record, 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:
|
|
106
84
|
|
|
107
85
|
```I am going to delete user:
|
|
108
86
|
* Username: john_doe
|
|
@@ -111,8 +89,6 @@ If you gonna delete user record, in confirmation please share full user info (no
|
|
|
111
89
|
* IP Country: USA
|
|
112
90
|
|
|
113
91
|
View [John Doe]({ADMIN_BASE_PATH}resource/users/show/123)
|
|
114
|
-
|
|
115
|
-
Are you sure?
|
|
116
92
|
```
|
|
117
93
|
|
|
118
94
|
## Creating
|
|
@@ -142,6 +118,4 @@ I am going to create user:
|
|
|
142
118
|
* Email: john_doe@example.com
|
|
143
119
|
|
|
144
120
|
View [John Doe]({ADMIN_BASE_PATH}resource/users/show/421) # 421 is id of new created record
|
|
145
|
-
|
|
146
|
-
Are you sure?
|
|
147
121
|
```
|
package/custom/types.ts
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
export interface IPartData {
|
|
2
2
|
toolCallId?: string;
|
|
3
3
|
toolName?: string;
|
|
4
|
+
sessionId?: string;
|
|
4
5
|
phase?: 'start' | 'end';
|
|
5
6
|
label?: string;
|
|
6
7
|
input?: any;
|
|
7
8
|
output?: any;
|
|
8
9
|
durationMs?: number;
|
|
9
10
|
toolInfo?: string;
|
|
11
|
+
status?: 'pending' | 'processing' | 'approved' | 'rejected';
|
|
12
|
+
messages?: string[];
|
|
10
13
|
}
|
|
11
14
|
export interface IPart {
|
|
12
|
-
type: 'reasoning' | 'data-tool-call' | 'data-rendering' | 'text';
|
|
15
|
+
type: 'reasoning' | 'data-tool-call' | 'data-rendering' | 'data-tool-approval' | 'text';
|
|
13
16
|
text?: string;
|
|
14
17
|
state?: 'started' | 'thinking' | 'processing' | 'streaming' | 'done';
|
|
15
18
|
data?: IPartData;
|