วิธีสร้าง Agent ด้วย OpenAI [frontend]

ทุกวันนี้คนรู้จัก OpenAI ซึ่งOpenAI ก็เป็นหนึ่งในLLMs ที่ใช้กันอย่างแพร่หลาย ซึ่ง LLMs หรือ Large Language Models เป็นระบบปัญญาประดิษฐ์ขั้นสูงที่ถูกออกแบบมาเพื่อเข้าใจและสร้างภาษามนุษย์ พวกมันมีความสามารถในการจัดการกับงานที่เกี่ยวข้องกับภาษาในรูปแบบต่างๆ ได้อย่างเชี่ยวชาญ ตั้งแต่การตอบคำถาม, การเขียนข้อความ, การแปลภาษา, ไปจนถึงการสร้างสรรค์เนื้อหาใหม่ๆ

หนึ่งในตัวอย่างที่โดดเด่นของ LLMs คือ Generative Pre-trained Transformer (GPT) จาก OpenAI, ซึ่งมีหลายเวอร์ชันเช่น GPT-3 และ GPT-4 ที่ใช้งานในปัจจุบัน

ซึ่งเราก็นำ LLMs ไปใช้งาน, ในโปรเจ็คครั้งนี้พวกเราได้นำ LLMs มาใช้ในให้ LLMs สร้าง Task งานให้กับบุคคล Role ต่างๆ ซึ่งประกอบด้วย Frontend, Backend และ Designer

สำหรับใครที่สนใจก็มาลองเรียน course กับพวกเราได้ The AI Curator | 6-AI Projects for non-AI developer  ที่สำคัญฟรีนะไม่มีค่าใช้จ่าย 😎

Implement input box for user prompts

ในการ Implement input เราจะใช้ library react-hook-form, @hookform/resolvers/zod และ 'zod'

ซึ่งในการขั้นแรกเราจะทำการสร้าง schema ขึ้นมา

ตัวของ schema จะประกอบด้วย query  สำหรับ input และ bot สำหรับใช้แสดง message error สำหรับตัว bot

export const askScheme = z.object({
  query: z.string().trim().min(1, { message: 'Please enter your message' }),
  bot: z.string({}).optional(),
})

เมื่อเราทำการสร้าง schema เเล้วเราจะทำการสร้าง type interface สำหรับ Form นี้

export interface IOpenAIForm extends z.infer<typeof askScheme> {}

หลังจากนั้นเราจะทำการสร้าง methods ขึ้นมา

const methods = useForm<IOpenAIForm>({
    resolver: zodResolver(askScheme),
    shouldFocusError: true,
    defaultValues: {
      query: '',
    },
  })
  
  const { handleSubmit, setError, setValue } = methods
  
  const onSubmit = async (data: IOpenAIForm) => {
  ...TO DO Something
  }
  
  
  return (
    <FormProvider methods={methods} onSubmit={handleSubmit(onSubmit)}>
      ...
    </FormProvider>
  )

จากตัวของ schema จะทำการ validate input ที่เรารับมาจาก user โดย หาก user ทำการ submit โดยไม่พิมพ์จะแสดง Error message ใน user interface ว่า 'Please enter your message'

ซึ่งใน component input จะใช้ lib ของ  'react-hook-form'และใช้ useFormContext, Controller

ในการจัดการ input

import { InputHTMLAttributes } from 'react'
import { useFormContext, Controller } from 'react-hook-form'
import { twMerge } from 'tailwind-merge'

// ----------------------------------------------------------------------

interface IProps extends InputHTMLAttributes<HTMLInputElement> {
  name: string
  helperText?: string
  label?: string
}

const RHFTextField = ({
  name,
  helperText,
  label,
  className,
  ...other
}: IProps) => {
  const { control } = useFormContext()

  return (
    <Controller
      name={name}
      control={control}
      render={({ field, fieldState: { error } }) => (
        <div className='w-full flex flex-col gap-y-2 '>
          {label && <label className='text-sm font-semibold'>{label}</label>}
          <input
            {...field}
            {...other}
            className={twMerge(
              'outline-none w-full  border border-gray-200 rounded-lg px-2 py-1',
              className
            )}
          />
          {(!!error || helperText) && (
            <div className={twMerge(error?.message && 'text-rose-500 text-sm')}>
              {error?.message || helperText}
            </div>
          )}
        </div>
      )}
    />
  )
}
export default RHFTextField

