Snowflake Cortex LLM関数を使ってチャットボットを作成して圧を掛けてみた

## はじめに

Snowflakeに[Snowflake Cortex (LLM Functions)](https://docs.snowflake.com/user-guide/snowflake-cortex/llm-functions)が搭載され、AIの機能が気軽に使用できるようになりました。

簡単に説明すると、対話、要約、翻訳等のタスクが、Snowflakeの組み込み関数を呼び出すだけで使えるというものです。

[Streamlit (Streamlit in Snowflake/SinS)](https://docs.snowflake.com/ja/developer-guide/streamlit/about-streamlit)とも組み合わせることで、簡単なGUIと組み合わせたアプリも作成できます。

ここでは、対話を実現する[COMPLETE関数](https://docs.snowflake.com/user-guide/snowflake-cortex/llm-functions#label-cortex-llm-complete)を用いて、ゆるーい自前チャットボットをカジュアルに作成してみました。

## 作ってみた

### アカウント準備

Cortexが利用できるリージョンにアカウントを新規作成しました。

- クラウド : AWS
- リージョン : US West (Oregon)
- エディション : スタンダード


#### 参考

- [Availability(利用できるリージョン)](https://docs.snowflake.com/user-guide/snowflake-cortex/llm-functions#availability)

### 事前準備

Snowsight上で操作します。

ウェアハウス、データベース、スキーマを作成します。

```sql
use role accountadmin;
create warehouse my_wh with warehouse_size='x-small';
grant usage on warehouse my_wh to role sysadmin;

use role sysadmin;
use warehouse my_wh;

create database test_db;
create schema test_schema;
```

対話のやり取りを記録するためのテーブル(履歴テーブル)を作成します。

```sql
create table test_db.test_schema.history (
    role varchar,
    message varchar
);
```

ここではSYSADMINロールを用いて、左のメニューより「+ > Streamlit App」を選択して、Streamlit Appを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617410/0ee5b71c-c928-59f9-bddb-17fa56b5d059.png)

### Streamlit Appのコード

Streamlit Appのコード(Python)を作成します。
モデルはとりあえず「snowflake-arctic」を使ってみました。

今回の実装では、こちらから与えた事前情報を活用しつつ、過去のやりとりも踏まえて回答できるようにしています。

RAGの構成として含まれるベクトルデータベースといった実装は入れていないので簡易的なもの(プロンプトエンジニアリング)になります。

また、簡単な情報を与えてのやり取りを例に用いているため、token上限の制御は含んでおりません。

コード全体は空行含めて120行くらいで、少なめかと思います。

<details><summary>コード全体</summary>

```python
import streamlit as st
from snowflake.snowpark.context import get_active_session

session = get_active_session()

if "chat_log" not in st.session_state:
    st.session_state.chat_log = []

def get_chat_logs():
    chat_log_list = []    
    dialog_format = "{{'role': '{user}', 'content': '{message}' }}"

    results_df = session.sql("select * from test_db.test_schema.history").collect()

    if not len(results_df):
        return
    
    for rows in results_df:
        user = rows["ROLE"]
        message = rows["MESSAGE"]
        dialog = dialog_format.format(
            user = user,
            message = message
        )
        chat_log_list.append(dialog)
    
    return ",\n".join(chat_log_list) + ","

def response_chat(input_info: str, message: str):
    
    sql = """
    select response:"choices"[0]:"messages" as ANSWER from(
        select snowflake.cortex.complete(
            'snowflake-arctic',
                [
                   {{'role': 'system', 'content': '{input_info}' }},
                   {chat_logs}
                   {{'role': 'user', 'content': '{message}' }}
                ],
                {{'temperature': 0.7}}
        ) as response
    )
    """

    caht_logs = get_chat_logs()
    result = session.sql(
        sql.format(
            input_info = input_info,
            chat_logs = caht_logs or "",
            message = message
        )   
    )

    return result

def record_history(messages: dict):

    for key in messages.keys():
        role = key
        content = messages[key]
    
        sql = """
        insert into test_db.test_schema.history
        values (
            '{role}',
            '{content}'
        );
        """

        session.sql(
            sql.format(
                role = role,
                content = content
            )   
        ).collect()
    
    return

st.title("お試しチャットアプリ")

input_info = """
あなたは、次に記載する [事前情報] を元に質問に回答してください。
また、会話が進んでいく中で途中であなたが回答した内容や、userから指示されて追加した情報があれば、その履歴も活用して回答をしてください。
なお、回答には余計な解説等は含めず、単純明快、シンプルにお願いします。

[事前情報]
"""

st.markdown("## 事前情報")
input_info = input_info + st.text_area(
    "",
    "私の好きな物は、「カレー、プリン、コーヒー」です。"
) + "\n"

st.markdown("## チャット履歴")

user_message = st.chat_input("ここにメッセージを入力")
if user_message:
    for chat in st.session_state.chat_log:
        with st.chat_message(chat["role"]):
            st.write(chat["content"])
    
    with st.chat_message("user"):
        st.write(user_message)

    response = response_chat(input_info, user_message)

    assistant_message = response.collect()[0]["ANSWER"].replace("\"", "")
    st.chat_message("assistant").write(assistant_message)

    st.session_state.chat_log.append({"role": "user", "content": user_message})
    st.session_state.chat_log.append({"role": "assistant", "content": assistant_message})

    record_history(
        {
            "user": user_message,
            "assistant": assistant_message
        }
    )
```
</details>

#### 参考

- [Model restrictions(モデルの一覧と制限)](https://docs.snowflake.com/user-guide/snowflake-cortex/llm-functions#model-restrictions)

## 動作確認

作ってみたチャットアプリを動かしてみます。

### 初期画面

こんな感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617410/45414d61-1f7a-ff7f-5432-824c815f02d7.png)

ここで、事前情報を少し変更してから始めてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617410/18bf4c5d-69c7-ea1f-4892-f0fb1cb92263.png)

この時、「[st.text_area](https://docs.streamlit.io/develop/api-reference/widgets/st.text_area)」で編集したテキストは「Command + Enter(Macの場合)」を押さないと反映されないので忘れずに実行しておきます。

### やりとり開始

メッセージを送ってみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617410/02075be7-7670-4d4e-796d-ea11fd1ab283.png)

いいですね。

続けてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617410/c1bb40aa-8ead-0c1d-1458-a515b7b9dd32.png)

ここで、情報を加えるように指示してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617410/04b80022-169a-293e-53bf-b739d538efed.png)

