AIチャットアプリを作る
筆者はAIプロンプトに入力中、うっかり改行キーを押してAIに送信するミスをよくします。AIを使いはじめた頃はよくあるポカミスだと思っていましたが、複雑なオーダーを投げるようになった今、そのミスは想像以上に損失が大きいことに気づきました。AIが確保しているコンテキストウィンドウ(会話の記憶領域)は有限で、思考の中断が積み重なると会話の履歴や連続性が静かに失われるためです。そうは言ってもチャット機能はAIサービスに組み込まれて改変しようがないので、構造的に誤送信が起きないAIチャットアプリを自分で作ることにしました。今回作成するアプリの外観このアプリはマウスカーソルで送信ボタンをクリックするまで何も起きません。エディタのように改行しながらプロンプトを編集し、完成してから送信します。これにより不完全なプロンプトでAIの思考を乱すことがなくなり、コンテキストウィンドウの無駄遣いを避けながら会話を続けられます。それでは早速アプリを作っていきます。手順1. OpenRouterでAPI Keyを取得するOpenRouterは、いうなればAIモデルの卸売市場です。取り扱っているAIモデルは数百に及び、様々な思惑があって本来なら庶民の手に届かない高性能なAIモデルが無料で使えたりします。OpenRouterThe unified interface for LLMs. Find the best models & prices for your promptsopenrouter.aiOpenRouterで行う設定は以下の2点です。 API Keyの発行 10USD以上のデボジットOpenRouterのアカウントを開設すると、”API Key”という文字列が発行されます。API Keyは利用料の支払いに紐付くため、他人に知られないよう注意深く管理する必要があります。無料のAIモデルへのアクセスは、デポジット金額を10USD以上に保つことが条件です。海外のサービスゆえにクレジットカード決済が便利です。手順2. Python実行環境を構築するAIチャットアプリの作成にPython言語を使います。まだ準備がなければ以下のURLからインストールしてください。Download PythonThe official home of the Python Programming Languagewww.python.orgコマンドプロンプトを立ち上げ、以下のコマンドを実行してPythonのバージョンが表示されることを確認します。>python --versionインストールしたPythonのバージョンが表示されたらOKです。手順3. 必須ライブラリを追加する素のPythonは基本的なプログラムしか組めません。AIアプリを作成するには以下のライブラリが必要です。1番から順にインストールするとスムーズです。 pip ライブラリマネージャ。以下のライブラリを管理するのにまず必要 dotenv 機密性の高い情報をコード本体から切り離して管理する openai AIモデルを呼び出すための業界標準的なライブラリ flet pythonコードだけでモダンなGUIを実現するフレームワークそれぞれのインストール方法はネットで検索してください。手順4. プログラムを作成する作成するファイルは以下の2つです。 .env 機密を含む情報を分別管理する。ファイル名なし拡張子のみの特殊ファイル chat.py AIチャットアプリ本体.envファイルはたった2行です。OPENROUTER_API_KEYには自分のAPIキーを登録します。モデル名は執筆時に稼働していた無料のAIモデルGemma 4 31B (free)を登録しています。モデルの最後にfreeとあれば無料で使えますが、常にモデルが加わったり削除されたりしているのでOpenRouterのダッシュボードで稼働状況を見ておく必要があります。.envOPENROUTER_API_KEY=sk-or-v1-xxxxxxxxxxxxMODEL_NAME=google/gemma-4-31b-it:freechat.pyは以下のコードを貼り付けます。お使いの環境によってはコードをコピー&ペーストする工程で空白文字の扱いがおかしくなる場合があります。Pythonの字下げ処理は1段あたり半角スペース4つなので、そうなっているか確認してください。幸いWindows11/Microsoft Edgeブラウザからメモ帳にコピペしたものはそのまま動作しました。chat.pyimport osimport asyncioimport flet as ftfrom openai import AsyncOpenAIfrom dotenv import load_dotenvload_dotenv()client = AsyncOpenAI( base_url="https://openrouter.ai/api/v1", api_key=os.getenv("OPENROUTER_API_KEY"),)model = os.getenv("MODEL_NAME")async def main(page: ft.Page): if not model: raise RuntimeError("MODEL_NAME environment variable is not set.") page.title = f"Original AI Chat - {model}" page.theme_mode = ft.ThemeMode.LIGHT message_history = [ {"role": "system", "content": "あなたは親切で有能なAIアシスタントです。"} ] ai_task = None chat_list = ft.ListView( expand=True, spacing=10, auto_scroll=True, ) input_field = ft.TextField( hint_text="メッセージを入力...\n(Enterで改行)", expand=True, multiline=True, min_lines=1, max_lines=10, ) send_button = ft.FilledButton("送信") def set_button_label(label: str): send_button.content = ft.Text(label) send_button.update() async def update_ui(): if hasattr(page, "update_async"): await page.update_async() else: page.update() await asyncio.sleep(0) async def call_ai_api(messages): completion = await client.chat.completions.create( model=model, messages=messages, ) return completion.choices[0].message.content or "" async def run_ai(user_message, messages_for_api): nonlocal ai_task try: response = await call_ai_api(messages_for_api) chat_list.controls.append( ft.Text(f"AI: {response}", selectable=True) ) message_history.append( {"role": "assistant", "content": response} ) await update_ui() except asyncio.CancelledError: chat_list.controls.append( ft.Text("AI: (中止しました)", selectable=True) ) if message_history and message_history[-1] == user_message: message_history.pop() raise except Exception as ex: chat_list.controls.append( ft.Text(f"Error: {ex}", selectable=True) ) if message_history and message_history[-1] == user_message: message_history.pop() await update_ui() finally: ai_task = None set_button_label("送信") send_button.disabled = False input_field.disabled = False await input_field.focus() await update_ui() async def on_send_button_click(e): nonlocal ai_task if ai_task is not None and not ai_task.done(): set_button_label("中止中...") send_button.disabled = True await update_ui() ai_task.cancel() return user_text = input_field.value if not user_text or not user_text.strip(): return user_text = user_text.strip() input_field.value = "" chat_list.controls.append( ft.Text(f"You: {user_text}", selectable=True) ) user_message = { "role": "user", "content": user_text, } message_history.append(user_message) messages_for_api = list(message_history) set_button_label("中止") send_button.disabled = False input_field.disabled = True await update_ui() await asyncio.sleep(0) ai_task = asyncio.create_task( run_ai(user_message, messages_for_api) ) send_button.on_click = on_send_button_click page.add( chat_list, ft.Row([input_field]), ft.Row([send_button], alignment=ft.MainAxisAlignment.END), )if __name__ == "__main__": ft.run(main)上記2つのファイルは同じディレクトリに置いて使います。作ったアプリを公開するときは、APIキーの分別管理を徹底してください。コード中のmessage_historyは会話履歴を格納するものですが、チャットの前提とするカスタム指示として利用できます。手順5. AIチャットアプリ起動以下のコマンドでアプリが起動します。> python chat.pyいちいちコマンドラインを入力するのが面倒なら、一般的なアプリのようにクリック1つで起動できる.exeファイルにも変換できます。アプリの出来映えに不満があればAIに相談すれば済むことなので、ここまでできたならもう何も困ることはないと思います。参考情報筆者がこのアプリを作ったときのバージョンは以下の通りです。いずれもこの付近のバージョンであれば紹介したコードで動作すると思います。 Python 3.14.3 pip 26.1.2 dotenv 1.2.2 openai 2.41.1 Flet 0.85.3 / Flutter 3.41.7 / Pyodide 0.27.7