見出し画像

Fletを使ったAIチャットアプリの作り方


Flatとは

Fletは、Pythonを使ってWebアプリ、デスクトップアプリ、モバイルアプリを簡単に開発できるフレームワークです。

準備

FlatのチュートリアルのチャットアプリをベースにAIチャットアプリに改造していきます。

実行環境
・flet 0.21.2
・llama_cpp_python 0.2.76
・Python 3.10.11
・CUDA 12.3

ライブラリのインストール

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を使います。

元のモデル▼

ArrowPro-7B-KUJIRAはMistral系のNTQAI/chatntq-ja-7b-v1.0をベースにAItuber、AIアシスタントの魂となるようにChat性能、および高いプロンプトインジェクション耐性を重視して作られました。

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チャットアプリの作成

  • すること

    1. ユーザがチャットを送信するとAIのメッセージを表示

    2. AIのチャットが返ってくるまで処理中が視覚的に分かるようにプログレスバーの追加

    3. デザインのアップデート

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. チャットに囲いをつける

  2. チャットの折り返し

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.Columnexpand=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)


この記事が気に入ったらサポートをしてみませんか?