Spaces:
Running
Running
wuyiqunLu
commited on
feat: update final_error to result and add cancel callback (#103)
Browse files<img width="1288" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/132986242/743a66c9-65f9-4698-9d67-3ef6eb209ef3">
<img width="1271" alt="image"
src="https://github.com/landing-ai/vision-agent-ui/assets/132986242/4ac27c6f-85b9-496b-b436-1921c2ba052c">
- .github/workflows/playwright.yml +1 -1
- app/api/vision-agent/route.ts +40 -5
- components/ChatInterface.tsx +6 -5
- components/chat/ChatMessage.tsx +8 -9
- lib/db/functions.ts +3 -1
- lib/db/prisma.ts +2 -0
- lib/hooks/useVisionAgent.ts +7 -3
- lib/types.ts +2 -1
- lib/utils/content.ts +42 -33
- lib/utils/message.ts +2 -2
- prisma/migrations/20240617015509_add_stream_duration_to_message/data-migration.mjs +59 -0
- prisma/migrations/20240617015509_add_stream_duration_to_message/migration.sql +2 -0
- prisma/schema.prisma +2 -1
- tests/e2e/index.spec.ts +7 -0
.github/workflows/playwright.yml
CHANGED
|
@@ -5,7 +5,7 @@ jobs:
|
|
| 5 |
run-e2es:
|
| 6 |
timeout-minutes: 5
|
| 7 |
runs-on: ubuntu-latest
|
| 8 |
-
if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success'
|
| 9 |
steps:
|
| 10 |
- uses: actions/checkout@v4
|
| 11 |
- name: Setup pnpm
|
|
|
|
| 5 |
run-e2es:
|
| 6 |
timeout-minutes: 5
|
| 7 |
runs-on: ubuntu-latest
|
| 8 |
+
if: github.event_name == 'deployment_status' && github.event.deployment_status.state == 'success' && github.event.deployment_status.environment_url
|
| 9 |
steps:
|
| 10 |
- uses: actions/checkout@v4
|
| 11 |
- name: Setup pnpm
|
app/api/vision-agent/route.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
| 1 |
import { StreamingTextResponse } from 'ai';
|
| 2 |
|
| 3 |
-
// import { auth } from '@/auth';
|
| 4 |
-
import { MessageUI } from '@/lib/types';
|
| 5 |
-
|
| 6 |
import { logger, withLogging } from '@/lib/logger';
|
| 7 |
import { getPresignedUrl } from '@/lib/aws';
|
| 8 |
import { dbPostUpdateMessageResponse } from '@/lib/db/functions';
|
|
@@ -179,6 +176,43 @@ export const POST = withLogging(
|
|
| 179 |
let time = Date.now();
|
| 180 |
const results: PrismaJson.MessageBody[] = [];
|
| 181 |
const stream = new ReadableStream({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
async start(controller) {
|
| 183 |
const parseLine = async (
|
| 184 |
line: string,
|
|
@@ -289,9 +323,10 @@ export const POST = withLogging(
|
|
| 289 |
await dbPostUpdateMessageResponse(messageId, {
|
| 290 |
response: processMsgs.map(res => JSON.stringify(res)).join('\n'),
|
| 291 |
result: results.find(
|
| 292 |
-
res => res.type === 'final_code',
|
| 293 |
-
) as PrismaJson.
|
| 294 |
responseBody: processMsgs,
|
|
|
|
| 295 |
});
|
| 296 |
logger.info(
|
| 297 |
session,
|
|
|
|
| 1 |
import { StreamingTextResponse } from 'ai';
|
| 2 |
|
|
|
|
|
|
|
|
|
|
| 3 |
import { logger, withLogging } from '@/lib/logger';
|
| 4 |
import { getPresignedUrl } from '@/lib/aws';
|
| 5 |
import { dbPostUpdateMessageResponse } from '@/lib/db/functions';
|
|
|
|
| 176 |
let time = Date.now();
|
| 177 |
const results: PrismaJson.MessageBody[] = [];
|
| 178 |
const stream = new ReadableStream({
|
| 179 |
+
async cancel(reason) {
|
| 180 |
+
logger.info(
|
| 181 |
+
session,
|
| 182 |
+
{
|
| 183 |
+
message: 'Streaming cancelled',
|
| 184 |
+
maxChunkSize,
|
| 185 |
+
reason,
|
| 186 |
+
},
|
| 187 |
+
request,
|
| 188 |
+
'__AGENT_STREAM_CANCELLED',
|
| 189 |
+
);
|
| 190 |
+
const processMsgs = results.filter(
|
| 191 |
+
res => res.type !== 'final_code',
|
| 192 |
+
) as PrismaJson.AgentResponseBodies;
|
| 193 |
+
await dbPostUpdateMessageResponse(
|
| 194 |
+
messageId,
|
| 195 |
+
{
|
| 196 |
+
response: processMsgs.map(res => JSON.stringify(res)).join('\n'),
|
| 197 |
+
result: {
|
| 198 |
+
type: 'final_error',
|
| 199 |
+
status: 'failed',
|
| 200 |
+
payload: {
|
| 201 |
+
name:
|
| 202 |
+
reason instanceof Error ? reason.name : 'Stream cancelled',
|
| 203 |
+
value:
|
| 204 |
+
reason instanceof Error
|
| 205 |
+
? reason.stack ?? ''
|
| 206 |
+
: 'Unknown error',
|
| 207 |
+
traceback_raw: [],
|
| 208 |
+
},
|
| 209 |
+
},
|
| 210 |
+
responseBody: processMsgs,
|
| 211 |
+
streamDuration: (Date.now() - time) / 1000,
|
| 212 |
+
},
|
| 213 |
+
false, // shouldRevalidatePath
|
| 214 |
+
);
|
| 215 |
+
},
|
| 216 |
async start(controller) {
|
| 217 |
const parseLine = async (
|
| 218 |
line: string,
|
|
|
|
| 323 |
await dbPostUpdateMessageResponse(messageId, {
|
| 324 |
response: processMsgs.map(res => JSON.stringify(res)).join('\n'),
|
| 325 |
result: results.find(
|
| 326 |
+
res => res.type === 'final_code' || res.type === 'final_error',
|
| 327 |
+
) as PrismaJson.FinalResultBody,
|
| 328 |
responseBody: processMsgs,
|
| 329 |
+
streamDuration: (Date.now() - time) / 1000,
|
| 330 |
});
|
| 331 |
logger.info(
|
| 332 |
session,
|
components/ChatInterface.tsx
CHANGED
|
@@ -24,11 +24,12 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({ chat, userId }) => {
|
|
| 24 |
data-state={messageCodeResult?.payload ? 'open' : 'closed'}
|
| 25 |
className="pl-4 peer absolute right-0 inset-y-0 hidden translate-x-full data-[state=open]:translate-x-0 z-30 duration-300 ease-in-out xl:flex flex-col items-start xl:w-1/2 h-full dark:bg-zinc-950 overflow-auto"
|
| 26 |
>
|
| 27 |
-
{messageCodeResult?.
|
| 28 |
-
|
| 29 |
-
<
|
| 30 |
-
|
| 31 |
-
|
|
|
|
| 32 |
</div>
|
| 33 |
<div className="w-full flex justify-center pr-0 animate-in duration-300 ease-in-out peer-[[data-state=open]]:xl:pr-[50%]">
|
| 34 |
<ChatList chat={chat} userId={userId} />
|
|
|
|
| 24 |
data-state={messageCodeResult?.payload ? 'open' : 'closed'}
|
| 25 |
className="pl-4 peer absolute right-0 inset-y-0 hidden translate-x-full data-[state=open]:translate-x-0 z-30 duration-300 ease-in-out xl:flex flex-col items-start xl:w-1/2 h-full dark:bg-zinc-950 overflow-auto"
|
| 26 |
>
|
| 27 |
+
{messageCodeResult?.type === 'final_code' &&
|
| 28 |
+
messageCodeResult.payload && (
|
| 29 |
+
<Card className="size-full overflow-auto">
|
| 30 |
+
<CodeResultDisplay codeResult={messageCodeResult.payload} />
|
| 31 |
+
</Card>
|
| 32 |
+
)}
|
| 33 |
</div>
|
| 34 |
<div className="w-full flex justify-center pr-0 animate-in duration-300 ease-in-out peer-[[data-state=open]]:xl:pr-[50%]">
|
| 35 |
<ChatList chat={chat} userId={userId} />
|
components/chat/ChatMessage.tsx
CHANGED
|
@@ -75,17 +75,16 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
|
|
| 75 |
}) => {
|
| 76 |
const [messageId, setMessageId] = useAtom(selectedMessageId);
|
| 77 |
const { id, mediaUrl, prompt, response, result, responseBody } = message;
|
| 78 |
-
const
|
| 79 |
() =>
|
| 80 |
formatStreamLogs(
|
| 81 |
-
|
| 82 |
-
|
| 83 |
(response ? getParsedStreamLogs(response) : []),
|
|
|
|
| 84 |
),
|
| 85 |
[response, wipAssistantMessage, responseBody],
|
| 86 |
);
|
| 87 |
-
// prioritize the result from the message over the WIP message
|
| 88 |
-
const codeResult = result?.payload ?? finalResult;
|
| 89 |
return (
|
| 90 |
<div
|
| 91 |
className={cn(
|
|
@@ -143,7 +142,7 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
|
|
| 143 |
</TableCell>
|
| 144 |
<TableCell className="font-medium">
|
| 145 |
<ChunkTypeToText
|
| 146 |
-
useTimer={!
|
| 147 |
chunk={section}
|
| 148 |
/>
|
| 149 |
</TableCell>
|
|
@@ -154,15 +153,15 @@ export const ChatMessage: React.FC<ChatMessageProps> = ({
|
|
| 154 |
))}
|
| 155 |
</TableBody>
|
| 156 |
</Table>
|
| 157 |
-
{
|
| 158 |
<>
|
| 159 |
<div className="xl:hidden">
|
| 160 |
-
<CodeResultDisplay codeResult={
|
| 161 |
</div>
|
| 162 |
<p>✨ Coding complete</p>
|
| 163 |
</>
|
| 164 |
)}
|
| 165 |
-
{!
|
| 166 |
<>
|
| 167 |
<p>❌ {finalError.name}</p>
|
| 168 |
<div>
|
|
|
|
| 75 |
}) => {
|
| 76 |
const [messageId, setMessageId] = useAtom(selectedMessageId);
|
| 77 |
const { id, mediaUrl, prompt, response, result, responseBody } = message;
|
| 78 |
+
const { formattedSections, finalResult, finalError } = useMemo(
|
| 79 |
() =>
|
| 80 |
formatStreamLogs(
|
| 81 |
+
wipAssistantMessage ??
|
| 82 |
+
responseBody ??
|
| 83 |
(response ? getParsedStreamLogs(response) : []),
|
| 84 |
+
result,
|
| 85 |
),
|
| 86 |
[response, wipAssistantMessage, responseBody],
|
| 87 |
);
|
|
|
|
|
|
|
| 88 |
return (
|
| 89 |
<div
|
| 90 |
className={cn(
|
|
|
|
| 142 |
</TableCell>
|
| 143 |
<TableCell className="font-medium">
|
| 144 |
<ChunkTypeToText
|
| 145 |
+
useTimer={!finalResult && !finalError}
|
| 146 |
chunk={section}
|
| 147 |
/>
|
| 148 |
</TableCell>
|
|
|
|
| 153 |
))}
|
| 154 |
</TableBody>
|
| 155 |
</Table>
|
| 156 |
+
{finalResult && (
|
| 157 |
<>
|
| 158 |
<div className="xl:hidden">
|
| 159 |
+
<CodeResultDisplay codeResult={finalResult} />
|
| 160 |
</div>
|
| 161 |
<p>✨ Coding complete</p>
|
| 162 |
</>
|
| 163 |
)}
|
| 164 |
+
{!finalResult && finalError && (
|
| 165 |
<>
|
| 166 |
<p>❌ {finalError.name}</p>
|
| 167 |
<div>
|
lib/db/functions.ts
CHANGED
|
@@ -183,19 +183,21 @@ export async function dbPostCreateMessage(
|
|
| 183 |
export async function dbPostUpdateMessageResponse(
|
| 184 |
messageId: string,
|
| 185 |
messageResponse: MessageAssistantResponse,
|
|
|
|
| 186 |
) {
|
| 187 |
await prisma.message.update({
|
| 188 |
data: {
|
| 189 |
response: messageResponse.response,
|
| 190 |
result: messageResponse.result ?? undefined,
|
| 191 |
responseBody: messageResponse.responseBody,
|
|
|
|
| 192 |
},
|
| 193 |
where: {
|
| 194 |
id: messageId,
|
| 195 |
},
|
| 196 |
});
|
| 197 |
|
| 198 |
-
revalidatePath('/chat');
|
| 199 |
}
|
| 200 |
|
| 201 |
export async function dbDeleteChat(chatId: string) {
|
|
|
|
| 183 |
export async function dbPostUpdateMessageResponse(
|
| 184 |
messageId: string,
|
| 185 |
messageResponse: MessageAssistantResponse,
|
| 186 |
+
shouldRevalidatePath = true,
|
| 187 |
) {
|
| 188 |
await prisma.message.update({
|
| 189 |
data: {
|
| 190 |
response: messageResponse.response,
|
| 191 |
result: messageResponse.result ?? undefined,
|
| 192 |
responseBody: messageResponse.responseBody,
|
| 193 |
+
streamDuration: messageResponse.streamDuration,
|
| 194 |
},
|
| 195 |
where: {
|
| 196 |
id: messageId,
|
| 197 |
},
|
| 198 |
});
|
| 199 |
|
| 200 |
+
shouldRevalidatePath && revalidatePath('/chat');
|
| 201 |
}
|
| 202 |
|
| 203 |
export async function dbDeleteChat(chatId: string) {
|
lib/db/prisma.ts
CHANGED
|
@@ -57,6 +57,8 @@ declare global {
|
|
| 57 |
payload: StructuredError;
|
| 58 |
}
|
| 59 |
|
|
|
|
|
|
|
| 60 |
type MessageBody =
|
| 61 |
| PlanAndToolsBody
|
| 62 |
| CodeBody
|
|
|
|
| 57 |
payload: StructuredError;
|
| 58 |
}
|
| 59 |
|
| 60 |
+
type FinalResultBody = FinalCodeBody | FinalErrorBody;
|
| 61 |
+
|
| 62 |
type MessageBody =
|
| 63 |
| PlanAndToolsBody
|
| 64 |
| CodeBody
|
lib/hooks/useVisionAgent.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
import { useChat } from 'ai/react';
|
| 2 |
import { toast } from 'react-hot-toast';
|
| 3 |
import { useEffect, useRef } from 'react';
|
| 4 |
-
import { ChatWithMessages
|
| 5 |
import { convertDBMessageToAPIMessage } from '../utils/message';
|
| 6 |
import { useSetAtom } from 'jotai';
|
| 7 |
import { selectedMessageId } from '@/state/chat';
|
|
@@ -25,7 +25,7 @@ const useVisionAgent = (chat: ChatWithMessages) => {
|
|
| 25 |
toast.error(response.statusText);
|
| 26 |
}
|
| 27 |
},
|
| 28 |
-
onFinish:
|
| 29 |
router.refresh();
|
| 30 |
setMessageId(currMessageId.current);
|
| 31 |
},
|
|
@@ -47,7 +47,11 @@ const useVisionAgent = (chat: ChatWithMessages) => {
|
|
| 47 |
*/
|
| 48 |
const once = useRef(true);
|
| 49 |
useEffect(() => {
|
| 50 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
once.current = false;
|
| 52 |
reload();
|
| 53 |
}
|
|
|
|
| 1 |
import { useChat } from 'ai/react';
|
| 2 |
import { toast } from 'react-hot-toast';
|
| 3 |
import { useEffect, useRef } from 'react';
|
| 4 |
+
import { ChatWithMessages } from '../types';
|
| 5 |
import { convertDBMessageToAPIMessage } from '../utils/message';
|
| 6 |
import { useSetAtom } from 'jotai';
|
| 7 |
import { selectedMessageId } from '@/state/chat';
|
|
|
|
| 25 |
toast.error(response.statusText);
|
| 26 |
}
|
| 27 |
},
|
| 28 |
+
onFinish: () => {
|
| 29 |
router.refresh();
|
| 30 |
setMessageId(currMessageId.current);
|
| 31 |
},
|
|
|
|
| 47 |
*/
|
| 48 |
const once = useRef(true);
|
| 49 |
useEffect(() => {
|
| 50 |
+
if (
|
| 51 |
+
!isLoading &&
|
| 52 |
+
!(latestDbMessage.response || latestDbMessage.responseBody) &&
|
| 53 |
+
once.current
|
| 54 |
+
) {
|
| 55 |
once.current = false;
|
| 56 |
reload();
|
| 57 |
}
|
lib/types.ts
CHANGED
|
@@ -5,9 +5,10 @@ export type ChatWithMessages = Chat & { messages: Message[] };
|
|
| 5 |
|
| 6 |
export type MessageUserInput = Pick<Message, 'prompt' | 'mediaUrl'>;
|
| 7 |
export type MessageAssistantResponse = {
|
| 8 |
-
result?: PrismaJson.
|
| 9 |
response: string;
|
| 10 |
responseBody: PrismaJson.AgentResponseBodies;
|
|
|
|
| 11 |
};
|
| 12 |
|
| 13 |
export type MessageUI = Pick<MessageAI, 'role' | 'content' | 'id'>;
|
|
|
|
| 5 |
|
| 6 |
export type MessageUserInput = Pick<Message, 'prompt' | 'mediaUrl'>;
|
| 7 |
export type MessageAssistantResponse = {
|
| 8 |
+
result?: PrismaJson.FinalResultBody;
|
| 9 |
response: string;
|
| 10 |
responseBody: PrismaJson.AgentResponseBodies;
|
| 11 |
+
streamDuration: number;
|
| 12 |
};
|
| 13 |
|
| 14 |
export type MessageUI = Pick<MessageAI, 'role' | 'content' | 'id'>;
|
lib/utils/content.ts
CHANGED
|
@@ -32,42 +32,51 @@ export type WIPChunkBodyGroup = PrismaJson.MessageBody & {
|
|
| 32 |
*/
|
| 33 |
export const formatStreamLogs = (
|
| 34 |
content: WIPChunkBodyGroup[] | null,
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
PrismaJson.
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
// Merge consecutive logs of the same type to the latest status
|
| 43 |
-
const groupedSections = content
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
-
return
|
| 67 |
-
groupedSections.filter(section =>
|
| 68 |
-
|
|
|
|
|
|
|
| 69 |
?.payload as PrismaJson.FinalCodeBody['payload'],
|
| 70 |
-
groupedSections.find(section => section.type === 'final_error')
|
| 71 |
?.payload as PrismaJson.StructuredError,
|
| 72 |
-
|
| 73 |
};
|
|
|
|
| 32 |
*/
|
| 33 |
export const formatStreamLogs = (
|
| 34 |
content: WIPChunkBodyGroup[] | null,
|
| 35 |
+
result: PrismaJson.FinalResultBody | null,
|
| 36 |
+
): {
|
| 37 |
+
formattedSections: WIPChunkBodyGroup[];
|
| 38 |
+
finalResult?: PrismaJson.FinalCodeBody['payload'];
|
| 39 |
+
finalError?: PrismaJson.StructuredError;
|
| 40 |
+
} => {
|
| 41 |
+
if (!content)
|
| 42 |
+
return {
|
| 43 |
+
formattedSections: [],
|
| 44 |
+
};
|
| 45 |
|
| 46 |
// Merge consecutive logs of the same type to the latest status
|
| 47 |
+
const groupedSections = [...content, ...(result ? [result] : [])].reduce(
|
| 48 |
+
(acc: WIPChunkBodyGroup[], curr: WIPChunkBodyGroup) => {
|
| 49 |
+
const lastGroup = acc[acc.length - 1];
|
| 50 |
+
if (
|
| 51 |
+
acc.length > 0 &&
|
| 52 |
+
lastGroup.type === curr.type &&
|
| 53 |
+
curr.status !== 'started'
|
| 54 |
+
) {
|
| 55 |
+
acc[acc.length - 1] = {
|
| 56 |
+
...curr,
|
| 57 |
+
// always use the timestamp of the first log
|
| 58 |
+
timestamp: lastGroup?.timestamp,
|
| 59 |
+
// duration is the difference between the last log and the first log
|
| 60 |
+
duration:
|
| 61 |
+
lastGroup?.timestamp && curr.timestamp
|
| 62 |
+
? Date.parse(curr.timestamp) - Date.parse(lastGroup.timestamp)
|
| 63 |
+
: undefined,
|
| 64 |
+
};
|
| 65 |
+
} else {
|
| 66 |
+
acc.push(curr);
|
| 67 |
+
}
|
| 68 |
+
return acc;
|
| 69 |
+
},
|
| 70 |
+
[],
|
| 71 |
+
);
|
| 72 |
|
| 73 |
+
return {
|
| 74 |
+
formattedSections: groupedSections.filter(section =>
|
| 75 |
+
WIPLogTypes.includes(section.type),
|
| 76 |
+
),
|
| 77 |
+
finalResult: groupedSections.find(section => section.type === 'final_code')
|
| 78 |
?.payload as PrismaJson.FinalCodeBody['payload'],
|
| 79 |
+
finalError: groupedSections.find(section => section.type === 'final_error')
|
| 80 |
?.payload as PrismaJson.StructuredError,
|
| 81 |
+
};
|
| 82 |
};
|
lib/utils/message.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
import { Message } from '@prisma/client';
|
| 2 |
-
import {
|
| 3 |
|
| 4 |
/**
|
| 5 |
* The Message we saved to database consists of a prompt and a response
|
|
@@ -17,7 +17,7 @@ export const convertDBMessageToAPIMessage = (
|
|
| 17 |
content: prompt,
|
| 18 |
});
|
| 19 |
}
|
| 20 |
-
if (result) {
|
| 21 |
acc.push({
|
| 22 |
id: id + '-assistant',
|
| 23 |
role: 'assistant',
|
|
|
|
| 1 |
import { Message } from '@prisma/client';
|
| 2 |
+
import { MessageUI } from '../types';
|
| 3 |
|
| 4 |
/**
|
| 5 |
* The Message we saved to database consists of a prompt and a response
|
|
|
|
| 17 |
content: prompt,
|
| 18 |
});
|
| 19 |
}
|
| 20 |
+
if (result && result.type === 'final_code') {
|
| 21 |
acc.push({
|
| 22 |
id: id + '-assistant',
|
| 23 |
role: 'assistant',
|
prisma/migrations/20240617015509_add_stream_duration_to_message/data-migration.mjs
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { PrismaClient } from '@prisma/client';
|
| 2 |
+
|
| 3 |
+
const prisma = new PrismaClient();
|
| 4 |
+
|
| 5 |
+
async function main() {
|
| 6 |
+
await prisma.$transaction(
|
| 7 |
+
async tx => {
|
| 8 |
+
const messages = await tx.message.findMany();
|
| 9 |
+
for (const message of messages) {
|
| 10 |
+
const { responseBody, response, result } = message;
|
| 11 |
+
let newResponseBody = responseBody;
|
| 12 |
+
let newResult = result;
|
| 13 |
+
if (responseBody && result) {
|
| 14 |
+
continue;
|
| 15 |
+
}
|
| 16 |
+
if (!responseBody && response) {
|
| 17 |
+
console.log('update response body', message.id);
|
| 18 |
+
newResponseBody = response
|
| 19 |
+
.split('\n')
|
| 20 |
+
.filter(chunk => !!chunk.trim())
|
| 21 |
+
.map(chunk => JSON.parse(chunk))
|
| 22 |
+
.filter(
|
| 23 |
+
body => body.type !== 'final_error' && body.type !== 'final_code',
|
| 24 |
+
);
|
| 25 |
+
}
|
| 26 |
+
if (!result && response) {
|
| 27 |
+
console.log('update response result', message.id);
|
| 28 |
+
newResult = response
|
| 29 |
+
.split('\n')
|
| 30 |
+
.filter(chunk => !!chunk.trim())
|
| 31 |
+
.map(chunk => JSON.parse(chunk))
|
| 32 |
+
?.find(
|
| 33 |
+
body => body.type === 'final_error' || body.type === 'final_code',
|
| 34 |
+
);
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
await tx.message.update({
|
| 38 |
+
where: { id: message.id },
|
| 39 |
+
data: {
|
| 40 |
+
...(newResult ? { result: newResult } : {}),
|
| 41 |
+
...(newResponseBody ? { responseBody: newResponseBody } : {}),
|
| 42 |
+
},
|
| 43 |
+
});
|
| 44 |
+
}
|
| 45 |
+
},
|
| 46 |
+
{ timeout: 30000 },
|
| 47 |
+
);
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
main()
|
| 51 |
+
.catch(async e => {
|
| 52 |
+
console.error(e);
|
| 53 |
+
await prisma.$disconnect();
|
| 54 |
+
process.exit(1);
|
| 55 |
+
})
|
| 56 |
+
.finally(async () => {
|
| 57 |
+
console.log('finished');
|
| 58 |
+
await prisma.$disconnect();
|
| 59 |
+
});
|
prisma/migrations/20240617015509_add_stream_duration_to_message/migration.sql
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
-- AlterTable
|
| 2 |
+
ALTER TABLE "message" ADD COLUMN "streamDuration" INTEGER;
|
prisma/schema.prisma
CHANGED
|
@@ -47,10 +47,11 @@ model Message {
|
|
| 47 |
mediaUrl String
|
| 48 |
prompt String
|
| 49 |
response String?
|
| 50 |
-
/// [
|
| 51 |
result Json?
|
| 52 |
/// [AgentResponseBodies]
|
| 53 |
responseBody Json?
|
|
|
|
| 54 |
chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
|
| 55 |
user User? @relation(fields: [userId], references: [id])
|
| 56 |
|
|
|
|
| 47 |
mediaUrl String
|
| 48 |
prompt String
|
| 49 |
response String?
|
| 50 |
+
/// [FinalResultBody]
|
| 51 |
result Json?
|
| 52 |
/// [AgentResponseBodies]
|
| 53 |
responseBody Json?
|
| 54 |
+
streamDuration Int?
|
| 55 |
chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
|
| 56 |
user User? @relation(fields: [userId], references: [id])
|
| 57 |
|
tests/e2e/index.spec.ts
CHANGED
|
@@ -83,3 +83,10 @@ test.describe('Page with example 1 can be opened', () => {
|
|
| 83 |
await checkMainElementVisible(page);
|
| 84 |
});
|
| 85 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
await checkMainElementVisible(page);
|
| 84 |
});
|
| 85 |
});
|
| 86 |
+
|
| 87 |
+
const TEST_FINAL_ERROR_CHAT_ID_1 = '6bLzMKS';
|
| 88 |
+
test('Page with final error in response can be opened', async ({ page }) => {
|
| 89 |
+
await page.goto(`/chat/${TEST_FINAL_ERROR_CHAT_ID_1}`);
|
| 90 |
+
|
| 91 |
+
await expect(page.getByText('❌ StreamingError')).toBeVisible();
|
| 92 |
+
});
|