ABOUT ME

-

Total
-
  • Python: 카카오 챗봇 서버 FastAPI로 만들기
    컴퓨터/파이썬 2021. 1. 17. 23:10
    728x90
    반응형

    예제 결과물

    결과 챗봇을 보고 싶으면 @참고

    더 빠르게 Go언어로 만들고 싶으면 @참고

    더욱 빠르게 Rust언어로 만들고 싶으면 @참고

    코드만 보고 싶으면 아래 참고

     

    Alfex4936/kakaoChatbot-Ajou

    아주대학교 공지 챗봇 FastAPI (learning objectives). Contribute to Alfex4936/kakaoChatbot-Ajou development by creating an account on GitHub.

    github.com

     

    1. 카카오 챗봇 만들기

    준비물

     

    위 준비를 다했으면 학교 일정을 MySQL에 저장해서,

    사용자가 "달력", "일정"... 이란 발화 문을 제시하면 MySQL에서 일정을 불러와서 보여줄 것이다.

    결과물

     

    2. 시나리오 만들기

    원하는 시나리오를 하나 만든다.

     

    발화 문 지정

    엔티티를 만들어서, "일정", "달력"과 같은 부분을 만들어서 엔티티를 받아서 하거나

    단순 내가 만든 발화 문에 있는 문구와 일치할 시 원하는 request를 줄 수 있다.

    단순히, "일정", "달력"

     

    3. 스킬 서버 만들기

    스킬 서버는 request를 받고 보낼 http 서버이다.

    일정을 보여주기 위해 사용할 end-point는 /schedule이다.

     

    스킬 서버를 만든 후 시나리오로 다시 가서 서버를 선택하고 스킬 데이터를 사용함을 체크한다.

    스킬 데이터 사용을 봇 응답으로 설정하면, 직접 응답 메시지를 json으로 보내주어야 한다.

     

    3-1. 메시지 스타일 종류

    json으로 직접 return 할 경우 최대 출력 크기를 맞춰야 정상적으로 메시지가 보인다.

    SimpleText

     

    SimpleImage

    간단 이미지만 보내기

    BasicCard

    Carousel에서도 쓰이는 기본 카드형 메시지이다.

    CommerceCard

    BasicCard에서 제품을 판매할 수 있는 스타일이다.

    ListCard

    헤더와 총 5개의 목록을 만들어서 보낼 수 있는 카드이다.

    Carousel

    BasicCard를 10개 가로로 보여줄 수 있는 메시지이다.

     

    4. MySQL과 FastAPI

    SQLAlchemy로 db를 사용하고, uvicorn으로 서버를 실행할 것이다.

    (FastAPI 용 SQLAchemy를 사용해도 된다.) @링크

    pip install fastapi uvicorn SQLAlchemy

     

    폴더를 하나 만들어서 db 코드를 모아준다. @예제 Github 링크

    db_model (db 코드 폴더)
        └---- __init__.py (모듈화)
        └---- crud.py (db CRUD 커맨드 모음)
        └---- database.py (db 초기화)
        └---- models.py (db 테이블 python으로 표시)
    

     

    models.py

    MySQL ajou_sched 테이블

    위 콜럼들을 그대로 파이썬으로 옮겨준다.

    from sqlalchemy import Column, Integer, String
    
    from .database import Base
    
    
    class Schedules(Base):
        __tablename__ = "ajou_sched"
    
        id = Column(Integer, primary_key=True)
        content = Column(String(50))
        start_date = Column(String(12))
        end_date = Column(String(12))

    crud.py

    db.query(models.Schedules).all()  == "SELECT * FROM ajou_sched"과 같음

    from sqlalchemy.orm import Session
    
    from . import models
    
    
    def get_all_sched(db: Session):
        scheds = db.query(models.Schedules).all()
        return scheds

    database.py

    서버를 직접 노출시키기 보단, 환경 변수에 담아서 불러온다.

    KAKAO_DB = AWS RDS 서버 링크

     

    SQLAlchemy를 사용할 때, MySQL 기본 타임아웃이 8시간이라

    장시간 사용하지 않다가 다시 사용하면 MySQL timeout error가 발생할 수 있다.

    그래서 항상 다른 pool로 지정(NullPool)하고, pool_recycle 기본 값은 -1이기 때문에 적당히 바꿔준다.

    (MySQL timeout > pool_recycle)

    import os
    
    from sqlalchemy import create_engine
    from sqlalchemy.ext.declarative import declarative_base
    from sqlalchemy.orm import sessionmaker
    from sqlalchemy.pool import NullPool
    
    SQLALCHEMY_DATABASE_URL = os.environ["KAKAO_DB"]
    
    engine = create_engine(SQLALCHEMY_DATABASE_URL, pool_recycle=3600, poolclass=NullPool)
    SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
    
    Base = declarative_base()

     

    FastAPI server.py

    from random import choice
    from typing import Dict
    
    import db_model.crud
    import db_model.database
    import db_model.models
    import db_model.schemas
    import uvicorn
    from fastapi import Depends, FastAPI
    from fastapi.middleware.cors import CORSMiddleware
    from fastapi.responses import JSONResponse
    from sqlalchemy.orm import Session
    
    # db 생성
    db_model.models.Base.metadata.create_all(bind=db_model.database.engine)
    
    application = FastAPI(
        title="Ajou notices server", description="for Kakao Chatbot", version="1.0.0"
    )
    application.add_middleware(  # 미들웨어 CORS
        CORSMiddleware,
        allow_origins=["*"],
        allow_methods=["*"],
        allow_headers=["*"],
        allow_credentials=True,
    )
    
    
    # SQL START
    
    # Dependency
    def get_db():
        db = db_model.database.SessionLocal()
        try:
            yield db
        finally:
            db.close()
    
    def getSchedule(db: Session):
        return db_model.crud.get_all_sched(db=db)
        
    # SQL END
    
    
    # Carousel 카드를 만든다.
    def makeCarouselCard(title, desc):
        card_imgs = ["ajou_carousel", "ajou_carousel_1", "ajou_carousel_2"]  # 랜덤 이미지
        card = {
            "title": title,
            "description": desc,
            "thumbnail": {
                "imageUrl": f"https://raw.githubusercontent.com/Alfex4936/kakaoChatbot-Ajou/main/imgs/{choice(card_imgs)}.png"
            },
            #   "buttons": [  optional
            #     {
            #       "action": "message",
            #       "label": "열어보기",
            #       "messageText": "짜잔! 우리가 찾던 보물입니다"
            #     },
            #   ]
        }
    
        return card
    
    
    @application.get("/")
    def hello():
        return "Welcome, the server is running well."
    
    
    # http링크/schedule로 스킬 서버를 연결
    @application.post("/schedule")
    def schedule(content: Dict, db: Session = Depends(get_db)):  # 자동으로 db 연결
        """MySQL DB 학사일정 불러오기 | 메시지 type: Carousel BasicCards """
        
        user_id = content["userRequest"]["user"]["id"]  # user Id
        print(">>> /schedule")
        
        cards = []
        append = cards.append
    
        scheds = getSchedule(db=db)
        for sched in scheds:  # 각 Row가 담겨있다. iterable
            append(
                makeCarouselCard(sched.content, f"{sched.start_date} ~ {sched.end_date}")
            )  # 모든 스케쥴을 Carousel BasicCard로 바꿔준다.
    
        content = {
            "version": "2.0",
            "template": {
                "outputs": [{"carousel": {"type": "basicCard", "items": cards[:10]}}],
            },  # 최대 10개이기 때문에 [:10]로 에러 방지
        }
    
        return JSONResponse(content=content)
    
    
    if __name__ == "__main__":
        uvicorn.run(application, host="0.0.0.0", port=8000, log_level="info")

     

    pandatic BaseModel을 사용한다면 아래처럼 만들면 될 것이다.

    class KakaoUser(BaseModel):
        id: str
        properties: Dict
        type: str
    
    
    class KakaoUserRequest(BaseModel):
        block: Dict
        lang: Optional[str]
        params: Dict
        timezone: str
        user: KakaoUser
        utterance: str
    
    
    class KakaoAction(BaseModel):
        clientExtra: Optional[Dict]
        detailParams: Dict
        id: str
        name: str
        params: Dict
    
    
    class KakaoAPI(BaseModel):
        """Main Kakao JSON"""
    
        action: KakaoAction
        bot: Dict
        contexts: Optional[List]
        intent: Dict
        userRequest: KakaoUserRequest
    
    @application.post("/route", response_model=KakaoAPI)
    async def message(content: KakaoAPI):
        ...

     

    또는 kakao-json (자가제작) 라이브러리 이용

     

    GitHub - Alfex4936/kakao-py: msgspec + 카카오챗봇 API json

    msgspec + 카카오챗봇 API json. Contribute to Alfex4936/kakao-py development by creating an account on GitHub.

    github.com

    pip install kakao-json
    from dataclasses import dataclass
    from typing import Dict, Union
    
    import uvicorn
    from fastapi import FastAPI
    from fastapi.middleware.cors import CORSMiddleware
    
    from kakao_json import Button, Kakao, ListItem
    
    app = FastAPI(title="FastAPI kakao-py example", version="1.0.0")
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_methods=["POST"],
        allow_headers=["*"],
        allow_credentials=True,
    )
    
    
    @app.get("/")
    def read_root():
        return {"Hello": "World"}
    
    
    @app.post("/list_card")
    def read_item(req: Dict):
        @dataclass
        class Item:
            name: str
            number: int
    
        # Make your python objects
        items = [Item(f"I {i}", i + 2) for i in range(5)]
        append = items.append
    
        # kakao-json part
        k = Kakao()
        k.add_qr("오늘", "카톡 발화문1")
        k.add_qr("어제")  # label becomes also messageText
    
        list_card = k.init_list_card().set_header("리스트 카드 제목")
        list_card.add_button(Button("그냥 텍스트 버튼", "message"))
        list_card.add_button(k.init_button("link label").set_link("https://google.com"))
        list_card.add_button(
            k.init_button("share label").set_action_share().set_msg("카톡에 보이는 메시지")
        )
        list_card.add_button(k.init_button("call label").set_number("010-1234-5678"))
    
        list_card.add_item(
            ListItem("title").set_desc("description").set_link("https://naver.com")
        )
    
        carousel = k.init_carousel()
        for i in range(5):
            basic_card = k.init_basic_card()
            basic_card.set_title(f"Hey {i}").set_image("https://kakao")
            carousel.add_card(basic_card)
    
        k.add_output(list_card)
        k.add_output(carousel)
    
        # import json
    
        # print(json.loads(k.to_json()))
    
        return k.to_json()
    
    
    if __name__ == "__main__":
        uvicorn.run(app)

     

    AWS EC2 인스턴스

    배포를 하고 이제 서버를 실행시킨다.

    graphic interface가 없기 때문에, s3에 저장해서 불러와서 사용 중이다.

    ubuntu:~$ virtualenv server
    ubuntu:~$ source ~/server/bin/activate
    
    (server) ubuntu:~$ pip install fastapi uvicorn SQLAlchemy
    
    (server) ubuntu:~$ aws s3 cp s3://bucket/kakao.py .
    (server) ubuntu:~$ aws s3 sync s3://bucket/db_model .
    
    (server) ubuntu:~$ python kakao.py
    INFO:     Started server process [id]
    INFO:     Waiting for application startup.
    INFO:     Application startup complete.
    INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

     

    풀 소스 @Github

     

    Alfex4936/kakaoChatbot-Ajou

    아주대학교 공지 챗봇 FastAPI (learning objectives). Contribute to Alfex4936/kakaoChatbot-Ajou development by creating an account on GitHub.

    github.com

    라이브러리

     

    GitHub - Alfex4936/kakao-py: msgspec + 카카오챗봇 API json

    msgspec + 카카오챗봇 API json. Contribute to Alfex4936/kakao-py development by creating an account on GitHub.

    github.com

     

    728x90

    댓글