@budibase/frontend-core 3.23.26 → 3.23.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@budibase/frontend-core",
3
- "version": "3.23.26",
3
+ "version": "3.23.28",
4
4
  "description": "Budibase frontend core libraries used in builder and client",
5
5
  "author": "Budibase",
6
6
  "license": "MPL-2.0",
@@ -12,10 +12,11 @@
12
12
  "@budibase/bbui": "*",
13
13
  "@budibase/shared-core": "*",
14
14
  "@budibase/types": "*",
15
+ "ai": "^5.0.93",
15
16
  "dayjs": "^1.10.8",
16
17
  "lodash": "4.17.21",
17
18
  "shortid": "2.2.15",
18
19
  "socket.io-client": "^4.7.5"
19
20
  },
20
- "gitHead": "c17fe00ac51b8f257b3e66dfcee10b82a6ea5264"
21
+ "gitHead": "bf96741b576cc61b4aae9af4ff390dbcf1eb34e4"
21
22
  }
package/src/api/agents.ts CHANGED
@@ -8,19 +8,19 @@ import {
8
8
  CreateToolSourceRequest,
9
9
  FetchAgentHistoryResponse,
10
10
  FetchAgentsResponse,
11
- LLMStreamChunk,
12
11
  UpdateAgentRequest,
13
12
  UpdateAgentResponse,
14
13
  } from "@budibase/types"
15
14
 
16
15
  import { Header } from "@budibase/shared-core"
17
16
  import { BaseAPIClient } from "./types"
17
+ import { UIMessageChunk } from "ai"
18
18
 