覚えてくれたようです。
では、改めて質問してみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617410/05f9c294-9b50-d769-b1cf-2807cf51e149.png)

覚えた情報を使いつつ、いい感じに回答できてますね。
好きなもののジャンル分けもOKですね。

ただ、若干変なところはありますが... (改行コード丸出しな点、「飲み物以外」がゲームだと答えた点)

レイアウトはPythonコード上の問題かと思うのでともかく、回答内容の日本語が変な部分は修正してもらいましょう。(圧)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617410/9643d1c9-0332-f36b-6b48-4c5a7d4396ae.png)

ここでまさかの圧を掛けての軌道修正。

ちゃんと修正して再度回答してくれました。
素晴らしい...!

補足情報も付け足してみて、どう回答してくれるかを試してみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617410/e09766d4-bb93-9402-58b4-1fca49275866.png)

いい感じですね。
ある特徴について、与えた情報を使って回答してくれるかも確認してみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617410/2974dd01-4f76-50fd-bb72-684ce4b85f15.png)

修正を指示して反映してくれる点も含め、理想的に動いてますね。

### データの確認

履歴テーブル(history)を見てみます。

```sql
use role sysadmin;
use warehouse my_wh;
use schema test_db.test_schema;
select * from history;
```

こんな感じで保存されており、これらが次の回答に活用されている感じです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617410/be542c76-36a0-2ac7-73fd-07d24b95a60d.png)

では、これを削除してみます。

```sql
truncate table history;
select * from history;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617410/4b858b94-9aa0-0d87-ef30-e17af7bb8c00.png)

過去のやりとりの記録(AIの記憶)がまっさらになってしまいました...!
この状態で先ほどと同じ質問を投げかけてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617410/a50c390f-d1db-c5d3-8dbb-b67fcff493c7.png)

想定通り、ちゃんと記憶もリセットされてますn...

**！？**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617410/5af16d3a-51f3-63fb-328f-c455cab5da24.png)

そんなわけないでしょう...
(「[カレーは飲み物](https://nomimono.co.jp/)」という意見はなくはないですが、一旦置いておいて...)

変な回答は修正してくださいね、ということで。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/617410/d76986ca-0ea6-5b6c-23a7-4245e3ad02d0.png)

直りましたね。

圧ですか？圧ですよ...

そして、追加した情報が消えていることも確認できましたね。

### その他

ちなみに、そもそもリージョンがUSということもあり、さらにSnowsightの言語設定もデフォルト(英語)のままでして、それでこの日本語を前提とした実装をしていたのにもかかわらず、ごく自然に日本語に対応できているのはすごいですね。

ここで使用したAIモデル(snowflake-arctic)と何度かやりとりしましたが、素直に聞いてくれる時とそうでない時がそれぞれありまして、何度か軌道修正(圧掛け)をしていい感じに回答してくれるようになっていきました。

こちらの[Medium models - snowflake-arctic](https://docs.snowflake.com/user-guide/snowflake-cortex/llm-functions#medium-models)の説明によりますと、

> snowflake-arctic is Snowflake’s top-tier enterprise-focused LLM. Arctic excels at enterprise tasks such as SQL generation, coding and instruction following benchmarks.

「Arcticは、SQLの生成、コーディング、ベンチマークに従った命令などのエンタープライズタスクに優れています」ということで、このタスクにはあまり向いてなかった説ありますね。
(Snowflake Arcticさん、スミマセン...)


## おわりに

Snowflake上で、アカウント作成直後のまっさらな状態から、非常に簡単にAIチャットボットが実装できるようになっていてビックリしました。

Streamlitとも統合されているので、簡単な画面なら気軽に実装して試せるのが素晴らしいですね。

Snowflakeといえばやはりメインはデータベース(DWH)なので、データベースとのやりとりもデフォルトでサクッとできるので、データを利活用したりチャット履歴を保存したりする際の親和性もバツグンですね。

以上です。