Spaces:
Running
Running
MingruiZhang
commited on
feat: conversation restructure, assistant message should be just code (#89)
Browse files![image](https://github.com/landing-ai/vision-agent-ui/assets/5669963/94e99862-a986-40d5-abb5-aab367618e67)
![image](https://github.com/landing-ai/vision-agent-ui/assets/5669963/a185e979-b0fa-4763-a3ac-d3da6a20230c)
Also fix backwards compatibly from
https://github.com/landing-ai/vision-agent-ui/pull/86
- app/api/vision-agent/route.ts +14 -43
- app/page.tsx +1 -1
- components/CodeResultDisplay.tsx +11 -6
- components/chat/ChatList.tsx +7 -3
- components/chat/ChatMessage.tsx +13 -16
- components/chat/TopPrompt.tsx +4 -9
- components/ui/CodeBlock.tsx +1 -1
- components/ui/Icons.tsx +1 -1
- lib/db/prisma.ts +23 -12
- lib/hooks/useVisionAgent.ts +4 -6
- lib/types.ts +0 -18
- lib/utils/content.ts +9 -27
- lib/utils/message.ts +5 -5
app/api/vision-agent/route.ts
CHANGED
@@ -1,11 +1,9 @@
|
|
1 |
import { JSONValue, StreamingTextResponse, experimental_StreamData } from 'ai';
|
2 |
|
3 |
// import { auth } from '@/auth';
|
4 |
-
import { MessageUI,
|
5 |
|
6 |
import { logger, withLogging } from '@/lib/logger';
|
7 |
-
import { CLEANED_SEPARATOR } from '@/lib/constants';
|
8 |
-
import { cleanAnswerMessage, cleanInputMessage } from '@/lib/utils/content';
|
9 |
import { getPresignedUrl } from '@/lib/aws';
|
10 |
|
11 |
// export const runtime = 'edge';
|
@@ -68,7 +66,7 @@ const modifyCodePayload = async (
|
|
68 |
) {
|
69 |
return msg;
|
70 |
}
|
71 |
-
const result = JSON.parse(msg.payload.result) as
|
72 |
if (msg.type === 'code') {
|
73 |
if (result && result.results) {
|
74 |
msg.payload.result = {
|
@@ -106,45 +104,18 @@ export const POST = withLogging(
|
|
106 |
async (
|
107 |
session,
|
108 |
json: {
|
109 |
-
|
110 |
id: string;
|
111 |
mediaUrl: string;
|
112 |
},
|
113 |
request,
|
114 |
) => {
|
115 |
-
const {
|
|
|
116 |
const user = session?.user?.email ?? 'anonymous';
|
117 |
|
118 |
-
// const session = await auth();
|
119 |
-
// if (!session?.user?.email) {
|
120 |
-
// return new Response('Unauthorized', {
|
121 |
-
// status: 401,
|
122 |
-
// });
|
123 |
-
// }
|
124 |
-
|
125 |
const formData = new FormData();
|
126 |
-
formData.append(
|
127 |
-
'input',
|
128 |
-
JSON.stringify(
|
129 |
-
messages.map(message => {
|
130 |
-
if (message.role !== 'assistant') {
|
131 |
-
return {
|
132 |
-
...message,
|
133 |
-
content: cleanInputMessage(message.content),
|
134 |
-
};
|
135 |
-
} else {
|
136 |
-
const splitedContent = message.content.split(CLEANED_SEPARATOR);
|
137 |
-
return {
|
138 |
-
...message,
|
139 |
-
content:
|
140 |
-
splitedContent.length > 1
|
141 |
-
? cleanAnswerMessage(splitedContent[1])
|
142 |
-
: message.content,
|
143 |
-
};
|
144 |
-
}
|
145 |
-
}),
|
146 |
-
),
|
147 |
-
);
|
148 |
formData.append('image', mediaUrl);
|
149 |
|
150 |
const agentHost = process.env.LND_TIER
|
@@ -213,14 +184,14 @@ export const POST = withLogging(
|
|
213 |
.filter(line => line.trim().length > 0);
|
214 |
if (lines.length === 0) {
|
215 |
if (Date.now() - time > TIMEOUT_MILI_SECONDS) {
|
216 |
-
logger.info(
|
217 |
-
|
218 |
-
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
);
|
224 |
controller.enqueue(
|
225 |
encoder.encode(JSON.stringify(FINAL_TIMEOUT_ERROR) + '\n'),
|
226 |
);
|
|
|
1 |
import { JSONValue, StreamingTextResponse, experimental_StreamData } from 'ai';
|
2 |
|
3 |
// import { auth } from '@/auth';
|
4 |
+
import { MessageUI, SignedPayload } from '@/lib/types';
|
5 |
|
6 |
import { logger, withLogging } from '@/lib/logger';
|
|
|
|
|
7 |
import { getPresignedUrl } from '@/lib/aws';
|
8 |
|
9 |
// export const runtime = 'edge';
|
|
|
66 |
) {
|
67 |
return msg;
|
68 |
}
|
69 |
+
const result = JSON.parse(msg.payload.result) as PrismaJson.StructuredResult;
|
70 |
if (msg.type === 'code') {
|
71 |
if (result && result.results) {
|
72 |
msg.payload.result = {
|
|
|
104 |
async (
|
105 |
session,
|
106 |
json: {
|
107 |
+
apiMessages: string;
|
108 |
id: string;
|
109 |
mediaUrl: string;
|
110 |
},
|
111 |
request,
|
112 |
) => {
|
113 |
+
const { apiMessages, mediaUrl } = json;
|
114 |
+
const messages: MessageUI[] = JSON.parse(apiMessages);
|
115 |
const user = session?.user?.email ?? 'anonymous';
|
116 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
117 |
const formData = new FormData();
|
118 |
+
formData.append('input', JSON.stringify(messages));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
119 |
formData.append('image', mediaUrl);
|
120 |
|
121 |
const agentHost = process.env.LND_TIER
|
|
|
184 |
.filter(line => line.trim().length > 0);
|
185 |
if (lines.length === 0) {
|
186 |
if (Date.now() - time > TIMEOUT_MILI_SECONDS) {
|
187 |
+
// logger.info(
|
188 |
+
// session,
|
189 |
+
// {
|
190 |
+
// message: 'Agent timed out',
|
191 |
+
// },
|
192 |
+
// request,
|
193 |
+
// '__Agent_timeout__',
|
194 |
+
// );
|
195 |
controller.enqueue(
|
196 |
encoder.encode(JSON.stringify(FINAL_TIMEOUT_ERROR) + '\n'),
|
197 |
);
|
app/page.tsx
CHANGED
@@ -15,7 +15,7 @@ const EXAMPLES = [
|
|
15 |
mediaUrl:
|
16 |
'https://vision-agent-dev.s3.us-east-2.amazonaws.com/examples/flower.png',
|
17 |
prompt:
|
18 |
-
'
|
19 |
},
|
20 |
{
|
21 |
title: 'Detecting sharks in video',
|
|
|
15 |
mediaUrl:
|
16 |
'https://vision-agent-dev.s3.us-east-2.amazonaws.com/examples/flower.png',
|
17 |
prompt:
|
18 |
+
'Detect flowers in this image, draw boxes and output the image, also return total number of flowers',
|
19 |
},
|
20 |
{
|
21 |
title: 'Detecting sharks in video',
|
components/CodeResultDisplay.tsx
CHANGED
@@ -1,6 +1,5 @@
|
|
1 |
import React from 'react';
|
2 |
|
3 |
-
import { ChunkBody, CodeResult, formatStreamLogs } from '@/lib/utils/content';
|
4 |
import { CodeBlock } from './ui/CodeBlock';
|
5 |
import {
|
6 |
Dialog,
|
@@ -12,7 +11,6 @@ import {
|
|
12 |
import { Button } from './ui/Button';
|
13 |
import { IconLog, IconTerminalWindow } from './ui/Icons';
|
14 |
import { Separator } from './ui/Separator';
|
15 |
-
import { ResultPayload } from '@/lib/types';
|
16 |
import Img from './ui/Img';
|
17 |
import {
|
18 |
Carousel,
|
@@ -25,13 +23,18 @@ import {
|
|
25 |
export interface CodeResultDisplayProps {}
|
26 |
|
27 |
const CodeResultDisplay: React.FC<{
|
28 |
-
codeResult:
|
29 |
}> = ({ codeResult }) => {
|
30 |
const { code, test, result } = codeResult;
|
31 |
const getDetail = () => {
|
32 |
if (!result) return {};
|
33 |
try {
|
34 |
-
|
|
|
|
|
|
|
|
|
|
|
35 |
return {
|
36 |
results: detail.results,
|
37 |
stderr: detail.logs.stderr,
|
@@ -108,7 +111,9 @@ const CodeResultDisplay: React.FC<{
|
|
108 |
<p>image output</p>
|
109 |
<Dialog>
|
110 |
<DialogTrigger asChild>
|
111 |
-
<Button variant="
|
|
|
|
|
112 |
</DialogTrigger>
|
113 |
<DialogContent className="max-w-5xl flex justify-center items-center">
|
114 |
<Carousel className="w-3/4">
|
@@ -138,7 +143,7 @@ const CodeResultDisplay: React.FC<{
|
|
138 |
</div>
|
139 |
)}
|
140 |
{!!videoResults.length && (
|
141 |
-
<div className="p-4 text-xs lowercase bg-zinc-900 space-y-4">
|
142 |
<p>video output</p>
|
143 |
<div className="flex flex-row space-x-4 overflow-auto">
|
144 |
{videoResults.map((mp4, index) => (
|
|
|
1 |
import React from 'react';
|
2 |
|
|
|
3 |
import { CodeBlock } from './ui/CodeBlock';
|
4 |
import {
|
5 |
Dialog,
|
|
|
11 |
import { Button } from './ui/Button';
|
12 |
import { IconLog, IconTerminalWindow } from './ui/Icons';
|
13 |
import { Separator } from './ui/Separator';
|
|
|
14 |
import Img from './ui/Img';
|
15 |
import {
|
16 |
Carousel,
|
|
|
23 |
export interface CodeResultDisplayProps {}
|
24 |
|
25 |
const CodeResultDisplay: React.FC<{
|
26 |
+
codeResult: PrismaJson.FinalChatResult['payload'];
|
27 |
}> = ({ codeResult }) => {
|
28 |
const { code, test, result } = codeResult;
|
29 |
const getDetail = () => {
|
30 |
if (!result) return {};
|
31 |
try {
|
32 |
+
// IMPORTANT: This is for backwards compatibility with old chat that save result as JSON string
|
33 |
+
// updated in https://github.com/landing-ai/vision-agent-ui/pull/86
|
34 |
+
const detail =
|
35 |
+
typeof result === 'object'
|
36 |
+
? result
|
37 |
+
: (JSON.parse(result) as PrismaJson.StructuredResult);
|
38 |
return {
|
39 |
results: detail.results,
|
40 |
stderr: detail.logs.stderr,
|
|
|
111 |
<p>image output</p>
|
112 |
<Dialog>
|
113 |
<DialogTrigger asChild>
|
114 |
+
<Button variant="outline" size="sm">
|
115 |
+
View all
|
116 |
+
</Button>
|
117 |
</DialogTrigger>
|
118 |
<DialogContent className="max-w-5xl flex justify-center items-center">
|
119 |
<Carousel className="w-3/4">
|
|
|
143 |
</div>
|
144 |
)}
|
145 |
{!!videoResults.length && (
|
146 |
+
<div className="p-4 text-xs lowercase bg-zinc-900 space-y-4 border-t border-muted">
|
147 |
<p>video output</p>
|
148 |
<div className="flex flex-row space-x-4 overflow-auto">
|
149 |
{videoResults.map((mp4, index) => (
|
components/chat/ChatList.tsx
CHANGED
@@ -30,7 +30,11 @@ const ChatList: React.FC<ChatListProps> = ({ chat, userId }) => {
|
|
30 |
// Only login and chat owner can compose
|
31 |
const canCompose = !chatUserId || userId === chatUserId;
|
32 |
|
33 |
-
const
|
|
|
|
|
|
|
|
|
34 |
const lastDbMessage = dbMessages[dbMessages.length - 1];
|
35 |
const setMessageId = useSetAtom(selectedMessageId);
|
36 |
|
@@ -67,8 +71,8 @@ const ChatList: React.FC<ChatListProps> = ({ chat, userId }) => {
|
|
67 |
message={message}
|
68 |
loading={isLastMessage && isLoading}
|
69 |
wipAssistantMessage={
|
70 |
-
|
71 |
-
?
|
72 |
: undefined
|
73 |
}
|
74 |
/>
|
|
|
30 |
// Only login and chat owner can compose
|
31 |
const canCompose = !chatUserId || userId === chatUserId;
|
32 |
|
33 |
+
const lastAssistantMessage = messages.length
|
34 |
+
? messages[messages.length - 1]?.role === 'assistant'
|
35 |
+
? messages[messages.length - 1]?.content
|
36 |
+
: undefined
|
37 |
+
: undefined;
|
38 |
const lastDbMessage = dbMessages[dbMessages.length - 1];
|
39 |
const setMessageId = useSetAtom(selectedMessageId);
|
40 |
|
|
|
71 |
message={message}
|
72 |
loading={isLastMessage && isLoading}
|
73 |
wipAssistantMessage={
|
74 |
+
lastAssistantMessage && isLastMessage
|
75 |
+
? lastAssistantMessage
|
76 |
: undefined
|
77 |
}
|
78 |
/>
|
components/chat/ChatMessage.tsx
CHANGED
@@ -11,11 +11,7 @@ import {
|
|
11 |
IconGlowingDot,
|
12 |
} from '@/components/ui/Icons';
|
13 |
import { MessageUI } from '@/lib/types';
|
14 |
-
import {
|
15 |
-
WIPChunkBodyGroup,
|
16 |
-
CodeResult,
|
17 |
-
formatStreamLogs,
|
18 |
-
} from '@/lib/utils/content';
|
19 |
import {
|
20 |
Table,
|
21 |
TableBody,
|
@@ -37,7 +33,7 @@ import { cn } from '@/lib/utils';
|
|
37 |
export interface ChatMessageProps {
|
38 |
message: Message;
|
39 |
loading?: boolean;
|
40 |
-
wipAssistantMessage?:
|
41 |
}
|
42 |
|
43 |
export const ChatMessage: React.FC<ChatMessageProps> = ({
|
@@ -48,8 +44,8 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
|
|
48 |
const [messageId, setMessageId] = useAtom(selectedMessageId);
|
49 |
const { id, mediaUrl, prompt, response, result } = message;
|
50 |
const [formattedSections, finalResult, finalError] = useMemo(
|
51 |
-
() => formatStreamLogs(response ?? wipAssistantMessage
|
52 |
-
[response, wipAssistantMessage
|
53 |
);
|
54 |
// prioritize the result from the message over the WIP message
|
55 |
const codeResult = result?.payload ?? finalResult;
|
@@ -100,10 +96,10 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
|
|
100 |
<div className="flex-1 px-1 space-y-4 ml-4 overflow-hidden">
|
101 |
<Table className="w-[400px]">
|
102 |
<TableBody>
|
103 |
-
{formattedSections.map(section => (
|
104 |
<TableRow
|
105 |
className="border-primary/50 h-[56px]"
|
106 |
-
key={
|
107 |
>
|
108 |
<TableCell className="text-center text-webkit-center">
|
109 |
{ChunkStatusToIconDict[section.status]}
|
@@ -131,13 +127,11 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
|
|
131 |
)}
|
132 |
{!codeResult && finalError && (
|
133 |
<>
|
134 |
-
<p>❌
|
135 |
<div>
|
136 |
<CodeBlock
|
137 |
language="error"
|
138 |
value={
|
139 |
-
finalError.name +
|
140 |
-
'\n' +
|
141 |
finalError.value +
|
142 |
'\n' +
|
143 |
finalError.traceback_raw.join('\n')
|
@@ -181,7 +175,7 @@ const ChunkTypeToText: React.FC<{
|
|
181 |
const isExecuting = !['completed', 'failed'].includes(status);
|
182 |
|
183 |
useEffect(() => {
|
184 |
-
if (isExecuting && timestamp &&
|
185 |
const timerId = setInterval(() => {
|
186 |
setMSeconds(Date.now() - Date.parse(timestamp));
|
187 |
}, 200);
|
@@ -272,7 +266,7 @@ const ChunkPayloadAction: React.FC<{
|
|
272 |
</DialogContent>
|
273 |
</Dialog>
|
274 |
);
|
275 |
-
} else {
|
276 |
return (
|
277 |
<Dialog>
|
278 |
<DialogTrigger asChild>
|
@@ -281,11 +275,14 @@ const ChunkPayloadAction: React.FC<{
|
|
281 |
</Button>
|
282 |
</DialogTrigger>
|
283 |
<DialogContent className="max-w-5xl">
|
284 |
-
<CodeResultDisplay
|
|
|
|
|
285 |
</DialogContent>
|
286 |
</Dialog>
|
287 |
);
|
288 |
}
|
|
|
289 |
};
|
290 |
|
291 |
export default ChatMessage;
|
|
|
11 |
IconGlowingDot,
|
12 |
} from '@/components/ui/Icons';
|
13 |
import { MessageUI } from '@/lib/types';
|
14 |
+
import { WIPChunkBodyGroup, formatStreamLogs } from '@/lib/utils/content';
|
|
|
|
|
|
|
|
|
15 |
import {
|
16 |
Table,
|
17 |
TableBody,
|
|
|
33 |
export interface ChatMessageProps {
|
34 |
message: Message;
|
35 |
loading?: boolean;
|
36 |
+
wipAssistantMessage?: string;
|
37 |
}
|
38 |
|
39 |
export const ChatMessage: React.FC<ChatMessageProps> = ({
|
|
|
44 |
const [messageId, setMessageId] = useAtom(selectedMessageId);
|
45 |
const { id, mediaUrl, prompt, response, result } = message;
|
46 |
const [formattedSections, finalResult, finalError] = useMemo(
|
47 |
+
() => formatStreamLogs(response ?? wipAssistantMessage),
|
48 |
+
[response, wipAssistantMessage],
|
49 |
);
|
50 |
// prioritize the result from the message over the WIP message
|
51 |
const codeResult = result?.payload ?? finalResult;
|
|
|
96 |
<div className="flex-1 px-1 space-y-4 ml-4 overflow-hidden">
|
97 |
<Table className="w-[400px]">
|
98 |
<TableBody>
|
99 |
+
{formattedSections.map((section, index) => (
|
100 |
<TableRow
|
101 |
className="border-primary/50 h-[56px]"
|
102 |
+
key={index}
|
103 |
>
|
104 |
<TableCell className="text-center text-webkit-center">
|
105 |
{ChunkStatusToIconDict[section.status]}
|
|
|
127 |
)}
|
128 |
{!codeResult && finalError && (
|
129 |
<>
|
130 |
+
<p>❌ {finalError.name}</p>
|
131 |
<div>
|
132 |
<CodeBlock
|
133 |
language="error"
|
134 |
value={
|
|
|
|
|
135 |
finalError.value +
|
136 |
'\n' +
|
137 |
finalError.traceback_raw.join('\n')
|
|
|
175 |
const isExecuting = !['completed', 'failed'].includes(status);
|
176 |
|
177 |
useEffect(() => {
|
178 |
+
if (isExecuting && timestamp && useTimer) {
|
179 |
const timerId = setInterval(() => {
|
180 |
setMSeconds(Date.now() - Date.parse(timestamp));
|
181 |
}, 200);
|
|
|
266 |
</DialogContent>
|
267 |
</Dialog>
|
268 |
);
|
269 |
+
} else if ((payload as PrismaJson.FinalChatResult['payload']).code) {
|
270 |
return (
|
271 |
<Dialog>
|
272 |
<DialogTrigger asChild>
|
|
|
275 |
</Button>
|
276 |
</DialogTrigger>
|
277 |
<DialogContent className="max-w-5xl">
|
278 |
+
<CodeResultDisplay
|
279 |
+
codeResult={payload as PrismaJson.FinalChatResult['payload']}
|
280 |
+
/>
|
281 |
</DialogContent>
|
282 |
</Dialog>
|
283 |
);
|
284 |
}
|
285 |
+
return null;
|
286 |
};
|
287 |
|
288 |
export default ChatMessage;
|
components/chat/TopPrompt.tsx
CHANGED
@@ -13,7 +13,6 @@ export interface TopPrompt {
|
|
13 |
|
14 |
export default async function TopPrompt({ chat, userId }: TopPrompt) {
|
15 |
const authorId = chat.userId;
|
16 |
-
console.log('[Ming] ~ TopPrompt ~ authorId:', authorId);
|
17 |
// 1. Viewer logged in, Viewer = Author
|
18 |
if (userId && authorId === userId) {
|
19 |
return null;
|
@@ -26,15 +25,11 @@ export default async function TopPrompt({ chat, userId }: TopPrompt) {
|
|
26 |
if (authorId && authorId !== userId) {
|
27 |
const chatAuthor = authorId ? await dbGetUser(authorId) : null;
|
28 |
return (
|
29 |
-
<Card className="group py-2 px-4 flex items-center">
|
30 |
-
<
|
|
|
31 |
<Avatar name={chatAuthor?.name} avatar={chatAuthor?.avatar} />
|
32 |
-
|
33 |
-
<div className="flex-1 px-1 ml-2 overflow-hidden">
|
34 |
-
<p className="leading-normal">
|
35 |
-
Code author:{' '}
|
36 |
-
<span className="font-medium">{chatAuthor?.name ?? 'Unknown'}</span>
|
37 |
-
</p>
|
38 |
</div>
|
39 |
</Card>
|
40 |
);
|
|
|
13 |
|
14 |
export default async function TopPrompt({ chat, userId }: TopPrompt) {
|
15 |
const authorId = chat.userId;
|
|
|
16 |
// 1. Viewer logged in, Viewer = Author
|
17 |
if (userId && authorId === userId) {
|
18 |
return null;
|
|
|
25 |
if (authorId && authorId !== userId) {
|
26 |
const chatAuthor = authorId ? await dbGetUser(authorId) : null;
|
27 |
return (
|
28 |
+
<Card className="group py-2 px-4 flex flex-row items-center">
|
29 |
+
<p className="leading-normal text-sm">Authored by</p>
|
30 |
+
<div className="flex-1 px-1 ml-2 flex flex-row items-center space-x-2">
|
31 |
<Avatar name={chatAuthor?.name} avatar={chatAuthor?.avatar} />
|
32 |
+
<p className="font-medium">{chatAuthor?.name ?? 'Unknown'}</p>
|
|
|
|
|
|
|
|
|
|
|
33 |
</div>
|
34 |
</Card>
|
35 |
);
|
components/ui/CodeBlock.tsx
CHANGED
@@ -47,7 +47,7 @@ export const programmingLanguages: languageMap = {
|
|
47 |
css: '.css',
|
48 |
// custom titles
|
49 |
print: '.txt',
|
50 |
-
error: '
|
51 |
// add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
|
52 |
};
|
53 |
|
|
|
47 |
css: '.css',
|
48 |
// custom titles
|
49 |
print: '.txt',
|
50 |
+
error: '.txt',
|
51 |
// add more file extensions here, make sure the key is same as language prop in CodeBlock.tsx component
|
52 |
};
|
53 |
|
components/ui/Icons.tsx
CHANGED
@@ -222,7 +222,7 @@ function IconCrossCircle({ className, ...props }: React.ComponentProps<'svg'>) {
|
|
222 |
return (
|
223 |
<svg
|
224 |
height="16"
|
225 |
-
|
226 |
viewBox="0 0 16 16"
|
227 |
width="16"
|
228 |
className={cn('size-4', className)}
|
|
|
222 |
return (
|
223 |
<svg
|
224 |
height="16"
|
225 |
+
strokeLinejoin="round"
|
226 |
viewBox="0 0 16 16"
|
227 |
width="16"
|
228 |
className={cn('size-4', className)}
|
lib/db/prisma.ts
CHANGED
@@ -4,24 +4,35 @@ declare global {
|
|
4 |
var prisma: PrismaClient | undefined;
|
5 |
namespace PrismaJson {
|
6 |
// you can use classes, interfaces, types, etc.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
type FinalChatResult = {
|
8 |
type: 'final_code';
|
9 |
status: 'completed' | 'failed';
|
10 |
payload: {
|
11 |
code: string;
|
12 |
test: string;
|
13 |
-
|
14 |
-
//
|
15 |
-
//
|
16 |
-
|
17 |
-
// stdout: string[];
|
18 |
-
// };
|
19 |
-
// results: Array<{
|
20 |
-
// png?: string;
|
21 |
-
// text: string;
|
22 |
-
// is_main_result: boolean;
|
23 |
-
// }>;
|
24 |
-
// };
|
25 |
};
|
26 |
};
|
27 |
}
|
|
|
4 |
var prisma: PrismaClient | undefined;
|
5 |
namespace PrismaJson {
|
6 |
// you can use classes, interfaces, types, etc.
|
7 |
+
|
8 |
+
type StructuredResult = {
|
9 |
+
logs: {
|
10 |
+
stderr: string[];
|
11 |
+
stdout: string[];
|
12 |
+
};
|
13 |
+
results: Array<{
|
14 |
+
png?: string;
|
15 |
+
mp4?: string;
|
16 |
+
text: string;
|
17 |
+
is_main_result: boolean;
|
18 |
+
}>;
|
19 |
+
error: {
|
20 |
+
name: string;
|
21 |
+
value: string;
|
22 |
+
traceback_raw: string[];
|
23 |
+
};
|
24 |
+
};
|
25 |
+
|
26 |
type FinalChatResult = {
|
27 |
type: 'final_code';
|
28 |
status: 'completed' | 'failed';
|
29 |
payload: {
|
30 |
code: string;
|
31 |
test: string;
|
32 |
+
// Change introduces https://github.com/landing-ai/vision-agent-ui/pull/86
|
33 |
+
// 1. Backward compatibility, it could be stringified StructuredResult
|
34 |
+
// 2. result not modified in stream server, could still be stringified StructuredResult
|
35 |
+
result: string | StructuredResult;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
36 |
};
|
37 |
};
|
38 |
}
|
lib/hooks/useVisionAgent.ts
CHANGED
@@ -2,13 +2,10 @@ import { useChat } from 'ai/react';
|
|
2 |
import { toast } from 'react-hot-toast';
|
3 |
import { useEffect, useRef, useState } from 'react';
|
4 |
import { ChatWithMessages, MessageUI, MessageUserInput } from '../types';
|
5 |
-
import {
|
6 |
-
dbPostCreateMessage,
|
7 |
-
dbPostUpdateMessageResponse,
|
8 |
-
} from '../db/functions';
|
9 |
import {
|
10 |
convertAssistantUIMessageToDBMessageResponse,
|
11 |
-
|
12 |
} from '../utils/message';
|
13 |
import { useSetAtom } from 'jotai';
|
14 |
import { selectedMessageId } from '@/state/chat';
|
@@ -38,10 +35,11 @@ const useVisionAgent = (chat: ChatWithMessages) => {
|
|
38 |
);
|
39 |
setMessageId(currMessageId.current);
|
40 |
},
|
41 |
-
initialMessages: convertDBMessageToUIMessage(dbMessages),
|
42 |
body: {
|
43 |
mediaUrl: currMediaUrl.current,
|
44 |
id,
|
|
|
|
|
45 |
},
|
46 |
onError: err => {
|
47 |
err && toast.error(err.message);
|
|
|
2 |
import { toast } from 'react-hot-toast';
|
3 |
import { useEffect, useRef, useState } from 'react';
|
4 |
import { ChatWithMessages, MessageUI, MessageUserInput } from '../types';
|
5 |
+
import { dbPostUpdateMessageResponse } from '../db/functions';
|
|
|
|
|
|
|
6 |
import {
|
7 |
convertAssistantUIMessageToDBMessageResponse,
|
8 |
+
convertDBMessageToAPIMessage,
|
9 |
} from '../utils/message';
|
10 |
import { useSetAtom } from 'jotai';
|
11 |
import { selectedMessageId } from '@/state/chat';
|
|
|
35 |
);
|
36 |
setMessageId(currMessageId.current);
|
37 |
},
|
|
|
38 |
body: {
|
39 |
mediaUrl: currMediaUrl.current,
|
40 |
id,
|
41 |
+
// for some reason, the messages has to be stringified to be sent to the API
|
42 |
+
apiMessages: JSON.stringify(convertDBMessageToAPIMessage(dbMessages)),
|
43 |
},
|
44 |
onError: err => {
|
45 |
err && toast.error(err.message);
|
lib/types.ts
CHANGED
@@ -16,21 +16,3 @@ export interface SignedPayload {
|
|
16 |
signedUrl: string;
|
17 |
fields: Record<string, string>;
|
18 |
}
|
19 |
-
|
20 |
-
export type ResultPayload = {
|
21 |
-
logs: {
|
22 |
-
stderr: string[];
|
23 |
-
stdout: string[];
|
24 |
-
};
|
25 |
-
results: Array<{
|
26 |
-
png?: string;
|
27 |
-
mp4?: string;
|
28 |
-
text: string;
|
29 |
-
is_main_result: boolean;
|
30 |
-
}>;
|
31 |
-
error: {
|
32 |
-
name: string;
|
33 |
-
value: string;
|
34 |
-
traceback_raw: string[];
|
35 |
-
};
|
36 |
-
};
|
|
|
16 |
signedUrl: string;
|
17 |
fields: Record<string, string>;
|
18 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
lib/utils/content.ts
CHANGED
@@ -1,26 +1,4 @@
|
|
1 |
import toast from 'react-hot-toast';
|
2 |
-
import { ResultPayload } from '../types';
|
3 |
-
const ANSWERS_PREFIX = 'answers';
|
4 |
-
|
5 |
-
export const generateAnswersImageMarkdown = (index: number, url: string) => {
|
6 |
-
return `![${ANSWERS_PREFIX}-${index}](${url})`;
|
7 |
-
};
|
8 |
-
|
9 |
-
export const cleanInputMessage = (content: string) => {
|
10 |
-
return content
|
11 |
-
.replace(/!\[input-.*?\)/g, '')
|
12 |
-
.replace(/<video[^>]*>.*?<\/video>/g, '');
|
13 |
-
};
|
14 |
-
|
15 |
-
export const cleanAnswerMessage = (content: string) => {
|
16 |
-
return content.replace(/!\[answers.*?\.png\)/g, '');
|
17 |
-
};
|
18 |
-
|
19 |
-
export type CodeResult = {
|
20 |
-
code: string;
|
21 |
-
test: string;
|
22 |
-
result: string;
|
23 |
-
};
|
24 |
|
25 |
const WIPLogTypes = ['plans', 'tools', 'code'];
|
26 |
const AllLogTypes = [
|
@@ -37,8 +15,8 @@ export type ChunkBody = {
|
|
37 |
timestamp?: string;
|
38 |
payload:
|
39 |
| Array<Record<string, string>> // PlansBody | ToolsBody
|
40 |
-
|
|
41 |
-
|
|
42 |
};
|
43 |
|
44 |
export type WIPChunkBodyGroup = ChunkBody & {
|
@@ -53,7 +31,11 @@ export type WIPChunkBodyGroup = ChunkBody & {
|
|
53 |
*/
|
54 |
export const formatStreamLogs = (
|
55 |
content: string | null | undefined,
|
56 |
-
): [
|
|
|
|
|
|
|
|
|
57 |
if (!content) return [[], undefined];
|
58 |
const streamLogs = content.split('\n').filter(log => !!log);
|
59 |
|
@@ -104,8 +86,8 @@ export const formatStreamLogs = (
|
|
104 |
return [
|
105 |
groupedSections.filter(section => WIPLogTypes.includes(section.type)),
|
106 |
groupedSections.find(section => section.type === 'final_code')
|
107 |
-
?.payload as
|
108 |
groupedSections.find(section => section.type === 'final_error')
|
109 |
-
?.payload as
|
110 |
];
|
111 |
};
|
|
|
1 |
import toast from 'react-hot-toast';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
|
3 |
const WIPLogTypes = ['plans', 'tools', 'code'];
|
4 |
const AllLogTypes = [
|
|
|
15 |
timestamp?: string;
|
16 |
payload:
|
17 |
| Array<Record<string, string>> // PlansBody | ToolsBody
|
18 |
+
| PrismaJson.FinalChatResult['payload'] // CodeBody & FinalCodeBody
|
19 |
+
| PrismaJson.StructuredResult['error']; // ErrorBody
|
20 |
};
|
21 |
|
22 |
export type WIPChunkBodyGroup = ChunkBody & {
|
|
|
31 |
*/
|
32 |
export const formatStreamLogs = (
|
33 |
content: string | null | undefined,
|
34 |
+
): [
|
35 |
+
WIPChunkBodyGroup[],
|
36 |
+
PrismaJson.FinalChatResult['payload']?,
|
37 |
+
PrismaJson.StructuredResult['error']?,
|
38 |
+
] => {
|
39 |
if (!content) return [[], undefined];
|
40 |
const streamLogs = content.split('\n').filter(log => !!log);
|
41 |
|
|
|
86 |
return [
|
87 |
groupedSections.filter(section => WIPLogTypes.includes(section.type)),
|
88 |
groupedSections.find(section => section.type === 'final_code')
|
89 |
+
?.payload as PrismaJson.FinalChatResult['payload'],
|
90 |
groupedSections.find(section => section.type === 'final_error')
|
91 |
+
?.payload as PrismaJson.StructuredResult['error'],
|
92 |
];
|
93 |
};
|
lib/utils/message.ts
CHANGED
@@ -4,13 +4,13 @@ import { ChunkBody } from './content';
|
|
4 |
|
5 |
/**
|
6 |
* The Message we saved to database consists of a prompt and a response
|
7 |
-
* for the
|
8 |
*/
|
9 |
-
export const
|
10 |
messages: Message[],
|
11 |
): MessageUI[] => {
|
12 |
return messages.reduce((acc, message) => {
|
13 |
-
const { id, mediaUrl, prompt,
|
14 |
if (mediaUrl && prompt) {
|
15 |
acc.push({
|
16 |
id: id + '-user',
|
@@ -18,11 +18,11 @@ export const convertDBMessageToUIMessage = (
|
|
18 |
content: prompt,
|
19 |
});
|
20 |
}
|
21 |
-
if (
|
22 |
acc.push({
|
23 |
id: id + '-assistant',
|
24 |
role: 'assistant',
|
25 |
-
content:
|
26 |
});
|
27 |
}
|
28 |
return acc;
|
|
|
4 |
|
5 |
/**
|
6 |
* The Message we saved to database consists of a prompt and a response
|
7 |
+
* for the API to use, we need to break them to 2 messages, User and Assistant(if responded)
|
8 |
*/
|
9 |
+
export const convertDBMessageToAPIMessage = (
|
10 |
messages: Message[],
|
11 |
): MessageUI[] => {
|
12 |
return messages.reduce((acc, message) => {
|
13 |
+
const { id, mediaUrl, prompt, result } = message;
|
14 |
if (mediaUrl && prompt) {
|
15 |
acc.push({
|
16 |
id: id + '-user',
|
|
|
18 |
content: prompt,
|
19 |
});
|
20 |
}
|
21 |
+
if (result) {
|
22 |
acc.push({
|
23 |
id: id + '-assistant',
|
24 |
role: 'assistant',
|
25 |
+
content: result?.payload.code,
|
26 |
});
|
27 |
}
|
28 |
return acc;
|