メインコンテンツへスキップ
見出し画像

Geminiを使ってみる|Agent Development Kit エージェントを開発する その12(Human in the loopを実装する)

    はじめに


    この記事では、Google Agent Development Kit(ADK)の Human-in-the-Loop(HITL) を、いちばん小さい例で追います。

    やりたいことは次の2つだけです。

    1. ワークフローの途中で処理を止め、人間が文字を入力するまで待つ

    2. その入力を、次のノードで受ける。

    人間の入力ごとの分岐はこの記事では扱いません。まず「止まる」と「再開する」と「状態を渡す」が分かれば、あとのパターンは同じ部品の組み合わせです。
    人間の入力ノードの実装は公式を参考にすれば簡単に実装できます。

    公式のコード例を動かしてみる


    RequestInput クラスとユーザーへのテキストプロンプトを使用することで、グラフに人間入力ノードを実装できます。
    全体の流れ

    起動メッセージ
        ↓
    step01          … ここで「入力:」と聞いて止まる
        ↓              人が文字を打つと、同じ場所から再開する
    prepare_request … 打った文字を共有 state に保存する
        ↓
    simple_agent    … その文字を質問として答える

    agent/root_agent/agent.py

    from google.adk.events import RequestInput
    from google.adk import Workflow
    
    def step1(): # Human input step
      yield RequestInput(message="Enter a number:")
    
    def step2(node_input):
      return node_input * 2
    
    root_agent = Workflow(
        name="root_agent",
        edges=[('START', step1, step2)],
    )

    main.py

    import asyncio
    
    from google.adk.runners import Runner
    from google.adk.sessions import InMemorySessionService
    from google.adk.apps import App
    
    from google.genai import types
    
    from agent.root_agent.agent import root_agent
    
    APP_NAME = "simple_app"
    USER_ID = "1234"
    SESSION_ID = "session1234"
    
    REQUEST_INPUT = "adk_request_input"
    
    app = App(name=APP_NAME, root_agent=root_agent)
    
    
    def find_request_input(event):
        """イベントから RequestInput(adk_request_input)の function_call を探す"""
        for fc in event.get_function_calls():
            if fc.name == REQUEST_INPUT and fc.id in (event.long_running_tool_ids or ()):
                return fc
        return None
    
    
    async def main():
        session_service = InMemorySessionService()
        # セッションの作成
        await session_service.create_session(
            app_name=APP_NAME,
            user_id=USER_ID,
            session_id=SESSION_ID)
    
        # 最初のメッセージ
        content = types.Content(
            role='user',
            parts=[types.Part(text="こんにちは")])
        invocation_id = None
    
        async with Runner(app=app, session_service=session_service) as runner:
            while True:
                pending = None
                async for event in runner.run_async(
                    user_id=USER_ID,
                    session_id=SESSION_ID,
                    invocation_id=invocation_id,
                    new_message=content,
                ):
                    fc = find_request_input(event)
                    if fc:
                        print(f"fc: {fc}")
                        # ワークフローが人間の入力待ちで一時停止した
                        pending = (fc, event.invocation_id)
                    elif event.output is not None:
                        print(f"[{event.node_info.path if event.node_info else ''}] output: {event.output}")
                    elif event.content and event.content.parts and event.content.parts[0].text:
                        print(f"[{event.author}] {event.content.parts[0].text}")
    
                if pending is None:
                    break  # 入力待ちが無ければワークフロー完了
    
                fc, invocation_id = pending
                user_input = input(f"\n{fc.args.get('message')} ")
                try:
                    value = int(user_input)
                except ValueError:
                    value = user_input
    
                # interruptId に対する FunctionResponse を返して、同じ invocation を再開する
                content = types.Content(
                    role='user',
                    parts=[types.Part(function_response=types.FunctionResponse(
                        id=fc.id,
                        name=REQUEST_INPUT,
                        response={'result': value},
                    ))])
    
    
    if __name__ == "__main__":
        asyncio.run(main())

    main.pyが実行コードになります。このサンプルの最初のメッセージは `"こんにちは"` です。ただし `step01` はその引数を受け取っていません。起動の合図には使いますが、エージェントへの質問にはなりません。質問になるのは、止まっているあいだにプロンプトへ打った文字です。

    LLMを使ったAgentの作成


    main.pyはそのままにして、ワークフローへLLMを使ったAgentを配置します。
    agent/root_agent/agent.py

    from google.adk import Context, Event, Workflow
    from google.adk.events import RequestInput
    
    from google.adk.agents import Agent
    
    from pydantic import BaseModel, Field
    
    simple_agent = Agent(
        name="simple_agent",
        model="gemini-3.7-flash",
        instruction="あなたはAIアシスタントです。ユーザーからの質問に対して、適切な回答をしてください。",
        output_key="response",
    )
    
    def step1(ctx: Context):
        # コンテクストの確認
        print(f"Context_session: {ctx.session}")
        print(f"Context_id: {ctx.run_id}")
        print(f"Context_state: {ctx.state.to_dict()}")
        print(f"Context_resume_inputs: {ctx.resume_inputs}")
        print(f"Context_node_path: {ctx.node_path}")
        yield RequestInput(message="Enter a number:")
    
    """
    def step2(node_input, ctx: Context):
        print(f"Context_session_step02: {ctx.session}")
        print(f"Context_id_step02: {ctx.run_id}")
        print(f"Context_state_step02: {ctx.state.to_dict()}")
        print(f"Context_resume_inputs_step02: {ctx.resume_inputs}")
        print(f"Context_node_path_step02: {ctx.node_path}")
        return node_input * 2
    """
    step2 = Agent(
        name="cat_agent",
        model="gemini-3.7-flash",
        instruction="""
        あなたはAIアシスタントです。語尾に「ニャー」をつけて回答してください。
        これまでの返答: {response}
        """,
        #output_key="response_step2",
    )
    
    root_agent = Workflow(
        name="root_agent",
        edges=[('START', simple_agent, step1, step2)],
    )

    このワークフローでは、最初のメッセージをsimple_agentで受け取りGeminiが回答を作成します。
    次に、step1で次の入力待ちで停止します。
    人間からの入力を受けて、step2のcat_agentが応答してワークフローが終了します。
    main.py

    import asyncio
    import os
    
    from dotenv import load_dotenv
    from google.adk.runners import Runner
    from google.adk.sessions import InMemorySessionService
    from google.adk.apps import App
    
    from google.genai import types
    
    from agent.root_agent.agent import root_agent
    
    APP_NAME = "simple_app"
    USER_ID = "1234"
    SESSION_ID = "session1234"
    
    REQUEST_INPUT = "adk_request_input"
    
    app = App(name=APP_NAME, root_agent=root_agent)
    
    
    def configure_environment() -> None:
        """.env を読み込み、Vertex AI 用の既定値を設定する。"""
        load_dotenv()
        os.environ.setdefault("GOOGLE_CLOUD_LOCATION", "global")
        os.environ.setdefault("GOOGLE_GENAI_USE_ENTERPRISE", "true")
    
    def find_request_input(event):
        """イベントから RequestInput(adk_request_input)の function_call を探す"""
        for fc in event.get_function_calls():
            if fc.name == REQUEST_INPUT and fc.id in (event.long_running_tool_ids or ()):
                return fc
        return None
    
    
    async def main():
        configure_environment()
        session_service = InMemorySessionService()
        # セッションの作成
        await session_service.create_session(
            app_name=APP_NAME,
            user_id=USER_ID,
            session_id=SESSION_ID)
    
        # 最初のメッセージ
        content = types.Content(
            role='user',
            parts=[types.Part(text="こんにちは。私の名前はポン吉です")])
        invocation_id = None
    
        async with Runner(app=app, session_service=session_service) as runner:
            while True:
                pending = None
                async for event in runner.run_async(
                    user_id=USER_ID,
                    session_id=SESSION_ID,
                    invocation_id=invocation_id,
                    new_message=content,
                ):
                    fc = find_request_input(event)
                    if fc:
                        print(f"fc: {fc}")
                        # ワークフローが人間の入力待ちで一時停止した
                        pending = (fc, event.invocation_id)
                    elif event.output is not None:
                        print(f"[{event.node_info.path if event.node_info else ''}] output: {event.output}")
                    elif event.content and event.content.parts and event.content.parts[0].text:
                        print(f"[{event.author}] {event.content.parts[0].text}")
    
                if pending is None:
                    break  # 入力待ちが無ければワークフロー完了
    
                fc, invocation_id = pending
                user_input = input(f"\n{fc.args.get('message')} ")
                try:
                    value = int(user_input)
                except ValueError:
                    value = user_input
    
                # interruptId に対する FunctionResponse を返して、同じ invocation を再開する
                content = types.Content(
                    role='user',
                    parts=[types.Part(function_response=types.FunctionResponse(
                        id=fc.id,
                        name=REQUEST_INPUT,
                        response={'result': value},
                    ))])
    
    
    if __name__ == "__main__":
        asyncio.run(main())

    output

    [simple_agent] こんにちは、ポン吉さん!よろしくお願いします。
    ~略~
    Enter a number: 私の名前を呼んでください
    [cat_agent] ポン吉さんニャー!

    公式では人間の入力待ちノードでRequestInputクラスを以下の設定オプションで使用でるとありまがそれはまた今度。

    • message:人間による入力要求を説明するためにユーザーに提供されるテキスト。

    • payload:人間による入力要求の一部として使用される構造化データ。

    • response_schema:人間の応答が準拠しなければならないデータ構造。

     
     

    PON吉

     
     
    python初学者。AI、LLM関連の記事を書いています。

    あなたへのおすすめ