MingruiZhang commited on
Commit
a86b547
1 Parent(s): d0a1b70

image rendering and vision agent endpoint boilerplate (#6)

Browse files

* check-in code

* image rendering and vision agent endpoint boilerplate

app/api/chat/route.ts CHANGED
@@ -7,8 +7,7 @@ import {
7
  ChatCompletionContentPart,
8
  ChatCompletionContentPartImage,
9
  } from 'openai/resources';
10
- import { MessageWithSelectedDataset } from '../../../lib/types';
11
- // import { postAgentChat } from '@/lib/fetch';
12
 
13
  export const runtime = 'edge';
14
 
@@ -19,9 +18,10 @@ const openai = new OpenAI({
19
  export async function POST(req: Request) {
20
  const json = await req.json();
21
  const { messages } = json as {
22
- messages: MessageWithSelectedDataset[];
 
 
23
  };
24
- console.log('[Ming] ~ POST ~ messages:', messages);
25
 
26
  const session = await auth();
27
  if (!session?.user?.email) {
@@ -32,20 +32,11 @@ export async function POST(req: Request) {
32
 
33
  const formattedMessage: ChatCompletionMessageParam[] = messages.map(
34
  message => {
35
- const { dataset, ...rest } = message;
36
-
37
  const contentWithImage: ChatCompletionContentPart[] = [
38
  {
39
  type: 'text',
40
  text: message.content as string,
41
  },
42
- ...(dataset ?? []).map(
43
- entity =>
44
- ({
45
- type: 'image_url',
46
- image_url: { url: entity.url },
47
- }) satisfies ChatCompletionContentPartImage,
48
- ),
49
  ];
50
  return {
51
  role: 'user',
@@ -53,7 +44,6 @@ export async function POST(req: Request) {
53
  };
54
  },
55
  );
56
-
57
  const res = await openai.chat.completions.create({
58
  model: 'gpt-4-vision-preview',
59
  messages: formattedMessage,
 
7
  ChatCompletionContentPart,
8
  ChatCompletionContentPartImage,
9
  } from 'openai/resources';
10
+ import { MessageBase } from '../../../lib/types';
 
11
 
12
  export const runtime = 'edge';
13
 
 
18
  export async function POST(req: Request) {
19
  const json = await req.json();
20
  const { messages } = json as {
21
+ messages: MessageBase[];
22
+ id: string;
23
+ url: string;
24
  };
 
25
 
26
  const session = await auth();
27
  if (!session?.user?.email) {
 
32
 
33
  const formattedMessage: ChatCompletionMessageParam[] = messages.map(
34
  message => {
 
 
35
  const contentWithImage: ChatCompletionContentPart[] = [
36
  {
37
  type: 'text',
38
  text: message.content as string,
39
  },
 
 
 
 
 
 
 
40
  ];
41
  return {
42
  role: 'user',
 
44
  };
45
  },
46
  );
 
47
  const res = await openai.chat.completions.create({
48
  model: 'gpt-4-vision-preview',
49
  messages: formattedMessage,
app/api/vision-agent/route.ts ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { StreamingTextResponse } from 'ai';
2
+
3
+ import { auth } from '@/auth';
4
+ import {
5
+ AIStream,
6
+ type AIStreamParser,
7
+ type AIStreamCallbacksAndOptions,
8
+ } from 'ai';
9
+ import { MessageBase } from '../../../lib/types';
10
+ import { fetcher } from '@/lib/utils';
11
+
12
+ export const runtime = 'edge';
13
+
14
+ export async function POST(req: Request) {
15
+ const json = await req.json();
16
+ const { messages, url } = json as {
17
+ messages: MessageBase[];
18
+ id: string;
19
+ url: string;
20
+ };
21
+
22
+ const session = await auth();
23
+ if (!session?.user?.email) {
24
+ return new Response('Unauthorized', {
25
+ status: 401,
26
+ });
27
+ }
28
+
29
+ function parseVisionAgentStream(): AIStreamParser {
30
+ let previous = '';
31
+
32
+ return data => {
33
+ console.log('[Ming] ~ parseVisionAgentStream ~ data:', data);
34
+ // const json = JSON.parse(data) as {
35
+ // completion: string;
36
+ // stop: string | null;
37
+ // stop_reason: string | null;
38
+ // truncated: boolean;
39
+ // log_id: string;
40
+ // model: string;
41
+ // exception: string | null;
42
+ // };
43
+
44
+ // // Anthropic's `completion` field is cumulative unlike OpenAI's
45
+ // // deltas. In order to compute the delta, we must slice out the text
46
+ // // we previously received.
47
+ // const text = json.completion;
48
+ // console.log('[Ming] ~ parseVisionAgentStream ~ text:', text);
49
+ // const delta = text.slice(previous.length);
50
+ // previous = text;
51
+
52
+ return data;
53
+ };
54
+ }
55
+
56
+ function visionAgentStream(
57
+ res: Response,
58
+ cb?: AIStreamCallbacksAndOptions,
59
+ ): ReadableStream {
60
+ return AIStream(res, parseVisionAgentStream(), cb);
61
+ }
62
+
63
+ const formData = new FormData();
64
+ formData.append('input', JSON.stringify(messages));
65
+ formData.append('image', url);
66
+
67
+ const fetchResponse = await fetcher(
68
+ 'https://api.dev.landing.ai/v1/agent/chat?agent_class=vision_agent',
69
+ {
70
+ method: 'POST',
71
+ headers: {
72
+ apikey: 'land_sk_DKeoYtaZZrYqJ9TMMiXe4BIQgJcZ0s3XAoB0JT3jv73FFqnr6k',
73
+ },
74
+ body: formData,
75
+ },
76
+ );
77
+ console.log('[Ming] ~ POST ~ fetchResponse:', fetchResponse);
78
+ const stream = visionAgentStream(fetchResponse, {
79
+ onStart: async () => {
80
+ console.log('Stream started');
81
+ },
82
+ onCompletion: async completion => {
83
+ console.log('Completion completed', completion);
84
+ },
85
+ onFinal: async completion => {
86
+ console.log('Stream completed', completion);
87
+ },
88
+ onToken: async token => {
89
+ console.log('Token received', token);
90
+ },
91
+ });
92
+ // Now you can consume the VisionAgentStream
93
+
94
+ return new StreamingTextResponse(stream);
95
+ }
app/chat/[id]/page.tsx CHANGED
@@ -1,8 +1,17 @@
1
  import { nanoid } from '@/lib/utils';
2
  import { Chat } from '@/components/chat';
 
3
 
4
- export default async function Page() {
5
- const id = nanoid();
 
 
 
 
 
 
 
 
6
 
7
- return <Chat id={id} />;
8
  }
 
1
  import { nanoid } from '@/lib/utils';
2
  import { Chat } from '@/components/chat';
3
+ import { getKVChat } from '@/lib/kv/chat';
4
 
5
+ interface PageProps {
6
+ params: {
7
+ id: string;
8
+ };
9
+ }
10
+
11
+ export default async function Page({ params }: PageProps) {
12
+ const { id: chatId } = params;
13
+
14
+ const chat = await getKVChat(chatId);
15
 
16
+ return <Chat chat={chat} />;
17
  }
components/chat/ChatList.tsx CHANGED
@@ -2,10 +2,10 @@
2
 
3
  import { Separator } from '@/components/ui/Separator';
4
  import { ChatMessage } from '@/components/chat/ChatMessage';
5
- import { MessageWithSelectedDataset } from '../../lib/types';
6
 
7
  export interface ChatList {
8
- messages: MessageWithSelectedDataset[];
9
  }
10
 
11
  export function ChatList({ messages }: ChatList) {
 
2
 
3
  import { Separator } from '@/components/ui/Separator';
4
  import { ChatMessage } from '@/components/chat/ChatMessage';
5
+ import { MessageBase } from '../../lib/types';
6
 
7
  export interface ChatList {
8
+ messages: MessageBase[];
9
  }
10
 
11
  export function ChatList({ messages }: ChatList) {
components/chat/ChatMessage.tsx CHANGED
@@ -9,10 +9,10 @@ import { CodeBlock } from '@/components/ui/CodeBlock';
9
  import { MemoizedReactMarkdown } from '@/components/chat/MemoizedReactMarkdown';
10
  import { IconOpenAI, IconUser } from '@/components/ui/Icons';
11
  import { ChatMessageActions } from '@/components/chat/ChatMessageActions';
12
- import { MessageWithSelectedDataset } from '../../lib/types';
13
 
14
  export interface ChatMessageProps {
15
- message: MessageWithSelectedDataset;
16
  }
17
 
18
  export function ChatMessage({ message, ...props }: ChatMessageProps) {
 
9
  import { MemoizedReactMarkdown } from '@/components/chat/MemoizedReactMarkdown';
10
  import { IconOpenAI, IconUser } from '@/components/ui/Icons';
11
  import { ChatMessageActions } from '@/components/chat/ChatMessageActions';
12
+ import { MessageBase } from '../../lib/types';
13
 
14
  export interface ChatMessageProps {
15
+ message: MessageBase;
16
  }
17
 
18
  export function ChatMessage({ message, ...props }: ChatMessageProps) {
components/chat/ChatMessageActions.tsx CHANGED
@@ -6,10 +6,10 @@ import { Button } from '@/components/ui/Button';
6
  import { IconCheck, IconCopy } from '@/components/ui/Icons';
7
  import { useCopyToClipboard } from '@/lib/hooks/useCopyToClipboard';
8
  import { cn } from '@/lib/utils';
9
- import { MessageWithSelectedDataset } from '../../lib/types';
10
 
11
  interface ChatMessageActionsProps extends React.ComponentProps<'div'> {
12
- message: MessageWithSelectedDataset;
13
  }
14
 
15
  export function ChatMessageActions({
 
6
  import { IconCheck, IconCopy } from '@/components/ui/Icons';
7
  import { useCopyToClipboard } from '@/lib/hooks/useCopyToClipboard';
8
  import { cn } from '@/lib/utils';
9
+ import { MessageBase } from '../../lib/types';
10
 
11
  interface ChatMessageActionsProps extends React.ComponentProps<'div'> {
12
+ message: MessageBase;
13
  }
14
 
15
  export function ChatMessageActions({
components/chat/ChatPanel.tsx CHANGED
@@ -5,7 +5,7 @@ import { Button } from '@/components/ui/Button';
5
  import { PromptForm } from '@/components/chat/PromptForm';
6
  import { ButtonScrollToBottom } from '@/components/chat/ButtonScrollToBottom';
7
  import { IconRefresh, IconShare, IconStop } from '@/components/ui/Icons';
8
- import { MessageWithSelectedDataset } from '../../lib/types';
9
 
10
  export interface ChatPanelProps
11
  extends Pick<
@@ -14,7 +14,7 @@ export interface ChatPanelProps
14
  > {
15
  id?: string;
16
  title?: string;
17
- messages: MessageWithSelectedDataset[];
18
  }
19
 
20
  export function ChatPanel({
 
5
  import { PromptForm } from '@/components/chat/PromptForm';
6
  import { ButtonScrollToBottom } from '@/components/chat/ButtonScrollToBottom';
7
  import { IconRefresh, IconShare, IconStop } from '@/components/ui/Icons';
8
+ import { MessageBase } from '../../lib/types';
9
 
10
  export interface ChatPanelProps
11
  extends Pick<
 
14
  > {
15
  id?: string;
16
  title?: string;
17
+ messages: MessageBase[];
18
  }
19
 
20
  export function ChatPanel({
components/chat/ImageSelector.tsx CHANGED
@@ -18,7 +18,11 @@ const examples: Example[] = [
18
  {
19
  url: 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg',
20
  initMessages: [
21
- { role: 'user', content: 'how many cereals are there in the image?' },
 
 
 
 
22
  ],
23
  },
24
  // 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/people-example.jpeg',
 
18
  {
19
  url: 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/cereal-example.jpg',
20
  initMessages: [
21
+ {
22
+ role: 'user',
23
+ content: 'how many cereals are there in the image?',
24
+ id: 'fake-id-1',
25
+ },
26
  ],
27
  },
28
  // 'https://landing-lens-support.s3.us-east-2.amazonaws.com/vision-agent-examples/people-example.jpeg',
components/chat/index.tsx CHANGED
@@ -1,37 +1,44 @@
1
  'use client';
 
2
  import { cn } from '@/lib/utils';
3
  import { ChatList } from '@/components/chat/ChatList';
4
  import { ChatPanel } from '@/components/chat/ChatPanel';
5
  import { ChatScrollAnchor } from '@/components/chat/ChatScrollAnchor';
6
- import ImageList from './ImageList';
7
- import useChatWithDataset from '../../lib/hooks/useChatWithDataset';
8
- import { useChat } from 'ai/react';
9
- import { Button } from '../ui/Button';
10
- import ImageSelector from './ImageSelector';
11
 
12
  export interface ChatProps extends React.ComponentProps<'div'> {
13
- id: string;
14
  }
15
 
16
- export function Chat({ id, className }: ChatProps) {
 
17
  const { messages, append, reload, stop, isLoading, input, setInput } =
18
- useChat();
19
 
20
  return (
21
  <>
22
- <div className={cn('pb-[150px] pt-4 md:pt-10 h-full', className)}>
23
  <div className="flex h-full">
24
- <div className="w-1/2 relative border-r border-gray-400 overflow-auto">
25
- {/* <ImageList /> */}
26
- <ImageSelector />
 
 
 
 
 
 
 
27
  </div>
28
  <div className="w-1/2 relative overflow-auto">
29
- {/* <ChatList messages={messages} /> */}
30
  <ChatScrollAnchor trackVisibility={isLoading} />
31
  </div>
32
  </div>
33
  </div>
34
- {/* <ChatPanel
35
  id={id}
36
  isLoading={isLoading}
37
  stop={stop}
@@ -40,7 +47,7 @@ export function Chat({ id, className }: ChatProps) {
40
  messages={messages}
41
  input={input}
42
  setInput={setInput}
43
- /> */}
44
  </>
45
  );
46
  }
 
1
  'use client';
2
+
3
  import { cn } from '@/lib/utils';
4
  import { ChatList } from '@/components/chat/ChatList';
5
  import { ChatPanel } from '@/components/chat/ChatPanel';
6
  import { ChatScrollAnchor } from '@/components/chat/ChatScrollAnchor';
7
+ import { ChatEntity } from '@/lib/types';
8
+ import Image from 'next/image';
9
+ import useVisionAgent from '@/lib/hooks/useVisionAgent';
 
 
10
 
11
  export interface ChatProps extends React.ComponentProps<'div'> {
12
+ chat: ChatEntity;
13
  }
14
 
15
+ export function Chat({ chat }: ChatProps) {
16
+ const { url, id } = chat;
17
  const { messages, append, reload, stop, isLoading, input, setInput } =
18
+ useVisionAgent(chat);
19
 
20
  return (
21
  <>
22
+ <div className={cn('pb-[150px] pt-4 md:pt-10 h-full')}>
23
  <div className="flex h-full">
24
+ <div className="w-1/2 relative border-r border-gray-400 overflow-auto p-4 flex items-center justify-center">
25
+ <div className="max-h-[600px] size-full relative">
26
+ <Image
27
+ draggable={false}
28
+ src={url}
29
+ alt={url}
30
+ fill
31
+ objectFit="contain"
32
+ />
33
+ </div>
34
  </div>
35
  <div className="w-1/2 relative overflow-auto">
36
+ <ChatList messages={messages} />
37
  <ChatScrollAnchor trackVisibility={isLoading} />
38
  </div>
39
  </div>
40
  </div>
41
+ <ChatPanel
42
  id={id}
43
  isLoading={isLoading}
44
  stop={stop}
 
47
  messages={messages}
48
  input={input}
49
  setInput={setInput}
50
+ />
51
  </>
52
  );
53
  }
lib/hooks/useImageUpload.ts CHANGED
@@ -2,7 +2,6 @@ import { useAtom } from 'jotai';
2
  import { DropzoneOptions, useDropzone } from 'react-dropzone';
3
  import { datasetAtom } from '../../state';
4
  import { toast } from 'react-hot-toast';
5
- import { DatasetImageEntity } from '../types';
6
 
7
  const useImageUpload = (options?: Partial<DropzoneOptions>) => {
8
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
 
2
  import { DropzoneOptions, useDropzone } from 'react-dropzone';
3
  import { datasetAtom } from '../../state';
4
  import { toast } from 'react-hot-toast';
 
5
 
6
  const useImageUpload = (options?: Partial<DropzoneOptions>) => {
7
  const { getRootProps, getInputProps, isDragActive } = useDropzone({
lib/hooks/{useChatWithDataset.ts → useVisionAgent.tsx} RENAMED
@@ -1,12 +1,10 @@
1
  import { useChat, type Message } from 'ai/react';
2
- import { useAtom } from 'jotai';
3
  import { toast } from 'react-hot-toast';
4
- import { datasetAtom } from '../../state';
5
  import { useEffect, useState } from 'react';
6
- import { MessageWithSelectedDataset } from '../types';
7
 
8
- const useChatWithDataset = () => {
9
- const [dataset] = useAtom(datasetAtom);
10
  const {
11
  messages,
12
  append,
@@ -18,11 +16,17 @@ const useChatWithDataset = () => {
18
  setMessages,
19
  } = useChat({
20
  sendExtraMessageFields: true,
 
21
  onResponse(response) {
22
  if (response.status !== 200) {
23
  toast.error(response.statusText);
24
  }
25
  },
 
 
 
 
 
26
  });
27
 
28
  const [loadingDots, setLoadingDots] = useState('');
@@ -67,43 +71,9 @@ const useChatWithDataset = () => {
67
  ? [...messages, assistantLoadingMessage]
68
  : messages;
69
 
70
- const selectedDataset = dataset.find(entity => entity.selected)
71
- ? dataset.filter(entity => entity.selected)
72
- : // If there is no selected dataset, use the entire dataset
73
- dataset;
74
-
75
- const appendWithDataset: typeof append = message => {
76
- // const newSystemMessage: Message = {
77
- // id: 'fake-id',
78
- // content:
79
- // 'For the next prompt, here are names of images provided by user, please use these name if you need reference: ' +
80
- // selectedDataset.map(entity => entity.name).join(', '),
81
- // role: 'system',
82
- // };
83
- // const newSystemMessage: Message = {
84
- // id: 'fake-id',
85
- // content: `For the next prompt, please use tags provided by the user to assign to corresponding images.
86
- // For example:
87
-
88
- // Input:
89
- // red, blue, round
90
-
91
- // Answer (each in a new line):
92
- // Image 1: red\n
93
- // Image 2: blue,round\n`,
94
- // role: 'system',
95
- // };
96
- // setMessages([...messages, newSystemMessage]);
97
- return append({
98
- ...message,
99
- // @ts-ignore this is extra fields
100
- dataset: selectedDataset,
101
- } as MessageWithSelectedDataset);
102
- };
103
-
104
  return {
105
- messages: messageWithLoading as MessageWithSelectedDataset[],
106
- append: appendWithDataset,
107
  reload,
108
  stop,
109
  isLoading,
@@ -112,4 +82,4 @@ const useChatWithDataset = () => {
112
  };
113
  };
114
 
115
- export default useChatWithDataset;
 
1
  import { useChat, type Message } from 'ai/react';
 
2
  import { toast } from 'react-hot-toast';
 
3
  import { useEffect, useState } from 'react';
4
+ import { ChatEntity, MessageBase } from '../types';
5
 
6
+ const useVisionAgent = (chat: ChatEntity) => {
7
+ const { messages: initialMessages, id, url } = chat;
8
  const {
9
  messages,
10
  append,
 
16
  setMessages,
17
  } = useChat({
18
  sendExtraMessageFields: true,
19
+ api: '/api/vision-agent',
20
  onResponse(response) {
21
  if (response.status !== 200) {
22
  toast.error(response.statusText);
23
  }
24
  },
25
+ initialMessages: initialMessages,
26
+ body: {
27
+ url,
28
+ id,
29
+ },
30
  });
31
 
32
  const [loadingDots, setLoadingDots] = useState('');
 
71
  ? [...messages, assistantLoadingMessage]
72
  : messages;
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  return {
75
+ messages: messageWithLoading as MessageBase[],
76
+ append,
77
  reload,
78
  stop,
79
  isLoading,
 
82
  };
83
  };
84
 
85
+ export default useVisionAgent;
lib/kv/chat.ts CHANGED
@@ -5,14 +5,19 @@ import { kv } from '@vercel/kv';
5
 
6
  import { auth } from '@/auth';
7
  import { ChatEntity } from '@/lib/types';
 
8
 
9
- export async function getKVChats() {
10
  const session = await auth();
11
  const email = session?.user?.email;
12
-
13
  if (!email) {
14
- return [];
15
  }
 
 
 
 
 
16
 
17
  try {
18
  const pipeline = kv.pipeline();
@@ -33,8 +38,13 @@ export async function getKVChats() {
33
  }
34
 
35
  export async function getKVChat(id: string) {
 
36
  const chat = await kv.hgetall<ChatEntity>(`chat:${id}`);
37
 
 
 
 
 
38
  return chat;
39
  }
40
 
 
5
 
6
  import { auth } from '@/auth';
7
  import { ChatEntity } from '@/lib/types';
8
+ import { redirect } from 'next/navigation';
9
 
10
+ async function authCheck() {
11
  const session = await auth();
12
  const email = session?.user?.email;
 
13
  if (!email) {
14
+ redirect('/');
15
  }
16
+ return { email, isAdmin: email.endsWith('landing.ai') };
17
+ }
18
+
19
+ export async function getKVChats() {
20
+ const { email } = await authCheck();
21
 
22
  try {
23
  const pipeline = kv.pipeline();
 
38
  }
39
 
40
  export async function getKVChat(id: string) {
41
+ const { email, isAdmin } = await authCheck();
42
  const chat = await kv.hgetall<ChatEntity>(`chat:${id}`);
43
 
44
+ if (chat?.user !== email || !isAdmin) {
45
+ redirect('/');
46
+ }
47
+
48
  return chat;
49
  }
50
 
lib/types.ts CHANGED
@@ -24,8 +24,9 @@ export type MessageWithSelectedDataset = Message & {
24
  };
25
 
26
  export type MessageBase = {
27
- role: 'user' | 'system' | 'assistant';
28
  content: string;
 
29
  };
30
 
31
  export type ChatEntity = {
 
24
  };
25
 
26
  export type MessageBase = {
27
+ role: Message['role'];
28
  content: string;
29
+ id: string;
30
  };
31
 
32
  export type ChatEntity = {