ซึ่งในส่วนของการเรียกใช้ input เราจะใส่ แอตทริบิวต์ name เป็น query เหมือนใน schema ที่เราได้ทำการประกาศตัวแปรในschema นั้นไว้

const methods = useForm<IOpenAIForm>({
    resolver: zodResolver(askScheme),
    shouldFocusError: true,
    defaultValues: {
      query: '',
    },
  })
  
  const { handleSubmit, setError, setValue } = methods
  
  const onSubmit = async (data: IOpenAIForm) => {
    console.log(data)
  }
  
  
  return (
    <FormProvider methods={methods} onSubmit={handleSubmit(onSubmit)}>
        ....
       <RHFTextField
          type='text'
          placeholder='What do you need ? ...'
          className='outline-none w-full border-none'
          name='query'
        />
        <button
          type='submit'
          disabled={isLoading}
          className='text-gray-400 disabled:text-gray-200'
        >Submit</button>
    </FormProvider>
  )

เมื่อเราทำการ กด submit ถ้า input validate ถูกต้องเเล้วเราจะเห็นข้อมูลที่เรา log มาจาก func onSubmit ซึ่งเราจะนำข้อมูลที่เราได้ไปเชื่อต่อกับ backend ได้เเล้ว

Handle API integration for chained-LLM agents & Display output from each API

ในการต่อกับ Backend เราจะก็จะมี state ต่างๆ เช่น loading, error เป็นต้น  ซึ่งในการเชื่อมกับ  API เราจะทำการ set default base URL ไว้

axios.defaults.baseURL = process.env.NEXT_PUBLIC_API

เมื่อทำการ set เสร็จเเล้ว เราจะทำการเชื่อม API  

const onSubmit = async (data: IOpenAIForm) =< {
  try {

    const { data: result } = await axios.post(
      `/query?question=${data.query}`,
      undefined,
    )
    console.log(result) //ค่าที่ได้จาก API
    //TO DO Something
  } catch (error) {
    const err = error as AxiosError<{ detail: string }<
    setError('bot', {
      message: err?.response?.data?.detail ?? 'Something went wrong',
    })   
  } 
}

เราจะใช้  import { useFormContext } from 'react-hook-form' ในการจัดการ state loading และ erorrs

const {
    formState: { isSubmitting, errors },
  } = useFormContext()

ซึ่งถ้าหาก API Error มาเราจะแสดง Message Error ที่ bot และ display ในส่วนของ Loading ด้วย  เราก็จะเรียกใช้ state จาก 'useFormContext'  เพื่อแสดง ซึ่งค่าที่เราใช้จะมีค่า  isSubmitting, errors

export default function ChatWidget({ answer }: { answer: ChatProps[] }) {
  const chatWindowRef = useRef<HTMLDivElement>(null)
  const {
    formState: { isSubmitting, errors },
  } = useFormContext()

  return (
    <div className='h-full  flex flex-col w-full'>
      <Header />
      <ChatWindow
        messages={answer}
        isLoading={isSubmitting}
        error={errors?.bot?.message as string}
        chatWindowRef={chatWindowRef}
      />
      <ChatInput isLoading={isSubmitting} />
    </div>
  )
}

ซึ่งใน Component ChatWindow เราจะทำการจัดการ state ต่างๆ เพื่อแสดงในส่วนของ UI รวมถึงการแสดงผลลัพธ์จาก API ที่เราได้มาด้วย

import { useEffect } from 'react'
import Image from 'next/image'
import { CopyClipboard } from './CopyClipboard'

interface Message {
  role: 'user' | 'ai'
  message: string
  id: string
  raw: string
}

interface ChatWindowProps {
  messages: Message[]
  isLoading?: boolean
  error?: string
  chatWindowRef: any | null
}

