ollamaを使ったSlack AIボット開発１

# ollamaを使ったSlack AIボット開発１

## はじめに
研究で画像認識AIの学習をやっている学生です。なんとなく論文を自動で要約してくれるようなBotを作りたいと思い、Bot作成を行ってみることにしました。OpenAIなどのWebサービスのAPIを用いたAiBot開発は様々な人がやっているので、ollamaを使って実装してみることにしました。
それでは、ローカルでLLMの推論を行うことができるollamaを用いてSlackのボット開発を行っていきます。

***
以下の手順で開発を進めます。
- ①ollama導入
- ②ollama APIの使用（この記事ではここまで）
- ③Slack Socket modeを用いたボット作成
- ④ollamaとボットを組み合わせてAIボットの完成

## 環境
- ollama サーバPC：MacBookPro2018(inteli5), Ventura13.2.1
- 開発PC：Ubuntu 24.04.1 LTS(WSL2, windows11)

私は開発PCからMacにSSH接続して作業しましたが、Macだけでも十分作成可能です。

## ①ollama導入
ollamaを使えばローカル環境で(GPUなしで)LLMを動かすことができます。
ollamaはLLMの推論を高速に行えるllama.cppを内部的に用いたアプリで以下のリンクからダウンロードできます。
- [ollamaダウンロードリンク(windows)](https://ollama.com/download/windows)
- [ollamaダウンロードリンク(mac)](https://ollama.com/download/mac) 
- [ollamaダウンロードリンク(linux)](https://ollama.com/download/linux)

基本的にはインストーラの指示に従ってゆくだけで推論を行うことができます。
```
~$ ollama --help
```
<details>
<summary>return</summary>

```
Large language model runner

Usage:
  ollama [flags]
  ollama [command]

Available Commands:
  serve       Start ollama
  create      Create a model from a Modelfile
  show        Show information for a model
  run         Run a model
  stop        Stop a running model
  pull        Pull a model from a registry
  push        Push a model to a registry
  list        List models
  ps          List running models
  cp          Copy a model
  rm          Remove a model
  help        Help about any command

Flags:
  -h, --help      help for ollama
  -v, --version   Show version information

Use "ollama [command] --help" for more information about a command.
```
</details>


```
~$ ollama run llama3.2
```
<details>
<summary>return</summary>

```
>>> Send a message (/? for help)

>>> 札幌市の特産品を教えてください。
札幌市は北海道の中核的な都市であり、多くの特産品があります。

1. **ハマシリ**: ハマシリは野菜が中心の日本料理です。主に夏季に作られる海老や大根などを使用します。札幌
市ではハマシリを初期段階で成功させたことで、北海道と日本の知名度が高まりました。

2. **アイスクリーム**: 札幌には多くのアイスクリーム屋があります。特にマウントエンアイスクリームやサルベ
デールアイスクリームなどは有名です。

3. **カニ**: 札幌市ではカニが豊富な魚種で、食べ物としても知られています。

4. **バラ**: 札幌市には多くの花壇があり、札幌市のバラは日本でも有名です。特に「さくら」や「ひまわり」な
どの季節ごとに生まれるバラが人気です。

5. **カニタマゴ**: カニタマゴはアリゲーターやエビなどをベースにして作るもので、食べ物としても知られてい
ます。札幌市ではこの食材を多く利用しています。

6. **ホットドッグ：北海道のホットドッグは、ほとんどが牛肉を使っています。これらのホットドッグは特に札幌
市で有名です。

7. **マルサカ**: マルサカとは北海道で生産される野菜や水産物を使用したお弁当です。札幌市ではこの食材を多
く利用しています。

8. 札幌みそ：札幌市では「さくら」などの花が生まれた時期に作られる、特有の味の味噌を使ったみそです。

```
</details>

サポートされているモデルは[こちらの](https://ollama.com/library)サイトで確認することができます。基本的にはサポートされているモデルで困ることはなさそうですが、カスタムモデルを使う方法もあるので必要に応じて試すのが良いと思います。

```
~$ ollama list
```
上記のコードでロードしたモデルを確認できます。


## ②ollama APIを使う
ollamaでは先ほどの`ollama run llama3.2`を実行した時点で特に設定を行わなくても、`http://localhost:11434`へアクセス可能です。アクセスすると、Ollama is runningの文字が表示されます。
デフォルトの状態ではローカルホストからの接続のみを受け付ける設定になっているので、環境変数として、
```
export OLLAMA_HOST=0.0.0.0
export OLLAMA_ORIGINS=192.168.*
```
を設定する必要があります。これはローカルネットワーク内の`192.168.hoge.hoge`のアドレスを持つ機器からのアクセスを許可する設定です。
この設定を終えた状態でOllamaのAPIサーバを起動させます。

```
~$ ollama serve
```
この状態で`http://LLMサーバアドレス:11434`にアクセス(ブラウザで入力)してOllama is runningが表示されれば接続がうまくいっています。この場合に
```
Error: listen tcp 0.0.0.0:11434: bind: address already in use
```
のように表示されることがありますが、これはポート11434をほかのプロセスが使用しているためにListenできないと言っています。先ほどの`ollama run llama3.2`の後すぐに実行するとこのようになることが多いです。この場合は、
```
~$ lsof -i :11434
 ```
<details><summary>ex. return</summary>

```
COMMAND  PID         USER   FD   TYPE             DEVICE SIZE/OFF NODE NAME
ollama  8524       myname   3u  IPv4              gggggg      0t0  TCP localhost:11434 (LISTEN)
```
</details>

 として使用中のプロセスを調べてプロセスをkillしてください。
 その後にもう一度`ollama serve`を実行すればうまくいくはずです。

### APIについて
- https://www.postman.com/postman-student-programs/ollama-api/documentation/suc47x8/ollama-rest-api

- `http://localhost:11434/api/generate`エンドポイントへのリクエストでモデルの推論結果がストリーミング形式で返ってくるので、それを表示させれば終わりです。
- `http://localhost:11434/api/chat`エンドポイントでは文脈を維持した会話が可能です。システムプロンプトを作れます。


`http://localhost:11434/api/generate`とPythonを利用してコマンドライン上で簡単に単発の質問に対する応答を得る機能を作成します。(Python 3.12.3)
```python
import requests
import json
from config import API_SERVER_URL, MODEL


def generate_text(prompt):
    url = API_SERVER_URL + "/api/generate"
    headers = {
        "accept": "application/json",
        "Content-Type": "application/json",
    }
    data = {
        "model": MODEL,
        "prompt": prompt,
    }

    response = requests.post(url, headers=headers, json=data, stream=True)

    full_response = ""
    for line in response.iter_lines(decode_unicode=True):
        if not line:
            continue
        try:
            response_json = json.loads(line)
            text_chunk = response_json.get("response", "")
            print(text_chunk, end="", flush=True)
            full_response += text_chunk
        except json.JSONDecodeError as e:
            print(f"\n[JSON decode error]: {e}")
    return full_response


if __name__ == "__main__":
    prompt = "札幌市の名物について教えてください。"
    user_input = input("プロンプトを入力してください (デフォルト: '札幌市の名物について教えてください。'): ")
    if user_input:
        prompt = user_input
    response = generate_text(prompt)
    print("\n")
```

以下には`http://localhost:11434/api/chat`を用いた同様のサンプルコードを記述します。

```python
import requests
import json
from config import API_SERVER_URL, MODEL


def stream_chat(messages):
    url = API_SERVER_URL + "/api/chat"
    headers = {
        "accept": "application/json",
        "Content-Type": "application/json",
    }
    data = {
        "model": MODEL,
        "messages": messages,
        "stream": True,
    }

    response = requests.post(url, headers=headers, json=data, stream=True)

    full_response = ""
    for line in response.iter_lines(decode_unicode=True):
        if not line:
            continue
        try:
            response_json = json.loads(line)
            text_chunk = response_json.get("message", {}).get("content", "")
            print(text_chunk, end="", flush=True)
            full_response += text_chunk
        except json.JSONDecodeError as e:
            print(f"\n[JSON decode error]: {e}")
    return full_response


if __name__ == "__main__":

    messages = [
        {
            "role": "system",
            "content": "あなたは日本語を流ちょうに話す猫です。語尾には「にゃ」をつけてください。",
        }
    ]

    print("Ollama Chat CLI (終了するには 'exit' と入力)")
    while True:
        user_input = input("\n>>> ")
        if user_input.lower() in ["exit", "quit"]:
            print("チャットを終了します。")
            break

        messages.append({
            "role": "user",
            "content": user_input
        })

        print("アシスタント: ", end="")
        assistant_reply = stream_chat(messages)
        print("")  # 改行

        messages.append({
            "role": "assistant",
            "content": assistant_reply
        })
```

- 多数のユーザがいる場合にはaiohttpで非同期処理をするとよいかもしれませんが、とりあえずここまでとしてSlackBotの方に進みます。