Skip to content

Commit 32a3361

Browse files
authored
fix(ui): message sources (#998)
1 parent c1be543 commit 32a3361

File tree

5 files changed

+85
-71
lines changed

5 files changed

+85
-71
lines changed

apps/beeai-ui/src/api/a2a/client.ts

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,8 @@ import { A2AClient } from '@a2a-js/sdk/client';
88
import { Subject } from 'rxjs';
99
import { match } from 'ts-pattern';
1010

11-
import type { FileEntity } from '#modules/files/types.ts';
12-
import type { UIMessagePart } from '#modules/messages/types.ts';
13-
import type { ContextId } from '#modules/tasks/api/types.ts';
11+
import type { UIMessagePart, UIUserMessage } from '#modules/messages/types.ts';
12+
import type { ContextId, TaskId } from '#modules/tasks/api/types.ts';
1413
import { getBaseUrl } from '#utils/api/getBaseUrl.ts';
1514
import { isNotNull } from '#utils/helpers.ts';
1615

@@ -56,26 +55,24 @@ export const buildA2AClient = (providerId: string) => {
5655
const agentUrl = `${getBaseUrl()}/api/v1/a2a/${providerId}`;
5756
const client = new A2AClient(agentUrl);
5857

59-
const chat = ({ text, files, contextId }: { text: string; files: FileEntity[]; contextId: ContextId }) => {
60-
const messageSubject = new Subject<UIMessagePart[]>();
58+
const chat = ({ message, contextId }: { message: UIUserMessage; contextId: ContextId }) => {
59+
const messageSubject = new Subject<{ parts: UIMessagePart[]; taskId: TaskId }>();
6160
let taskId: string | null = null;
6261

6362
const iterateOverStream = async () => {
64-
const stream = client.sendMessageStream({ message: createUserMessage({ text, files, contextId }) });
63+
const stream = client.sendMessageStream({ message: createUserMessage({ message, contextId }) });
6564

6665
for await (const event of stream) {
6766
match(event)
68-
.with(
69-
{
70-
kind: 'task',
71-
},
72-
(task) => {
73-
taskId = task.id;
74-
},
75-
)
67+
.with({ kind: 'task' }, (task) => {
68+
taskId = task.id;
69+
})
7670
.with({ kind: 'status-update' }, (event) => {
71+
taskId = event.taskId;
72+
7773
const messageParts = handleStatusUpdate(event);
78-
messageSubject.next(messageParts);
74+
75+
messageSubject.next({ parts: messageParts, taskId });
7976
});
8077
}
8178
messageSubject.complete();

apps/beeai-ui/src/api/a2a/part-processors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export function processMessageMetadata(message: Message): UIMessagePart[] {
2525
if (trajectory) {
2626
return [createTrajectoryPart(trajectory)];
2727
} else if (citation) {
28-
const sourcePart = createSourcePart(citation, message.messageId);
28+
const sourcePart = createSourcePart(citation, message.taskId);
2929

3030
if (sourcePart) {
3131
return [sourcePart];

apps/beeai-ui/src/api/a2a/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
*/
55

66
import type { UIMessagePart } from '#modules/messages/types.ts';
7+
import type { TaskId } from '#modules/tasks/api/types.ts';
78

89
export interface ChatRun {
910
done: Promise<void>;
10-
subscribe: (fn: (parts: UIMessagePart[]) => void) => () => void;
11+
subscribe: (fn: (data: { parts: UIMessagePart[]; taskId: TaskId }) => void) => () => void;
1112
cancel: () => Promise<void>;
1213
}

apps/beeai-ui/src/api/a2a/utils.ts

Lines changed: 44 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,20 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6-
import type { FilePart, FileWithUri, Message } from '@a2a-js/sdk';
6+
import type { FilePart, FileWithUri, Message, Part, TextPart } from '@a2a-js/sdk';
77
import { v4 as uuid } from 'uuid';
88

9-
import type { FileEntity } from '#modules/files/types.ts';
109
import { getFileContentUrl } from '#modules/files/utils.ts';
11-
import type { UISourcePart, UITextPart, UITrajectoryPart } from '#modules/messages/types.ts';
10+
import type {
11+
UIMessagePart,
12+
UISourcePart,
13+
UITextPart,
14+
UITrajectoryPart,
15+
UIUserMessage,
16+
} from '#modules/messages/types.ts';
1217
import { UIMessagePartKind } from '#modules/messages/types.ts';
1318
import type { ContextId, TaskId } from '#modules/tasks/api/types.ts';
19+
import { isNotNull } from '#utils/helpers.ts';
1420

1521
import type { CitationMetadata } from './extensions/citation';
1622
import { citationExtension } from './extensions/citation';
@@ -31,47 +37,51 @@ export function extractTextFromMessage(message: Message | undefined) {
3137
return text;
3238
}
3339

34-
export function convertFileToFilePart(file: FileEntity): FilePart {
35-
const { originalFile, uploadFile } = file;
36-
37-
if (!uploadFile) {
38-
throw new Error('File upload file is not present');
39-
}
40-
41-
return {
42-
kind: 'file',
43-
file: {
44-
uri: getFileContentUrl({ id: uploadFile.id, addBase: true }),
45-
name: uploadFile.filename,
46-
mimeType: originalFile.type,
47-
},
48-
};
40+
export function convertMessageParts(uiParts: UIMessagePart[]): Part[] {
41+
const parts: Part[] = uiParts
42+
.map((part) => {
43+
switch (part.kind) {
44+
case UIMessagePartKind.Text:
45+
const { text } = part;
46+
47+
return {
48+
kind: 'text',
49+
text,
50+
} as TextPart;
51+
case UIMessagePartKind.File:
52+
const { id, filename, type } = part;
53+
54+
return {
55+
kind: 'file',
56+
file: {
57+
uri: getFileContentUrl({ id, addBase: true }),
58+
name: filename,
59+
mimeType: type,
60+
},
61+
} as FilePart;
62+
}
63+
})
64+
.filter(isNotNull);
65+
66+
return parts;
4967
}
5068

5169
export function createUserMessage({
52-
text,
53-
files,
54-
taskId,
70+
message,
5571
contextId,
72+
taskId,
5673
}: {
57-
text: string;
58-
files: FileEntity[];
74+
message: UIUserMessage;
5975
contextId: ContextId;
6076
taskId?: TaskId;
6177
}): Message {
6278
return {
6379
kind: 'message',
64-
messageId: uuid(),
80+
role: 'user',
81+
messageId: message.id,
6582
contextId,
6683
taskId,
67-
parts: [
68-
{
69-
kind: 'text',
70-
text,
71-
},
72-
...files.map(convertFileToFilePart),
73-
],
74-
role: 'user',
84+
parts: convertMessageParts(message.parts),
7585
};
7686
}
7787

@@ -91,10 +101,10 @@ export function getFileUri(file: FilePart['file']): string {
91101
return `data:${mimeType};base64,${bytes}`;
92102
}
93103

94-
export function createSourcePart(metadata: CitationMetadata, messageId: string): UISourcePart | null {
104+
export function createSourcePart(metadata: CitationMetadata, messageId: string | undefined): UISourcePart | null {
95105
const { url, start_index, end_index, title, description } = metadata;
96106

97-
if (!url) {
107+
if (!url || !messageId) {
98108
return null;
99109
}
100110

apps/beeai-ui/src/modules/runs/contexts/agent-run/AgentRunProvider.tsx

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { v4 as uuid } from 'uuid';
1111

1212
import { buildA2AClient } from '#api/a2a/client.ts';
1313
import type { ChatRun } from '#api/a2a/types.ts';
14+
import { createTextPart } from '#api/a2a/utils.ts';
1415
import { getErrorCode } from '#api/utils.ts';
1516
import { useHandleError } from '#hooks/useHandleError.ts';
1617
import { useImmerWithGetter } from '#hooks/useImmerWithGetter.ts';
@@ -123,31 +124,36 @@ function AgentRunProvider({ agent, children }: PropsWithChildren<Props>) {
123124
setIsPending(true);
124125
setStats({ startTime: Date.now() });
125126

126-
try {
127-
setMessages((messages) => {
128-
const userMessage: UIUserMessage = {
129-
id: uuid(),
130-
role: Role.User,
131-
parts: [{ kind: UIMessagePartKind.Text, id: uuid(), text: input }, ...convertFilesToUIFileParts(files)],
132-
};
133-
const agentMessage: UIAgentMessage = {
134-
id: uuid(),
135-
role: Role.Agent,
136-
parts: [],
137-
status: UIMessageStatus.InProgress,
138-
};
139-
140-
messages.push(userMessage, agentMessage);
141-
});
127+
const userMessage: UIUserMessage = {
128+
id: uuid(),
129+
role: Role.User,
130+
parts: [createTextPart(input), ...convertFilesToUIFileParts(files)],
131+
};
132+
const agentMessage: UIAgentMessage = {
133+
id: uuid(),
134+
role: Role.Agent,
135+
parts: [],
136+
status: UIMessageStatus.InProgress,
137+
};
138+
139+
setMessages((messages) => {
140+
messages.push(userMessage, agentMessage);
141+
});
142142

143+
clearFiles();
144+
145+
try {
143146
const run = a2aAgentClient.chat({
144-
text: input,
145-
files,
147+
message: userMessage,
146148
contextId: conversationId,
147149
});
148150
pendingRun.current = run;
149151

150-
pendingSubscription.current = run.subscribe((parts) => {
152+
pendingSubscription.current = run.subscribe(({ parts, taskId }) => {
153+
updateLastAgentMessage((message) => {
154+
message.id = taskId;
155+
});
156+
151157
parts.forEach((part) => {
152158
updateLastAgentMessage((message) => {
153159
match(part)
@@ -188,7 +194,7 @@ function AgentRunProvider({ agent, children }: PropsWithChildren<Props>) {
188194
pendingSubscription.current = undefined;
189195
}
190196
},
191-
[a2aAgentClient, files, conversationId, handleError, updateLastAgentMessage, setMessages],
197+
[a2aAgentClient, files, conversationId, handleError, updateLastAgentMessage, setMessages, clearFiles],
192198
);
193199

194200
const sources = useMemo(() => getMessageSourcesMap(messages), [messages]);

0 commit comments

Comments
 (0)