Bedrock Llama3でコード実行できるSlackチャットボットを作ってみた

SlackからOpen Interpreterを実行できるようにしてみました。
2024.05.11

こんにちは。CX事業本部のたにもんです。

先日BedrockでLlama3が利用できるようになりました。

Meta Llama 3 が Amazon Bedrock で利用可能に

Llama3を利用して何か作ってみたかったので、今回はLlama3をBedrockから利用し、コード実行できるSlackチャットボットを作ってみたので紹介します。

実際に動かしてみたときの様子は以下のとおりです。 指示内容遂行に必要な行動計画を立て、それに必要なコードを書いて実行し、最終的な結果を返してくれます。

今回作成したコード全体は以下のGitHubリポジトリで公開しているので、興味のある方はご参照ください。

https://github.com/tanimon/ai-chatbot-slack/tree/v2024.05.09

環境

  • Rancher Desktop: 1.12.2
  • docker: 24.0.7-rd, build 72ffac
  • node: v20.12.2
  • npm: 10.7.0
  • typescript: 5.4.5
  • @aws-cdk/aws-apprunner-alpha: 2.140.0-alpha.0
  • aws-cdk-lib: 2.140.0
  • constructs: 10.3.0
  • Python: 3.12.1
  • slack-bolt: 1.18.1
  • slack-sdk: 3.27.1
  • boto3: 1.34.97
  • open-interpreter: 0.2.5

構成図

Slack Boltを用いてSlackボットを作成し、App Runner上で動かしています。 Slackボットはユーザーからの入力をOpen Interpreterに投げて実行します。 Open InterpreterではBedrock経由でLlama3モデルを利用しています。

Open Interpreterとは?

Open Interpreter をざっくりと説明すると、OpenAIのCode InterpreterのOSS版のようなものです。 ユーザーから与えられたタスク遂行のために必要なコードを書いて実行することができます。 Code InterpreterはOpenAIのサンドボックス環境で実行されるため、以下のような制限がありますが、Open Interpreterはローカル環境や自分で用意したサーバ上で動かすことができるため、このような制限がありません1

  • インターネットにアクセスできない
  • 事前インストールされたパッケージしか利用できない
  • アップロードできるファイルサイズは最大100MB
  • 実行時間は最大120秒
  • サンドボックス環境が終了すると生成されたファイルやリンクなどは失われる

また、Code InterpreterではOpenAIが提供しているGPT系のLLMしか利用できませんが、Open InterpreterではGPT系モデルに加えて様々なモデルを利用することができます2

Open Interpreterをローカルマシンで動かしてみる

まずは、ローカルマシン上でBedrock経由でLlama3を利用してOpen Interpreterを動かしてみます。以下のコードを書いて実行しました。

import os

from interpreter import interpreter

# us-east-1のBedrock APIを利用するように設定
os.environ["AWS_REGION_NAME"] = "us-east-1"

# 生成したコードを自動的に実行するように設定。実行すべきではないコードが生成される可能性もあるため注意して利用すること
interpreter.auto_run = True

# 利用するモデルを設定
interpreter.llm.model = "bedrock/meta.llama3-70b-instruct-v1:0"
# interpreter.llm.model = "bedrock/meta.llama2-70b-chat-v1"

# モデルのコンテキストウィンドウサイズを設定
interpreter.llm.context_window = 8000  # type: ignore

message = """以下のデータから各科目の平均点を計算してください。

氏名,国語,算数,理科,社会,英語
田中 太郎,85,92,78,71,68
鈴木 花子,91,67,83,88,75
佐藤 一郎,77,89,65,79,82
高橋 美咲,63,73,91,65,88
山本 健太,79,81,69,92,71
渡辺 優子,88,59,77,68,93
伊藤 大輔,72,85,83,76,67
木村 莉子,65,77,89,81,79
中村 翔,91,68,72,69,85
小林 遥香,84,91,65,87,71
"""
interpreter.chat(message, display=True, stream=False)

# 途中でユーザーからの入力待ちになるので、処理を続行するよう指示
interpreter.chat("続けてください。", display=True, stream=False)

