Snowpark ML Python Cortex ライブラリがマルチターンに対応していた！

# はじめに

Snowflake の Snowpark ML Python Cortex ライブラリが履歴を含むマルチターンの入力にネイティブ対応していたので、使用方法を共有します。

:::note info
現時点（2024/11/24）及び、Snowpark ML Python ライブラリのバージョン 1.6.4 における内容となります。
:::

※ドキュメントや GitHub Issue にサンプルや型ヒントなどの情報がすぐ見つからなかったため執筆しました。

# 結論
下記のコードで実現できました。★を付けてる箇所を確認ください。なお、コードは Streamlit in Snowflake で実行しているため、ローカルで実行する場合は session の取得方法を変更してください。
```python
import json

from snowflake.cortex import Complete
from snowflake.snowpark.context import get_active_session


def extract_message_from_response(response: str) -> str:
    # JSON文字列を辞書に変換
    response_dict = json.loads(response)
    
    # choices 配列からメッセージを取得
    return response_dict["choices"][0]["messages"]

session = get_active_session()

# 会話履歴をセッション状態により保持
if "messages" not in st.session_state:
    st.session_state.messages = []

# 過去の会話を表示
for message in st.session_state.messages:
    with st.chat_message(message["role"]):
        st.markdown(message["content"])

# 新しいチャットを表示
if prompt := st.chat_input("チャットを入力してください"):
    # ユーザーのメッセージを追加
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

    # LLMに履歴を含むリクエストを送信★
    response_dict = Complete(llm_model, st.session_state.messages)  # めっちゃハマった
    response = extract_message_from_response(response_dict)  # めっちゃハマった
    
    # アシスタントのレスポンスを追加
    st.session_state.messages.append({"role": "assistant", "content": response})
    with st.chat_message("assistant"):
        st.markdown(response)

```


# 経緯
~~面白かった~~ 苦労したので、思考と試行錯誤の流れをメモしておきます。

## 直感する

当初（2024年4月頃以前）はマルチターンに対応していなかったのですが、そろそろ対応した頃かな？とドキュメントを眺めてみたところ、下記のように対応していそうな雰囲気を感じました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3540224/3b90078d-9631-775a-87ff-ea7a7c28e1f5.png)
https://docs.snowflake.com/en/developer-guide/snowpark-ml/reference/1.7.0/index

ただ、ドキュメントのどこを読んでも、その使い方が載っていませんでした。`ConversationMessage` ってなんなんだ。


また、あまり知られていないですが、Snowpark 専用の Release Notes にも記載がありませんでした。（これ結構いいアップデートだと思うんですけどね）
https://docs.snowflake.com/en/release-notes/clients-drivers/snowpark-ml-2024

## 試行錯誤

実はこの段階でちゃんと使えていたタイミングもあったのですが、レスポンスが `str` 型で返ってきていることに気が付かずエラーを出してしまっていたりして、試行錯誤もうまくいきません。

ググっても `ConversationMessage` というものが何なのかは分かりませんでした。

## GitHub を見に行く

Snowpark ML Python ライブラリは GitHub に公開されているライブラリなので、GitHub リポジトリを見に行ってみました。
https://github.com/snowflakedb/snowflake-ml-python/

すると、ビンゴっぽい Close された Issue を見つけました。（機械翻訳：機能強化のリクエスト: _complete が SQL バージョンのように prompt オブジェクトの配列を受け入れるようにする予定はありますか?）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3540224/f68f62e4-5037-3845-082e-4f826227e74d.png)
https://github.com/snowflakedb/snowflake-ml-python/issues/99

しかし、ここにも使い方が載っていません。

となれば、Pull Request を見るか。と思っても、Pull Request の粒度がバージョンごとでどの Pull Request が対応しているのか分かりませんでした。

そこで、再度ドキュメントに戻り、`List[ConversationMessage]` をサポートした最小のバージョンを確認してみました。その結果、1.5.4 っぽいことが分かりました。

しかし、Pull Request の 1.5.4 を見ても、それらしき変更は加わっていません。

## ソースコードを見る

それならばと、重い腰を上げてソースコードを読みに行ったら、それらしきコードが見つかりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3540224/2e2955d2-e489-90ee-e279-27b869681b06.png)
https://github.com/snowflakedb/snowflake-ml-python/blob/7bc5f40806cf15e65ae6603e67c53d0bc1cb85e6/snowflake/cortex/_complete.py#L100


## 再度試行錯誤

そこで、下記のようなコードを実行してみました。
```python
prompt = [{"role": "user", "content": "日本の人口を教えて下さい"},
          {"role": "assistant", "content": "1億人です。"},
          {"role": "user", "content": "アメリカについてはどうですか？"}]
response = Complete("mistral-large2", prompt)
st.write(response)
```

すると、下記のような結果が得られました。ビンゴですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3540224/1206ee75-4721-b4e9-bfea-b5fe40f13647.png)

それならばと、下記のようなコードを実行してみます。
```python
response = Complete("mistral-large2", prompt)["choices"][0]["messages"]
st.write(response)
```

すると、次のようなエラーが生じました。残念ながらビンゴではなくリーチだったようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3540224/0bf86895-67be-827b-0b6e-b171b5310fcc.png)

string? `Dict` じゃないのか。まぁ確かにドキュメントの返り値にはいないもんな。`Iterator[str]` って訳でもないのか。

ということは、一度 Dict にしてあげないといけない？ということでようやくビンゴです。
```python
prompt = [{"role": "user", "content": "日本の人口を教えて下さい"},
          {"role": "assistant", "content": "1億人です。"},
          {"role": "user", "content": "アメリカについてはどうですか？"}]
response = Complete("mistral-large2", prompt)

import json
response_dict = json.loads(response)
st.write(response_dict["choices"][0]["messages"])
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3540224/4cc5049a-9a02-4cde-cfe4-4af5668352cd.png)


この内容を組み込んだのが、冒頭のコードでした。

# おわりに
ぜひ参考にしていただき、Streamlit における Snowflake Cortex 活用を楽しんでいきましょう！