export const ChatWindow: React.FC<ChatWindowProps> = ({
  messages,
  isLoading,
  error,
  chatWindowRef,
}) => {
  useEffect(() => {
    if (
      chatWindowRef !== null &&
      chatWindowRef?.current &&
      messages.length > 0
    ) {
      chatWindowRef.current.scrollTop = chatWindowRef.current.scrollHeight
    }
  }, [messages.length, chatWindowRef])

  return (
    <div
      ref={chatWindowRef}
      className='flex-1 overflow-y-auto p-4 space-y-8'
      id='chatWindow'
    >
      {messages.map((item, index) => (
        <div key={item.id} className='w-full'>
          {item.role === 'user' ? (
            <div className='flex gap-x-8 '>
              <div className='min-w-[48px] min-h-[48px]'>
                <Image
                  src='/img/chicken.png'
                  width={48}
                  height={48}
                  alt='user'
                />
              </div>
              <div>
                <p className='font-bold'>User</p>
                <p>{item.message}</p>
              </div>
            </div>
          ) : (
            <div className='flex gap-x-8 w-full'>
              <div className='min-w-[48px] min-h-[48px]'>
                <Image
                  src='/img/robot.png'
                  width={48}
                  height={48}
                  alt='robot'
                />
              </div>
              <div className='w-full'>
                <div className='flex justify-between mb-1 w-full '>
                  <p className='font-bold'>Ai</p>
                  <div />
                  <CopyClipboard content={item.raw} />
                </div>

                <div
                  className='prose whitespace-pre-line'
                  dangerouslySetInnerHTML={{ __html: item.message }}
                />
              </div>
            </div>
          )}
        </div>
      ))}
      {isLoading && (
        <div className='flex gap-x-8 w-full mx-auto'>
          <div className='min-w-[48px] min-h-[48px]'>
            <Image src='/img/robot.png' width={48} height={48} alt='robot' />
          </div>
          <div>
            <p className='font-bold'>Ai</p>

            <div className='mt-4 flex space-x-2 items-center '>
              <p>Hang on a second </p>
              <span className='sr-only'>Loading...</span>
              <div className='h-2 w-2 bg-blue-600 rounded-full animate-bounce [animation-delay:-0.3s]'></div>
              <div className='h-2 w-2 bg-blue-600 rounded-full animate-bounce [animation-delay:-0.15s]'></div>
              <div className='h-2 w-2 bg-blue-600 rounded-full animate-bounce'></div>
            </div>
          </div>
        </div>
      )}
      {error && (
        <div className='flex gap-x-8 w-full mx-auto'>
          <div className='min-w-[48px] min-h-[48px]'>
            <Image src='/img/error.png' width={48} height={48} alt='error' />
          </div>
          <div>
            <p className='font-bold'>Ai</p>
            <p className='text-rose-500'>{error}</p>
          </div>
        </div>
      )}
    </div>
  )
}

Implement process continuation functionality

หลังจากที่เราต่อกับ Backend เรียบร้อยเเล้วเราจะต้องทำการ เก็บ result message ที่เราได้จาก API ออกมา ซึ่งเราจะทำการสร้าง state ไว้สำหรับเก็บ answer ของ user และ bot  

export default function ChatBotDemo() {
  const [answer, setAnswer] = useState<ChatProps[]>([])

  const methods = useForm<IOpenAIForm>({
    resolver: zodResolver(askScheme),
    shouldFocusError: true,
    defaultValues: {
      query: '',
    },
  })

  const { handleSubmit, setError, setValue } = methods

  const onSubmit = async (data: IOpenAIForm) => {
    try {
      const id = answer.length
      setAnswer((prevState) => [
        ...prevState,
        {
          id: id.toString(),
          role: 'user',
          message: data.query,
          raw: '',
        },
      ])
      setValue('query', '')
      const { data: result } = await axios.post(
        `/query?question=${data.query}`,
        undefined
      )
      setAnswer((prevState) => [
        ...prevState,
        {
          id: (prevState.length + 1).toString(),
          role: 'ai',
          message: result.raw,
          raw: result.json.customer_need,
        },
      ])
    } catch (error) {
      const err = error as AxiosError<{ detail: string }>
      setError('bot', {
        message: err?.response?.data?.detail ?? 'Something went wrong',
      })
    }
  }

  return (
    <FormProvider methods={methods} onSubmit={handleSubmit(onSubmit)}>
      <div className='flex justify-center flex-col items-center bg-white mx-auto max-w-7xl h-screen '>
        <ChatWidget answer={answer} />
      </div>
    </FormProvider>
  )
}

ซึ่งเมือเราถามกับ AI ก็ได้ เช่น เราอยากสร้าง Blog ข่าว ตัว LLMs Agent จะทำการตอบ  Task การทำงานของแต่ละ Role เช่น Design, Frontend และ Backend  

Aa

© 2023, All Rights Reserved, VulturePrime co., ltd.