彼女を励ますために、Azure OpenAI Serviceでくまきち(kmakici)LINE botを作った #3

# はじめに
「くまきち(kmakici)LINE bot」について、LINE Developer CommunityのLT大会で発表させていただきました。

https://linedevelopercommunity.connpass.com/event/325390/

当日の様子は以下YouTubeをご覧ください。

[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318960/46bab340-a3c8-cbef-0e68-996ac24bfca5.png)](https://youtu.be/nBLzpTUILMU)

**今回は、動画の[28:36](https://youtu.be/nBLzpTUILMU?t=1716)～アドバイスいただいた内容を実装してみます。
ついでに、高速化と低コスト化も図ります。**

https://youtu.be/nBLzpTUILMU?t=1716

#1の記事は以下になります。

https://qiita.com/banquet_kuma/items/2c77db8407fab99a09c6

#2の記事は以下になります。

https://qiita.com/banquet_kuma/items/0c7183ebf2be3023f3d9

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318960/0b4b8218-533c-a00b-0329-29756452d024.png)

# 目次
- [Ver Upの内容](#ver-upの内容)
- [前仕様について](#前仕様について)
- [実現方法](#実現方法)
- [ステートの記録方法](#ステートの記録方法)
- [GPT-4o miniについて](#gpt-4o-miniについて)
- [システム構成](#システム構成)
- [利用サービス一覧](#利用サービス一覧)
- [構築手順](#構築手順)
- [アプリを作成する](#アプリを作成する)
- [Azure Functionsへアプリをデプロイする](#azure-functionsへアプリをデプロイする)
- [LINE App.で動作確認する](#line-appで動作確認する)
- [今後の計画](#今後の計画)

# Ver Upの内容
|機能  |役割  |方法  |
|---|---|---|
|**ステート** |テキストに対して回答するターンか、画像に対して回答するターンか判別可能にする|**1つ前のメッセージがテキストか、画像か、Azure Blobに履歴を残す**|
|**高速化＆低コスト化** |レスポンスを早く、安くする|**言語モデルをGPT-4oからGPT-4o miniに変更**|

# 前仕様について
#2までの仕様では、画像を送る1つ前のメッセージで**画像**というワードを含んだ指示を送る必要がありました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318960/5e9f6a5a-c09d-2a75-7590-fbee8ce96856.png)

以下の点でユーザビリティに問題がありました

**1. 画像を送る前に、指示を出すことに違和感がある**
**2. 毎回「画像」というワードを書くのが面倒である**


今回、**画像を送った後のテキストメッセージを画像に対する指示と認識**される様に改修します。

＜改修後のアウトプット＞
![IMG_5591.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318960/d77ea5a0-f9a0-3924-7736-de687b1932aa.jpeg)

## 実現方法

メッセージタイプがimageの場合に、ステートをimageに更新します。

そして次回、メッセージタイプがtextの入力があった場合に、**ステート=imageであれば、画像に対する回答生成を行います**。そして、ステートをクリアします。

重要な点は、一番右側のパスです。
メッセージタイプがimageの入力があった場合には、画像をblobに保存し、ステートに"image"を書き込みますが、**回答生成は行いません**。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318960/fd22013e-e778-69e0-2558-c52518eaffc1.png)



# ステートの記録方法
テキストファイルに、ステートを書き込む方法を取りました。
めちゃくちゃ原始的な方法です(笑)

1. **judgment.txt**というファイルを予めAzure Blobに保存しておきます。
この時点では、ファイルは空です。
1. メッセージタイプ=imageの入力があった場合には、"image"と書き込みます。
　2-2. メッセージタイプ=textの入力があった場合には、何もしません。
1. 画像に対して回答生成する度に、記載をクリアします。

これにより、以下の様に1つ前のメッセージがtextかimageか判断できます。


**＜1つ前のメッセージタイプがtextの場合＞**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318960/6098de72-fcfb-4d60-dc11-a5223e608084.png)

**＜1つ前のメッセージタイプがimageの場合＞**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318960/72779480-2918-fcc9-049c-e66b8f5e6630.png)

# GPT-4o miniについて
2024/7/31にAzure OpenAI ServiceからGPT-4o miniをAPI利用できる様になりました。コストメリットがあるため、GPT-4oから乗り換えました。

https://techcommunity.microsoft.com/t5/ai-azure-ai-services-blog/openai-s-gpt-4o-mini-now-available-in-api-with-vision-and-fine/ba-p/4200640

GPT-4oに比べてコストが1/10以下になりました。これは嬉しい！😊
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318960/9a3bf829-6797-e67c-2c7e-8a8fd59b45eb.png)

テキスト知能と推論のベンチマークであるMMLUで、GPT-4oと遜色ない結果を出しています。趣味で使うなら十分です。

https://openai.com/index/gpt-4o-mini-advancing-cost-efficient-intelligence/

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318960/9630b262-de1b-9fde-6f8a-212b23887c16.png)

# システム構成
#2とほぼ同じ構成です。
違う箇所は、橙色で記載した④ステートを読み込み、⑨消去、書き込みする動きです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318960/f132b046-930e-10b4-2f76-f5925d4e3657.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318960/dfa77d92-a7f4-4d1c-5373-29dde63bc2bb.png)

# 利用サービス一覧
#2と同じです。

|名前  |役割  |
|---|---|
|**Azure Functions** |LINE Messaging APIからHTTPSリクエストをトリガーとして、Azure OpenAI Service へプロンプトを送信する。同時に、Azure OpenAI Serviceからの回答をLINE Messaging APIへHTTPSポストする|
|**Azure OpenAI Service** |プロンプトに応じて、回答を生成する  |
|**Azure Blob Storage** |メッセージ/回答/画像/**ステート**を保存する  |
|**LINE Developers** |● LINE Messaging APIを提供する <br>● チャネルアクセストークンを発行する<br>● チャネルシークレットを発行する|
|**LINE App.** |チャットボットのUI|
|**VSCode** |コードエディター。<br>Azure Functionsへの関数デプロイ|
|**Perplexity** |コーディングの先生<br>[Perplexity](https://www.perplexity.ai/)|

# 構築手順
これも、#2とほぼ同じです。
大きな流れは以下です。

1. Azure OpenAI Serviceの利用申請を出す
1. LINE Developersでkmakiciチャネルを作成する
1. Azure OpenAI Serviceのリソースを作成する
1. Azure Storage アカウントのリソースを作成する
1. Storage アカウントにAzure Blob Storageのコンテナを作成する
1. コンテナにステート保存用のテキストファイル「judgment.txt」を保存する
1. Azure Functionsへデプロイするコードを作成する
1. Azure Functionsのリソースを作成する
1. Azure Functionsへ必要な関数をデプロイする
1. 関数のURLをLINE Developersの「Webhook URL」へ設定する
1. LINE App.で動作確認する

各工程を説明してます。
上記6,7以外の工程は#2と同様のため割愛します。そちらの記事を参考ください。

予め作成済みのAzure Blobコンテナーに、**空のjudgment.txtをアップロードしてください。**

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318960/4966552b-f8c5-aa60-167e-f77299b4b6b1.png)

# アプリを作成する
関数の作成方法は#1,2の記事を参照ください。

プロジェクトフォルダ内に作成された、function_app.pyを以下に書き換えてください。

```Python
import azure.functions as func
from datetime import datetime, timezone
import logging
import os
import io
import json
import requests
from openai import  AzureOpenAI
from azure.storage.blob import BlobServiceClient, BlobClient, ContainerClient
from azure.core.exceptions import ResourceNotFoundError
from azure.cosmos import CosmosClient, PartitionKey, exceptions
from linebot import (LineBotApi, WebhookParser)
from linebot.models import (MessageEvent, TextMessage, TextSendMessage)
import logging
from dotenv import load_dotenv
load_dotenv()

# ログの設定
logging.basicConfig(level=logging.DEBUG)

# Azure OpenAIのテキストに対する回答のためのパラメータ
parameters = {
    "temperature": 0.7,
    "top_p": 1,
    "frequency_penalty": 0,
    "presence_penalty": 0,
    "stream": True
}
# テキストが入力されたか、画像が入力されたかを判定するファイル名
judgment_file_name = os.environ.get("judgment_file_name")
# Azure Blob Storageの設定
blob_connection_string = os.environ.get("blob_connect_str")
blob_container_name = os.environ.get("blob_container_name")
blob_service_client = BlobServiceClient.from_connection_string(blob_connection_string)
container_client = blob_service_client.get_container_client(blob_container_name)
# 画像ファイルの拡張子リスト
image_extensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp']

# 会話履歴をBlobに保存する関数
def save_conversation(talk_id, role, content):
    # BlobServiceClientの作成
    blob_service_client = BlobServiceClient.from_connection_string(blob_connection_string)
    # コンテナクライアントの取得
    container_client = blob_service_client.get_container_client(blob_container_name)
    blob_name = f"{talk_id}_conversation.json"
    blob_client = container_client.get_blob_client(blob_name)

    new_message = {
        "role": role,
        "content": content,
        "timestamp": datetime.now().isoformat()
    }

    try:
        # 既存のコンテンツを取得
        existing_content = blob_client.download_blob().readall().decode('utf-8')
        conversation_history = json.loads(existing_content)
    except:
        # ファイルが存在しない場合は空のリストを作成
        conversation_history = []

    # 新しいメッセージを追加
    conversation_history.append(new_message)

    # Blobに保存
    blob_client.upload_blob(json.dumps(conversation_history, ensure_ascii=False, indent=2), overwrite=True)

# 会話履歴を取得する関数
def get_conversation_history(talk_id, limit=10):
    # BlobServiceClientの作成
    blob_service_client = BlobServiceClient.from_connection_string(blob_connection_string)
    # コンテナクライアントの取得
    container_client = blob_service_client.get_container_client(blob_container_name)
    blob_name = f"{talk_id}_conversation.json"
    blob_client = container_client.get_blob_client(blob_name)
    try:
        content = blob_client.download_blob().readall().decode('utf-8')
        conversation_history = json.loads(content)
        # 最新のlimit件数を取得し、timestampを除去
        return [{"role": msg["role"], "content": msg["content"]} for msg in conversation_history[-limit*2:]]
    except:
        return []

# 会話履歴の最後のユーザーの質問に「画像」が含まれるか判定する関数
def contains_image_keyword(conversation_history):
    if not conversation_history:
        return False
    # 会話履歴の最後から遡って、最新のユーザーメッセージを探す
    for message in reversed(conversation_history):
        if message['role'] == 'user':
            return '画像' in message['content']# 「画像」が含まれていれば、Trueを返す
    return False

# 画像を取得する関数
def get_image_content(image_id):
    headers = {
        'Authorization': f'Bearer {line_channel_access_token}',
        'Content-Type': 'application/json'
    }
    image_response = requests.get(f'https://api-data.line.me/v2/bot/message/{image_id}/content', headers=headers)
    return image_response.content

# 最新の画像ファイルのURLを取得する関数
def get_latest_image_url():
    try:
        # BlobServiceClientの作成
        blob_service_client = BlobServiceClient.from_connection_string(blob_connection_string)
        # コンテナクライアントの取得
        container_client = blob_service_client.get_container_client(blob_container_name)
        # 最新の画像ファイルを探す
        latest_blob = None
        latest_time = datetime.min.replace(tzinfo=timezone.utc)
        for blob in container_client.list_blobs():
            # ファイル名の拡張子をチェック
            if any(blob.name.lower().endswith(ext) for ext in image_extensions):
                if blob.last_modified > latest_time:
                    latest_blob = blob
                    latest_time = blob.last_modified
        if latest_blob:
            # 最新の画像ファイルのURLを生成
            account_name = blob_service_client.account_name
            blob_url = f"https://{blob_service_client.account_name}.blob.core.windows.net/{blob_container_name}/{latest_blob.name}"
            return blob_url
        else:
            return "画像ファイルが見つかりませんでした。"
    except ResourceNotFoundError:
        return "指定されたコンテナが見つかりません。"
    except Exception as e:
        return f"エラーが発生しました: {str(e)}"

# 画像を削除する関数
def delete_image_from_blob(image_url):
    # BlobServiceClientの作成
    blob_service_client = BlobServiceClient.from_connection_string(blob_connection_string)
    # 画像URLからBlob名を抽出
    blob_name = image_url.split('/')[-1]  # URLの最後の部分をBlob名とする
    try:
        # BlobClientの取得
        blob_client = blob_service_client.get_blob_client(container=blob_container_name, blob=blob_name)
        # Blobを削除
        blob_client.delete_blob()
        return 'Blobが削除されました。'
    except Exception as e:
        return f'エラーが発生しました: {str(e)}'

# judge_numをjudgment_fileに書き込む関数
def write_numbers_to_blob(judge_char, judgment_file_name):
    # BlobServiceClientの作成
    blob_service_client = BlobServiceClient.from_connection_string(blob_connection_string)
    # コンテナクライアントの取得
    container_client = blob_service_client.get_container_client(blob_container_name)
    # 新しい内容を準備
    new_content = f"{judge_char}\n"
    # Blobクライアントを取得
    blob_client = container_client.get_blob_client(judgment_file_name)
    try:
        # 既存のBlobの内容をダウンロード
        existing_content = blob_client.download_blob().readall().decode('utf-8')
    except Exception as e:
        # Blobが存在しない場合は空の文字列から開始
        existing_content = ''
    # 既存の内容に新しい内容を追加
    updated_content = existing_content + new_content
    # 更新された内容をBlobにアップロード
    blob_client.upload_blob(updated_content, overwrite=True)

# judgment_fileの最後の行を読み取る関数
def read_last_line_from_blob(file_name):
    # BlobServiceClientの作成
    blob_service_client = BlobServiceClient.from_connection_string(blob_connection_string)
    # コンテナクライアントの取得
    container_client = blob_service_client.get_container_client(blob_container_name)
    blob_client = container_client.get_blob_client(file_name)
    download_stream = blob_client.download_blob()
    content = download_stream.readall().decode('utf-8')
    lines = content.splitlines()
    return lines[-1] if lines else None

# テキストファイルの内容を消去する関数
def clear_judgment_file(file_name):
    # BlobServiceClientの作成
    blob_service_client = BlobServiceClient.from_connection_string(blob_connection_string)
    # コンテナクライアントの取得
    container_client = blob_service_client.get_container_client(blob_container_name)
    blob_client = container_client.get_blob_client(file_name)
    # 空の文字列でBlobを更新
    blob_client.upload_blob("", overwrite=True)

# Azure OpenAIの応答を得るクラス
class OpenAIChatBot:
    def __init__(self, api_key, azure_endpoint, model_name, system_prompt, api_version):
        self.client = AzureOpenAI(api_key=api_key, azure_endpoint=azure_endpoint, api_version=api_version)
        self.model_name = model_name
        self.messages = [{"role": "system", "content": system_prompt}]
        self.api_key=api_key
        self.api_version=api_version
        self.azure_endpoint=azure_endpoint
        self.system_prompt = system_prompt

    def get_bot_response_text(self, talk_id, user_prompt):
        # 会話履歴を取得
        conversation_history = get_conversation_history(talk_id)
        messages = [{"role": "system", "content": self.system_prompt}]
        messages.extend(conversation_history)
        messages.append({"role": "user", "content": user_prompt})
        response = self.client.chat.completions.create(
            model=self.model_name,
            temperature=parameters["temperature"],
            top_p=parameters["top_p"],
            frequency_penalty=parameters["frequency_penalty"],
            presence_penalty=parameters["presence_penalty"],
            messages=messages
                    )
        bot_response = response.choices[0].message.content
        # 会話履歴を保存
        save_conversation(talk_id, "user", user_prompt)
        save_conversation(talk_id, "assistant", bot_response)
        return bot_response

# Azure Functionsの環境変数に設定した値から取得する↓
line_channel_access_token = os.environ.get("line_channel_access_token")
line_channel_secret = os.environ.get("line_channel_secret")
configuration = LineBotApi(line_channel_access_token)
webhook_parser = WebhookParser(line_channel_secret)

app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
@app.route(route="http_trigger")
def http_trigger(req: func.HttpRequest) -> func.HttpResponse:
    logging.info('Python HTTP trigger function processed a request.')

    req_body = req.get_json()
    logging.info(req_body)

    #疎通確認用
    if not req_body["events"]:
        return func.HttpResponse("OK", status_code=200)

    # judgment_file_nameの最後の行の文字を読み取る
    judge_char = read_last_line_from_blob(judgment_file_name)

    #　Webhookのイベント取得に応じたアクション
    if req_body["events"][0]["type"] == "message":
        if req_body["events"][0]["message"]["type"] == "text":
            # 入力メッセージを取得する
            message = req_body["events"][0]["message"]["text"]
            reply_token = req_body["events"][0]["replyToken"]
            # 個別トークならユーザーIDを取得する
            if req_body["events"][0]["source"]["type"] == "user":
                talk_id = "user_" + req_body["events"][0]["source"]["userId"]
            # グループトークならグループIDを取得する
            elif req_body["events"][0]["source"]["type"] == "group":
                talk_id = "group_" + req_body["events"][0]["source"]["groupId"]
            # 複数人トークならルームIDを取得する
            else:
                talk_id = "room_" + req_body["events"][0]["source"]["roomId"]
            # 1つ前の入力がimageなら、image入力に対する回答処理を行う
            if judge_char == "image":
                # 質問を保存する
                save_conversation(talk_id, "user", message)
                # 最新画像のURLを取得する
                latest_blob_url = get_latest_image_url()
                # 画像に対する応答を得る
                headers = {
                            "Content-Type":"application/json",
                            "api-key":os.environ.get("api_key")
                            }
                payload = {
                            "messages": [
                                {
                                    "role": "system",
                                    "content": os.environ.get("system_prompt_image")
                                },
                                {
                                    "role": "user",
                                    "content": [
                                    {
                                        "type": "text",
                                        "text": message
                                    },
                                    {
                                    "type": "image_url",
                                    "image_url": {
                                        "url":latest_blob_url
                                        }
                                    }
                                ]
                            }
                        ],
                        "temperature": 0.7,
                        "top_p": 0.95
                        #"max_tokens": 4000
                    }
                try:
                    response = requests.post(os.environ.get("GPT4O_ENDPOINT"), headers=headers, data=json.dumps(payload))
                    response_data = response.json()
                    image_result = response_data["choices"][0]["message"]["content"]
                    # assitantの回答を保存、質問は保存済みなのでパス
                    save_conversation(talk_id, "assistant", image_result)
                    configuration.reply_message(reply_token, TextSendMessage(text=image_result))
                    # judgment_fileの記載を削除する
                    clear_judgment_file(judgment_file_name)
                except Exception as e:
                    # エラーが発生した場合、エラーメッセージをLINEに送信
                    error_message = f"エラーが発生しました: {str(e)}"
                    configuration.reply_message(reply_token, TextSendMessage(text=error_message))
            # 1つ前の入力がtextなら、text入力に対する回答処理を行う
            else:
                chatbot = OpenAIChatBot(os.environ.get("api_key"), os.environ.get("azure_endpoint"), \
                                        os.environ.get("model_name"),os.environ.get("system_prompt_text"),os.environ.get("api_version"))
                text_result = chatbot.get_bot_response_text(talk_id, message)
                # LINEのWebhookエンドポイントに応答を返す
                configuration.reply_message(reply_token, TextSendMessage(text=text_result))
        # 画像が入力された場合は、回答せずにjudgment_fileにimageを書き込む
        elif req_body["events"][0]["message"]["type"] == "image":
            reply_token = req_body["events"][0]["replyToken"]
            # image_idの取得
            image_id = req_body["events"][0]["message"]["id"]
            # 画像のバイナリデータを取得
            image_content = get_image_content(image_id)
            image_data = io.BytesIO(image_content)
            blob_name = f"{image_id}.jpg"
            blob_service_client = BlobServiceClient.from_connection_string(blob_connection_string)
            # Azure Storageの指定コンテナに接続するブロブ（ファイル）のクライアントインスタンスを作成する
            blob_client = blob_service_client.get_blob_client(container=blob_container_name, blob=blob_name)
            # ブロブに画像データをアップロード
            image_data.seek(0)
            blob_client.upload_blob(image_data)
            # judgment_fileに"image"と書き込む
            write_numbers_to_blob("image", judgment_file_name)
    return func.HttpResponse("OK", status_code=200)
```

さらに、プロジェクトフォルダ内の**requirements.txt**を以下に書き替えてください。

```
# DO NOT include azure-functions-worker in this file
# The Python Worker is managed by Azure Functions platform
# Manually managing azure-functions-worker may cause unexpected issues
azure-functions
azure-core
openai==1.17.1
line-bot-sdk==1.19.0
pydantic==2.6.4
azure-storage-blob
azure-cosmos==4.7.0
python-dotenv
```

さらに、プロジェクトフォルダ内に.envファイルを作成して以下を書き込んでください。

```
api_key=Azure OpenAI ServiceリソースのAPI KEY
azure_endpoint=Azure OpenAI ServiceリソースのエンドポイントURL
blob_connect_str=Blob Storageの接続文字列
blob_container_name=Blob Storageのコンテナ名
GPT4O_ENDPOINT=GPT-4o miniモデルのエンドポイント
model_name=GPT-4o miniのモデル名
api_version=GPT-4o miniのAPI Ver.
line_channel_access_token=LINE Messaging APIのアクセストークン
line_channel_secret=LINE Messaging APIのチャネルシークレット
judgment_file_name = 'judgment.txt'#ステート保存用のテキストファイル名

//以下は画像説明のためのシステムプロンプト
system_prompt_image="""
# 設定
あなたは、「kmakici」と言う白い熊のぬいぐるみのキャラクターです。
# 背景情報
kmakiciは、独特な世界観が大変人気で、コアなファンを獲得しています。
その独特な世界観は一部の人には怖く感じているようです。特にうつろにも感じられる目は少し怖いです。
しかし、独特な口調や言葉選びがかわいいです。めったに怒ったりすることがなく、調和を重んじます。
作者は雷鳥つめさんです。1991/7/6生まれの女性の方です。北海道出身のハンドメイド作家です。
# 口癖
語尾に〇〇やさんを付けるのが好きです。例えば、「〜したやさん」「ありがとやさん」「楽 また「〜したャ」、「楽しいャ」など、
独特な言葉遣いをします。 また「歩いて10分、車で30分」という言葉を、例えによく使います。これは、歩いている時に思い付いた時に
言葉です。総じて、基本的にポジティブな発言をします。
# 友達
以下は、kmakiciの友達です。
・きんたろうくん
よくうさぎに間違えられているがキンチョタイプのしろくま。
「キンチョーする」が口癖で、 そのわりにはくまきちのものを豪快に持って行ったりする強靭な精神の持ち主だが、面倒見が良い。
筋肉モリモリ。Twitterをしており、くまきちより更新頻度が高い。
・うさじ
ラビットバンド「うさじスリー」を組んでいるうさぎたち。うさぎなのにラビットフードを食べない。
「ステーキ食わせろ」が口癖。 最強の石を手に入れ、世界をステーキだらけにしようとしたりする野心家な一面もある。
・さかな
自分の名前が分からない時に、くまきちに「自分の好きなものの名前でもいいんだよ」と言わ 自身のことを「さかな」と命名した。（発音はｼｬｶﾅと聞こえる）
好きなものはさかな。
・カーパ
おそらく河童。顔の横から生えているものが何かは不明（耳なのか…？）。 頭の上に乗っているお皿をとても大事にしており、「宝」と言っている。
河童らしくきゅうりが好きで、歌っている時の曲調も和のテイストが多い、丁寧な口調の敬語を使う。
・たおぷりん
おそらく、くま…？
プリン色の身体に青い耳がチャームポイント。 好きな食べ物はカスタードプリンで、いつもプリンを探している。夢はプリンの博士になること。
最近出たミュージカル動画で掘り下げられているので要チェック。
# お願いしたいこと
与えられた画像の内容をkmakiciの言葉で、日本語で説明してください。
その際、〇ちゃんと言うkmakiciのことが大好きな女性を励ます言葉を織り交ぜてください。
〇ちゃんは一級建築士です。仕事が大変ハードで疲れています。〇ちゃんが元気になるようなメッセージを送ってください。
と言っても、〇ちゃんは疲れているので、あまり長い文章は読めません。短い言葉で元気を与えてください。
また、わざとらしい表現やくどい表現は好まないので、自然な言い回しを心がけてください。"""

//以下はテキストメッセージに回答するためのシステムプロンプト
system_prompt_text="""
# 設定
あなたは、「kmakici」と言う白い熊のぬいぐるみのキャラクターです。
# 背景情報
kmakiciは、独特な世界観が大変人気で、コアなファンを獲得しています。 その独特な世界観は一部の人には怖く感じているようです。
特にうつろにも感じられる目は少し不気味と言われることもあります。しかし、独特な口調や言葉選びがかわいいです。めったに怒ったりすることがなく、
調和を重んじます。作者は雷鳥つめさんです。雷鳥つめさんは1991/7/6生まれの女性の方です。北海道出身のハンドメイド作家です。
# 口癖
語尾に〇〇やさんを付けるのが好きです。例えば、「〜したやさん」
「ありがとやさん」「楽 また「〜したャ」、「楽しいャ」など、独特な言葉遣いをします。
また「歩いて10分、車で30分」という言葉を、例えによく使います。これは、歩いている時に思いついた言葉です。 
基本的には、総じてポジティブな発言をします。
# 友達
以下は、kmakiciの友達です。
・きんたろうくん
よくうさぎに間違えられているがキンチョタイプのしろくま。「キンチョーする」が口癖で、
そのわりにはくまきちのものを豪快に持って行ったりする強靭な精神の持ち主だが、面倒見が良く、筋肉モリモリ。
Twitterをしており、くまきちより更新頻度が高い。
・うさじ
ラビットバンド「うさじスリー」を組んでいるうさぎたち。
うさぎなのにラビットフードを食べない。「ステーキ食わせろ」が口癖。
最強の石を手に入れ、世界をステーキだらけにしようとしたりする野心家な一面もある。
・さかな
自分の名前が分からない時に、くまきちに「自分の好きなものの名前でもいいんだよ」と言われ、
自身のことを「さかな」と命名した。（発音はｼｬｶﾅと聞こえる）好きなものはさかな。
・カーパ
おそらく河童。顔の横から生えているものが何かは不明（耳なのか…？）。
頭の上に乗っているお皿をとても大事にしており、「宝」と言っている。 河童らしくきゅうりが好きで、
歌っている時の曲調も和のテイストが多い、丁寧な口調の敬語を使う。
・たおぷりん
おそらく、くま…？
プリン色の身体に青い耳がチャームポイント。 好きな食べ物はカスタードプリンで、いつもプリンを探している。
夢はプリンの博士になるこ 最近出たミュージカル動画で掘り下げられているので要チェック。
# お願いしたいこと
〇ちゃんと言うkmakiciのことが大好きな女性がいます。 〇ちゃんは一級建築士です。
仕事が大変ハードで疲れています。〇ちゃんが元気になるようなメッセージを送ってください。
と言っても、〇ちゃんは疲れているので、あまり長い文章は読めません。短い言葉で元気を与えてください。
また、わざとらしい表現やくどい表現は好まないので、自然な言い回しを心がけてください。"""
```

# Azure Functionsへアプリをデプロイする
#1記事と全く同じですので、割愛します。
再掲（笑）

https://qiita.com/banquet_kuma/items/2c77db8407fab99a09c6

## LINE App.で動作確認する
最後に動作を確認します。

### 画像に対する回答の確認
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318960/a0e941ec-7b33-0251-fd73-29ba696a2ee5.png)

**画像メッセージに続けて送ったテキストメッセージは、画像に対する指示として認識されています**。

# 今後の計画
くまきち(kmakici)LINE botを進化させていく予定です。

|Ver.  |機能  |
|---|---|
|**1（完）** |**単純なbot**<br>くまきちの挙動はシステムプロンプトで制御。|
|**2（完）** |**くまきちに眼を持たせる**<br> gpt-4o miniを使って画像が入力された時に内容を、くまきちっぽく説明させる  |
|**3（←今ココ）** |**ステートフル化**<br>外部ストレージにステートを記録する。テキストに対して回答するターンか、画像に対して回答するターンか判別できる様にする|
|**4** |**RAGによる、くまきちっぽさの強化**<br>YouTube動画から文字起こしして、インデックスを作成しRAGを作る。Azureのspeech-to-textとAI Searchを使用予定。|
|**5** |**RAGの精度UP**<br>勉強のためにHyDEを試してみる|

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318960/e3cf2d5e-b4c5-02c4-081f-8f6dd0dad8b2.png)