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

OpenAI Agents SDK 入門 (2) - Agent ・ Runner ・ Result

    「OpenAI Agents SDK」の「Agent」「Runner」「Result」についてまとめました。

    ・Agents
    ・
    Running agents
    ・
    Results
    ・
    Streaming

    前回

    1. Agents

    「Agent」は、アプリのコアとなる構成要素です。指示とツールで構成されたLLMです。

    1-1. 基本構成

    「Agent」の主なプロパティは次のとおりです。

    ・instructions : 開発者メッセージまたはシステムプロンプト
    ・model : LLM と パラメータ (temperature、top_pなど)
    ・tools : Agentが使用できるツール

    from agents import Agent, ModelSettings, function_tool
    
    # Tool
    @function_tool
    def get_weather(city: str) -> str:
        return f"The weather in {city} is sunny"
    
    # Agent
    agent = Agent(
        name="Haiku agent",
        instructions="常に俳句形式で応答する",
        model="o3-mini",
        tools=[get_weather],
    )

    1-2. Context

    「Agent」は「Context」の型に対してジェネリック(汎用的)です。「Context」は依存性注入のツールであり、Runner.run() に渡すオブジェクトです。このオブジェクトはすべての「Agent」「Tool」「Handoff」などに渡され、「Agent」の実行における依存関係や状態をまとめて管理するためのものです。「Context」として任意の Python オブジェクトを提供できます。

    # Context
    @dataclass
    class UserContext:
      uid: str
      is_pro_user: bool
    
      async def fetch_purchases() -> list[Purchase]:
         return ...
    
    # Agent
    agent = Agent[UserContext](
        ...,
    )

    1-3. Output Type

    デフォルトでは、「Agent」はプレーンテキスト(str)を出力します。特定の型の出力をAgentに生成させたい場合は、output_type パラメータを使用できます。一般的な選択肢として Pydantic オブジェクトがよく使われますが、Pydantic の TypeAdapter でラップできるあらゆる型(dataclass、list、TypedDict など)をサポートしています。

    from pydantic import BaseModel
    from agents import Agent
    
    # Output Type
    class CalendarEvent(BaseModel):
        name: str
        date: str
        participants: list[str]
    
    # Agent
    agent = Agent(
        name="Calendar extractor",
        instructions="テキストからカレンダーイベントを抽出する",
        output_type=CalendarEvent,
    )

    1-4. Handioff

    「Handoff」は、「Agent」が委任できる「Agent」です。「Handoff」のリストを提供すると、「Agent」は必要に応じてそれらに処理を委任できます。これは、特定のタスクに特化したモジュール型の「Agent」をオーケストレーションするための強力なパターンです。詳しくは「Handoff」を参照してください。

    from agents import Agent
    
    # Handoff
    booking_agent = Agent(...)
    refund_agent = Agent(...)
    
    # Agent
    triage_agent = Agent(
        name="Triage agent",
        instructions=(
            "ユーザーの質問に対応する。"
            "予約に関する質問の場合は、booking agentにハンドオフする。"
            "返金に関する質問の場合は、refund agentにハンドオフする。"
        ),
        handoffs=[booking_agent, refund_agent],
    )

    1-5. Dynamic Instruction

    通常、「Agent」を作成するときに指示を与えることができます。しかし、関数を通じて動的な指示を提供することも可能です。この関数は「Agent」と「Context」を受け取り、プロンプトを返す必要があります。通常の関数と非同期関数の両方が使用できます。

    # Dynamic Instruction
    def dynamic_instructions(
        context: RunContextWrapper[UserContext], agent: Agent[UserContext]
    ) -> str:
        return f"The user's name is {context.context.name}. Help them with their questions."
    
    # Agent
    agent = Agent[UserContext](
        name="Triage agent",
        instructions=dynamic_instructions,
    )

    1-6. ライフサイクルイベント(フック)

    「Agent」のライフサイクルを監視したい場合があります。たとえば、イベントを記録したり、特定のイベントが発生した際にデータを事前取得したりすることが考えられます。「Agent」のライフサイクルにフックするには、「hooks」を使用します。「AgentHooks」クラスをサブクラス化し、必要なメソッドをオーバーライドしてください。

    1-7. Guardrail

    「Guardrail」を使用すると、「Agent」の実行と並行して、ユーザー入力のチェックや検証を行うことができます。たとえば、ユーザーの入力が適切かどうかをフィルタリングすることが可能です。詳しくは「Guardrail」を参照してください。

    1-8. AgentのClone / Copy

    「Agent」の clone() を使用すると、「Agent」を複製できます。また、必要に応じて任意のプロパティを変更することも可能です。

    # Agent
    pirate_agent = Agent(
        name="Pirate",
        instructions="Write like a pirate",
        model="o3-mini",
    )
    
    # クローンしたAgent
    robot_agent = pirate_agent.clone(
        name="Robot",
        instructions="Write like a robot",
    )

    2. Runner

    2-1. Agentの実行

    「Agent」は「Runner」を使用して実行できます。
    以下の3つのオプションがあります。

    ・Runner.run()
    非同期で実行され、RunResult を返します。

    ・Runner.run_sync()
    同期メソッドで、内部的には run() を実行します。

    ・Runner.run_streamed()
    非同期で実行され、RunResultStreaming を返します。LLMをストリーミングモードで呼び出し、受信したイベントを逐次ストリーミングします。

    from agents import Agent, Runner
    
    async def main():
        # Agentの作成
        agent = Agent(name="Assistant", instructions="あなたは役に立つアシスタントです。")
    
        # Agentの実行
        result = await Runner.run(agent, "プログラミングにおける再帰について俳句を書いてください。")
        print(result.final_output)

    2-2 Agent Loop

    「Runner」の run() を使用すると、開始Agentと入力を渡します。
    入力は、文字列(ユーザーメッセージと見なされる)または OpenAI Responses API のアイテムが含まれるリストのいずれかになります。

    Runner は以下のようなループを実行します。

    (1) 現在のAgentと現在の入力を使用して LLM を呼び出す。
    (2) LLM が出力を生成する。
     (a) もし LLM が final_output を返した場合、ループを終了し、結果を返す。
     (b) もし LLM がHandoffを行った場合、現在のAgentと入力を更新し、ループを再実行する。
     (c) もし LLM がツール呼び出し(tool calls)を生成した場合、それらを実行し、結果を追加したうえでループを再実行する。
    (3) 指定された max_turns を超えた場合、MaxTurnsExceeded 例外を発生させる。

    LLM の出力が「final output」と見なされるかどうかのルールは、
    望ましいタイプのテキスト出力を生成し、かつツール呼び出しがないこと です。

    2-3. ストリーミング

    「ストリーミング」を使用すると、LLM の実行中にストリーミングイベントを受け取ることができます。ストリームが完了すると、RunResultStreaming に、すべての新しい出力を含む実行の完全な情報が格納されます。
    ストリーミングイベントを取得するには、.stream_events() を呼び出します。詳しくはストリーミングガイドを参照してください。

    2-4. run_config

    「run_config」を使用すると、「Agent」の実行に関するグローバル設定を指定できます。

    ・model
    各Agentが持つモデルとは関係なく、グローバルに使用する LLM モデルを設定できます。

    ・model_provider
    モデル名を参照するためのモデルプロバイダを指定できます(デフォルトは OpenAI)。

    ・model_settings
    Agent固有の設定を上書きできます(例: グローバルな temperature や top_p を設定)。

    ・input_guardrails, output_guardrails
    すべての実行に適用する入力・出力のGuardrailのリストを設定できます。

    ・handoff_input_filter
    すべてのHandoffに適用するグローバルな入力フィルタを設定できます(Handoffに既存のフィルタがない場合のみ適用)。このフィルタを使うと、新しいAgentに渡す入力を編集できます。詳しくは Handoff.input_filter のドキュメントを参照してください。

    ・tracing_disabled
    実行全体のトレース(追跡)を無効にできます。

    ・trace_include_sensitive_data
    LLM やツール呼び出しの入力・出力など、機密データをトレースに含めるかどうかを設定できます。

    ・workflow_name, trace_id, group_id
    実行のトレースに関する ワークフロー名・トレース ID・トレースグループ ID を設定できます。少なくとも workflow_name を設定することを推奨します。セッション ID はオプションで、複数の実行にわたってトレースをリンクできます。

    ・trace_metadata
    すべてのトレースに含めるメタデータを設定できます。

    2-5. 会話/チャットスレッド

    run()を呼び出すと、1つまたは複数のAgent(および1つ以上の LLM 呼び出し)が実行される可能性がありますが、これは チャット会話内の1つの論理的なターン を表します。

    【例】

    ・ユーザーのターン
    ユーザーがテキストを入力する。
    ・Runner の実行
    最初のAgentはLLMを呼び出し、ツールを実行し、2番目のAgentにハンドオフを行い、2番目のAgentはより多くのツールを実行し、出力を生成します。

    Agentの実行が終了すると、ユーザーに何を表示するかを選択できます。
    例えば、Agentが生成した すべての新しいアイテムを表示 するか、最終的な出力のみを表示 するかを決めることができます。どちらの方法でも、ユーザーが フォローアップの質問 をする可能性があり、その場合は run() を再度呼び出します。

    次のターンの入力を取得するには、RunResultBase.to_input_list() を使用できます。

    async def main():
        agent = Agent(name="Assistant", instructions="簡潔に返信してください。")
    
        with trace(workflow_name="Conversation", group_id=thread_id):
            # 最初のターン
            result = await Runner.run(agent, "ゴールデンゲートブリッジはどの都市にありますか?")
            print(result.final_output)
            # San Francisco
    
            # 2番目のターン
            new_input = result.to_input_list() + [{"role": "user", "content": "それはどの州にありますか?"}]
            result = await Runner.run(agent, new_input)
            print(result.final_output)
            # California

    2-6. 例外

    SDK は特定のケースで例外を発生させます。完全なリストは agents.exceptions にあります。

    ・AgentsException
    SDKで発生したすべての例外のベースクラスです。

    ・MaxTurnsExceeded
    実行メソッドに渡されたmax_turnsを超えると発生します。

    ・ModelBehaviorError
    モデルが不正なJSONや存在しないツールを使用したり、無効な出力を生成するときに発生します。

    ・UserError
    ユーザーがSDKを使用してエラーになった時に発生します。

    ・InputGuardrailTripwireTriggered・OutputGuardrailTripwireTriggered
    Guardrailがトリップすると発生します。

    3. Result

    3-1. Result

    Runner.run() を呼び出すと、以下のいずれかが返されます。

    ・RunResult
    run または run_sync を呼び出した場合。

    ・RunResultStreaming
    run_streamed を呼び出した場合。

    これらは RunResultBase を継承しており、ほとんどの有用な情報はそこに含まれています。

    3-2. final_output

    「final_output」には、最後に実行されたAgentの最終出力が含まれます。これは以下のいずれかです。

    ・文字列(str)
    最後のAgentに output_type が定義されていない場合。

    ・last_agent.output_type 型のオブジェクト
    Agentに output_type が定義されている場合。

    「final_output」は any型です。Handoffが発生する可能性があるため、これを静的に型付けすることはできません。Handoffが発生すると、どのAgentが最後になるかわからないためです。

    3-3. 次ターンの入力

    result.to_input_list() を使用すると、結果を入力リストに変換できます。このリストは、元の入力に加えて、Agentの実行中に生成された項目を連結したものになります。

    これにより、一つのAgentの出力を別のAgentの入力として渡したり、ループ内で実行して新しいユーザー入力を毎回追加したりするのが簡単になります。

    3-4. last_agent

    「last_agent」には、最後に実行されたAgentが含まれます。

    アプリケーションの用途によっては、これは次回ユーザーが入力を行う際に役立つことがよくあります。たとえば、初期対応を行うAgentが言語別のAgentに引き継ぐ場合、last_agent を保存しておけば、次回ユーザーがメッセージを送信した際に同じAgentを再利用できます。

    3-5. new_items

    「new_items」には、実行中に生成された新しいアイテムが含まれます。これらのアイテムは RunItemsであり、RunItem は LLM によって生成された生のアイテムをラップするものです。

    ・MessageOutputItem
    LLM からのメッセージを示します。生のアイテムは生成されたメッセージです。

    ・HandoffCallItem
    LLM がハンドオフツールを呼び出したことを示します。生のアイテムは LLM からのツール呼び出しアイテムです。

    HandoffOutputItem
    Handoffが発生したことを示します。生のアイテムはHandのツールの呼び出しに対するツールの応答です。また、このアイテムから送信元・送信先のエージェントにもアクセスできます。

    ・ToolCallItem
    LLM がツールを呼び出したことを示します。

    ・ToolCallOutputItem
    ツールが呼び出されたことを示します。生のアイテムはツールの応答です。また、このアイテムからツールの出力にもアクセスできます。

    ・ReasoningItem
    LLM によるReasoningアイテムを示します。生のアイテムは生成された推論です。

    3-6. その他の情報

    ・ガードレールの結果
    「input_guardrail_results
    」および「output_guardrail_results」には、Guardrail(制約)の結果が含まれます(存在する場合)。
    Guardrail の結果には、ログや保存したい有用な情報が含まれることがあるため、これらのデータを利用できるようにしています。

    ・生のレスポンス
    「raw_responses」には、LLM によって生成された ModelResponses が含まれます。

    ・元の入力
    「input」には、run() に提供した元の入力が含まれます。ほとんどの場合、この情報を直接使用することはありませんが、必要な場合にアクセスできるようになっています。

    4. Streaming

    4-1. Streaming

    「Streaming」を使用すると、「Agent」の実行が進行するにつれて更新を購読できます。これにより、エンドユーザーに進行状況の更新や部分的な応答を表示するのに役立ちます。

    「Streaming」を行うには、Runner.run_streamed() を呼び出すことで、RunResultStreamingを取得できます。その後、result.stream_events() を呼び出すことで、以下に説明する StreamEvent オブジェクトの非同期ストリームを取得できます。

    4-2. 生のレスポンスイベント

    RawResponsesStreamEvent は、LLMから直接渡される生のイベントです。これらは「OpenAI」のレスポンスAPIの形式に従っており、それぞれのイベントには response.created や response.output_text.delta などの型とデータが含まれます。

    これらのイベントは、生成されたレスポンスメッセージを即座にユーザーにStreamingしたい場合に便利です。

    例えば、次のようにすることで、LLMが生成したテキストをトークンごとに出力できます。

    import asyncio
    from openai.types.responses import ResponseTextDeltaEvent
    from agents import Agent, Runner
    
    async def main():
        # Agent
        agent = Agent(
            name="Joker",
            instructions="あなたは役に立つアシスタントです。",
        )
    
        # Agentの実行
        result = Runner.run_streamed(agent, input="ジョークを5つ教えてください。")
        async for event in result.stream_events():
            if event.type == "raw_response_event" and isinstance(event.data, ResponseTextDeltaEvent):
                print(event.data.delta, end="", flush=True)
    
    if __name__ == "__main__":
        asyncio.run(main())

    4-3. 実行アイテムイベントとエージェントイベント

    RunItemStreamEvents は、より高レベルのイベントです。これらのイベントは、アイテムが完全に生成されたタイミングを通知します。そのため、トークンごとではなく「メッセージが生成された」「ツールが実行された」などの単位で進行状況を更新できます。

    同様に、AgentUpdatedStreamEvent は、現在の「Agent」が変更されたとき(例えば、「Handoff」の結果として)に更新情報を提供します。

    例えば、以下の方法では、生のイベントを無視し、ユーザーへの更新情報のみをストリーミングできます。

    import asyncio
    import random
    from agents import Agent, ItemHelpers, Runner, function_tool
    
    # Tool
    @function_tool
    def how_many_jokes() -> int:
        return random.randint(1, 10)
    
    async def main():
        # Agent
        agent = Agent(
            name="Joker",
            instructions="まず how_many_jokes ツールを呼び出し、その数だけジョークを言います。",
            tools=[how_many_jokes],
        )
    
        # Agentの実行
        result = Runner.run_streamed(
            agent,
            input="こんにちは",
        )
        print("=== Run starting ===")
    
        async for event in result.stream_events():
            # 生の応答イベントのdeltasは無視
            if event.type == "raw_response_event":
                continue
            # Agentが更新されたら印刷
            elif event.type == "agent_updated_stream_event":
                print(f"Agent updated: {event.new_agent.name}")
                continue
            # アイテムが生成されたら出力
            elif event.type == "run_item_stream_event":
                if event.item.type == "tool_call_item":
                    print("-- Tool was called")
                elif event.item.type == "tool_call_output_item":
                    print(f"-- Tool output: {event.item.output}")
                elif event.item.type == "message_output_item":
                    print(f"-- Message output:\n {ItemHelpers.text_message_output(event.item)}")
                else:
                    pass  # 他のイベントタイプを無視
    
        print("=== Run complete ===")
    
    
    if __name__ == "__main__":
        asyncio.run(main())

    次回



     
     

    npaka

     
     
    プログラマー。iPhone / Android / Unity / ROS / AI / AR / VR / RasPi / ロボット / ガジェット。年2冊ペースで技術書を執筆。アニソン / カラオケ / ギター / 猫 twitter : @npaka123

    あなたへのおすすめ