Fletを使ったAIチャットアプリの作り方
Flatとは
Fletは、Pythonを使ってWebアプリ、デスクトップアプリ、モバイルアプリを簡単に開発できるフレームワークです。
準備
FlatのチュートリアルのチャットアプリをベースにAIチャットアプリに改造していきます。
ライブラリのインストール
pip install flet
LLMを使うのにllama-cpp-pythonパッケージをインストール
GPUを使う場合はビルドされたものをインストールする必要があります。
# CPUだけを使う場合
pip install llama-cpp-python
# GPUを使う場合
pip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/<cudaのバージョン> --upgrade --force-reinstall --no-cache-dir
<cudaのバージョン>は以下のいずれかになります。
cu121: CUDA12.1
cu122: CUDA12.2
cu123: CUDA12.3
cu124: CUDA12.4
nvidia-smiを使い自身の使っているcudaのバージョンを確認してください。
例えばCUDA12.1の場合のインストール方法は以下のようになります。
pip install llama-cpp-python --extra-index-url https://abetlen.github.io/llama-cpp-python/whl/cu121 --upgrade --force-reinstall --no-cache-dir
参考▼
使用するLLMのダウンロード
llama-cpp-pythonでLLMを使う際は、GGUF化したモデルを使う必要があります。
高校生が作った日本語に強いLLMを使います。
元のモデル▼
GGUF化されたモデル▼
今回使うモデル
ベースのコード
チュートリアルの不要なコードを削除したベースになるコード▼
import flet as ft
from llama_cpp import Llama
class Message():
def __init__(self, user_name: str, text: str, message_type: str):
self.user_name = user_name
self.text = text
self.message_type = message_type
# アイコン、名前、チャットの再利用可能なチャットメッセージ
class ChatMessage(ft.Row):
def __init__(self, message:Message):
super().__init__()
self.vertical_alignment = "start"
self.controls = [
# アイコン
ft.CircleAvatar(
content=ft.Text(self.get_initials(message.user_name)),
color=ft.colors.WHITE,
bgcolor=self.get_avatar_color(message.user_name)
),
# 名前とメッセージのカラム
ft.Column(
[
ft.Text(message.user_name, weight="bold"),
ft.Text(message.text, selectable=True)
],
tight=True,
spacing=5,
)
]
# ユーザ名の頭文字の取得
def get_initials(self, user_name: str):
return user_name[:1].capitalize()
# ユーザ名に基づきハッシュを使いアイコンの色をランダムに決める
def get_avatar_color(self, user_name: str):
colors_lookup = [
ft.colors.AMBER,
ft.colors.BLUE,
ft.colors.BROWN,
ft.colors.CYAN,
ft.colors.GREEN,
ft.colors.INDIGO,
ft.colors.LIME,
ft.colors.ORANGE,
ft.colors.PINK,
ft.colors.PURPLE,
ft.colors.RED,
ft.colors.TEAL,
ft.colors.YELLOW,
]
return colors_lookup[hash(user_name) % len(colors_lookup)]
def main(page: ft.Page):
page.title = 'AIチャット'
# 送られてきたメッセージをchatに追加
def on_message(message: Message):
m = ChatMessage(message)
chat.controls.append(m)
page.update()
# メッセージの送信
def send_message_click(e):
if new_message.value != "":
on_message(Message(user_name='user', text=new_message.value, message_type='human'))
new_message.value = ''
new_message.focus()
page.update()
# スクロールをつける
chat = ft.ListView(
expand = True,
spacing = 10,
auto_scroll = True
)
# メッセージボックス
new_message = ft.TextField(
hint_text = "Write a message...",
autocorrect = True,
shift_enter = True,
min_lines = 1,
max_lines = 5,
filled = True,
expand = True,
on_submit = send_message_click
)
# ページに表示
page.add(
ft.Container(
content = chat,
border = ft.border.all(1, ft.colors.OUTLINE),
border_radius = 5,
padding = 10,
expand = True,
),
ft.Row(
[
new_message,
ft.IconButton(
icon = ft.icons.SEND_ROUNDED,
tooltip = "Send message",
on_click = send_message_click
)
]
)
)
ft.app(target=main, view=ft.AppView.WEB_BROWSER)
ベースコードの動作確認
AIチャットアプリの作成
すること
ユーザがチャットを送信するとAIのメッセージを表示
AIのチャットが返ってくるまで処理中が視覚的に分かるようにプログレスバーの追加
デザインのアップデート
1.ユーザがチャットを送信するとAIのメッセージを表示
AIがチャットをするためのコードを追加します。
llm = Llama(
model_path="LLMのファイルパス",
# n_gpu_layers=-1, # コメントをはずしてGPUを使う
n_ctx=2048
)
def ai_chat(message):
chat_history = [
{"role": "system", "content": "あなたは日本語を話す優秀なアシスタントです。回答には必ず日本語で答えてください。"},
{
"role": "user",
"content": message}
]
output = llm.create_chat_completion(messages=chat_history)
return output["choices"][0]["message"]["content"]
ai_chat関数にユーザが送信したチャットを渡すコードをsend_message_click関数に追記
message_creation関数はメッセージ情報をon_message関数に渡すコードの関数化
# ~~ コード ~~
def main(page: ft.Page):
# ~~ コード ~~
def message_creation(name, text, message_type):
on_message(Message(user_name=name, text=text, message_type=message_type))
def send_message_click(e):
if new_message.value != "":
message_creation('user', new_message.value, 'human') # 追加
send_message = new_message.value # Aiに送信するメッセージをsend_messageにコピー 追加
new_message.value = '' # 入力のメッセージを空白にする 追加
page.update() # 追加
ai_mes = ai_chat(send_message) # 追加
message_creation('AI', ai_mes, 'ai') # 追加
new_message.focus()
page.update()
2.AIのチャットが返ってくるまで処理中が視覚的に分かるようにプログレスバーの追加
main関数に新しくft.ProgressBar(プログレスバー)を追加します。
# ~~ コード ~~
def main(page: ft.Page):
# ~~ コード ~~
# プログレスバー
progress = ft.ProgressBar(
color = ft.colors.PINK, # 進むバーの色
bgcolor = ft.colors.GREY_200, # バーの背景色
visible = False # 非表示にする
)
page.add(
ft.Container(
content = chat,
border = ft.border.all(1, ft.colors.OUTLINE),
border_radius = 5,
padding = 10,
expand = True,
),
progress, # プログレスバー
ft.Row(
[
new_message,
ft.IconButton(
icon = ft.icons.SEND_ROUNDED,
tooltip = "Send message",
on_click = send_message_click
)
]
)
)
メッセージを送信していない時は非表示にしています。
send_message_click関数にチャットが送信されたときに表示させるコードを追加します。
1つ目は、ユーザのチャットが送信されpage.update()前にprogress.visibleをTrueに変更することでブログレスバーを表示させることができます。
2つ目は、AIのチャットが返信されたときにprogress.visibleをFalesに変更することでプログレスバーを非表示にしています。
※page.update()のあとにprogress.visibleを追加するとprogress.visibleの変数は変更されているが、変更された状態でページのアップデートがされません。
# ~~ コード ~~
def main(page: ft.Page):
# ~~ コード ~~
def send_message_click(e):
if new_message.value != "":
message_creation('user', new_message.value, 'human')
send_message = new_message.value
new_message.value = ''
progress.visible = True # プログレスバーの表示 追加
page.update()
ai_mes = ai_chat(send_message)
message_creation('AI', ai_mes, 'ai')
progress.visible = False # プログレスバーの非表示 追加
new_message.focus()
page.update()
3.デザインのアップデート
チャットに囲いをつける
チャットの折り返し
1.チャットに囲いをつける
チャットによくある囲いをつけていきます。
ChatMessageクラスのチャットを表示するコードを変更します。
class ChatMessage(ft.Row):
def __init__(self, message:Message):
super().__init__()
self.vertical_alignment = "start"
self.controls = [
ft.CircleAvatar(
content=ft.Text(self.get_initials(message.user_name)),
color=ft.colors.WHITE,
bgcolor=self.get_avatar_color(message.user_name)
),
ft.Column(
[
ft.Text(message.user_name, weight="bold"),
ft.Text(message.text, selectable=True) # ここを変更
],
tight=True,
spacing=5,
),
]
変更すると以下のようになります。
class ChatMessage(ft.Row):
def __init__(self, message:Message):
super().__init__()
self.vertical_alignment = "start"
self.controls = [
ft.CircleAvatar(
content=ft.Text(self.get_initials(message.user_name)),
color=ft.colors.WHITE,
bgcolor=self.get_avatar_color(message.user_name)
),
ft.Column(
[
ft.Text(message.user_name, weight="bold"),
# 囲い付きチャット 変更したコード
ft.Container(
content = ft.Text(message.text, selectable=True),
bgcolor = self.get_bgcolor(message.message_type), #背景色
border = ft.border.all(1, ft.colors.OUTLINE), # 囲い
border_radius = 5, # 囲いの丸み
padding = 5
)
],
tight=True,
spacing=5,
),
]
囲いにユーザとAIで背景を変えて見やすくするコードを追加します。
AIならグレーにするようにしています。
別の色にしたい場合はカラー表を参照
class ChatMessage(ft.Row):
def __init__(self, message:Message):
# ~~ コード ~~
# チャットの装飾
def get_bgcolor(self, chat_type):
if chat_type == 'ai':
color = ft.colors.BLUE_GREY_50
else:
color = ft.colors.WHITE
return color
囲いと色を付けると以下のようになります。
2.チャットの折り返し
今の状態だと長文が来たときに、文字の折り返しがされていません。
一行追加するだけで文字の折り返しすることができます。
ChatMessageクラスのft.Columnにexpand=Trueを追加します。
class ChatMessage(ft.Row):
def __init__(self, message:Message):
super().__init__()
self.vertical_alignment = "start"
self.controls = [
ft.CircleAvatar(
content=ft.Text(self.get_initials(message.user_name)),
color=ft.colors.WHITE,
bgcolor=self.get_avatar_color(message.user_name)
),
ft.Column(
[
ft.Text(message.user_name, weight="bold"),
# 囲い付きチャット
ft.Container(
content = ft.Text(message.text, selectable=True),
bgcolor = self.get_bgcolor(message.message_type), #背景色
border = ft.border.all(1, ft.colors.OUTLINE), # 囲い
border_radius = 5, # 囲いの丸み
padding = 5
)
],
tight=True,
spacing=5,
expand=True #文字のスペースを埋める 追加
),
]
追加することで文字の折り返しがされています。
完成コード
import flet as ft
from llama_cpp import Llama
class Message():
def __init__(self, user_name: str, text: str, message_type: str):
self.user_name = user_name
self.text = text
self.message_type = message_type
class ChatMessage(ft.Row):
def __init__(self, message:Message):
super().__init__()
self.vertical_alignment = "start"
self.controls = [
ft.CircleAvatar(
content=ft.Text(self.get_initials(message.user_name)),
color=ft.colors.WHITE,
bgcolor=self.get_avatar_color(message.user_name)
),
ft.Column(
[
ft.Text(message.user_name, weight="bold"),
# ft.Text(message.text, selectable=True)
ft.Container(
content = ft.Text(message.text, selectable=True),
bgcolor = self.get_bgcolor(message.message_type), #背景色
border = ft.border.all(1, ft.colors.OUTLINE), # 囲い
border_radius = 5, # 囲いの丸み
padding = 5
)
],
tight=True,
spacing=5,
expand=True #文字のスペースを埋める(テキストをはみ出ないようにした)
),
]
# チャットの装飾
def get_bgcolor(self, chat_type):
if chat_type == 'ai':
color = ft.colors.BLUE_GREY_50
else:
color = ft.colors.WHITE
return color
def get_initials(self, user_name: str):
return user_name[:1].capitalize()
def get_avatar_color(self, user_name: str):
colors_lookup = [
ft.colors.AMBER,
ft.colors.BLUE,
ft.colors.BROWN,
ft.colors.CYAN,
ft.colors.GREEN,
ft.colors.INDIGO,
ft.colors.LIME,
ft.colors.ORANGE,
ft.colors.PINK,
ft.colors.PURPLE,
ft.colors.RED,
ft.colors.TEAL,
ft.colors.YELLOW,
]
return colors_lookup[hash(user_name) % len(colors_lookup)]
llm = Llama(
model_path="LLMのファイルパス",
# n_gpu_layers=-1, # コメントをはずしてGPUを使う
n_ctx=2048
)
def ai_chat(message):
chat_history = [
{"role": "system", "content": "あなたは日本語を話す優秀なアシスタントです。回答には必ず日本語で答えてください。"},
{
"role": "user",
"content": message}
]
output = llm.create_chat_completion(messages=chat_history)
return output["choices"][0]["message"]["content"]
def main(page: ft.Page):
page.title = 'AIチャット'
def on_message(message: Message):
m = ChatMessage(message)
chat.controls.append(m)
page.update()
def message_creation(name, text, message_type):
on_message(Message(user_name=name, text=text, message_type=message_type))
def send_message_click(e):
if new_message.value != "":
message_creation('user', new_message.value, 'human')
send_message = new_message.value
new_message.value = ''
progress.visible = True # プログレスバーの表示
page.update()
ai_mes = ai_chat(send_message)
message_creation('AI', ai_mes, 'ai')
progress.visible = False # プログレスバーの非表示
new_message.focus()
page.update()
# プログレスバー
progress = ft.ProgressBar(
color = ft.colors.PINK, # 進むバーの色
bgcolor = ft.colors.GREY_200, # バーの背景色
visible = False # 非表示にする
)
chat = ft.ListView(
expand = True,
spacing = 10,
auto_scroll = True
)
new_message = ft.TextField(
hint_text = "Write a message...",
autocorrect = True,
shift_enter = True,
min_lines = 1,
max_lines = 5,
filled = True,
expand = True,
on_submit = send_message_click
)
page.add(
ft.Container(
content = chat,
border = ft.border.all(1, ft.colors.OUTLINE),
border_radius = 5,
padding = 10,
expand = True,
),
progress,
ft.Row(
[
new_message,
ft.IconButton(
icon = ft.icons.SEND_ROUNDED,
tooltip = "Send message",
on_click = send_message_click
)
]
)
)
ft.app(target=main, view=ft.AppView.WEB_BROWSER)