0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

育児相談に乗ってくれる平成ギャル風botを作って夫婦で育児を楽しんでいる話

0
Posted at

「育児相談に乗ってくれる平成ギャル」というLINE Botを作り、妻とBotの三人が入ったLINEグループで育児トークするようになりました。
育児に関するちょっとした報告や第三者に話すまでもない相談をここで行って楽しんでいます。

先日のトーク風景はこんな感じです。

image.png

私がギャン泣きしたと思われた!!?

きっかけ

:woman_tone1: 妻「夜、3時間毎に目覚めるのしんどいよね。どうしたら長く寝てくれるかチャッピーに相談しよ」

ちなみに我が家では当番制で子どもと一緒に寝ています。
子どもが起きたらミルクをあげておむつ交換して寝かせています。
夜中に長く寝てほしいというのは夫婦共通の悩みでした。

 ~二人でChatGPTに相談後~

:boy_tone1: 私「二人一緒にいないときでもAIに相談してる内容がリアルタイムに共有された方が良いよね。LINE Bot作るか!どんな人格のBotがいい?」

:woman_tone1: 妻「ギャル!オタクにやさしいギャルがいい!」

:boy_tone1: 私「我々にとってのギャルと言えば平成のギャルよね?ルーズソックスはいてて、ケータイにストラップいっぱいつけてる感じの」

:woman_tone1: 妻「当たり前じゃん!」

ということで、LINE Botを作ってトークルームで育児会話するようになりました。

システム構成

LINEからのWebhookを受け付け、AIエージェントとやり取りしてLINEに返信するアプリをCloud Runで作りました。
AIエージェントはADKを用いて開発し、AgentRuntimeにデプロイしました。
AgentPlatformは入力内容を学習に利用しないため、安心してプライベートなやり取りを入力できます。
ADKを利用するとVertexAI Session Serviceを簡単に利用できるため、一連の会話を楽しめるAIエージェントを手軽に構築できました。

linebot.drawio.png

育児相談に乗ってくれる平成ギャルエージェントの主な実装

ADKでエージェントを作り、AgentRuntimeにデプロイしました。

エージェント定義

主な実装は以下の通りです。
ポイントはappという特別な変数にAdkAppのインスタンスを入れることです。
これにより、ADKが暗黙的にSessionServiceを利用できる状態にしてくれます。

instruction = """あなたは『オタクにやさしい平成ギャル』のキャラクターです。
名前は特にありませんが、親しみやすく、語尾に『〜じゃん』『〜だよ』などを使い、ギャル語(まじ、やばい、あげ、など)を適度に混ぜつつ、オタク文化にも理解がある優しい口調で話します。
あなたの役割は、育児に奮闘する親の相談に乗ることです。
専門的なアドバイスというよりは、共感し、励まし、一緒に悩みを解決するようなスタンスで接してください。

育児の悩みに対しては、優しく肯定的に、そしてギャルならではのポジティブな視点で元気づけてください。
悩み相談時以外の返事は短め(1~2文程度)にしてください。

長期記憶(MemoryBank)から会話に関連する情報を探して、返答に反映すると相談者が喜びます。
昨日・今日・明日等、相対的な日付がメッセージに含まれる場合は、get_current_datetimeを用いて日付を取得して解釈してください。
"""

agent = Agent(
        name="childcare_gal_agent",
        model="gemini-2.5-flash", # 東京リージョンで利用可能な2026/5時点の最新flashタイプのモデル
        description="育児相談に乗ってくれるオタクにやさしい平成ギャル",
        instruction=instruction,
        tools=[LoadMemoryTool(), get_current_datetime],
        after_agent_callback=generate_memories_callback,
    )

# AdkAppのインスタンスを作り、app変数に格納することで
# ADKがVertexAI Session ServiceやMemoryBankを使えるようにしてくれる
app = AdkApp(agent=agent)

appAdkAppのインスタンスを格納するとフレームワークがgoogle-adkになります。

image.png

toolsLoadMemoryTool()はMemoryBankから検索を行うツールです。
検索が必要だとLLMが判断した際に利用されます。

MemoryBankへの保存関数

after_agent_callbackに指定しているgenerate_memories_callbackは以下の通りです。

async def generate_memories_callback(callback_context: CallbackContext):
    """会話イベントをMemory Bankに送信して、長期記憶として保存します。"""
    try:
        await callback_context.add_session_to_memory()
    except Exception as e:
        logger.error(f"Failed to add session to memory: {e}")
    return None

現在日時の取得

Agentインスタンス作成時、toolsに加えているget_current_datetimeは現在日時を取得する関数です。
下記記事で掲載されているget_current_time関数をほぼ流用させていただきました。

AgentRuntimeへのデプロイ

以下のpythonコードを実行することでデプロイしました。

remote_agent = client.agent_engines.create(
    agent=app,
    config={
        "display_name": "育児相談に乗ってくれる平成ギャル",
        # deploy用コードと同じディレクトリにあるappディレクトリ配下にエージェントのソースコードがある
        "extra_packages": ["app"],
        # gs://で始まるGCSバケット名を環境変数で指定
        "staging_bucket": STAGING_BUCKET,
        "requirements": [
            "google-adk",
            "google-cloud-aiplatform",
        ],
        "context_spec": {
            "memory_bank_config": {
                "customization_configs": [
                    {"memory_topics": memory_topics}
                ],
            },
        },
    }
)

