stliteからOpenAI APIを呼び出そう

## stliteとは

StreamlitはPython環境上で実行されているサーバにブラウザでアクセスする仕組みですが、このサーバを同じブラウザ上で動くようにしたものが[stlite](https://github.com/whitphx/stlite)です。  
Streamlit Creator[^1]であるWhitphxさんが開発しています。  

https://github.com/whitphx/stlite

[^1]: Streamlitコミュニティへの貢献が世界トップクラスな方々のこと。https://streamlit.io/creators

PythonをWebAssembly(WASM)に移植してブラウザ上で動くようにした[Pyodide](https://pyodide.org/en/stable/)というPythonディストリビューションがあり、stliteはStreamlitをこのPyodideで動くようにすることでサーバレスを実現しているそうです[^2]。  

[^2]: 「サーバレス」ではなく「静的サイトで動かせる」と表現した方が分かりやすいかも

ちょっとした可視化アプリを配布したい場合やWebページにStreamlitのプレイグラウンドを組み込みたい場合に便利そうですね。  
例えば[Electron](https://www.electronjs.org/ja/docs/latest/)と組み合わせればデスクトップで動くStreamlitアプリも実現できるそうです！  

## stliteにおけるHTTPリクエストの制限

stliteからのHTTPリクエストには残念ながら制限があって、いくつかのメジャーなライブラリとPyodideが提供している関数のみが使用できます。  

https://github.com/whitphx/stlite?tab=readme-ov-file#http-requests

OpenAIの公式ライブラリは内部的に[HTTPX](https://www.python-httpx.org/)を使っているようなので、残念ながらstliteで使うことはできなさそうです…  
実際に試すと```APITimeoutError: Request timed out.```になりました。  

![Error](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2917580/787bead0-0a2b-b882-f088-1dc013a6d384.png)

## stliteからOpenAI APIを呼び出す

OpenAI APIはCURLでも呼び出せるインタフェースなので、少し不便ですがOpenAIの公式ライブラリを使わなくても呼び出すことができます。  
今回はCURLコマンドを各プログラミング言語での書き方に変換してくれる[curlconverter](https://curlconverter.com/)を使ってプリミティブなHTTPリクエスト関数向けの書き方に変換してみました。  
公式に書かれているCURLコマンドの例をcurlconverterに変換してもらうと以下のようになります。  

```py:converted.py
import os
import requests

headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + os.getenv('OPENAI_API_KEY', ''),
}

json_data = {
    'model': 'gpt-3.5-turbo',
    'messages': [
        {
            'role': 'system',
            'content': 'You are a helpful assistant.',
        },
        {
            'role': 'user',
            'content': 'Who won the world series in 2020?',
        },
        {
            'role': 'assistant',
            'content': 'The Los Angeles Dodgers won the World Series in 2020.',
        },
        {
            'role': 'user',
            'content': 'Where was it played?',
        },
    ],
}

response = requests.post('https://api.openai.com/v1/chat/completions', headers=headers, json=json_data)
```

今回は```pyodide.http.pyfetch()```を使うように手動でさらに書き換えてみました。  

```py:stlite_chat.py
import streamlit as st
from pyodide.http import pyfetch
import json

st.title("stlite OpenAI API example")

# APIキーを入力する
api_key = st.text_input("API Key", "")

# プロンプトを入力する
prompt = st.text_input("Prompt", "Hello!")

# OpenAI APIを呼び出す
async def call_openai_api(api_key, prompt, model="gpt-3.5-turbo"):
    # APIキーをヘッダに設定する
    headers = {
        "Content-Type": "application/json",
        "Authorization": "Bearer " + api_key,
    }
    # リクエストボディを設定する
    body = {
        "model": model,
        "messages": [
            {
                "role": "user",
                "content": prompt,
            },
        ],
    }
    # APIを呼び出す。今回はpyfetch()を使う
    API_URL = "https://api.openai.com/v1/chat/completions"
    response = await pyfetch(API_URL, method="POST", headers=headers, body=json.dumps(body))
    response_json = await response.json()
    # レスポンスをパースする
    role = response_json["choices"][0]["message"]["role"]
    message = response_json["choices"][0]["message"]["content"]
    return (role, message)

# APIキーとプロンプトが入力されたらAPIを呼び出す
if api_key and prompt:
    role, message = await call_openai_api(api_key, prompt)
    with st.chat_message(role):
        st.write(message)
```

動きました！  

![Worked](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2917580/32fb7d2f-dcdc-f012-5237-3ae06f873df4.png)

### 注意事項

stliteはこれまでサーバで動いていたものがブラウザ上で動くため、```secrets.toml```のようにユーザから見えない形で安全にAPIキーを書いておく方法がありません。  
ソースコードにハードコーディングするのも一般的によくないので、今回は別ファイルにメモしたAPIキーを手動で都度コピペする形を想定して実装しています。  

## まとめ

stliteでHTTPリクエストする方法を覚えると、HTTPリクエストベースの外部APIが呼び出せるようになるため、かなり遊びの幅が広がるかと思います！  
ぜひ活用してみてください。  

## 追記：その他の方法

OpenAIの公式ライブラリを使うこともできそうというアドバイスをいただきました。今後試してみようと思います！  

https://x.com/whitphx_ja/status/1803964441224188214