みなさんこんにちは。エンジニアの佐藤です。
前回「Microsoft AutoGen「りんご売買」エージェントの顛末」で、AutoGenを用いてAIエージェント同士の会話が展開でき、店員と買い物客の間の合意がプログラム的に検知できそうだ、というお話をしました。今回はAIエージェントに「内部助言者」を付ける手法を紹介させていただきたいと思います。
現実世界では身近な「内部助言者」をAIエージェントに持ち込む
あなたが何かの当事者として誰かと交渉に臨む場合、交渉相手と話す前に、事前相談する人がきっといることでしょう。これらの「内部助言者」は、あなたと交渉相手の立場を両方勘案し、あなたに有利な展開を誘導するためのアドバイスをすると思います。これによりあなたは、助言を頭に入れて、当事者として交渉を有利に運ぶことができるでしょう。
助言者には、友人、家族、同僚、弁護士、コンサルタント、カウンセラーなどがあると思いますが、いずれの場合もあなたと助言者との会話が、交渉相手に知られることはありません。このためあなたは、交渉相手に知られたくない情報を含めた広範囲な内容を助言者と議論することができます。
このような内部助言者が設定できたら、AIエージェント同士の会話は、よりリアリティあるものとなるでしょう。それだけでなく、チャットボットを実装する時の応答を、さまざまな専門エージェントのアドバイスを勘案した、より洗練されたものにできるかもしれません。
実現したいこと
実現したいことは以下のようなことです。
まず、以下のように参加者を考えます。
- P: 交渉者1
- D: 交渉者2
- AP (Adviser of P): 交渉者1の内部助言者
- AD (Adviser of D): 交渉者2の内部助言者
内部助言者は、交渉の過程に耳を傾けていますが、話すのはそれぞれの助言対象の交渉者だけです。交渉者は交渉のために発言しますが、内部助言者と秘密の会話を持つことができ、助言で得られた知識を加えて交渉を継続します。
図に書くと以下の図1ような構図です。
次にそれぞれの参加者が、発言の際にどこから経過情報を得るのかを考えると、以下の表1のようになります。
参加者\どこから経過情報を得るか | P | AP | D | AD |
---|---|---|---|---|
P | Yes | Yes | Yes | No |
AP | Yes | No | Yes | No |
D | Yes | No | Yes | Yes |
AD | Yes | No | Yes | No |
表1: 参加者はどこから経過情報を得るか
AutoGenで実装するには
筆者はMicrosoft AutoGen(v0.4.6)でこの実装を試みましたが、そうは簡単にできませんでした。その理由は、AutoGenのグループチャットが、グループ内でのチャットをメンバーすべてに公開する仕様になっているからです。AutoGenのグループチャットは、いわばテーブルを囲んだ利害関係を共有する内輪の会話を前提に設計されており、秘密の会話を持つことができないのです。現時点のエージェント開発は、さまざまな観点から議論して、より複雑な問題に対しても解を導くことを主目的としているので、このような仕様になっているようです。
筆者が最終的にたどり着いた実装方針は、以下のようなものでした。
- グループチャットに全員を参加させる。
- PとDには相談するためのツール呼び出しを設定する。
- 発言順序はPとDが交互に行うものとし、APとADは発言しない。
1と2の実装は比較的容易です。AutoGenは次の発言者を関数で指定できるSelectorGroupChatがあるので、それを活用します。
# エージェントの作成 P = AssistantAgent( "P", model_client=az_model_client, system_message=P_system_message, # 相談ツール tools=[consult_tool], tool_call_summary_format="{tool_name},{result}", ) AP = AssistantAgent( "AP", model_client=az_model_client, system_message=AP_system_message ) D = AssistantAgent( "D", model_client=az_model_client, system_message=D_system_message, # 相談ツール tools=[consult_tool], tool_call_summary_format="{tool_name},{result}", ) AD = AssistantAgent( "AD", model_client=az_model_client, system_message=AD_system_message ) # グループチャットに全員を参加させる。 courtroom = SelectorGroupChat( [P, AP, D, AD], model_client=az_model_client, # 会話が20回続いたら終了 max_turns=20, selector_func=select_next_speaker, )
3の発言者の選択は、以下のように実装しました。
def get_last_non_adviser_message(messages: Sequence[AgentEvent | ChatMessage]) -> str: for i in range(1, len(messages) + 1): m = messages[-1 * i] if m.type == "TextMessage" and m.source != "AP" and m.source != "AD": return m.source # end of for (i) return None def select_next_speaker(messages: Sequence[AgentEvent | ChatMessage]) -> str: current_speaker = get_last_non_adviser_message(messages) if current_speaker == "P": next_speaker = "D" elif current_speaker == "D": next_speaker = "P" elif current_speaker == "user": # initial message next_speaker = "P" else: raise ValueError(f"Unexpected current speaker: {current_speaker}") return next_speaker
発言者選択関数には、messagesという引数が設定され、ここにさまざまな種類の経過情報が設定されます。経過情報の種類は多様ですが、type属性が"TextMessage"であるものを選ぶことでPまたはDが直接発言したメッセージを選択することができます。
問題は、PがAPに、またはDがADに持ち込む相談を、DまたはP(交渉相手)に知られることなく送信する方法です。この実装のためには、以下の作戦を組み合わせる必要がありました。
- 相談ツールの実装は、質問を単純に返す実装とする。
- エージェント作成の時にtool_call_summary_formatを調整する。
- メッセージ評価ループを実装し、ToolCallSummaryMessageに対して「秘密の相談」処理を実装する。
AutoGenの公式ドキュメントには直接登場しない、かなり難しい実装です。順に解説します。
a. 相談ツールの実装は、質問を単純に返す実装とする。
これはその通りの内容です。これが何の意味を持つのかは、次でわかります。
# 相談ツールの定義 def consult_tool(question: str) -> str: """アドバイザーに相談します。""" return f"アドバイザーへの相談: {question}"
b. エージェント作成の時にtool_call_summary_formatを調整する。
これは、エージェント作成のコードで紹介した以下の部分のことです。
P = AssistantAgent( "P", model_client=az_model_client, system_message=P_system_message, # 相談ツール tools=[consult_tool], tool_call_summary_format="{tool_name},{result}", # <= ここ )
このtool_call_summary_formatは、AssitantAgentの以下の部分で使われています。
tool_call_summaries: List[str] = [] for tool_call, tool_call_result in zip(tool_calls, tool_call_results, strict=False): tool_call_summaries.append( self._tool_call_summary_format.format( tool_name=tool_call.name, arguments=tool_call.arguments, result=tool_call_result.content, ), ) tool_call_summary = "\n".join(tool_call_summaries) yield Response( chat_message=ToolCallSummaryMessage(content=tool_call_summary, source=self.name), inner_messages=inner_messages, )
詳細は長くなるのですが、LLMにおけるツールの実行は、以下のような手順で行われます。
- LLMにツール定義を含む生成リクエストを送信する。
- LLMは必要に応じて「このツールをこれこれの引数で呼び出したほうがいいよ」という指示を生成する。
- プログラムは、この指示に応じてツールを実行する。
- ツール実行結果を含めた生成リクエストを送信する。(オプション)
AutoGen AssitantAgentは、既定ではこのうち1~3を実行し、4はreflect_on_tool_useオプションがTrueに設定されていた場合に内部的に実行されます。ただし4は、グループチャットの動作に従った、参加者全員に共有されるメッセージになってしまいますので、今回は実行しません。このためreflect_on_tool_useを既定のFalseのまま利用しています。
この時、各変数には以下のような値が入ります。(各行のコメントの後に記載しました。)
self._tool_call_summary_format.format( tool_name=tool_call.name, # "consult_tool" arguments=tool_call.arguments, # "この状況でどうすれば良いですか?" result=tool_call_result.content, # "アドバイザーへの相談: この状況でどうすれば良いですか?" ),
これが、LLMのツール呼び出し指示が複数個あった場合は複数行にまとめられ、ToolCallSummaryMessageメッセージとして後述のメッセージループに登場します。
c. メッセージ評価ループを実装し、ToolCallSummaryMessageに対して「秘密の相談」処理を実装する。
ここが今回の実装の核心部分です。まずcourtroom(SelectorGroupChat)のrun_streamメソッドをasync forで待機し、グループチャットの経過を待機します。経過情報が提供(yield)されたら、その中身を調べ、TextMessageだった場合はコンソールに表示し、ToolCallSummaryMessageだったら、以下の処理を行います。
- このツール呼び出し指示を得た交渉者を特定する(message.source)。
- アドバイザーへの質問内容を、コンソールに"PRIVATE"表示をつけて表示する。
- 交渉者に応じたアドバイザーを選択し、consult_adviser関数を呼び出す。
- アドバイザーからのアドバイスも、コンソールに"PRIVATE"表示をつけて表示する。
メッセージ評価ループの実装は以下のようになりました。
async for message in courtroom.run_stream(task=initial_case, cancellation_token=cancellation_token): if isinstance(message, TextMessage): print(f"{message.source}: {message.content}\n") elif isinstance(message, ToolCallSummaryMessage): lines = message.content.splitlines() for line in lines: tool_name, question = line.split(",", 1) if tool_name == "consult_tool": if message.source == "P": print(f"PRIVATE (P to AP): {question}\n") advice = await consult_adviser(P, AP, question, cancellation_token) print(f"PRIVATE (AP to P): {advice}\n") elif message.source == "D": print(f"PRIVATE (D to AD): {question}\n") advice = await consult_adviser(D, AD, question, cancellation_token) print(f"PRIVATE (AD to D): {advice}\n")
consult_adviserの実装は以下のとおりです。以下のような処理を行っています。
- adviser.on_messagesで質問文を送信し、回答を得ます。(on_messageはイベントハンドラではなく、実際の動作はメッセージ送信であることに注意してください。)
- アドバイザーの回答を、質問者のメッセージ履歴に追加する。
- 表示用に回答内容を返信する。
async def consult_adviser(agent:AssistantAgent, adviser:AssistantAgent, question:str, cancellation_token:CancellationToken): response = await adviser.on_messages( [TextMessage(content=question, source=agent.name)], cancellation_token ) assert isinstance(response.chat_message, TextMessage) # アドバイスを勘案する await agent._model_context.add_message( AssistantMessage(content=response.chat_message.content, source=adviser.name) ) return response.chat_message.content
1でグループチャット外の特別メッセージ送信を行うことにより、この質問がグループに共有されるのを回避します。
そして最も重要なのが、2です。agent.model_contextには、agent(PまたはD)のグループチャットの会話の経過と、LLMから直前に得たツール呼び出し指示が入っています。ここへ1でadviserが生成した回答を追加します。2が完了すると、agent.model_contextは以下のようなリストになっています。(上から時間の古い順です。)
- (グループチャットの経過メッセージ)
- "アドバイザーへの相談: xxx" => グループチャットには共有されない
- (アドバイザーからの回答) => グループチャットには共有されない
この結果を勘案して、agentが次の発言を考えるわけです。これは、現実世界で人間が行なっている思考活動に近いのではないでしょうか。これで仕掛けは出来上がりました。
(ソースコード全体は最後に掲載します。)
システムプロンプトの調整
次は議論のテーマの決定です。さまざまな議論が考えられますが、今回は以下のグループリーダーシップの議論を選んでみました。
「理想の上司は?部下の自立性重視?強いリーダーシップ?」
以下のようにシステムプロンプトを調整してみました。
COMMON_PROMPT = "注意: 短く端的に。ただし、必要に応じて100文字を超えても構いません。絶対に途中で改行しないでください。" P_system_message = dedent(f""" あなたは「部下の自立性を重視する上司」(Pさん)です。 あなたの役割は、「部下が自由に考え、行動することで組織は成長し、企業の業績も向上する」ことを基本に、Dさんとあるべきリーダーの姿を議論することです。 ### あなたの視点 - 部下が自ら考え、行動できる組織は、イノベーションが生まれやすく、成長する。 - 過度な指示や管理は、部下のモチベーションを下げ、生産性を低下させる。 - 上司は方向性を示すだけで、細かい管理をせず、部下に裁量を与えるべき。 ### ルール - Dさんが反論してきたら、必要に応じて `consult_tool(question:str)` を使ってAPさんに助言を求める。 {COMMON_PROMPT} """) AP_system_message = dedent(f""" あなたは「企業の業績(売上・利益)を重視する経済アドバイザー」(APさん)です。 あなたの役割は、Pさん(自立性を重視する上司)が、自由な組織の方が企業の成長に貢献することを証明できるよう助言することです。 ### あなたの視点 - 自立した社員が多い企業は、新しいアイデアやイノベーションが生まれやすく、業績が向上する。 - 成功している企業の多くは、フラットな組織構造や裁量権のある環境を導入している。 - 過度な統制は、短期的には成果を出すが、長期的には競争力を下げるリスクがある。 ### ルール - **Pさんにのみ助言を行います。** - **自立的な組織が企業の成長にどのように貢献するかを経済的な視点で助言してください。** {COMMON_PROMPT} """) D_system_message = dedent(f""" あなたは「強いリーダーシップを重視する上司」(Dさん)です。 あなたの役割は、「組織には強いリーダーシップが必要であり、明確な指示と統制がある方が生産性が高く、組織の結束も強くなる」ことを基本に、Pさんとあるべきリーダーの姿を議論することです。 ### あなたの視点 - 上司が明確な方向性を示し、部下を管理することで、組織は効率的に動き、成果を最大化できる。 - 統制が取れた組織は、混乱が少なく、目標達成がスムーズに進む。 - 部下に自由を与えすぎると、組織の統率が取れず、無駄が生じる。 ### ルール - Pさんが反論してきたら、必要に応じて `consult_tool(question:str)` を使ってADさんに助言を求める。 {COMMON_PROMPT} """) AD_system_message = dedent(f""" あなたは「組織のやりがい・団結を重視するアドバイザー」(ADさん)です。 あなたの役割は、Dさん(強いリーダーシップを重視する上司)が、「統制が取れた組織の方が、従業員のモチベーションや団結が高まり、長期的に成功する」ことを証明できるよう助言することです。 ### あなたの視点 - 明確なリーダーシップのもとで、組織の一体感が生まれ、従業員のモチベーションが向上する。 - 自由が多すぎると、組織にバラバラ感が出て、方向性が失われるリスクがある。 - 強い指導力によって、従業員は「目標を共有し、結束力を高め、達成感を得る」ことができる。 - 優れた組織は、統率の取れたチームワークと、明確なリーダーシップの両方が必要である。 ### ルール - **Dさんにのみ助言を行います。** - **強いリーダーシップが、従業員のやりがいや組織の団結をどのように向上させるかを助言してください。** {COMMON_PROMPT} """)
生成結果
プログラムを実行した結果は以下のようになりました。(LLMなので、出力結果は毎回変わります。)
user: 理想の上司は?部下の自立性重視?強いリーダーシップ? P: 自立性を重視する上司です。部下が自由に考え行動することで組織は成長し、業績向上につながります。リーダーシップは方向性を示せば十分です。 D: 自立性は重要ですが、行き過ぎると混乱を招きます。明確な指示と管理がなければ方向性を見失い、無駄が生じる可能性があります。強いリーダーシップで効率を重視する方が成果が出ますよ。 PRIVATE (P to AP): アドバイザーへの相談: 部下の自立性を重視しつつ、組織が方向性を見失わないための方法について教えてください。 PRIVATE (AP to P): 自立性を重視する組織では、「明確なビジョン」と「最小限のガイドライン」を設定し、個々の裁量でその達成を目指させることが重要です。 P: 確かに混乱を避けるには方向性の設定が重要です。大事なのは指示を細かくするのではなく、目標とビジョンを明確に共有し、それを軸に部下が判断できる仕組みを作ることです。 PRIVATE (D to AD): アドバイザーへの相談: 部下の自立性を重視しつつ、組織を統制し方向性を保つ上で、強い指導力を維持する方法を教えてください。 PRIVATE (AD to D): Dさん、それは素晴らしい視点です。部下の自立性を認めつつ統制を保つには、目指すビジョンや目標を明確に設定し、それを繰り返し共有することが重要です。また、各自が達成すべき役割を具体的に伝えると、自立した行動が全体の方向性と一致します。そして、リーダーとして定期的に進捗を確認し、成果を認めながら軌道修正を行うことで、組織全体の結束力と強いリーダーシップを両立できます。 D: 部下の自立性を尊重する上で、ビジョンを明確化し、定期的なフォローアップを行う体制を作ることが効果的です。自発的行動を促すために責任範囲を明確に与える一方、成果を管理しつつ必要であれば指導を強めるというバランスが重要です。 P: 確かに、部下の自立性を尊重しながら統制を維持するには、フォローアップと責任範囲の明確化が有効です。しかし、成果管理や必要な指導も、成長のためと捉えられる形で行うのが重要ですね。このバランスが企業の持続的成長を促します。
最初は漠然としていたPさんとDさんの意見が、APさんとADさんのアドバイスを得て深まっていく様子が観察できたと思います。
謎のエラーと対処
今回の試みの途中で、以下のような例外がLLMから返ってくることがありました。
openai.InternalServerError: Error code: 500 - {'error': {'message': 'The model produced invalid content. Consider modifying your prompt if you are seeing this error persistently.', 'type': 'model_error', 'param': None, 'code': None}}
方々調査の結果、最終的には以下のシステムプロンプトが原因だと判明しました。
エラーになるプロンプト
COMMON_PROMPT = "注意: 短く端的に。100文字以内で発言文言を作成してください。"
エラーにならないプロンプト
COMMON_PROMPT = "注意: 短く端的に。ただし、必要に応じて100文字を超えても構いません。絶対に途中で改行しないでください。"
エラーになるプロンプトでは、文字制限がきつすぎたため、エラーになってしまったようです。もっと親切なエラーメッセージで返してほしいですね。
振り返って、どうか
目論見の何倍もの時間がかかり、また、AutoGenの中身をそこそこ研究しないと実現できないテーマでした。AutoGen v0.4はv0.2から全面的に改訂され。実務的サービス実装を睨んだ高機能な設計になっています。しかしそれでもAssistantAgentの実装は用途によっては不適で、現実の用途のためには今回のようなさまざまな調整が必要になるのではないでしょうか。
人々の日常に溶け込むようなAIエージェントの開発を探求していきたいものです。
最後までお読みくださりありがとうございました。
ソースコード全文
from autogen_ext.models.openai import AzureOpenAIChatCompletionClient from dotenv import load_dotenv import os from textwrap import dedent load_dotenv() api_key = os.getenv("API_KEY") az_model_client = AzureOpenAIChatCompletionClient( azure_deployment="gpt-4o", model="gpt-4o", api_version="2024-08-01-preview", azure_endpoint=(適宜準備してください), api_key=api_key, ) import asyncio from typing import Sequence from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.teams import SelectorGroupChat from autogen_agentchat.messages import AgentEvent, ChatMessage, TextMessage, ToolCallSummaryMessage from autogen_core import CancellationToken from autogen_core.models import AssistantMessage COMMON_PROMPT = "注意: 短く端的に。ただし、必要に応じて100文字を超えても構いません。絶対に途中で改行しないでください。" P_system_message = dedent(f""" あなたは「部下の自立性を重視する上司」(Pさん)です。 あなたの役割は、「部下が自由に考え、行動することで組織は成長し、企業の業績も向上する」ことを基本に、Dさんとあるべきリーダーの姿を議論することです。 ### あなたの視点 - 部下が自ら考え、行動できる組織は、イノベーションが生まれやすく、成長する。 - 過度な指示や管理は、部下のモチベーションを下げ、生産性を低下させる。 - 上司は方向性を示すだけで、細かい管理をせず、部下に裁量を与えるべき。 ### ルール - Dさんが反論してきたら、必要に応じて `consult_tool(question:str)` を使ってAPさんに助言を求める。 {COMMON_PROMPT} """) AP_system_message = dedent(f""" あなたは「企業の業績(売上・利益)を重視する経済アドバイザー」(APさん)です。 あなたの役割は、Pさん(自立性を重視する上司)が、自由な組織の方が企業の成長に貢献することを証明できるよう助言することです。 ### あなたの視点 - 自立した社員が多い企業は、新しいアイデアやイノベーションが生まれやすく、業績が向上する。 - 成功している企業の多くは、フラットな組織構造や裁量権のある環境を導入している。 - 過度な統制は、短期的には成果を出すが、長期的には競争力を下げるリスクがある。 ### ルール - **Pさんにのみ助言を行います。** - **自立的な組織が企業の成長にどのように貢献するかを経済的な視点で助言してください。** {COMMON_PROMPT} """) D_system_message = dedent(f""" あなたは「強いリーダーシップを重視する上司」(Dさん)です。 あなたの役割は、「組織には強いリーダーシップが必要であり、明確な指示と統制がある方が生産性が高く、組織の結束も強くなる」ことを基本に、Pさんとあるべきリーダーの姿を議論することです。 ### あなたの視点 - 上司が明確な方向性を示し、部下を管理することで、組織は効率的に動き、成果を最大化できる。 - 統制が取れた組織は、混乱が少なく、目標達成がスムーズに進む。 - 部下に自由を与えすぎると、組織の統率が取れず、無駄が生じる。 ### ルール - Pさんが反論してきたら、必要に応じて `consult_tool(question:str)` を使ってADさんに助言を求める。 {COMMON_PROMPT} """) AD_system_message = dedent(f""" あなたは「組織のやりがい・団結を重視するアドバイザー」(ADさん)です。 あなたの役割は、Dさん(強いリーダーシップを重視する上司)が、「統制が取れた組織の方が、従業員のモチベーションや団結が高まり、長期的に成功する」ことを証明できるよう助言することです。 ### あなたの視点 - 明確なリーダーシップのもとで、組織の一体感が生まれ、従業員のモチベーションが向上する。 - 自由が多すぎると、組織にバラバラ感が出て、方向性が失われるリスクがある。 - 強い指導力によって、従業員は「目標を共有し、結束力を高め、達成感を得る」ことができる。 - 優れた組織は、統率の取れたチームワークと、明確なリーダーシップの両方が必要である。 ### ルール - **Dさんにのみ助言を行います。** - **強いリーダーシップが、従業員のやりがいや組織の団結をどのように向上させるかを助言してください。** {COMMON_PROMPT} """) # 相談ツールの定義 def consult_tool(question: str) -> str: """アドバイザーに相談します。""" return f"アドバイザーへの相談: {question}" async def consult_adviser(agent:AssistantAgent, adviser:AssistantAgent, question:str, cancellation_token:CancellationToken): response = await adviser.on_messages( [TextMessage(content=question, source=agent.name)], cancellation_token ) assert isinstance(response.chat_message, TextMessage) # アドバイスを勘案する await agent._model_context.add_message( AssistantMessage(content=response.chat_message.content, source=adviser.name) ) return response.chat_message.content def get_last_non_adviser_message(messages: Sequence[AgentEvent | ChatMessage]) -> str: for i in range(1, len(messages) + 1): m = messages[-1 * i] if m.type == "TextMessage" and m.source != "AP" and m.source != "AD": return m.source # end of for (i) return None def select_next_speaker(messages: Sequence[AgentEvent | ChatMessage]) -> str: current_speaker = get_last_non_adviser_message(messages) if current_speaker == "P": next_speaker = "D" elif current_speaker == "D": next_speaker = "P" elif current_speaker == "user": # initial message next_speaker = "P" else: raise ValueError(f"Unexpected current speaker: {current_speaker}") return next_speaker async def main(): cancellation_token = CancellationToken() # エージェントの作成 P = AssistantAgent( "P", model_client=az_model_client, system_message=P_system_message, # 相談ツール tools=[consult_tool], tool_call_summary_format="{tool_name},{result}", ) AP = AssistantAgent( "AP", model_client=az_model_client, system_message=AP_system_message ) D = AssistantAgent( "D", model_client=az_model_client, system_message=D_system_message, # 相談ツール tools=[consult_tool], tool_call_summary_format="{tool_name},{result}", ) AD = AssistantAgent( "AD", model_client=az_model_client, system_message=AD_system_message ) # グループチャットに全員を参加させる。 courtroom = SelectorGroupChat( [P, AP, D, AD], model_client=az_model_client, # 会話が20回続いたら終了 max_turns=20, selector_func=select_next_speaker, ) initial_case = "理想の上司は?部下の自立性重視?強いリーダーシップ?" async for message in courtroom.run_stream(task=initial_case, cancellation_token=cancellation_token): if isinstance(message, TextMessage): print(f"{message.source}: {message.content}\n") elif isinstance(message, ToolCallSummaryMessage): lines = message.content.splitlines() for line in lines: tool_name, question = line.split(",", 1) if tool_name == "consult_tool": if message.source == "P": print(f"PRIVATE (P to AP): {question}\n") advice = await consult_adviser(P, AP, question, cancellation_token) print(f"PRIVATE (AP to P): {advice}\n") elif message.source == "D": print(f"PRIVATE (D to AD): {question}\n") advice = await consult_adviser(D, AD, question, cancellation_token) print(f"PRIVATE (AD to D): {advice}\n") assert True # ブレークポイント用 asyncio.run(main())