動作結果は以下のようになりました。 指示したタスクに対して、実行計画を立て、それに必要なコードを書いて実行し、エラーが発生した場合は修正し、最終的な結果を返してくれています。 返してくれた結果を確認してみましたが、正しく計算してくれていました。

Llama3で実行したときのログ(クリックで開けます)
❯ poetry run -C server python server/llama_interpreter.py
                                                                                                                                  
  Plan                                                                                                                            
                                                                                                                                  
   1 Read the data from the text                                                                                                  
   2 Split the data into individual subjects                                                                                      
   3 Calculate the average score for each subject                                                                                 
   4 Print the results                                                                                                            
                                                                                                                                  
  Step 1: Read the data from the text                                                                                             
                                                                                                                                  

                                                                                                                                  
  data = """                                                                                                                      
  氏名,国語,算数,理科,社会,英語                                                                                                   
  田中 太郎,85,92,78,71,68                                                                                                        
  鈴木 花子,91,67,83,88,75                                                                                                        
  佐藤 一郎,77,89,65,79,82                                                                                                        
  高橋 美咲,63,73,91,65,88                                                                                                        

ai-chatbot-slack main*​ 
server-py3.12 ❯ poetry run -C server python server/llama_interpreter.py
                                                                                                                                  
  Plan                                                                                                                            
                                                                                                                                  
   1 Read the data from the provided text                                                                                         
   2 Parse the data into a format that can be easily processed                                                                    
   3 Calculate the average score for each subject                                                                                 
   4 Output the results                                                                                                           
                                                                                                                                  
  Step 1: Read the data                                                                                                           
                                                                                                                                  

                                                                                                                                  
  data = """                                                                                                                      
  氏名,国語,算数,理科,社会,英語                                                                                                   
  田中 太郎,85,92,78,71,68                                                                                                        
  鈴木 花子,91,67,83,88,75                                                                                                        
  佐藤 一郎,77,89,65,79,82                                                                                                        
  高橋 美咲,63,73,91,65,88                                                                                                        
  山本 健太,79,81,69,92,71                                                                                                        
  渡辺 優子,88,59,77,68,93                                                                                                        
  伊藤 大輔,72,85,83,76,67                                                                                                        
  木村 莉子,65,77,89,81,79                                                                                                        
  中村 翔,91,68,72,69,85                                                                                                          
  小林 遥香,84,91,65,87,71                                                                                                        
  """                                                                                                                             
                                                                                                                                  

                                                                                                                                  
  Recap of the plan                                                                                                               
                                                                                                                                  
   1 Read the data from the provided text                                                                                         
   2 Parse the data into a format that can be easily processed                                                                    
   3 Calculate the average score for each subject                                                                                 
   4 Output the results                                                                                                           
                                                                                                                                  
  Step 2: Parse the data                                                                                                          
                                                                                                                                  

                                                                                                                                  
  lines = data.split('\n')                                                                                                        
  headers = lines[0].split(',')                                                                                                   
  data_list = [line.split(',') for line in lines[1:]]                                                                             
                                                                                                                                  

                                                                                                                                  
  Recap of the plan                                                                                                               
                                                                                                                                  
   1 Read the data from the provided text                                                                                         
   2 Parse the data into a format that can be easily processed                                                                    
   3 Calculate the average score for each subject                                                                                 
   4 Output the results                                                                                                           
                                                                                                                                  
  Step 3: Calculate the average score for each subject                                                                            
                                                                                                                                  

                                                                                                                                  
  subject_scores = {}                                                                                                             
  for header in headers[1:]:                                                                                                      
      subject_scores[header] = [int(line[i+1]) for i, line in enumerate(data_list)]                                               
                                                                                                                                  
  average_scores = {}                                                                                                             
  for subject, scores in subject_scores.items():                                                                                  
      average_scores[subject] = sum(scores) / len(scores)                                                                         
                                                                                                                                  

ai-chatbot-slack main*​ 
server-py3.12 ❯ poetry run -C server python server/llama_interpreter.py
                                                                                                                                  
  Plan                                                                                                                            
                                                                                                                                  
   1 Read the data from the provided text.                                                                                        
   2 Parse the data into a format that can be easily processed.                                                                   
   3 Calculate the average score for each subject.                                                                                
   4 Print the results.                                                                                                           
                                                                                                                                  
  Step 1: Read the data                                                                                                           
                                                                                                                                  

                                                                                                                                  
  data = """                                                                                                                      
  氏名,国語,算数,理科,社会,英語                                                                                                   
  田中 太郎,85,92,78,71,68                                                                                                        
  鈴木 花子,91,67,83,88,75                                                                                                        
  佐藤 一郎,77,89,65,79,82                                                                                                        
  高橋 美咲,63,73,91,65,88                                                                                                        
  山本 健太,79,81,69,92,71                                                                                                        
  渡辺 優子,88,59,77,68,93                                                                                                        
  伊藤 大輔,72,85,83,76,67                                                                                                        
  木村 莉子,65,77,89,81,79                                                                                                        
  中村 翔,91,68,72,69,85                                                                                                          
  小林 遥香,84,91,65,87,71                                                                                                        
  """                                                                                                                             
                                                                                                                                  

                                                                                                                                  
  Plan Recap                                                                                                                      
                                                                                                                                  
   1 Read the data from the provided text.                                                                                        
   2 Parse the data into a format that can be easily processed.                                                                   
   3 Calculate the average score for each subject.                                                                                
   4 Print the results.                                                                                                           

ai-chatbot-slack main*​ 
server-py3.12 ❯ poetry run -C server python server/llama_interpreter.py
                                                                                                                                  
  Plan                                                                                                                            
                                                                                                                                  
   1 Read the data from the text into a data structure (e.g. a list of dictionaries)                                              
   2 Calculate the average score for each subject                                                                                 
   3 Print the results                                                                                                            
                                                                                                                                  
  Step 1: Read the data                                                                                                           
                                                                                                                                  

                                                                                                                                  
  data = []                                                                                                                       
  with open('data.txt', 'r') as f:                                                                                                
      next(f)  # skip header                                                                                                      
      for line in f:                                                                                                              
          row = line.strip().split(',')                                                                                           
          data.append({                                                                                                           
              'name': row[0],                                                                                                     
              '国語': int(row[1]),                                                                                                
              '算数': int(row[2]),                                                                                                
              '理科': int(row[3]),                                                                                                
              '社会': int(row[4]),                                                                                                
              '英語': int(row[5])                                                                                                 
          })                                                                                                                      
                                                                                                                                  
                                                                                                                                  
  ---------------------------------------------------------------------------                                                     
  FileNotFoundError                         Traceback (most recent call last)                                                     
  Cell In[3], line 4                                                                                                              
        2 data = []                                                                                                               
        3 print('##active_line2##')                                                                                               
  ----> 4 with open('data.txt', 'r') as f:                                                                                        
        5     print('##active_line3##')                                                                                           
        6     next(f)                                                                                                             
                                                                                                                                  
  File ~/ghq/github.com/tanimon/ai-chatbot-slack/server/.venv/lib/python3.12/site-packages/IPython/core/interactiveshell.py:324,  
  in _modified_open(file, *args, **kwargs)                                                                                        
      317 if file in {0, 1, 2}:                                                                                                   
      318     raise ValueError(                                                                                                   
      319         f"IPython won't let you open fd={file} by default "                                                             
      320         "as it is likely to crash IPython. If you know what you are doing, "                                            
      321         "you can use builtins' open."                                                                                   
      322     )                                                                                                                   
  --> 324 return io_open(file, *args, **kwargs)                                                                                   
                                                                                                                                  
  FileNotFoundError: [Errno 2] No such file or directory: 'data.txt'                                                              
                                                                                                                                  
                                                                                                                                  
  Plan (recap)                                                                                                                    
                                                                                                                                  
   1 Read the data from the text into a data structure (e.g. a list of dictionaries)                                              
   2 Calculate the average score for each subject                                                                                 
   3 Print the results                                                                                                            
                                                                                                                                  
  Step 1: Read the data (retry) Since the data is provided as a text block, I will use it directly instead of reading from a      
  file.                                                                                                                           
                                                                                                                                  

                                                                                                                                  
  data = [                                                                                                                        
      {'name': '田中 太郎', '国語': 85, '算数': 92, '理科': 78, '社会': 71, '英語': 68},                                          
      {'name': '鈴木 花子', '国語': 91, '算数': 67, '理科': 83, '社会': 88, '英語': 75},                                          
      {'name': '佐藤 一郎', '国語': 77, '算数': 89, '理科': 65, '社会': 79, '英語': 82},                                          
      {'name': '高橋 美咲', '国語': 63, '算数': 73, '理科': 91, '社会': 65, '英語': 88},                                          
      {'name': '山本 健太', '国語': 79, '算数': 81, '理科': 69, '社会': 92, '英語': 71},                                          
      {'name': '渡辺 優子', '国語': 88, '算数': 59, '理科': 77, '社会': 68, '英語': 93},                                          
      {'name': '伊藤 大輔', '国語': 72, '算数': 85, '理科': 83, '社会': 76, '英語': 67},                                          
      {'name': '木村 莉子', '国語': 65, '算数': 77, '理科': 89, '社会': 81, '英語': 79},                                          
      {'name': '中村 翔', '国語': 91, '算数': 68, '理科': 72, '社会': 69, '英語': 85},                                            
      {'name': '小林 遥香', '国語': 84, '算数': 91, '理科': 65, '社会': 87                                                        
                                                                                                                                  
                                                                                                                                  
  Cell In[5], line 11                                                                                                             
      {'name': '小林 遥香', '国語': 84, '算数': 91, '理科': 65, '社会': 87                                                        
                                                              ^                                                                   
  SyntaxError: incomplete input                                                                                                   
                                                                                                                                  
                                                                                                                                  
  Plan (recap)                                                                                                                    
                                                                                                                                  
   1 Read the data from the text into a data structure (e.g. a list of dictionaries)                                              
   2 Calculate the average score for each subject                                                                                 
   3 Print the results                                                                                                            
                                                                                                                                  
  Step 1: Read the data (retry) I will complete the data structure.                                                               
                                                                                                                                  

                                                                                                                                  
  data = [                                                                                                                        
      {'name': '田中 太郎', '国語': 85, '算数': 92, '理科': 78, '社会': 71, '英語': 68},                                          
      {'name': '鈴木 花子', '国語': 91, '算数': 67, '理科': 83, '社会': 88, '英語': 75},                                          
      {'name': '佐藤 一郎', '国語': 77, '算数': 89, '理科': 65, '社会': 79, '英語': 82},                                          
      {'name': '高橋 美咲', '国語': 63, '算数': 73, '理科': 91, '社会': 65, '英語': 88},                                          
      {'name': '山本 健太', '国語': 79, '算数': 81, '理科': 69, '社会': 92, '英語': 71},                                          
      {'name': '渡辺 優子', '国語': 88, '算数': 59, '理科': 77, '社会': 68, '英語': 93},                                          
      {'name': '伊藤 大輔', '国語': 72, '算数': 85, '理科': 83, '社会': 76, '英語': 67},                                          
      {'name': '木村 莉子', '国語': 65, '算数': 77, '理科': 89, '社会': 81, '英語': 79},                                          
      {'name': '中村 翔', '国語': 91, '算数': 68, '理科': 72, '社会': 69, '英語': 85},                                            
      {'name': '小林 遥香', '国語': 84, '算数': 91, '理科': 65, '社会': 87, '英語': 71}                                           
  ]                                                                                                                               
                                                                                                                                  

                                                                                                                                  
  Plan (recap)                                                                                                                    
                                                                                                                                  
   1 Read the data from the text into a data structure (e.g. a list of dictionaries)                                              
   2 Calculate the average score for each subject                                                                                 
   3 Print the results                                                                                                            
                                                                                                                                  
  Step 2: Calculate the average score for each subject                                                                            
                                                                                                                                  

                                                                                                                                  
  subjects = ['国語', '算数', '理科', '社会', '英語']                                                                             
  averages = {}                                                                                                                   
                                                                                                                                  
  for subject in subjects:                                                                                                        
      scores = [d[subject] for d in data]                                                                                         
      average = sum(scores) / len(scores)                                                                                         
      averages[subject] = average                                                                                                 
                                                                                                                                  
  print(averages)                                                                                                                 
                                                                                                                                  
                                                                                                                                  
  {'国語': 79.5, '算数': 78.2, '理科': 77.2, '社会': 77.6, '英語': 77.9}                                                          
                                                                                                                                  
                                                                                                                                  
  Plan (recap)                                                                                                                    
                                                                                                                                  
   1 Read the data from the text into a data structure (e.g. a list of dictionaries)                                              
   2 Calculate the average score for each subject                                                                                 
   3 Print the results                                                                                                            
                                                                                                                                  
  Step 3: Print the results (done)                                                                                                
                                                                                                                                  
  The average scores for each subject have been calculated and printed.

Open Interpreterでは複数のLLMに対応するために内部で LiteLLM というライブラリを利用しています。 ブログ執筆時点で Open Interpreterのドキュメント にはBedrockに対応している記述は見当たらなかったのですが、LiteLLMはBedrockに対応している ようだったので、試してみたらちゃんと動いてくれました。

ただし、Llama2を利用した場合はOpen Interpreterを動かすことはできたのですが、正常に動作するコードを生成することはできませんでした。 Llama2を利用してOpen Interpreterを実行したときのログを以下に示します。

Llama2で実行したときのログ(クリックで開けます)
❯ poetry run -C server python server/llama_interpreter.py


  # 平均点を計算する

  # データを読み込み
  data = []
  with open("data.csv", "r") as f:
      for line in f:
          data.append(line.split(","))

  # 各科目の平均点を計算
  subjects = ["国語", "算数", "理科", "社会", "英語"]
  averages = {}
  for subject in subjects:
      scores = [int(x[1]) for x in data if x[0] == subject]
      averages[subject] = sum(scores) / len(scores)

  # 結果を出力
  for subject, average in averages.items()
      print(f"{subject}: {average}")


  ---------------------------------------------------------------------------
  ZeroDivisionError                         Traceback (most recent call last)
  Cell In[3], line 28
       26     scores = [int(x[1]) for x in data if x[0] == subject]
       27     print('##active_line14##')
  ---> 28     averages = sum(scores) / len(scores)
       29 print('##active_line15##')
       30 pass

  ZeroDivisionError: division by zero


  It looks like the code is trying to calculate the average score for each subject, but thelen(scores in the denominator of the fraction is zero, causing a division by zero error. This
  means that thescores list is empty, which suggests that there are no data points for a particular subject.

  To fix this error, you can add a check to make sure that thescores list is not empty before trying to calculate the average. Here's an updated version of the code with the check added:



  # 平均点を計算する

  # データを読み込み
  data = []
  with open("data.csv", "r") as f:
      for line in f:
          data.append(line.split(","))

  # 各科目の平均点を計算
  subjects = ["国語", "算数", "理科", "社会", "英語"]
  averages = {}
  for subject in subjects
      scores = [int(x[1]) for x in data if x[0] == subject]
      if len(scores) > 0:
          averages[subject] = sum(scores) / len(scores)
      else:
          averages[subject] = "No data"

  # 結果を出力
  for subject, average in averages.items():
      print(f"{subject}: {average}")


  国語: No data
  算数: No data
  理科: No data
  社会: No data
  英語: No data


  This code checks the length of the scores list before trying to calculate the average. If the length is zero, it sets the average to "No data" instead of trying to divide by zero.

  It's also worth noting that the data in the CSV file appears to be in a different format than what the code is expecting. The code is expecting the data to be in the format                 
  "name,subject1,subject2,...", but the data in the file is in the format "name,score1,score2,...". You may need to modify the code to match the format of the data in the file.





  # 平均点を計算する

  # データを読み込み
  data = []
  with open("data.csv", "r") as f:
      for line in f:
          data.append(line.split(","))

  # 各科目の平均点を計算
  subjects = ["国語", "算数", "理科", "社会", "英語"]
  averages = {}
  for subject in subjects:
      scores = [int(x[1]) for x in data if x[0] == subject]
      if len(scores) > 0:
          averages[subject] = sum(scores) / len(scores)
      else:
          averages[subject] = "No data"

  # 結果を出力
  for subject, average in averages.items():
      print(f"{subject}: {average}")


  国語: No data
  算数: No data
  理科: No data
  社会: No data
  英語: No data


  This code reads data from a CSV file and calculates the average score for each subject. However, the data in the file does not match the format expected by the code, so the averages are    
  not calculated correctly.

  To fix this, you can modify the code to match the format of the data in the file. For example, you can change the line



  scores = [int(x[1]) for x in data if x[0] == subject]



  to <\s



  scores = [int(x[2]) for x in data if x[0] == subject]



  This will read the score for each subject from the third column of the data, rather than the second column.
  
 
  Alternatively, you can also modify the code to read the data from a different file or change the format of the data in the file to match the format expected by the code.

SlackからOpen Interpreterを動かせるようにSlackボットを作成する

ローカルマシンでOpen Interpreterを動かすことができたので、次はSlackからOpen Interpreterを動かせるようにしてみます。

Slack Appの作成

https://api.slack.com/apps にアクセスし、右上の Create New App ボタンを押下して新規アプリを作成します。

Create an app ダイアログで From an app manifest を選択し、Enter app manifest below ダイアログで以下のマニフェストを入力すると、今回必要な権限を付与したアプリを作成できます。

{
    "display_information": {
        "name": "llama3-chatbot",
        "description": "ボット",
        "background_color": "#000000"
    },
    "features": {
        "bot_user": {
            "display_name": "llama3-chatbot",
            "always_online": false
        }
    },
    "oauth_config": {
        "scopes": {
            "bot": [
                "app_mentions:read",
                "chat:write",
                "channels:history",
                "groups:history",
                "im:history"
            ]
        }
    },
    "settings": {
        "event_subscriptions": {
            "request_url": "https://xxxxxxxxxx.ap-northeast-1.awsapprunner.com/slack/events",
            "bot_events": [
                "app_mention"
            ]
        },
        "org_deploy_enabled": false,
        "socket_mode_enabled": false,
        "token_rotation_enabled": false
    }
}

Slackアプリの詳しい作成方法は Slack Boltのドキュメント が参考になります。

アプリの作成が完了したら、以下の認証情報を控えておいてください。 CDKデプロイ時にパラメータとして設定する必要があります。

  • Basic Information > App Credentialsに記載されているSigning Secret
  • OAuth & Permissions > OAuth Tokens for Your Workspaceに記載されているBot User OAuth Token

Slack Boltアプリケーションの作成

Slack Boltを用いて以下のようなボットを作成しました。 このボットはメンションに反応し、メンション内容をOpen Interpreterに投げ、Open Interpreterの実行結果を返信します。

import logging
import os
import re

import slack_sdk
from interpreter import interpreter
from slack_bolt import App

logging.basicConfig(format="%(asctime)s %(message)s", level=logging.DEBUG)


# Open InterpreterがBedrock経由でLlama3モデルを利用するように設定
interpreter.llm.model = "bedrock/meta.llama3-70b-instruct-v1:0"
interpreter.llm.context_window = 8000  # type: ignore

# LLMが生成したコードを自動実行するように設定
# 実行すべきでないコードが生成される可能性もあるので、この設定を利用する際は注意が必要
interpreter.auto_run = True

# Open Interpreterでus-east-1のBedrock APIが利用されるように設定
os.environ["AWS_REGION_NAME"] = "us-east-1"


def chat(message):
    for chunk in interpreter.chat(message, display=True, stream=True):  # type: ignore
        logging.info(f"chunk: {chunk}")
        yield chunk


def remove_mention(text):
    """メンションを除去する"""

    mention_regex = r"<@.*>"
    return re.sub(mention_regex, "", text).strip()


# ボットトークンと署名シークレットを使ってアプリを初期化する
app = App(
    token=os.environ.get("SLACK_BOT_TOKEN"),
    signing_secret=os.environ.get("SLACK_SIGNING_SECRET"),
)


# ボットへのメンションに対するイベントリスナー
@app.event("app_mention")
def handle_app_mention(event, say, client: slack_sdk.WebClient):
    print(f"app_mention event: {event}")

    text = event["text"]
    channel = event["channel"]
    thread_ts = event.get("thread_ts") or event["ts"]

    response = say(
        channel=channel, thread_ts=thread_ts, text="考え中です...少々お待ちください..."
    )

    payload = remove_mention(text)
    print(f"payload: {payload}")

    text = ""
    prev_text_length = 0

    try:
        for chunk in chat(payload):
            content = chunk.get("content")
            print(f"content: {content}")

            if not content:
                continue

            text += str(content)

            # `msg_too_long` エラー抑制のため、2000文字を超えたら新しいメッセージとして送信する
            if len(text) > 2000:
                text = str(content)
                prev_text_length = len(text)
                response = client.chat_postMessage(
                    channel=channel, thread_ts=thread_ts, text=text
                )

            # 20文字ごとに既存のメッセージを更新することで、Streaming形式で表示させる
            elif len(text) - prev_text_length > 20:
                client.chat_update(
                    channel=channel,
                    ts=response["ts"],
                    text=text,
                )
                prev_text_length = len(text)

        # ループ完了後、未送信の内容が残っていればメッセージを更新
        if len(text) > prev_text_length:
            client.chat_update(
                channel=channel,
                ts=response["ts"],
                text=text,
            )
    except Exception as e:
        logging.error(
            "エラー",
            e,
            e.__cause__,
            e.__class__,
        )
        say(channel=channel, thread_ts=thread_ts, text=f"エラーが発生しました: {e}")


app.start(port=int(os.environ.get("PORT", 3000)))

CDKでインフラを作成

Slackボットを動かすためのインフラをCDKを用いて構築しました。

import * as apprunner from "@aws-cdk/aws-apprunner-alpha";
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";

export class MainStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const slackBotToken = cdk.aws_ssm.StringParameter.valueForStringParameter(
      this,
      "SlackBotToken",
    );
    const slackSignSecret = cdk.aws_ssm.StringParameter.valueForStringParameter(
      this,
      "SlackSignSecret",
    );

    const serverService = new apprunner.Service(this, "ServerService", {
      source: apprunner.Source.fromAsset({
        imageConfiguration: {
          port: 3000,
          environmentVariables: {
            SLACK_BOT_TOKEN: slackBotToken,
            SLACK_SIGNING_SECRET: slackSignSecret,
          },
        },
        asset: new cdk.aws_ecr_assets.DockerImageAsset(this, "ImageAsset", {
          directory: "../server",
          platform: cdk.aws_ecr_assets.Platform.LINUX_AMD64,
        }),
      }),
      cpu: apprunner.Cpu.FOUR_VCPU,
      memory: apprunner.Memory.TEN_GB,
      healthCheck: apprunner.HealthCheck.tcp({}),
      autoDeploymentsEnabled: true,
    });
    serverService.addToRolePolicy(
      new cdk.aws_iam.PolicyStatement({
        effect: cdk.aws_iam.Effect.ALLOW,
        actions: [
          "bedrock:InvokeModel",
          "bedrock:InvokeModelWithResponseStream",
        ],
        resources: ["*"],
      }),
    );
  }
}

Slackボットの認証に必要なシークレットは以下のSSMパラメータから取得し、環境変数経由でSlack Boltアプリケーションに設定しています。 そのため、デプロイ前に以下のSSMパラメータを作成してください。

  • SlackBotToken: Bot User OAuth Tokenの値を設定する
  • SlackSignSecret: Signing Secretの値を設定する

デプロイが完了したら、Slack AppのEvent Subscriptions > Enable EventsのRequest URLのドメインをApp Runnerサービスのデフォルトドメインに変更してください。 このURLのパス部分は/slack/eventsを指定する必要がある点にご注意ください。

注意点

auto_runオプションの利用には注意が必要

Open InterpreterではLLMがコードを生成し、それを実行しますが、LLMが生成したコードの中にはシステムにダメージを与えるものが含まれている可能性があります(例: rm -rf /)。 そのため、Open InterpreterはデフォルトでLLMが生成したコードを実行するか否かをユーザに確認します。

今回作成したコードでは、auto_run オプションを有効化することでこの確認プロセスをスキップするように設定しています。 この設定により不都合が発生する可能性もあるため、実際に本番環境で利用する場合は注意してください。

その他、Open Interpreterの安全な利用に関して 公式ドキュメント に目を通しておくことをおすすめします。

セッション管理すべき

今回はApp RunnerでOpen Interpreterを起動し、SlackボットへのメンションをOpen Interpreterに投げるコードを書きました。 Open Interpreterはプロセス稼働中会話履歴をメモリ上に保持しますが、今回はセッション管理を行っていません。 そのため、以下の問題が発生する可能性があります。

  • 複数人が同時にSlackボットを利用した場合に、会話履歴が混ざってしまう
  • 会話の途中でリクエストが別なコンテナインスタンスで処理されてしまい、会話履歴が引き継がれない
  • しばらく時間をおいて会話を再開した場合に、コンテナインスタンスが終了してしまっており、会話履歴がリセットされてしまう

このような問題を回避するために、Slackのスレッドごとに会話履歴をDBなどに保存し、イベントの処理が終了する度にメモリ上の会話履歴を削除するのが望ましいです。 Open Interpreterのドキュメント に記載されているとおり、以下のように会話履歴の取得・削除・復元が行えます。

# Save messages to 'messages'
messages = interpreter.chat("My name is Killian.")

# Reset interpreter ("Killian" will be forgotten)
interpreter.messages = []

# Resume chat from 'messages' ("Killian" will be remembered)
interpreter.messages = messages

シークレットはよりセキュアに扱うべき

今回はシークレットをSSM Parameter Storeで保持し、環境変数経由でApp Runnerに設定しましたが、この方法だとマネジメントコンソールからApp Runnerサービスの設定を確認することでシークレット値を確認できてしまいます。 シークレットをSecrets Managerに保存し、App Runnerの環境変数ソースとしてSecrets Managerを指定することで、よりセキュアにシークレットを扱うことができます。

[アップデート] AWS App Runner の環境変数ソースで Secrets Manager と SSM パラメータストアがサポートされました | DevelopersIO

所感

今回はOpen InterpreterをローカルマシンとSlackボット経由でそれぞれ実行してみました。 SlackボットでOpen Interpreterを利用するメリットとしては次のようなものが挙げられます。

  • PCだけでなくスマートフォンやタブレットからも利用できる
  • 各自のマシンに環境構築する必要がないため、一度デプロイさえしてしまえば他の人が利用開始する際のハードルが低くなる

一方で、Open InterpreterをSlackボットとして実行するには以下の課題があるようにも感じました。

処理時間が長くなってしまう

Slackボット経由で実行した場合はローカルマシンで実行した場合に比べて5〜6倍程度の時間がかかってしまいました。

ボトルネックを調査したわけではないのですが、この原因はOpen Interpreterの実行結果をインターネットを介して送信しているからではないかと推測しています。 Slackボットとして実行するという形態上、インターネット通信が発生するのは回避しようがないため、ローカルマシンに比べて時間がかかってしまうのは仕方ないかと思います。

処理時間を短縮する方法としては、LLMの処理スピードが超高速と評判の Groq API の利用が考えられます。 LiteLLMはGroq APIに対応しているようなので、Open InterpreterでもGroq APIを利用できそうです。

ファイル連携が手軽に行えない

ローカルマシンでOpen Interpreterにグラフを描画するよう指示すると、Matplotlibなどを用いて描画したグラフを表示してくれます。 同様のことをSlackボットで行おうとすると、Open Interpreterが出力したファイルをS3バケットなどに配置して、インターネットからアクセス可能なURLを発行する必要があります。 技術的には可能ですが、多少面倒です。

参考