기본 콘텐츠로 건너뛰기

#2 랭체인으로 문서기반(엑셀, 구글시트, PDF) 슬랙 챗봇 만들기 Langchain Document basis Slack chatbot server like the ChatPDF (Langchain + GPT API + PDF, CSV, Google docs + Python)

 


지난 시간에는 챗봇 서버를 flask로 구성하고 langchain을 메인으로 pyPDF, Pandas 를 통해 pdf파일 과 csv 파일을 토대로 대답을 하는 봇을 만들어 보았다.  그리고 그 Flask 서버에 JSON requests를 보내서 응답을 받는 것 까지 확인했다. 이번엔 pdf요약이나 csv, xlsx 파일로 질답 말고 지난번에 해보지 않았던 Google Docs를 불러와 작업하는 것을 해보겠다. 

 But, 나의 최종 목적지는 바로 Slack Bot! 업무자동화의 끝은 봇이 아니겠는가!!

 사실, 2월에 RTM(real time message) 기반으로 코드를 작성했는데 이 API가 슬랙 정책상 바뀌었다고 한다.(https://api.slack.com/rtm)

 후.. 그래서 정말 이 문제를 해결하는데 많은 시간이 걸렸던 것 같다. 일단 개인pc를 서버로 사용해야하고 pc가 꺼지면 봇도 돌지 않는 문제가 있지만 뭐.. 항상 켜놓는 pc가 있으니 작업이 완료되면 옮기면 된다는 생각으로 도전해봤다. 


 일단 내가 겪은 문제를 설명하기 전에 내 인터넷 환경은 이렇다.

인터넷 -  LG U+ 기가 인터넷 모뎀 -> LGU+ 공유기  - ASUS 공유기

                                                                       -> RaspberryPi3 (Home Assistant docker 서버가 구동 중)

                                                                       -> RaspberryPi4 (TelsaMate 용 docker 서버가 구동 중)

                                                                       -> 지금 사용중인 랩탑과 같은 기기들


헌데 문제는 Home Assistant 를 구성하기 위해서.. 내가 사용중인 것들이 있었으니.. 


두둥~ DuckDNS와 Nginx Proxy Manager다.

공유기 자체 DNS를 쓰려고 했으나, LG 기가 인터넷 모뎀이 가장 끝단에서 버티고 있고 이걸 내 공유기로 대체하면 기가인터넷이 아닌게 된다고 하더라... 나에겐 기가인터넷의 빠른!! 속도가 필요했기에 중꺾마(중요한건 꺾이지 않는 마음)로 대충 어떻게 돌아가도록 구성을 해놓았다. 

이게 dns를 통해서 들어온 신호들을 Nginx가 방어선을 구축하고 불필요한건 튕겨내는 구조라고 해야하나..   8/1 수정 : 설정 문제였고 nginx와 ngrok은 동시에 구현이 가능한 것으로 확인되었다.

 그러다보니, slack event subscription URL reuqest을 보내고 그 URL에 flask 서버가 화답을 해줘야 내 URL이 살아있다는 것을 캐치하고 승인을 해주는데 flask 앱으로 만든 서버로 신호를 보내도 응답이 없다는 말만 하면 실패를 거듭해왔다. 거기다가 https 여야 한다고..?으잉?!? 

 일단 거두 절미하고 문제와 해결법은 이렇다. 

1. Nginx proxy, duckdns 사용으로 인해 flask server 로 보낸 slack event url request failed 문제

 - https 문제 : 파이썬 py 파일이 있는 폴더에 가서 포트를 지정하면서 Terminal 에서 아래와 같이 입력 (나의 경우엔 Airplay 5000번 포트가 겹쳐서 5005 번으로 했다.)  여기서 main 은 py 파일의 이름이다. 바꿔서 사용하시길!

gunicorn --certfile=cert.pem --keyfile=key.pem --bind 127.0.0.1:5005 main:app 

이러면 그 폴더에 cert.pem 과 key.pem 파일이 생긴다. (SSL 인증서를 발급받는 절차이다.)

그리고 파이썬 코드에서  flask app.run 부분에 아래와 같은 파라미터를 추가해준다.

 if __name__ == '__main__':

    app.run(host='0.0.0.0', port=5005, ssl_context=('cert.pem', 'key.pem'))



2. Nginx 설정은 이렇게. nginx 설정 페이지로 이동한다. (아마 대부분은 이걸 안쓰실거라 ngrok으로 구동 가능하실 것으로 예상.)









이렇게 해줬다. 그럼 이렇게 대망의 Verified 표시를 볼 수 있게 된다.



다음으로 Slack bot의 권한은 이렇게 줬다. 
가장 중요한게 message.channels
private 채널로 전환시에도 읽게 해주려고 message.groups

Subscribe to events on behalf of users 는 주지 않았다. 봇의 권한을 가지고 있는 사람을 대신해서 메시지를 받는것이 필요하다면 했겠지만, 나의 경우엔 봇과 나는 함께 일한다기보단 따로 일하는 것이기에.. subscribe to bot events만 2개의 읽기  권한을 줬다.


자 다음은 코드이다. 

data frame으로 csv를 읽거나 요약하는 코드도 나중에 활성화 하기 위해 모두 다 담아놓았고 import 한 모듈들은 그냥 그대로 두었다. 필요한 경우 주석에서 해제처리하면서 약간씩 수정해주면 바로 사용할 수 있다.

from flask import Flask, request, jsonify
from flask_talisman import Talisman
import pandas as pd
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.text_splitter import CharacterTextSplitter
from langchain.vectorstores import ElasticVectorSearch, Pinecone, Weaviate, FAISS
from langchain.agents import create_pandas_dataframe_agent
from langchain.chains.question_answering import load_qa_chain
from langchain.chat_models import ChatOpenAI
from langchain.chains import AnalyzeDocumentChain
from langchain import OpenAI
from PyPDF2 import PdfReader
import os
from langchain.chains.summarize import load_summarize_chain

from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
import json

#Google docs의 경우
from google.oauth2 import service_account
from googleapiclient.discovery import build

'''
Google Docs
'''
# 서비스 계정 키 파일 경로를 지정.
SERVICE_ACCOUNT_FILE = '[본인의 Google service account file의 Path 를 적어주세요]'
# 필요한 권한 범위를 설정.
SCOPES = ['https://www.googleapis.com/auth/documents.readonly']
# 서비스 계정 키 파일을 사용하여 인증 정보를 가져옴.
creds = service_account.Credentials.from_service_account_file(SERVICE_ACCOUNT_FILE, scopes=SCOPES)
# 인증 정보를 사용하여 구글 시트 API 서비스를 생성.
service = build('docs', 'v1', credentials=creds)

# 구글 시트 ID와 범위를 지정.
document_id = 'Google Docs의 ID이다. URL에서 d/다음 /전까지의 값'
document = service.documents().get(documentId=document_id).execute()


result_doc = ""
for content in document['body']['content']:
if 'paragraph' in content:
for element in content['paragraph']['elements']:
if 'textRun' in element:
result_doc += element['textRun']['content']



# 이벤트를 추적하는 파일을 만들고. 파일이 없다면, dict를 사용.
# 이건 한 번 응답한 것을 재응답하는 것을 막기 위해서 이 방식을 썼다.processed_events.json 파일을
# py 파일과 같은 디렉토리에 넣어줘야한다.
if os.path.isfile('processed_events.json'):
try:
with open('processed_events.json', 'r') as f:
processed_events = json.load(f)
except json.JSONDecodeError:
processed_events = {}
else:
processed_events = {}

# Slack API 토큰 및 채널 ID
slack_token = "슬랙토큰 자리"
slack_channel = "#슬랙채널명 자리 # 포함적어야함"
SLACK_SIGNING_SECRET = '슬랙 사이닝 시크릿 자리'
VERIFICATION_TOKEN = '슬랙 인증 토큰 자리'
# OpenAI API Key
os.environ["OPENAI_API_KEY"] = "Open AI API 유료 키 자리"


# Slack Web API 클라이언트 초기화
slack_client = WebClient(token=slack_token)

app = Flask(__name__)
Talisman(app)


# reader = PdfReader("expense_rule.pdf") # pdf
raw_text = result_doc

# # Summarize 요약
llm = OpenAI(temperature=0)
# summary_chain = load_summarize_chain(llm, chain_type="map_reduce")
# summarize_document_chain = AnalyzeDocumentChain(combine_docs_chain=summary_chain)
# summary_result = summarize_document_chain.run(raw_text)
# print(summary_result)

# Question Answering 질문 답변
model = ChatOpenAI(model="gpt-3.5-turbo") # gpt-3.5-turbo, gpt-4
qa_chain = load_qa_chain(model, chain_type="map_reduce", verbose=True)
qa_document_chain = AnalyzeDocumentChain(combine_docs_chain=qa_chain)

#엑셀, csv 검색 및 aggregation 선세팅
# df = pd.read_csv("test_members.csv")
# df.head #df.head 출력하기
# agent = create_pandas_dataframe_agent(OpenAI(temperature=0), df, verbose=True)

@app.route('/slack_events', methods=['POST'])
def process_slack_event():
# 슬랙에서 전달된 데이터 받기
data = request.get_json()
# print(data) #data 변수 디버깅용
# 슬랙 스레드 ID(ts) 및 메시지 text 추출
thread_ts = ""
thread_ts = data['event'].get('ts', None)
question = data['event']['text']

# url_verification 이벤트 처리
if data['type'] == 'url_verification':
return jsonify({'token': data['challenge']})

# 슬랙 메시지 이벤트 처리
if data['type'] == 'event_callback' and data['event']['type'] == 'message':

# If the event was triggered by the bot itself, ignore it
if 'bot_id' in data['event'] or ('subtype' in data['event'] and data['event']['subtype'] == 'bot_message'):
return jsonify({'status': 'success'})

# 'text' 키가 없는 이벤트는 무시
if question is None:
return jsonify({'status': 'success'})

# 이벤트 ID 추출
event_id = data['event']['client_msg_id']

# If event_id is None or it has already been processed, ignore it
if event_id is None or event_id in processed_events:
return jsonify({'status': 'success'})

# 새 이벤트라면, 처리하고 이벤트 ID를 저장
processed_events[event_id] = True
with open('processed_events.json', 'w') as f:
json.dump(processed_events, f)

print("Bot got this message from the channel : " + question, "\n/ Thread timestamp : " + str(thread_ts))

# DataFrame 방식일 때
# result = agent.run(question)

#document 방식일 때
result = qa_document_chain.run(input_document=raw_text, question=question)

# 응답을 슬랙 스레드에 전송
try:
response = slack_client.chat_postMessage(
channel=slack_channel,
text=result,
thread_ts=thread_ts,
username="Doc_IvanBot"
)
assert response["ok"]
except SlackApiError as e:
print(f"Error posting message: {e}")

return jsonify({'status': 'success'})

if __name__ == '__main__':
# app 실행
app.run(host='0.0.0.0', port=5005, ssl_context=('cert.pem', 'key.pem'), debug=True)
#
# # 연결 확인 메시지 전송
# try:
# response = slack_client.chat_postMessage(
# channel=slack_channel,
# text="Ivan Bot has been connected!"
# )
# assert response["ok"]
# except SlackApiError as e:
# print(f"Error posting message: {e}")








완성되어 슬랙 채널에서 질문을 하면 봇이 댓글로 답변을 달아준다. 질문이 영어면 영어로 대답을, 질문이 한글이면 한글로 대답을 하는 똑똑이!


자 이제, 회사 비용규정, 각종 룰, 보안관련 어려운 질문들을 IvanBot이 촤라라락 응답해 줄 일만 남았다. 


앞으로 조금 더 개선해보고 싶은 부분이 있다면 아래와 같다.

0. 테스트가 다 끝나면 google cloud 에 도커로 말아서 올려(?) 보고싶다.
1. 봇 프로필 이미지 넣고싶다. 이게 슬랙 웹에서 지정한 이름과 이미지가 뜨지 않아서 생기는 문제인데.. 귀여운 로봇 하나 넣고싶은데 아쉽다. 
2. 1개의 스레드를 ts(timestamp)값을 기준으로 context를 유지하며 계속 세부 질문을 해갈 수 있다면 가장 좋겠다. 
3. 봇을 @Security_Bot / @Policy_Bot / @Expense_Bot / @Welcome_Bot 등으로 세분화하고 assign을 넣어서 콜 할 경우에만 응답하도록
   - Security_Bot : Sales craft에서 고객사가 요구하는 보안요구사항들에 대해서 바로바로 질문하고 답을 얻어갈 수 있도록 하는 봇
  - Policy_Bot : 각종 휴가나 휴직,  비자 관련 회사의 정책에 대해서 설명해주는 봇
  - Expense_Bot : 비용처리를 어떻게 해야하는지 설명해주는 봇
  - Welcome_Bot : HR에서 늘 고민인 welcome card 메시지를 기존 입사자들과 겹치지 않게 미려한 문장으로 생성해주는 봇 (langchain - prompt template)
4. 대화를 통해 부족한 부분을 학습하도록 지시 할 수 있다면 좋겠다.
5. [번외] langchain - prompt template 기능으로 기존 나의 AI recipe 사이트(http://www.recipegarden.live) 의 getGPT로 만든 페이지를 업그레이드하는 것.


8/22 추가 : 현재는 도커에 말아서 google cloud에는 아직 안올렸고 서버용 쓰지 않는 맥북에서 24/7 돌아가도록 설정해놨다. 추가로 봇프로필 이미지도 설정해서 잘 사용중이다. (1번)













댓글

이 블로그의 인기 게시물

#1 (진행 중)아두이노 뇌파센서 헤드셋 만들기(Arduino EEG brain wave headset for psychological test) 만들어 뇌파 읽기

 15년 겨울쯤엔가 TED에서 흥미로운 동영상을 봤다. 뇌파를 통해 컴퓨터 안의 객체를 조종하는 모습을 시연하는 것이었다. 뇌파로 이런 것들이 가능하다는 것이 놀라웠다. 나는 심리학도가 아닌가. 뇌파가 더 정확한 심리검사를 만들 수 있는 도구가 될 수 있다는 생각이 들었다.  예를들어 검사문항(디지털 검사)이 100개짜리 라면 핵심 문항들(각 10번 단위)을 체크할 때마다 심경의 변화, 뇌파변화를 센서(객관적)도 기록하고 디지털검사(주관적)로도 기록해서 함께 데이터화 한다면 더 정확한 심경을 읽어 낼 수 있지 않을까? 라는 생각이었다. 2011년 대학원 다닐 때 컴공과 학부생들 겨울방학 특강으로 Objective-C를 무려1개월간 청강했고, C언어를 무려 2개월동안 학원에 다니면서 공부한 사람이기에 ! -_-;;;; 할 수 있을 것이다............  우선 뇌파센서를 구매해야겠지.  알리 익스프레스에서 구매한 EEG 뇌파센서 kit.  2개를 구매했다.비싸군 ㅠㅠ 배송이 한달정도 걸렸다. 학창시절 라디오 만들기인가..실과시간에 도전해본 납땜 이후로는 처음 해보는 납땜이어서 고생좀 했다. 뇌파를 측정해서 hex 코드로 컴퓨터로 읽어들일 수 있는 상태다. 읽어들인 hex값들을 10진수로 변환하고 유의미한 그래프로 그리거나 데이터화 하는 것이 필요 해 보이지만 아직 받은 값을 10진수로 변환하는 방법을 모르겠다. ㅠㅠ 소스코드는 그냥 단순히 hex값으로 읽어오는것이다보니.. 별거 없다;; 나중에 10진수로 변환하여 읽어들이고 자료화 하는 단계가 필요한 것 같은데 차근차근 진행 해 봐야겠다. 준비물 :  1. HM-08 블루투스 모듈 ($5.30) 2. 아두이노 나노 호환품 ($1.89) 3. direct nerosky e eg  brain...

1# (17.03.19 실패)파이썬(Python)을 활용해 사내식당 금일 메뉴 텔레그램 메세지로 전달받기

우리 사내식당 밥은 아주 맛있다. 맛이 없어서 그런건 아니고.. 그냥 미리 어떤 음식이 나오는지 알고싶을 뿐이다. 맛없는게 나오면 안먹고 라면을 먹기 위해서 만드는 것은 아니다. Brian Park 님의 블로그에서 초등학생 아들 알림장/급식메뉴를 텔레그램으로 알려주는 라즈베리파이 서버 관련 글을 읽고 처음으로 파이썬(웹프로그래밍 언어)에 도전하게 되었다. 의외로 스크립트언어? 사실 난 전문용어 잘 몰라유 ㅠㅠ 틀린게 있으면 바로잡아주세요 ㅠㅠ 처럼 순서대로 실행하고, C언어처럼 중괄호 개념이 아닌 들여쓰기로 구분..하는 것이 나에게는 심플하고 쉽게 느껴졌다. 물론 이런저런 명령어 외워야하는건 어쩔 수 없..ㅠ_ㅠ  아무튼 나의 특기인 따라하기 신공을 통해 맥에 python 2.7을 설치하고, 적절한 편집기로 eclipse를 골랐다. 나처럼 일자무식자가 단순 연습을 하기 위해서는 eclipse보다 그냥 python 기본 제공 앱(?)인 IDLE을 활용하여 코드 한 줄 한 줄이 어떻게 실행되고 왜 실행이 안되는지 확인 할 수 있어서 더 좋은 것 같다. 어쨋든, 삼성웰스토리에 신규 가입을 해서 아이디와 비밀번호를 알아 둔 뒤.. urllib, urllib2, cookielib 라는 라이브러리를 임포트하여 내가 읽어올 페이지 특정 부분에서 내 아이디와 비밀번호 전송값을 대입하여 처리하는.... 말해놓고 나니 무슨말인지...하아  뭐 그런 방식인 것 같다. urlencode가 핵심 키워드가 아닐까 싶다. 그래서~! 지금 아이디와 비번을 입력하여 접속된 화면이 출력되는 것 까지는 확인이 되었다. 이는, 아래에서  f=opener.open('https://www.samsungwelstory.com/member/login.jsp') for line in f: print line.strip() 부분에서처럼 사이트를 열고, 한줄씩 열거하여 보여달라고 요청하여 나온 값들과 사파리에서 소스보기 값들과...

#1 (완료) 아두이노 음주 측정기(Alcohol tester with Arduino)

음주운전을 하지 않는 가장 좋은방법은 대리운전 비용 1만원~1.2만원이 아깝지 않으면서 대리운전 전화번호를 누를 수 있는 정도의 취함 상태인 것 같다. 그래서 생각해본 아이디어가 아예 법적으로 차량에 의무적으로 장치를 설치하도록 하는데 이 장치는 차에 시동을 걸기 전 음주측정을 해야하고 정상 수치내에 있을때만 시동이 걸리는 장치!  물론 조수석에 앉을 누군가가 음주운전을 돕기 위해 대신불어준다면 안되겠지만..ㅠㅠ 아침 출근을 위해 정말 급하게 가글을 하고 나와 출근하려 시동을 걸었는데..가글액에 섞인 알코올 성분때문에 지각을 하는 경우도 생길 수 있겠다만.. 그래도 한번 만들어 보자. 어차피 내게는 차량과 연동할 기술적 지식이 아직 없으므로! 하하하하 06.13 진행 중이나 아직 정리가 안됨 07.01 에 05.28 진행 내용 추가 실제 경찰들 처럼 더더더~ 멘트로 몰입감+정확성(3회 불어서 나온수치의 평균을 활용하는 벙법)을 높일 수 있도록 개선하였다. -_-; Ready 상태. 이후 3,2,1 카운트 후 blow! 그리고 수치를 반복하여 깜빡이며 한다. 이하 소스코드 #include <LiquidCrystal.h> // initialize the library with the numbers of the interface pins LiquidCrystal lcd(12, 11, 5, 4, 3, 2); void setup() {   // set up the LCD's number of columns and rows:   lcd.begin(16, 2); } void loop() { lcd.setCursor(1, 0); //라인1로 커서 위치   lcd.print("Ready...");   delay(4000); lcd.clear();   // set the cursor to column 0, line 1   /...