この AI/ML ハッカソンの投稿では、昨年の Docker AI/ML ハッカソンで優勝した別のプロジェクトを共有したいと思います。 今回は、 Justin Garrison 氏が作成した特別賞受賞者である Local LLM Messenger に飛び込みます。
開発者は、人工知能 (AI) の力をすべての人に届けるために、限界を押し広げています。 エキサイティングなアプローチの 1 つは、大規模言語モデル (LLM) を Slack や iMessage などの使い慣れたメッセージング プラットフォームと統合することです。 これは利便性だけではありません。これらのプラットフォームを、強力なAIツールと対話するためのランチパッドに変えることです。
想像してみてください: 簡単なコード スニペットや、コーディングの問題に対する解決策をブレインストーミングするための支援が必要です。 メッセージングアプリにLLMが統合されているため、使い慣れたインターフェース内で直接AIアシスタントとチャットして、創造的なアイデアを生み出したり、解決策のブレーンストーミングを支援したりすることができます。 複雑なコマンドや不便なインターフェースはもう必要ありません - AIの力を解き放つための自然な会話だけです。
メッセージングプラットフォームとの統合は、特にmacOSユーザーにとっては時間のかかる作業です。 そこで、Local LLM Messenger(LoLLMM)の出番となり、iMessageを介してAIと接続するための合理化されたソリューションを提供します。
LoLLM Messengerの特徴は何ですか?
次のデモは、AI/ML Hackathon に提出されたもので、LoLLM Messenger の概要を示しています (図 1)。
LoLLM Messengerボットを使用すると、コンピューター上で直接実行されているGenerative AI(GenAI)モデルにiMessageを送信できます。 このアプローチにより、複雑なセットアップやクラウドサービスが不要になり、開発者はローカルでLLMを簡単に試すことができます。
LoLLM Messengerの主な機能
LoLLM Messengerには、次のような同様のプロジェクトの中で際立った優れた機能が含まれています。
- ローカル実行:コンピューター上で実行されるため、クラウドベースのサービスが不要になり、データのプライバシーが確保されます。
- スケーラビリティ:複数のAIモデルを同時に処理するため、ユーザーはさまざまなモデルを試したり、モデルを簡単に切り替えたりすることができます。
- ユーザーフレンドリーなインターフェース:シンプルで直感的なインターフェースを提供し、あらゆるスキルレベルのユーザーがアクセスできます。
- Sendblueとの統合: Sendblueとシームレスに統合され、ユーザーはiMessageをボットに送信し、受信トレイで直接応答を受け取ることができます。
- ChatGPTのサポート:GPT-3をサポートします。5 ターボとDALL-Eは 2 モデルで、ユーザーは強力なAI機能にアクセスできます。
- カスタマイズ: ユーザーは、使用可能なコマンドを変更し、独自の AI モデルを統合することで、ボットの動作をカスタマイズできます。
それはどのように機能しますか?
図 2 に示すアーキテクチャ図は、LoLLM Messenger プロジェクト内のコンポーネントと相互作用の概要を示しています。 これは、メインアプリケーション、AIモデル、メッセージングプラットフォーム、および外部APIがどのように連携して、ユーザーがコンピューターで実行されているAIモデルにiMessageを送信できるようにするかを示しています。
Docker、Sendblue、Ollamaを活用することで、LoLLM Messengerは、クラウドベースのサービスを必要とせずにAIモデルを探索したいユーザーにシームレスで効率的なソリューションを提供します。 LoLLM Messengerは 、Docker Compose を利用して必要なサービスを管理します。
Docker Compose は、メイン アプリケーション、ngrok (安全なトンネルを作成するため)、Ollama (メッセージング アプリと AI モデルの間のギャップを埋めるサーバー) など、複数のコンテナーのセットアップと構成を処理することで、プロセスを簡素化します。
テクニカルスタック
LoLLM Messengerの技術スタックには、次のものが含まれます。
- Lollmmサービス:このサービスは、メインアプリケーションの実行を担当します。 受信 iMessage を処理し、ユーザー要求を処理し、AI モデルと対話します。 lollmmサービスは、テキストと画像生成のための強力なAIモデルであるOllamaモデルと通信します。
- Ngrok: このサービスは、
ngrok
を使用してメイン アプリケーションのポート 8000をインターネットに公開するために使用されます。Alpine イメージで実行され、ポート 8000 から ngrok トンネルにトラフィックを転送します。 サービスは、ホスト・ネットワーク・モードで実行されるように設定されています。 - オラマ: このサービスは、テキストと画像の生成のための強力な AI モデルである Ollama モデルを実行します。 ポート 11434 でリッスンし、
./run/ollama
から/home/ollama
までのボリュームをマウントします。 このサービスは GPU リソースを使用してデプロイするように設定されており、利用可能な場合は NVIDIA GPU を利用できるようにします。 - センドブルー: このプロジェクトはSendblueと統合され、iMessageを処理します。 Sendblueを設定するには、APIキーとAPIシークレットを
app/.env
に追加します ファイルを作成し、電話番号をSendblueの連絡先として追加します。
はじめ
開始するには、次のコンポーネントをインストールして設定していることを確認します。
- 最新の Dockerデスクトップをインストールします。
- Sendblue https://app.sendblue.co/auth/login にご登録ください。
- お好みの方法を使用して ngrok アカウントを作成し、authtoken https://dashboard.ngrok.com/signup を取得します。
リポジトリのクローンを作成する
ターミナル ウィンドウを開き、次のコマンドを実行して、このサンプル アプリケーションを複製します。
git clone https://github.com/dockersamples/local-llm-messenger
これで、 local-llm-messenger
ディレクトリに次のファイルが作成されます。
.
├── LICENSE
├── README.md
├── app
│ ├── Dockerfile
│ ├── Pipfile
│ ├── Pipfile.lock
│ ├── default.ai
│ ├── log_conf.yaml
│ └── main.py
├── docker-compose.yaml
├── img
│ ├── banner.png
│ ├── lasers.gif
│ └── lollm-demo-1.gif
├── justfile
└── test
├── msg.json
└── ollama.json
4 directories, 15 files
/app
ディレクトリの下にあるスクリプトmain.py
ファイルは、FastAPIフレームワークを使用してAI搭載のメッセージングアプリケーション用のWebサーバーを作成するPythonスクリプトです。このスクリプトは、OpenAIのGPT-3 モデルとOllamaエンドポイントと対話して応答を生成します。 SendblueのAPIを使用してメッセージを送信します。
このスクリプトは、FastAPI、リクエスト、ロギング、その他の必要なモジュールなど、必要なライブラリを最初にインポートします。
from dotenv import load_dotenv
import os, requests, time, openai, json, logging
from pprint import pprint
from typing import Union, List
from fastapi import FastAPI
from pydantic import BaseModel
from sendblue import Sendblue
このセクションでは、API キー、コールバック URL、Ollama API エンドポイント、最大コンテキストと単語数の制限などの設定変数を設定します。
SENDBLUE_API_KEY = os.environ.get("SENDBLUE_API_KEY")
SENDBLUE_API_SECRET = os.environ.get("SENDBLUE_API_SECRET")
openai.api_key = os.environ.get("OPENAI_API_KEY")
OLLAMA_API = os.environ.get("OLLAMA_API_ENDPOINT", "http://ollama:11434/api")
# could also use request.headers.get('referer') to do dynamically
CALLBACK_URL = os.environ.get("CALLBACK_URL")
MAX_WORDS = os.environ.get("MAX_WORDS")
次に、スクリプトはログ設定を実行し、ログレベルを INFO に設定します。 app.log
という名前のファイルにメッセージをログに記録するためのファイルハンドラを作成します。
次に、AI モデルとの対話、コンテキストの管理、メッセージの送信、コールバックの処理、スラッシュ コマンドの実行など、さまざまな関数を定義します。
def set_default_model(model: str):
try:
with open("default.ai", "w") as f:
f.write(model)
f.close()
return
except IOError:
logger.error("Could not open file")
exit(1)
def get_default_model() -> str:
try:
with open("default.ai") as f:
default = f.readline().strip("\n")
f.close()
if default != "":
return default
else:
set_default_model("llama2:latest")
return ""
except IOError:
logger.error("Could not open file")
exit(1)
def validate_model(model: str) -> bool:
available_models = get_model_list()
if model in available_models:
return True
else:
return False
def get_ollama_model_list() -> List[str]:
available_models = []
tags = requests.get(OLLAMA_API + "/tags")
all_models = json.loads(tags.text)
for model in all_models["models"]:
available_models.append(model["name"])
return available_models
def get_openai_model_list() -> List[str]:
return ["gpt-3.5-turbo", "dall-e-2"]
def get_model_list() -> List[str]:
ollama_models = []
openai_models = []
all_models = []
if "OPENAI_API_KEY" in os.environ:
# print(openai.Model.list())
openai_models = get_openai_model_list()
ollama_models = get_ollama_model_list()
all_models = ollama_models + openai_models
return all_models
DEFAULT_MODEL = get_default_model()
if DEFAULT_MODEL == "":
# This is probably the first run so we need to install a model
if "OPENAI_API_KEY" in os.environ:
print("No default model set. openai is enabled. using gpt-3.5-turbo")
DEFAULT_MODEL = "gpt-3.5-turbo"
else:
print("No model found and openai not enabled. Installing llama2:latest")
pull_data = '{"name": "llama2:latest","stream": false}'
try:
pull_resp = requests.post(OLLAMA_API + "/pull", data=pull_data)
pull_resp.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
set_default_model("llama2:latest")
DEFAULT_MODEL = "llama2:latest"
if validate_model(DEFAULT_MODEL):
logger.info("Using model: " + DEFAULT_MODEL)
else:
logger.error("Model " + DEFAULT_MODEL + " not available.")
logger.info(get_model_list())
pull_data = '{"name": "' + DEFAULT_MODEL + '","stream": false}'
try:
pull_resp = requests.post(OLLAMA_API + "/pull", data=pull_data)
pull_resp.raise_for_status()
except requests.exceptions.HTTPError as err:
raise SystemExit(err)
def set_msg_send_style(received_msg: str):
"""Will return a style for the message to send based on matched words in received message"""
celebration_match = ["happy"]
shooting_star_match = ["star", "stars"]
fireworks_match = ["celebrate", "firework"]
lasers_match = ["cool", "lasers", "laser"]
love_match = ["love"]
confetti_match = ["yay"]
balloons_match = ["party"]
echo_match = ["what did you say"]
invisible_match = ["quietly"]
gentle_match = []
loud_match = ["hear"]
slam_match = []
received_msg_lower = received_msg.lower()
if any(x in received_msg_lower for x in celebration_match):
return "celebration"
elif any(x in received_msg_lower for x in shooting_star_match):
return "shooting_star"
elif any(x in received_msg_lower for x in fireworks_match):
return "fireworks"
elif any(x in received_msg_lower for x in lasers_match):
return "lasers"
elif any(x in received_msg_lower for x in love_match):
return "love"
elif any(x in received_msg_lower for x in confetti_match):
return "confetti"
elif any(x in received_msg_lower for x in balloons_match):
return "balloons"
elif any(x in received_msg_lower for x in echo_match):
return "echo"
elif any(x in received_msg_lower for x in invisible_match):
return "invisible"
elif any(x in received_msg_lower for x in gentle_match):
return "gentle"
elif any(x in received_msg_lower for x in loud_match):
return "loud"
elif any(x in received_msg_lower for x in slam_match):
return "slam"
else:
return
Msg
と Callback
の 2 つのクラスは、受信メッセージとコールバック データの構造を表すために定義されています。このコードには、デフォルトモデルの設定、モデルの検証、Sendblue APIとの対話、メッセージの処理など、メッセージングプラットフォームのさまざまな側面を処理するためのさまざまな関数とクラスも含まれています。 また、スラッシュ コマンドの処理、コンテキストからのメッセージの作成、ファイルへのコンテキストの追加を行う関数も含まれています。
class Msg(BaseModel):
accountEmail: str
content: str
media_url: str
is_outbound: bool
status: str
error_code: int | None = None
error_message: str | None = None
message_handle: str
date_sent: str
date_updated: str
from_number: str
number: str
to_number: str
was_downgraded: bool | None = None
plan: str
class Callback(BaseModel):
accountEmail: str
content: str
is_outbound: bool
status: str
error_code: int | None = None
error_message: str | None = None
message_handle: str
date_sent: str
date_updated: str
from_number: str
number: str
to_number: str
was_downgraded: bool | None = None
plan: str
def msg_openai(msg: Msg, model="gpt-3.5-turbo"):
"""Sends a message to openai"""
message_with_context = create_messages_from_context("openai")
# Add the user's message and system context to the messages list
messages = [
{"role": "user", "content": msg.content},
{"role": "system", "content": "You are an AI assistant. You will answer in haiku."},
]
# Convert JSON strings to Python dictionaries and add them to messages
messages.extend(
[
json.loads(line) # Convert each JSON string back into a dictionary
for line in message_with_context
]
)
# Send the messages to the OpenAI model
gpt_resp = client.chat.completions.create(
model=model,
messages=messages,
)
# Append the system context to the context file
append_context("system", gpt_resp.choices[0].message.content)
# Send a message to the sender
msg_response = sendblue.send_message(
msg.from_number,
{
"content": gpt_resp.choices[0].message.content,
"status_callback": CALLBACK_URL,
},
)
return
def msg_ollama(msg: Msg, model=None):
"""Sends a message to the ollama endpoint"""
if model is None:
logger.error("Model is None when calling msg_ollama")
return # Optionally handle the case more gracefully
ollama_headers = {"Content-Type": "application/json"}
ollama_data = (
'{"model":"' + model +
'", "stream": false, "prompt":"' +
msg.content +
" in under " +
str(MAX_WORDS) + # Make sure MAX_WORDS is a string
' words"}'
)
ollama_resp = requests.post(
OLLAMA_API + "/generate", headers=ollama_headers, data=ollama_data
)
response_dict = json.loads(ollama_resp.text)
if ollama_resp.ok:
send_style = set_msg_send_style(msg.content)
append_context("system", response_dict["response"])
msg_response = sendblue.send_message(
msg.from_number,
{
"content": response_dict["response"],
"status_callback": CALLBACK_URL,
"send_style": send_style,
},
)
else:
msg_response = sendblue.send_message(
msg.from_number,
{
"content": "I'm sorry, I had a problem processing that question. Please try again.",
"status_callback": CALLBACK_URL,
},
)
return
app/
ディレクトリに移動し、環境変数を追加するための新しいファイルを作成します。
touch .env
SENDBLUE_API_KEY=your_sendblue_api_key
SENDBLUE_API_SECRET=your_sendblue_api_secret
OLLAMA_API_ENDPOINT=http://host.docker.internal:11434/api
OPENAI_API_KEY=your_openai_api_key
次に、ngrok authtoken を Docker Compose ファイルに追加します。 authtoken は、このリンクから取得できます。
services:
lollm:
build: ./app
# command:
# - sleep
# - 1d
ports:
- 8000:8000
env_file: ./app/.env
volumes:
- ./run/lollm:/run/lollm
depends_on:
- ollama
restart: unless-stopped
network_mode: "host"
ngrok:
image: ngrok/ngrok:alpine
command:
- "http"
- "8000"
- "--log"
- "stdout"
environment:
- NGROK_AUTHTOKEN=2i6iXXXXXXXXhpqk1aY1
network_mode: "host"
ollama:
image: ollama/ollama
ports:
- 11434:11434
volumes:
- ./run/ollama:/home/ollama
network_mode: "host"
アプリケーションスタックの実行
次に、次のようにアプリケーション スタックを実行できます。
$ docker compose up
次のような出力が表示されます。
[+] Running 4/4
✔ Container local-llm-messenger-ollama-1 Create... 0.0s
✔ Container local-llm-messenger-ngrok-1 Created 0.0s
✔ Container local-llm-messenger-lollm-1 Recreat... 0.1s
! lollm Published ports are discarded when using host network mode 0.0s
Attaching to lollm-1, ngrok-1, ollama-1
ollama-1 | 2024/06/20 03:14:46 routes.go:1011: INFO server config env="map[OLLAMA_DEBUG:false OLLAMA_FLASH_ATTENTION:false OLLAMA_HOST:http://0.0.0.0:11434 OLLAMA_KEEP_ALIVE: OLLAMA_LLM_LIBRARY: OLLAMA_MAX_LOADED_MODELS:1 OLLAMA_MAX_QUEUE:512 OLLAMA_MAX_VRAM:0 OLLAMA_MODELS:/root/.ollama/models OLLAMA_NOHISTORY:false OLLAMA_NOPRUNE:false OLLAMA_NUM_PARALLEL:1 OLLAMA_ORIGINS:[http://localhost https://localhost http://localhost:* https://localhost:* http://127.0.0.1 https://127.0.0.1 http://127.0.0.1:* https://127.0.0.1:* http://0.0.0.0 https://0.0.0.0 http://0.0.0.0:* https://0.0.0.0:* app://* file://* tauri://*] OLLAMA_RUNNERS_DIR: OLLAMA_TMPDIR:]"
ollama-1 | time=2024-06-20T03:14:46.308Z level=INFO source=images.go:725 msg="total blobs: 0"
ollama-1 | time=2024-06-20T03:14:46.309Z level=INFO source=images.go:732 msg="total unused blobs removed: 0"
ollama-1 | time=2024-06-20T03:14:46.309Z level=INFO source=routes.go:1057 msg="Listening on [::]:11434 (version 0.1.44)"
ollama-1 | time=2024-06-20T03:14:46.309Z level=INFO source=payload.go:30 msg="extracting embedded files" dir=/tmp/ollama2210839504/runners
ngrok-1 | t=2024-06-20T03:14:46+0000 lvl=info msg="open config file" path=/var/lib/ngrok/ngrok.yml err=nil
ngrok-1 | t=2024-06-20T03:14:46+0000 lvl=info msg="open config file" path=/var/lib/ngrok/auth-config.yml err=nil
ngrok-1 | t=2024-06-20T03:14:46+0000 lvl=info msg="starting web service" obj=web addr=0.0.0.0:4040 allow_hosts=[]
ngrok-1 | t=2024-06-20T03:14:46+0000 lvl=info msg="client session established" obj=tunnels.session
ngrok-1 | t=2024-06-20T03:14:46+0000 lvl=info msg="tunnel session started" obj=tunnels.session
ngrok-1 | t=2024-06-20T03:14:46+0000 lvl=info msg="started tunnel" obj=tunnels name=command_line addr=http://localhost:8000 url=https://94e1-223-185-128-160.ngrok-free.app
ollama-1 | time=2024-06-20T03:14:48.602Z level=INFO source=payload.go:44 msg="Dynamic LLM libraries [cpu cuda_v11]"
ollama-1 | time=2024-06-20T03:14:48.603Z level=INFO source=types.go:71 msg="inference compute" id=0 library=cpu compute="" driver=0.0 name="" total="7.7 GiB" available="3.9 GiB"
lollm-1 | INFO: Started server process [1]
lollm-1 | INFO: Waiting for application startup.
lollm-1 | INFO: Application startup complete.
lollm-1 | INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
ngrok-1 | t=2024-06-20T03:16:58+0000 lvl=info msg="join connections" obj=join id=ce119162e042 l=127.0.0.1:8000 r=[2401:4900:8838:8063:f0b0:1866:e957:b3ba]:54384
lollm-1 | OLLAMA API IS http://host.docker.internal:11434/api
lollm-1 | INFO: 2401:4900:8838:8063:f0b0:1866:e957:b3ba:0 - "GET / HTTP/1.1" 200 OK
NVIDIA GPU を搭載していないシステムでテストする場合は、Compose ファイルの deploy
属性をスキップできます。
ngrok エンドポイントの出力を監視します。 私たちの場合、それは示しています: https://94e1-223-185-128-160.ngrok-free.app/
次に、次の ngrok webhook URL に ngrok webhook URL に /msg
を追加します: https://94e1-223-185-128-160.ngrok-free.app/
次に、SendblueのwebhooksURLセクションに追加して保存します(図 3)。 ngrok
サービスは、ポート 8000 で lollmm
サービスを公開し、ngrok.io
ドメインを使用してパブリック インターネットへのセキュリティで保護されたトンネルを提供するように構成されています。
ngrok サービス ログは、Web サービスを開始し、トンネルとのクライアント セッションを確立したことを示します。 また、トンネル セッションが開始され、lollmm サービスで正常に確立されたことも示しています。
ngrok サービスは、ngrok サービスにアクセスするために必要な、指定された ngrok 認証トークンを使用するように構成されています。 全体として、ngrok サービスは正しく実行されており、lollmm サービスへの安全なトンネルを確立できます。
ngrok コンテナを実行するときにエラーログがないことを確認します (図 4)。
LoLLM Messenger コンテナがアクティブに稼働していることを確認します (図 5)。
ログは、Ollama サービスが指定されたポート (11434) を開き、着信接続をリッスンしていることを示しています。 ログには、Ollama サービスがホスト マシンの /home/ollama
ディレクトリをコンテナ内の /home/ollama
ディレクトリにマウントしたことも示されています。
全体として、Ollama サービスは正しく実行されており、推論のための AI モデルを提供する準備ができています。
機能のテスト
lollmサービスの機能をテストするには、まず連絡先番号をSendblueダッシュボードに追加する必要があります。 その後、Sendblue番号にメッセージを送信し、lollmmサービスからの応答を観察できるはずです(図 6)。
Sendblueプラットフォームは、lollmmサービスの /msg
エンドポイントにHTTPリクエストを送信し、lollmmサービスはこれらのリクエストを処理して適切なレスポンスを返します。
- lollmm サービスは、ポート 8000でリッスンするように設定されています。
- ngrok トンネルが開始され、 https://94e1-223-185-128-160.ngrok-free.app などの公開 URL が提供されます。
- lollmm サービスは、ルート パス (
/
) やその他のパス (/favicon.ico
など) への GET 要求を含む HTTP 要求を ngrok トンネルから受信します。/predict
、/mdg
、および/msg
。 - lollmm サービスは、これらのリクエストに対して、成功したリクエストの場合は 200 OK、存在しないパスへのリクエストの場合は 404 Not Found など、適切な HTTP ステータスコードで応答します。
- ngrok トンネルは、クライアントが ngrok トンネルを介して lollmm サービスに接続していることを示す結合接続をログに記録します。
/list
を入力して LLM と初めてチャットするときは (図 7)、次のようにログを確認できます。
ngrok-1 | t=2024-07-09T02:34:30+0000 lvl=info msg="join connections" obj=join id=12bd50a8030b l=127.0.0.1:8000 r=18.223.220.3:44370
lollm-1 | OLLAMA API IS http://host.docker.internal:11434/api
lollm-1 | INFO: 18.223.220.3:0 - "POST /msg HTTP/1.1" 200 OK
ngrok-1 | t=2024-07-09T02:34:53+0000 lvl=info msg="join connections" obj=join id=259fda936691 l=127.0.0.1:8000 r=18.223.220.3:36712
lollm-1 | INFO: 18.223.220.3:0 - "POST /msg HTTP/1.1" 200 OK
次に、「/install codellama:latest
」と入力してcodellama
モデルをインストールしましょう(図8)。
次に示すように、デフォルトモデルを codellama:latest
に設定すると、次のコンテナログが表示されます。
ngrok-1 | t=2024-07-09T03:39:23+0000 lvl=info msg="join connections" obj=join id=026d8fad5c87 l=127.0.0.1:8000 r=18.223.220.3:36282
lollm-1 | setting default model
lollm-1 | INFO: 18.223.220.3:0 - "POST /msg HTTP/1.1" 200 OK
lollmmサービスは正しく実行されており、ngrokトンネルからのHTTPリクエストを処理できます。 ngrok トンネル URL を使用して、HTTP リクエストを適切なパスに送信することで lollmm サービスの機能をテストできます (図 9)。
結論
LoLLM Messengerは、メッセージングアプリ内のLLM統合の限界を押し広げたいと考えている開発者や愛好家にとって貴重なツールです。 これにより、開発者は特定のニーズに合わせてカスタムチャットボットを作成したり、メッセージにリアルタイムの感情分析を追加したり、メッセージングエクスペリエンスでまったく新しいAI機能を探索したりできます。
まず、 GitHub の LoLLM Messenger プロジェクト を探索し、ローカル LLM の可能性を発見できます。
さらに詳しく
- Docker Newsletter を購読してください。
- AI/ML ハッカソン コレクションをお読みください。
- Docker デスクトップの最新リリースを入手します。
- 次のものに投票してください! 公開ロードマップをご覧ください。
- 質問がありますか? Docker コミュニティがお手伝いします。
- ドッカーは初めてですか? 始めましょう。