19
19
  export interface AgentEndpoints {
20
20
  agentChatStream: (
21
21
  chat: AgentChat,
22
22
  workspaceId: string,
23
- onChunk: (chunk: LLMStreamChunk) => void,
23
+ onChunk: (chunk: UIMessageChunk) => void,
24
24
  onError?: (error: Error) => void
25
25
  ) => Promise<void>
26
26
 
@@ -44,7 +44,6 @@ export const buildAgentEndpoints = (API: BaseAPIClient): AgentEndpoints => ({
44
44
  const body: ChatAgentRequest = chat
45
45
 
46
46
  try {
47
- // TODO: add support for streaming into the frontend-core API object
48
47
  const response = await fetch("/api/agent/chat/stream", {
49
48
  method: "POST",
50
49
  headers: {
@@ -88,8 +87,9 @@ export const buildAgentEndpoints = (API: BaseAPIClient): AgentEndpoints => ({
88
87
  if (line.startsWith("data: ")) {
89
88
  try {
90
89
  const data = line.slice(6) // Remove 'data: ' prefix
91
- if (data.trim()) {
92
- const chunk: LLMStreamChunk = JSON.parse(data)
90
+ const trimmedData = data.trim()
91
+ if (trimmedData && trimmedData !== "[DONE]") {
92
+ const chunk: UIMessageChunk = JSON.parse(data)
93
93
  onChunk(chunk)
94
94
  }
95
95
  } catch (parseError) {
@@ -1,12 +1,14 @@
1
1
  <script lang="ts">
2
- import { MarkdownViewer, notifications } from "@budibase/bbui"
3
- import type { UserMessage, AgentChat } from "@budibase/types"
2
+ import { Helpers, MarkdownViewer, notifications } from "@budibase/bbui"
3
+ import type { AgentChat } from "@budibase/types"
4
4
  import BBAI from "../../icons/BBAI.svelte"
5
5
  import { tick } from "svelte"
6
6
  import { onDestroy } from "svelte"
7
7
  import { onMount } from "svelte"
8
- import { createAPIClient } from "@budibase/frontend-core"
9
8
  import { createEventDispatcher } from "svelte"
9
+ import { createAPIClient } from "@budibase/frontend-core"
10
+ import type { UIMessage, UIMessageChunk } from "ai"
11
+ import { v4 as uuidv4 } from "uuid"
10
12
 
11
13
  export let API = createAPIClient()
12
14
 
@@ -44,7 +46,11 @@
44
46
  chat = { title: "", messages: [], agentId: "" }
45
47
  }
46
48
 
47
- const userMessage: UserMessage = { role: "user", content: inputValue }
49
+ const userMessage: UIMessage = {
50
+ id: uuidv4(),
51
+ role: "user",
52
+ parts: [{ type: "text", text: inputValue }],
53
+ }
48
54
 
49
55
  const updatedChat = {
50
56
  ...chat,
@@ -60,93 +66,60 @@
60
66
  inputValue = ""
61
67
  loading = true
62
68
 
63
- let streamingContent = ""
64
- let isToolCall = false
65
- let toolCallInfo: string = ""
69
+ let streamingText = ""
70
+ let assistantIndex = -1
71
+ let streamCompleted = false
66
72
 
67
73
  try {
68
74
  await API.agentChatStream(
69
75
  updatedChat,
70
76
  workspaceId,
71
- chunk => {
72
- if (chunk.type === "content") {
73
- // Accumulate streaming content
74
- streamingContent += chunk.content || ""
75
-
76
- // Update chat with partial content
77
- const updatedMessages = [...updatedChat.messages]
78
-
79
- // Find or create assistant message
80
- const lastMessage = updatedMessages[updatedMessages.length - 1]
81
- if (lastMessage?.role === "assistant") {
82
- lastMessage.content =
83
- streamingContent + (isToolCall ? toolCallInfo : "")
84
- } else {
85
- updatedMessages.push({
86
- role: "assistant",
87
- content: streamingContent + (isToolCall ? toolCallInfo : ""),
88
- })
77
+ (chunk: UIMessageChunk) => {
78
+ if (chunk.type === "text-start") {
79
+ const assistantMessage: UIMessage = {
80
+ id: Helpers.uuid(),
81
+ role: "assistant",
82
+ parts: [{ type: "text", text: "", state: "streaming" }],
89
83
  }
90
-
91
84
  chat = {
92
85
  ...chat,
93
- messages: updatedMessages,
86
+ messages: [...updatedChat.messages, assistantMessage],
94
87
  }
95
-
96
- // Auto-scroll as content streams
88
+ assistantIndex = chat.messages.length - 1
97
89
  scrollToBottom()
98
- } else if (chunk.type === "tool_call_start") {
99
- isToolCall = true
100
- toolCallInfo = `\n\n**🔧 Executing Tool:** ${chunk.toolCall?.name}\n**Parameters:**\n\`\`\`json\n${chunk.toolCall?.arguments}\n\`\`\`\n`
101
-
102
- const updatedMessages = [...updatedChat.messages]
103
- const lastMessage = updatedMessages[updatedMessages.length - 1]
104
- if (lastMessage?.role === "assistant") {
105
- lastMessage.content = streamingContent + toolCallInfo
106
- } else {
107
- updatedMessages.push({
108
- role: "assistant",
109
- content: streamingContent + toolCallInfo,
110
- })
111
- }
112
-
113
- chat = {
114
- ...chat,
115
- messages: updatedMessages,
116
- }
117
-
118
- scrollToBottom()
119
- } else if (chunk.type === "tool_call_result") {
120
- const resultInfo = chunk.toolResult?.error
121
- ? `\n**❌ Tool Error:** ${chunk.toolResult.error}`
122
- : `\n**✅ Tool Result:** Complete`
123
-
124
- toolCallInfo += resultInfo
125
-
126
- const updatedMessages = [...updatedChat.messages]
127
- const lastMessage = updatedMessages[updatedMessages.length - 1]
128
- if (lastMessage?.role === "assistant") {
129
- lastMessage.content = streamingContent + toolCallInfo
130
- }
131
-
132
- chat = {
133
- ...chat,
134
- messages: updatedMessages,
90
+ } else if (chunk.type === "text-delta") {
91
+ streamingText += chunk.delta || ""
92
+ if (assistantIndex >= 0) {
93
+ const messages = [...chat.messages]
94
+ const assistant = { ...messages[assistantIndex] }
95
+ const parts = [...assistant.parts]
96
+ const textPart = parts.find(p => p.type === "text")
97
+ if (textPart) {
98
+ textPart.text = streamingText
99
+ }
100
+ assistant.parts = parts
101
+ messages[assistantIndex] = assistant
102
+ chat = { ...chat, messages }
135
103
  }
136
-
137
104
  scrollToBottom()
138
- } else if (chunk.type === "chat_saved") {
139
- if (chunk.chat) {
140
- chat = chunk.chat
141
- if (chunk.chat._id) {
142
- dispatch("chatSaved", { chatId: chunk.chat._id })
105
+ } else if (chunk.type === "text-end") {
106
+ loading = false
107
+ streamCompleted = true
108
+ if (assistantIndex >= 0) {
109
+ const messages = [...chat.messages]
110
+ const assistant = { ...messages[assistantIndex] }
111
+ const parts = [...assistant.parts]
112
+ const textPart = parts.find(p => p.type === "text")
113
+ if (textPart) {
114
+ textPart.state = "done"
143
115
  }
116
+ assistant.parts = parts
117
+ messages[assistantIndex] = assistant
118
+ chat = { ...chat, messages }
144
119
  }
145
- } else if (chunk.type === "done") {
146
- loading = false
147
120
  scrollToBottom()
148
121
  } else if (chunk.type === "error") {
149
- notifications.error(chunk.content || "An error occurred")
122
+ notifications.error(chunk.errorText || "An error occurred")
150
123
  loading = false
151
124
  }
152
125
  },
@@ -156,6 +129,13 @@
156
129
  loading = false
157
130
  }
158
131
  )
132
+
133
+ if (streamCompleted && chat) {
134
+ setTimeout(() => {
135
+ const chatId = chat._id || ""
136
+ dispatch("chatSaved", { chatId })
137
+ }, 500)
138
+ }
159
139
  } catch (err: any) {
160
140
  console.error(err)
161
141
  notifications.error(err.message)
@@ -170,8 +150,6 @@
170
150
  }
171
151
 
172
152
  onMount(async () => {
173
- chat = { title: "", messages: [], agentId: chat.agentId }
174
-
175
153
  // Ensure we always autoscroll to reveal new messages
176
154
  observer = new MutationObserver(async () => {
177
155
  await tick()
@@ -205,22 +183,57 @@
205
183
  {#if message.role === "user"}
206
184
  <div class="message user">
207
185
  <MarkdownViewer
208
- value={typeof message.content === "string"
209
- ? message.content
210
- : message.content.length > 0
211
- ? message.content
212
- .map(part =>
213
- part.type === "text"
214
- ? part.text
215
- : `${part.type} content not supported`
216
- )
217
- .join("")
218
- : "[Empty message]"}
186
+ value={message.parts && message.parts.length > 0
187
+ ? message.parts
188
+ .filter(part => part.type === "text")
189
+ .map(part => (part.type === "text" ? part.text : ""))
190
+ .join("")
191
+ : "[Empty message]"}
219
192
  />
220
193
  </div>
221
- {:else if message.role === "assistant" && message.content}
194
+ {:else if message.role === "assistant"}
222
195
  <div class="message assistant">
223
- <MarkdownViewer value={message.content} />
196
+ {#each message.parts || [] as part}
197
+ {#if part.type === "text"}
198
+ <MarkdownViewer value={part.text || ""} />
199
+ {:else if part.type === "reasoning"}
200
+ <div class="reasoning-part">
201
+ <div class="reasoning-label">Reasoning</div>
202
+ <div class="reasoning-content">{part.text || ""}</div>
203
+ </div>
204
+ {:else if part.type?.startsWith("tool-") || part.type === "dynamic-tool"}
205
+ {@const toolPart = part}
206
+ <div class="tool-part">
207
+ <div class="tool-header">
208
+ <span class="tool-icon">🔧</span>
209
+ <span class="tool-name"
210
+ >{("toolName" in toolPart && toolPart.toolName) ||
211
+ "Tool"}</span
212
+ >
213
+ {#if "state" in toolPart}
214
+ {#if toolPart.state === "output-available"}
215
+ <span class="tool-status success">✓</span>
216
+ {:else if toolPart.state === "output-error"}
217
+ <span class="tool-status error">✗</span>
218
+ {:else if toolPart.state === "input-streaming"}
219
+ <span class="tool-status pending">...</span>
220
+ {:else if toolPart.state === "input-available"}
221
+ <span class="tool-status pending">...</span>
222
+ {/if}
223
+ {/if}
224
+ </div>
225
+ {#if "state" in toolPart && toolPart.state === "output-available" && "output" in toolPart && toolPart.output}
226
+ <div class="tool-output">
227
+ <div class="tool-output-label">Output:</div>
228
+ <pre class="tool-output-content">{typeof toolPart.output ===
229
+ "string"
230
+ ? toolPart.output
231
+ : JSON.stringify(toolPart.output, null, 2)}</pre>
232
+ </div>
233
+ {/if}
234
+ </div>
235
+ {/if}
236
+ {/each}
224
237
  </div>
225
238
  {/if}
226
239
  {/each}
@@ -334,4 +347,86 @@
334
347
  border: 1px solid var(--grey-3);
335
348
  border-radius: 4px;
336
349
  }
350
+
351
+ /* Tool parts styling */
352
+ .tool-part {
353
+ margin: var(--spacing-m) 0;
354
+ padding: var(--spacing-m);
355
+ background-color: var(--grey-2);
356
+ border: 1px solid var(--grey-3);
357
+ border-radius: 8px;
358
+ }
359
+
360
+ .tool-header {
361
+ display: flex;
362
+ align-items: center;
363
+ gap: var(--spacing-s);
364
+ margin-bottom: var(--spacing-s);
365
+ font-weight: 600;
366
+ font-size: 14px;
367
+ }
368
+
369
+ .tool-icon {
370
+ font-size: 16px;
371
+ }
372
+
373
+ .tool-name {
374
+ color: var(--spectrum-global-color-gray-900);
375
+ font-family: var(--font-mono), monospace;
376
+ }
377
+
378
+ .tool-status {
379
+ margin-left: auto;
380
+ font-size: 12px;
381
+ }
382
+
383
+ .tool-status.success {
384
+ color: var(--spectrum-global-color-green-600);
385
+ }
386
+
387
+ .tool-status.error {
388
+ color: var(--spectrum-global-color-red-600);
389
+ }
390
+
391
+ .tool-status.pending {
392
+ color: var(--spectrum-global-color-gray-600);
393
+ }
394
+
395
+ .tool-output,
396
+ .tool-output-label,
397
+ .tool-output-content {
398
+ background-color: var(--background);
399
+ border: 1px solid var(--grey-3);
400
+ border-radius: 4px;
401
+ padding: var(--spacing-s);
402
+ font-size: 12px;
403
+ font-family: var(--font-mono), monospace;
404
+ overflow-x: auto;
405
+ white-space: pre-wrap;
406
+ word-break: break-word;
407
+ }
408
+
409
+ /* Reasoning parts styling */
410
+ .reasoning-part {
411
+ margin: var(--spacing-m) 0;
412
+ padding: var(--spacing-m);
413
+ background-color: var(--grey-1);
414
+ border-left: 3px solid var(--spectrum-global-color-static-seafoam-700);
415
+ border-radius: 4px;
416
+ }
417
+
418
+ .reasoning-label {
419
+ font-size: 12px;
420
+ font-weight: 600;
421
+ color: var(--spectrum-global-color-static-seafoam-700);
422
+ margin-bottom: 4px;
423
+ text-transform: uppercase;
424
+ letter-spacing: 0.5px;
425
+ }
426
+
427
+ .reasoning-content {
428
+ font-size: 13px;
429
+ color: var(--spectrum-global-color-gray-800);
430
+ font-style: italic;
431
+ }
337
432
  </style>