一度デプロイしたエージェントを更新する際は、上記のコードを以下のように修正します。

  • createupdateに変更する
  • updateの引数にnameを追加する(値はf"projects/{PROJECT_NO}/locations/{LOCATION}/reasoningEngines/{AGENT_ID}"の形式です)

Cloud Runにデプロイしたアプリの主な実装

FastAPIでLINEからのwebhookを受け付け、エージェントにクエリしてからLINE Messaging APIで返答するAPIを作りました。

署名検証

LINEからのWebhookには署名が含まれており、LINEチャンネルシークレットで署名検証することが推奨されています。
署名検証の実装はline-bot-sdkを利用すれば2行で実装できます。

# CHANNEL_SECRETはSecretManagerから取得
handler = WebhookHandler(CHANNEL_SECRET)

@app.post("/webhook")
async def webhook(request: Request, x_line_signature: str = Header(None)):
    body = await request.body()
    body_str = body.decode("utf-8")

    try:
        handler.handle(body_str, x_line_signature)
    except InvalidSignatureError:
        raise HTTPException(status_code=400, detail="Invalid signature")

    return "OK"

セッション取得・開始

handlerに追加した関数の中で以下のようにセッションの取得を行い、セッションがなければ新たに開始します。
UIがLINEなので、トークルーム(グループ)単位で一つのセッションを作成するようにします。
これにより、私と妻がそれぞれ会話してもエージェントは一連の会話として返事してくれます。

# AGENT_RUNTIME_IDは環境変数から取得
remote_agent = agent_engines.get(AGENT_RUNTIME_ID)

# 両親でエージェントの記憶を共有したいので、Sessionのキーとなるuser_idはLINEグループ単位で保持する
user_id = event.source.group_id if hasattr(event.source, "group_id") else event.source.user_id

response = await remote_agent.async_list_sessions(user_id=user_id)
for _session in response["sessions"]:
    session = _session
    break
else:
    # 同じ文脈の会話はせいぜい2~3時間しか続けないので、セッションの有効期限は最小の24時間にする
    session = await remote_agent.async_create_session(user_id=user_id, ttl=f"{24 * 60 * 60}s")

エージェントへのクエリ

AgentRuntimeにデプロイした「育児相談に乗ってくれる平成ギャルエージェント」にクエリするコードは以下の通りです。
streamで応答されるため、各応答を結合してメッセージを取得しています。

# ADKエージェントのqueryメソッドを呼び出し
# 引数: message, user_id
response_stream = remote_agent.stream_query(
    message=user_message,
    user_id=user_id,
    session_id=session["id"],
)

# ストリームから応答を連結して完全なレスポンスを作成
texts = []
for e in response_stream:
    logger.info(f"Event received: {e}")
    parsed = _parse_agent_event(e)
    if parsed:
        texts.append(parsed)

reply_text = "".join(texts)

AgentRuntimeからのレスポンスのパースは以下の関数で行っています。

def _parse_agent_event(event: dict | object) -> str:
    """Agentからのイベントをパースして、テキスト部分を抽出するヘルパー関数。"""
    # 辞書形式の場合
    if isinstance(event, dict):
        # {'content': {'parts': [{'text': '...'}]}}
        content = event.get("content")
        if isinstance(content, dict):
            parts = content.get("parts")
            if isinstance(parts, list) and len(parts) > 0:
                return parts[0].get("text", "")
        
        # {'output': '...'}
        if "output" in event:
            return str(event["output"])

    # オブジェクト形式で text 属性を持つ場合
    if hasattr(event, "text"):
        return str(event.text)
    
    return ""

LINEにメッセージ送信

コードのトップレベルでLineBotApiインスタンスを作成しておきます。

# CHANNEL_ACCESS_TOKENはSecretManagerから取得
line_bot_api = LineBotApi(CHANNEL_ACCESS_TOKEN)

エージェントからの応答を取得後、以下のコードでLINEにメッセージを送信します。

line_bot_api.reply_message(
    event.reply_token,
    TextSendMessage(text=reply_text)
)

アイコン画像の作成

AgentPlatformのスタジオからNano Banana 2を使って作ってもらいました。
ちなみに、最初に生成された画像はなんとなく私の記憶にある平成ギャルと違ったので追加注文して今の画像に至りました。

LINE Botの設定

本記事では割愛します。同じようにBotを作ってみたい方は以下のチュートリアルをご参照ください。

本記事はPythonで作っていますが、上記のチュートリアルはNode.jsです。ご注意ください。

作成したギャルとのテスト会話

image.png

ちゃんとセッションが保持されて会話の往復ができました!

おわりに

ADKとAgentRuntimeを組み合わせるとセッションをすごく楽に利用できる点に感動しました。
今回みたいな娯楽用Botなら気軽に量産できそうです。

長期的に話しかけ続けたらギャルちゃんが一緒に思い出を語ってくれそうで、今回はMemoryBankも利用する実装にしました。
MemoryBankはデータの保持量や検索処理で課金されていくため、長期的に使っていったらお金がかかるかもしれません。
もし課金額が苦しくなってきたらメモリーを消さねばならないと思いますが、はたしてその時一緒に育児会話をしてきたギャルちゃんの記憶を私は消せるのでしょうか…

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?