Mesopの基本コードとGradioとの比較
始めに
マシンラーニング・ソリューションズアルバイトの吉田岳史です。機械学習を用いたPythonによるAIアプリの構築、共有を行う際にはGradioというフレームワークが有名です。本ブログでは新たにGoogleが開発したAIアプリ開発に適したフレームワークであるMesopを解説します。本ブログを読むことで、Mesopの始め方や簡単な使い方、そして、Gradioとの違いを理解することが目的です。
Mesopとは
MesopのGitHubに
Create web apps without the complexity of frontend development.
Used at Google for rapid AI app development.
とあるように、AIのアプリ開発に使えるフレームワークです。
Mesopの構文は既存のPythonの構文と似ており、また、各コンポーネントの配置がCSSをもとにできるという利点があります。
インストール方法
pip install mesop
pipコマンドを使ってインストールできます。詳細はこのページに書いてあります。
使用方法
最初のHello World
Hello Worldをブラウザに表示することから始めましょう。main.pyという名前で以下のファイルを作成します。
import mesop as me
@me.page(path="/")
def app():
me.text("Hello World")
その後、コマンドラインにおいてmesop main.py
と打ちます。
mesop main.py
はMesopを使って、main.pyを起動する意味のコマンドになっています。
ターミナル上でのコマンド入力
コマンドライン上で上記のような応答が出てきます。localhost:32123にアクセスして、以下のような画面が出てきたら成功です。
Webページへの出力
一部変更されていますが、上記のコードは公式GitHubに記載されています。
- Every Mesop app starts with
import mesop as me
. This is the only recommended way to import mesop, otherwise your app may break in the future because you may be relying on internal implementation details.@me.page
is a function decorator which makes a function a root component for a particular path. If you omit thepath
parameter, this is the equivalent of@me.page(path="/")
.app
is a Python function that we will call a component because it's creating Mesop components in the body.
上記のようにMesopを用いたアプリでは、まずimport mesop as me でMesopをインポートすることが推奨されています。また、@meから始まる部分は関数デコレーターと呼ばれる部分です。今回使用している@me.page(path=”/”) は/以下のurlにアクセスした際に表示する関数を指定します。この関数デコレーターがついている関数appはMesopのComponentと呼ばれる部分です。
Mesopにおける概念
Component とは
Componentとは上記のコード内にある関数appのように、アプリケーションのブロックです。Mesopによるアプリケーションは本質的にはこのブロックの木構造によって構成されます。
State とは
Stateはアプリケーションのひとつのセッションごとの状態を保持するクラスです。
Gradioとの比較
Gradioと比べてMesopを使う利点は主に学習コストの低さです。上記で上げた概念のStateを使うことでWebアプリに必要なボタンやテキストボックスなどの実装が直感的に行えます。また、これらのコンポーネントの配置がCSSをベースに行えます。PythonとCSSの基礎知識がある程度ある人にとってはMesopを始めるメリットがありそうです。
一方でGradioの利点は以下の部分が挙げられます。
- demo appの共有が容易
- 日本語記事が多い
- 現状の使用率が高い
現状では、Mesopを使用する場合には基本的に英語の公式ドキュメントを読む必要があります。一方でGradioで開発を行う場合には日本語でのアプリ開発の記事を参考にすることもできます。
コードによる比較
テキストの入出力を行うコード
GradioとMesopそれぞれにおいてのコードの比較を行います。まずはテキストの入力、そして、入力されたテキストを表示するコードをそれぞれ実装してみましょう。まずはGradioを使用して実装してみます。上からコード、起動時の画面です。
import gradio as gr
def display(text: str) -> str:
return "Hello " + text + "!"
with gr.Blocks() as demo:
text_input = gr.Textbox(label = "input")
output = gr.Textbox(label = "output")
text_input.change(display, inputs = text_input, outputs = output)
if __name__ == "__main__":
demo.launch()
Gradioによるテキスト入出力の画像
続いて、Mesopを使用して同様の機能のものを実装します。
import mesop as me
@me.stateclass
class State:
input: str
def on_input(e: me.InputEvent):
state = me.state(State)
state.input = e.value
@me.page(path = "/")
def app():
state = me.state(State)
me.input(label = "input", on_input = on_input)
me.text(f"Hello {state.input}!" if state.input else "")
これを実行すると以下のような結果が得られます。
Mesopによるテキスト入出力の画像
実装の違いとして一番大きな部分は受け取った入力の保存の部分です。Gradioでは受け取った値はtext_inputという変数に保存された後、displayという関数を通したのちにoutputのtext boxに渡されています。一方Mesopでは、受け取った値は一度Stateのクラスに保存され、その値がtextボックスに渡されています。
また、見た目のフォーマットとしても何も改善しない場合にはMesopのほうが少し簡素なつくりになっていることがわかります。
Mesop を用いたAIアプリ (ChatGPT)
先に挙げているようにMesopを使うことで効率的にAIアプリを作成することができます。
公式GitHubのチュートリアルではGeminiとAnthropic AIを使用したチャットボットの開発ができます。ここでは公式チュートリアルのコードを参考にして、OpenAIのChatGPTとMesopを利用してChatBotを作成してみます。
今回のChatBotはmain.py, openai_prompt.py, data_model.pyの三つのファイルから構成されています。
import mesop as me
from data_model import State, ChatMessage
from openai_prompt import send_prompt
def header():
def navigate_home(e: me.ClickEvent):
me.navigate("/")
state = me.state(State)
state.conversations = []
with me.box(
on_click=navigate_home,
style=me.Style(
cursor="pointer",
padding=me.Padding.all(16),
),
):
me.text(
"AIChat",
style=me.Style(
font_weight=500,
font_size=24,
color="#3D3929",
letter_spacing="0.3px",
),
)
def on_blur(e: me.InputBlurEvent):
state = me.state(State)
state.input = e.value
def chat_input():
state = me.state(State)
with me.box():
me.native_textarea(
value = state.input,
placeholder = "Enter a prompt",
on_blur = on_blur,
style=me.Style(
padding=me.Padding(top=16, left=16),
outline="none",
width="100%",
border=me.Border.all(me.BorderSide(style="none")),
),
)
with me.content_button(
type = "icon", on_click = send_prompt,
):
me.icon("send")
@me.page(path = "/")
def app():
header()
with me.box(
style=me.Style(
width="min(680px, 100%)",
margin=me.Margin.symmetric(horizontal="auto", vertical=36),
)
):
chat_input()
display_conversations()
def display_conversations():
state = me.state(State)
for chat in state.conversations:
if chat.role == "system": continue
with me.box(style=me.Style(margin=me.Margin(bottom=24))):
display_message(chat)
def display_message(message: ChatMessage):
style = me.Style(
padding=me.Padding.all(12),
border_radius=8,
margin=me.Margin(bottom=8),
)
if message.role == "user":
style.background = "#e7f2ff"
else:
style.background = "#ffffff"
with me.box(style=style):
me.markdown(message.content)
if message.in_progress:
me.progress_spinner()
import mesop as me
from openai import OpenAI
from data_model import ChatMessage, State
import os
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
client = OpenAI(
api_key = os.environ.get("OPENAI_API_KEY")
)
def chatmessage2json(data: ChatMessage):
return {"role" : data.role, "content" : data.content}
def send_prompt(e: me.ClickEvent):
state = me.state(State)
if not state.conversations:
state.conversations.append(ChatMessage(role = "system", content = "You are a helpful assistant"))
input = state.input
messages = state.conversations
messages.append(ChatMessage(role="user", content = input, in_progress = True))
history = []
for conversation in state.conversations:
history.append(chatmessage2json(conversation))
response = client.chat.completions.create(
messages = history,
model = state.model
)
message = response.choices[0].message.content
messages[-1].in_progress = False
messages.append(ChatMessage(role="assistant", content = message))
#data_model.py
from dataclasses import dataclass, field
from typing import Literal
import mesop as me
Role = Literal["user", "assistant", "system"]
@dataclass(kw_only=True)
class ChatMessage:
role: Role = "user"
content: str = ""
in_progress: bool = False
@me.stateclass
class State:
input: str = ""
conversations: list[ChatMessage] = field(default_factory=list)
model: str = "gpt-4o-mini"
コードの実行のみを確認したい方は、これらのコードを同一ディレクトリ上にコピーしてコマンドラインにて mesop main.py
と実行することで動作できます。このコードはmesopに加えて、python-dotenv, openaiを使用しています。コマンド上で
pip install python-dotenv
pip install openai
を実行して、python-dotenv, openaiをインストールしてください。また、このコードはOpenAI APIを使用しています。API KEYを未取得の場合はOpenAI Platformよりアカウントを作成してAPI KEYの生成を行ってください。作成したAPI KEYはmain.pyと同一ディレクトリに.envというファイルを作成します。そして、以下の例のようにファイル内にAPI KEYを記載します。YOUR_API_KEYの部分を作成したAPI KEYに対して置き換えてください。
OPENAI_API_KEY=YOUR_API_KEY
demoアプリの実行画面
main.pyに関しての説明
まずは、main.pyの内容に関して詳しく説明していきます。
ここに出てくるコンポーネントは app関数、header関数、chat_input関数、display_conversations関数、display_message関数の5つがあります。先に説明した通りMesopによるアプリはコンポーネントの木構造となっています。今回の例では以下の画像のように、appがheader, chat_input, display_conversationsを子とする木構造の根になっています。
demoアプリの構造図
app関数について
実際にapp関数の中でheader関数, chat_input関数、display_conversations関数を表示していることが関数の内部を見てみるとわかります。また、display_conversations関数の内部ではメッセージの伝達回数分、display_message関数を表示しています。
@me.page(path = "/")
def app():
header()
with me.box(
style=me.Style(
width="min(680px, 100%)",
margin=me.Margin.symmetric(horizontal="auto", vertical=36),
)
):
chat_input()
display_conversations()
app関数の先頭のpathの部分ではurlの指定をしています。また、関数内部ではchat_input関数 display_conversations関数に対してCSSを適用させています。with me.box以降のchat_input(), display_conversations()の部分をインデントすることで、CSSの適用範囲を指定しています。
header関数について
def header():
def navigate_home(e: me.ClickEvent):
me.navigate("/")
state = me.state(State)
state.conversations = []
with me.box(
on_click=navigate_home,
style=me.Style(
cursor="pointer",
padding=me.Padding.all(16),
),
):
me.text(
"AIChat",
style=me.Style(
font_weight=500,
font_size=24,
color="#3D3929",
letter_spacing="0.3px",
),
)
このheader関数の内部ではまず、navigate_home関数を定義しています。me.navigate(url)は呼び出されたときにurlに遷移します。また、state.conversations = []の部分ではホーム画面に遷移した場合には履歴が消すようにしています。また、me.text()の文字をクリックされた際に上記のnavigate_home()が呼ばれるようになっています。
左上のAIChatの部分がこの関数によって生成されています。
chat_input関数について
def on_blur(e: me.InputBlurEvent):
state = me.state(State)
state.input = e.value
def chat_input():
state = me.state(State)
with me.box():
me.native_textarea(
value = state.input,
placeholder = "Enter a prompt",
on_blur = on_blur,
style=me.Style(
padding=me.Padding(top=16, left=16),
outline="none",
width="100%",
border=me.Border.all(me.BorderSide(style="none")),
),
)
with me.content_button(
type = "icon", on_click = send_prompt,
):
me.icon("send")
chat_input関数は以下の写真の部分を表示しています。
テキストを入力するエリアに関してはme.native_textarea()で実装されています。ここでは、on_blurの関数を定義することでテキストエリアへのフォーカスがなくなったタイミングでstate.inputにテキストエリアへと入力されたテキストが代入されます。
その下の紙飛行機マークのボタンはme.content_button()によって定義されています。このボタンを押すとopenai_prompt.pyの内部にあるsend_prompt関数が呼ばれます。
display_conversations関数について
def display_conversations():
state = me.state(State)
for chat in state.conversations:
if chat.role == "system": continue
with me.box(style=me.Style(margin=me.Margin(bottom=24))):
display_message(chat)
def display_message(message: ChatMessage):
style = me.Style(
padding=me.Padding.all(12),
border_radius=8,
margin=me.Margin(bottom=8),
)
if message.role == "user":
style.background = "#e7f2ff"
else:
style.background = "#ffffff"
with me.box(style=style):
me.markdown(message.content)
if message.in_progress:
me.progress_spinner()
display_conversations関数はuser と assistantの会話の履歴を表示する部分です。stateの内部にはすべての会話の記録が残っているのでそれをループで表示しています。システムプロンプトとしてAPIに渡しているプロンプトは会話の記録として表示したくないので、role = “system”のときのみループをそのまま継続させています。
ひとつひとつのメッセージに関してはdisplay_message関数のほうで表示しています。この関数の内部ではCSSのスタイルを先に定義してしまっています。そして、メッセージがuser側なのか、assistant側なのかということで背景色を変えています。その下のmessage.in_progressではuserが入力をしてAPIの応答を待っている間、渦巻マークを表示するようにしています。
openai_prompt.pyに関しての説明
この関数の内部では主にOpenAI APIを使って、会話の応答を得ています。
import mesop as me
from openai import OpenAI
from data_model import ChatMessage, State
import os
from dotenv import load_dotenv, find_dotenv
load_dotenv(find_dotenv())
client = OpenAI(
api_key = os.environ.get("OPENAI_API_KEY")
)
def chatmessage2json(data: ChatMessage):
return {"role" : data.role, "content" : data.content}
def send_prompt(e: me.ClickEvent):
state = me.state(State)
if not state.conversations:
state.conversations.append(ChatMessage(role = "system", content = "You are a helpful assistant"))
input = state.input
messages = state.conversations
messages.append(ChatMessage(role="user", content = input, in_progress = True))
history = []
for conversation in state.conversations:
history.append(chatmessage2json(conversation))
response = client.chat.completions.create(
messages = history,
model = state.model
)
message = response.choices[0].message.content
messages[-1].in_progress = False
messages.append(ChatMessage(role="assistant", content = message))
前半の関数内部に書かれていない部分は主にapi_keyに関連した部分ですね。send_prompt関数の内部実際にAPIをたたいて会話の応答を得ています。ここで、historyと別で会話の履歴を定義しているのは、state内部の会話履歴の記録の方法ではJSON化できずmessageをOpenAIに渡せないからです。stateでの記録方法は公式チュートリアルの方法を参考にしています。
data_model.pyに関しての説明
from dataclasses import dataclass, field
from typing import Literal
import mesop as me
Role = Literal["user", "assistant", "system"]
@dataclass(kw_only=True)
class ChatMessage:
role: Role = "user"
content: str = ""
in_progress: bool = False
@me.stateclass
class State:
input: str = ""
conversations: list[ChatMessage] = field(default_factory=list)
model: str = "gpt-4o-mini"
ここでは、Mesopのセッションの記録を保持するStateクラスとチャットメッセージのクラスについて定義しています。チャットメッセージに関しては、チャットの内容とそのチャットが誰から送られているのか、そして、そのチャットメッセージが返答されているのかという部分を持っています。会話の応答待ちのための渦巻マークの表示はこのin_progressの状態をもとに判断されています。
また、Stateクラス内部のmodelの部分を変更すれば使用するモデルを変更することができます。
まとめ
本ブログではMesopを用いたHello World、Gradioとの比較、OpenAIをつかった簡単なChatBotの作成までを行いました。Mesop自体日本語記事が少ないというデメリットはありますが、書きやすいPythonのフレームワークです。本ブログが開発時のツールの選択やバグの解消などの一助になれば幸いです。
Discussion