Dev/Go

AI와 채팅

newtype 2022. 12. 2. 17:42

chat-ai

간단하게 AI와 채팅을 구현 하고자 합니다.
AI는 https://beta.openai.com/docs/guides/completion/conversation의 내용을 사용합니다.
openai.com 에서는 영문만 지원하기 때문에, naver의 papago api를 사용해서 번역합니다. 현재는 한글도 지원합니다.

제약사항

  • openai.com은 가입하면 베타 버전이라 3달간만 무료로 사용할 수 있습니다. 서비스가 아닌 스터디 목적이니 3달만 사용하려 합니다. (만기: 23년 1월 중순) 만료 되었네요.
  • naver papago api는 하루 10,000 글자만 번역이 무료 입니다. 번역 대상은 내가 입력한 내용 + AI가 대답한 내용 입니다. 10,000글자를 초과하면 translater fail이 발생합니다.

app.yaml

설정을 위해 app.yaml 파일이 필요합니다.

listenPort: 8888
naverClientId: Naver Client ID
naverClientSecret: Naver Client Secret
openai:
  apiKey: OpenAI Client Secret
  model: text-davinci-003
  max_tokens: 150
  temperature: 0.9
  top_p: 1
  frequency_penalty: 0
  presence_penalty: 0.6
  • https://openapi.naver.com/v1/papago/n2mt 연동을 위한 ID와 key가 필요합니다.
  • https://openai.com/api/ 연동을 위해 API key가 필요합니다.
  • 구현을 시작할땐 text-davinci-002를 사용했는데, 구현 중간에 text-davinci-003이 나왔습니다. 답변이 더 향상 되었다고 합니다. 단점은 답변이 길이졌네요.
  • 기타 openai 관련 설정 값은, https://beta.openai.com/playground/p/default-chat을 참고했습니다.

구조

main ㅡ openai
     ㄴ translater
     ㄴ public

main 패키지 아래 openai, translater 두개의 패키지로 분리했습니다. public 에는 채팅 UI를 구현한 html과 css 파일을 위치했습니다.

흐름

  • 내 채팅은 web-browser -> rest api -> papago api -> openai api 순으로 요청하고, AI 채팅은 역순입니다.
  • 소스 파일 기준으로는 public/index.html -> App.go -> translater/translater.go -> openai/completion.go 순입니다.

code review

public/index.html

  • 처음에는 native javascript로 구현하려 했는데, 코드가 길어지고 가독성이 떨어져서 jquery를 사용했습니다.
  • 종이 비행기 버튼 사용을 위해 fontawesome을 사용했습니다.
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.4.1/css/all.css" integrity="sha384-5sAR7xN1Nv6T6+dT2mhtzEpVJvfS3NScPQTrOxhwjIuvcA67KV2R5Jz6kr4abQsz" crossorigin="anonymous">
<button class="btn fa fa-paper-plane" type="submit"></button>
  • AI와 연결된 대화를 위해 전체 대화 내용이 필요합니다. chat-all라는 hidden 엘리먼트에 저장합니다.
<textarea class="hidden"  id="chat-all"></textarea>
  • 채팅 인터페이스는 최대한 간단하게 li 태그를 사용했고, child로 div에 영문을 넣었습니다. chat-box가 처음에는 내용이 없지만, 채팅을 입력하면 동적으로 추가됩니다.
<ul id="chat-box">
    <li class="chat-box me">반가워!
        <div class="chat-appendix">Nice to meet you!</div>
    </li>
    <li class="chat-box ai">저도 만나서 반가워요!
        <div class="chat-appendix">Nice to meet you too!</div>
    </li>
  • 시각적인 효과는 css를 활용했습니다.
.chat-box {
  padding: 20px 30px;
  margin: 4px 4px;
  border-radius: 10px;
  clear:both;
}

.me {
  background-color: #a9edc1;
  float: right;
}

.ai {
  background-color: #e9e9e9;
  float: left;
}
  • 대화 내용이 입력되면, rest api를 호출 합니다.
    // 채팅 데이터 전송
    function requestMsg(msg) {
      var chatAll =  $('#chat-all').val()
      $.post( "/api/v1/chat", { text: chatAll, say: msg })
        .done(function( data ) {

App.go

go에서 가장 많이 사용한다는 gin을 사용해서 rest api 서버를 구현했습니다.

    r.Run(fmt.Sprintf(":%d", conf.ListenPort))

하위 디렉토리 라우팅은 아래와 같이 합니다.

r.Use(static.Serve("/", static.LocalFile("public", true)))

POST 라우팅과 Request 데이터를 받는 방법은 아래와 같습니다.

func StartWeb() {
    r := gin.Default()
    r.POST("/api/v1/chat", func(c *gin.Context) {

        text := c.PostForm("text")
    reqKrMsg := c.PostForm("say")

Response은 아래와 같이 합니다.

        c.JSON(200, gin.H{
            "text":   text,
            "req-en": reqEnMsg,
            "req-kr": reqKrMsg,
            "res-en": resEnMsg,
            "res-kr": resKrMsg,
        })

연결된 대화를 위해 모든 대화 내용을 학습합니다.

        reqKrMsg := c.PostForm("say")
        reqEnMsg, err := translater.Ko2En(reqKrMsg)
        text += openai.ME + reqEnMsg + "\n"
        resEnMsg, err := openai.Chat(text)

translater/translater.go

rest client는 resty를 사용했습니다.
파파고 api는 body에 소스 언어, 타켓 언어 그리고 번역할 텍스트를 전달합니다.

    reqUrl := NewReqUrl("").
        SetParam("source", src).
        SetParam("target", dst).
        SetParam("text", msg)

    client := resty.New()
    respJson, err := client.R().
        SetHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8").
        SetHeader("X-Naver-Client-Id", NaverClientId).
        SetHeader("X-Naver-Client-Secret", NaverClientSecret).
        SetBody(reqUrl.Get()).
        Post(URI)

결과는 body에 json으로 담겨 옵니다.
json 파싱은 로직에서 하지 않고, 구조체를 만들어서 json.Unmarshal() 합니다.

    var result NaverPapagoRespon

    err = json.Unmarshal(respJson.Body(), &result)

openai/completion.go

요청 파라미터는 https://beta.openai.com/examples/default-friend-chat의 값을 참고했습니다.

  "Model": "text-davinci-003",
  "MaxTokens": 60,
  "Temperature": 0.5,
  "TopP": 1,
  "FrequencyPenalty": 0.5,
  "PresencePenalty": 0,
  "stop": ["You:"]

연결된 대화를 위해, 매번 전체 메시지를 학습합니다.

You:Nice to meet you!
Friend:Nice to meet you too!
You:Who are you?
Friend:I'm your new neighbor. I just moved in down the street.
You:Congratulations!
Friend:Thank you!

AI의 이름이 대화를 시작할때마다 다르게 와서, ":" 구분자로 메시지를 잘랐습니다.

func StripPrefix(text string) string {
    i := strings.LastIndex(text, ":")

    if i >= 0 {
        text = strings.Trim(text[i+1:], " ")
    }

    return text
}

참조

반응형