{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# 终止条件\n\n在上一节中,我们探讨了如何定义智能体,并将它们组织成能够解决任务的团队。然而,运行可能会无限持续下去,在许多情况下,我们需要知道_何时_停止它们。这就是终止条件的作用。\n\nAgentChat 通过提供基础类 {py:class}`~autogen_agentchat.base.TerminationCondition` 和几个继承自它的实现,支持多种终止条件。\n\n终止条件是一个可调用对象,它接收自上次调用该条件以来的一系列 {py:class}`~autogen_agentchat.messages.BaseAgentEvent` 或 {py:class}`~autogen_agentchat.messages.BaseChatMessage` 对象,并返回一个 {py:class}`~autogen_agentchat.messages.StopMessage`(如果对话应该终止)或 `None`(否则)。\n一旦达到终止条件,必须通过调用 {py:meth}`~autogen_agentchat.base.TerminationCondition.reset` 重置后才能再次使用。\n\n关于终止条件需要注意的一些重要事项:\n- 它们是有状态的,但每次运行({py:meth}`~autogen_agentchat.base.TaskRunner.run` 或 {py:meth}`~autogen_agentchat.base.TaskRunner.run_stream`)结束后会自动重置。\n- 可以使用 AND 和 OR 运算符组合它们。\n\n```{note}\n对于群聊团队(即 {py:class}`~autogen_agentchat.teams.RoundRobinGroupChat`、\n{py:class}`~autogen_agentchat.teams.SelectorGroupChat` 和 {py:class}`~autogen_agentchat.teams.Swarm`),\n终止条件在每个智能体响应后调用。\n虽然一个响应可能包含多条内部消息,但团队会为单个响应中的所有消息调用其终止条件一次。\n因此,条件调用时会传入自上次调用以来的消息\"增量序列\"。\n```\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "内置终止条件:\n1. {py:class}`~autogen_agentchat.conditions.MaxMessageTermination`:在生成指定数量的消息(包括智能体和任务消息)后停止。\n2. {py:class}`~autogen_agentchat.conditions.TextMentionTermination`:当消息中提到特定文本或字符串(例如\"TERMINATE\")时停止。\n3. {py:class}`~autogen_agentchat.conditions.TokenUsageTermination`:当使用一定数量的提示或完成令牌时停止。这要求智能体在其消息中报告令牌使用情况。\n4. {py:class}`~autogen_agentchat.conditions.TimeoutTermination`:在指定的秒数持续时间后停止。\n5. {py:class}`~autogen_agentchat.conditions.HandoffTermination`:当请求切换到特定目标时停止。切换消息可用于构建诸如 {py:class}`~autogen_agentchat.teams.Swarm` 之类的模式。当您想暂停运行并允许应用程序或用户在智能体切换到它们时提供输入时,这非常有用。\n6. {py:class}`~autogen_agentchat.conditions.SourceMatchTermination`:在特定智能体响应后停止。\n7. {py:class}`~autogen_agentchat.conditions.ExternalTermination`:支持从运行外部以编程方式控制终止。这对于 UI 集成(例如聊天界面中的\"停止\"按钮)非常有用。\n8. {py:class}`~autogen_agentchat.conditions.StopMessageTermination`:当智能体生成 {py:class}`~autogen_agentchat.messages.StopMessage` 时停止。\n9. {py:class}`~autogen_agentchat.conditions.TextMessageTermination`:当智能体生成 {py:class}`~autogen_agentchat.messages.TextMessage` 时停止。\n10. {py:class}`~autogen_agentchat.conditions.FunctionCallTermination`:当智能体生成包含具有匹配名称的 {py:class}`~autogen_core.models.FunctionExecutionResult` 的 {py:class}`~autogen_agentchat.messages.ToolCallExecutionEvent` 时停止。\n11. {py:class}`~autogen_agentchat.conditions.FunctionalTermination`:当函数表达式在最后的消息增量序列上评估为 `True` 时停止。这对于快速创建内置条件未涵盖的自定义终止条件非常有用。\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 基本用法\n\n为了演示终止条件的特性,我们将创建一个由两个智能体组成的团队:一个负责文本生成的主要智能体和一个对生成文本进行审查并提供反馈的评论智能体。\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from autogen_agentchat.agents import AssistantAgent\n", "from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermination\n", "from autogen_agentchat.teams import RoundRobinGroupChat\n", "from autogen_agentchat.ui import Console\n", "from autogen_ext.models.openai import OpenAIChatCompletionClient\n", "\n", "model_client = OpenAIChatCompletionClient(\n", " model=\"gpt-4o\",\n", " temperature=1,\n", " # api_key=\"sk-...\", # 如果设置了 OPENAI_API_KEY 环境变量则为可选\n", ")\n", "\n", "# 创建主要智能体。\n", "primary_agent = AssistantAgent(\n", " \"primary\",\n", " model_client=model_client,\n", " system_message=\"You are a helpful AI assistant.\",\n", ")\n", "\n", "# 创建评论员代理\n", "critic_agent = AssistantAgent(\n", " \"critic\",\n", " model_client=model_client,\n", " system_message=\"Provide constructive feedback for every message. Respond with 'APPROVE' to when your feedbacks are addressed.\",\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "让我们探讨终止条件如何在每次`run`或`run_stream`调用后自动重置,使得团队能够从中断处继续对话\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "---------- user ----------\n", "Write a unique, Haiku about the weather in Paris\n", "---------- primary ----------\n", "Gentle rain whispers, \n", "Cobblestones glisten softly— \n", "Paris dreams in gray.\n", "[Prompt tokens: 30, Completion tokens: 19]\n", "---------- critic ----------\n", "The Haiku captures the essence of a rainy day in Paris beautifully, and the imagery is vivid. However, it's important to ensure the use of the traditional 5-7-5 syllable structure for Haikus. Your current Haiku lines are composed of 4-7-5 syllables, which slightly deviates from the form. Consider revising the first line to fit the structure.\n", "\n", "For example:\n", "Soft rain whispers down, \n", "Cobblestones glisten softly — \n", "Paris dreams in gray.\n", "\n", "This revision maintains the essence of your original lines while adhering to the traditional Haiku structure.\n", "[Prompt tokens: 70, Completion tokens: 120]\n", "---------- Summary ----------\n", "Number of messages: 3\n", "Finish reason: Maximum number of messages 3 reached, current message count: 3\n", "Total prompt tokens: 100\n", "Total completion tokens: 139\n", "Duration: 3.34 seconds\n" ] }, { "data": { "text/plain": [ "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='Write a unique, Haiku about the weather in Paris'), TextMessage(source='primary', models_usage=RequestUsage(prompt_tokens=30, completion_tokens=19), content='Gentle rain whispers, \\nCobblestones glisten softly— \\nParis dreams in gray.'), TextMessage(source='critic', models_usage=RequestUsage(prompt_tokens=70, completion_tokens=120), content=\"The Haiku captures the essence of a rainy day in Paris beautifully, and the imagery is vivid. However, it's important to ensure the use of the traditional 5-7-5 syllable structure for Haikus. Your current Haiku lines are composed of 4-7-5 syllables, which slightly deviates from the form. Consider revising the first line to fit the structure.\\n\\nFor example:\\nSoft rain whispers down, \\nCobblestones glisten softly — \\nParis dreams in gray.\\n\\nThis revision maintains the essence of your original lines while adhering to the traditional Haiku structure.\")], stop_reason='Maximum number of messages 3 reached, current message count: 3')" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "max_msg_termination = MaxMessageTermination(max_messages=3)\n", "round_robin_team = RoundRobinGroupChat([primary_agent, critic_agent], termination_condition=max_msg_termination)\n", "\n", "# 如果将此脚本作为独立脚本运行,请使用asyncio.run(...)\n", "await Console(round_robin_team.run_stream(task=\"Write a unique, Haiku about the weather in Paris\"))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "对话在达到最大消息限制后停止。由于主代理尚未对反馈作出回应,让我们继续这个对话\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "---------- primary ----------\n", "Thank you for your feedback. Here is the revised Haiku:\n", "\n", "Soft rain whispers down, \n", "Cobblestones glisten softly — \n", "Paris dreams in gray.\n", "[Prompt tokens: 181, Completion tokens: 32]\n", "---------- critic ----------\n", "The revised Haiku now follows the traditional 5-7-5 syllable pattern, and it still beautifully captures the atmospheric mood of Paris in the rain. The imagery and flow are both clear and evocative. Well done on making the adjustment! \n", "\n", "APPROVE\n", "[Prompt tokens: 234, Completion tokens: 54]\n", "---------- primary ----------\n", "Thank you for your kind words and approval. I'm glad the revision meets your expectations and captures the essence of Paris. If you have any more requests or need further assistance, feel free to ask!\n", "[Prompt tokens: 279, Completion tokens: 39]\n", "---------- Summary ----------\n", "Number of messages: 3\n", "Finish reason: Maximum number of messages 3 reached, current message count: 3\n", "Total prompt tokens: 694\n", "Total completion tokens: 125\n", "Duration: 6.43 seconds\n" ] }, { "data": { "text/plain": [ "TaskResult(messages=[TextMessage(source='primary', models_usage=RequestUsage(prompt_tokens=181, completion_tokens=32), content='Thank you for your feedback. Here is the revised Haiku:\\n\\nSoft rain whispers down, \\nCobblestones glisten softly — \\nParis dreams in gray.'), TextMessage(source='critic', models_usage=RequestUsage(prompt_tokens=234, completion_tokens=54), content='The revised Haiku now follows the traditional 5-7-5 syllable pattern, and it still beautifully captures the atmospheric mood of Paris in the rain. The imagery and flow are both clear and evocative. Well done on making the adjustment! \\n\\nAPPROVE'), TextMessage(source='primary', models_usage=RequestUsage(prompt_tokens=279, completion_tokens=39), content=\"Thank you for your kind words and approval. I'm glad the revision meets your expectations and captures the essence of Paris. If you have any more requests or need further assistance, feel free to ask!\")], stop_reason='Maximum number of messages 3 reached, current message count: 3')" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# 如果将此脚本作为独立脚本运行,请使用asyncio.run(...)\n", "await Console(round_robin_team.run_stream())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "团队从上次中断的地方继续,允许主代理对反馈做出响应。\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 组合终止条件\n\n我们将展示如何通过 AND (`&`) 和 OR (`|`) 运算符组合终止条件,以创建更复杂的终止逻辑。例如,我们将创建一个团队,该团队在生成10条消息或评论代理批准消息时停止。\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "---------- user ----------\n", "Write a unique, Haiku about the weather in Paris\n", "---------- primary ----------\n", "Spring breeze gently hums, \n", "Cherry blossoms in full bloom— \n", "Paris wakes to life.\n", "[Prompt tokens: 467, Completion tokens: 19]\n", "---------- critic ----------\n", "The Haiku beautifully captures the awakening of Paris in the spring. The imagery of a gentle spring breeze and cherry blossoms in full bloom effectively conveys the rejuvenating feel of the season. The final line, \"Paris wakes to life,\" encapsulates the renewed energy and vibrancy of the city. The Haiku adheres to the 5-7-5 syllable structure and portrays a vivid seasonal transformation in a concise and poetic manner. Excellent work!\n", "\n", "APPROVE\n", "[Prompt tokens: 746, Completion tokens: 93]\n", "---------- Summary ----------\n", "Number of messages: 3\n", "Finish reason: Text 'APPROVE' mentioned\n", "Total prompt tokens: 1213\n", "Total completion tokens: 112\n", "Duration: 2.75 seconds\n" ] }, { "data": { "text/plain": [ "TaskResult(messages=[TextMessage(source='user', models_usage=None, content='Write a unique, Haiku about the weather in Paris'), TextMessage(source='primary', models_usage=RequestUsage(prompt_tokens=467, completion_tokens=19), content='Spring breeze gently hums, \\nCherry blossoms in full bloom— \\nParis wakes to life.'), TextMessage(source='critic', models_usage=RequestUsage(prompt_tokens=746, completion_tokens=93), content='The Haiku beautifully captures the awakening of Paris in the spring. The imagery of a gentle spring breeze and cherry blossoms in full bloom effectively conveys the rejuvenating feel of the season. The final line, \"Paris wakes to life,\" encapsulates the renewed energy and vibrancy of the city. The Haiku adheres to the 5-7-5 syllable structure and portrays a vivid seasonal transformation in a concise and poetic manner. Excellent work!\\n\\nAPPROVE')], stop_reason=\"Text 'APPROVE' mentioned\")" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "max_msg_termination = MaxMessageTermination(max_messages=10)\n", "text_termination = TextMentionTermination(\"APPROVE\")\n", "combined_termination = max_msg_termination | text_termination\n", "\n", "round_robin_team = RoundRobinGroupChat([primary_agent, critic_agent], termination_condition=combined_termination)\n", "\n", "# 如果将此脚本作为独立脚本运行,请使用 asyncio.run(...)。\n", "await Console(round_robin_team.run_stream(task=\"Write a unique, Haiku about the weather in Paris\"))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "尽管在生成10条消息时也可能停止,但对话在评论代理批准消息后停止了。\n\n另外,如果我们希望仅在两个条件都满足时才停止运行,可以使用 AND (`&`) 运算符。\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "combined_termination = max_msg_termination & text_termination" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 自定义终止条件\n\n内置终止条件适用于大多数用例。\n但在某些情况下,您可能需要实现一个不符合现有条件自定义终止条件。\n您可以通过子类化 {py:class}`~autogen_agentchat.base.TerminationCondition` 类来实现这一点。\n\n在这个例子中,我们创建了一个自定义终止条件,当进行特定函数调用时停止对话。\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from typing import Sequence\n", "\n", "from autogen_agentchat.base import TerminatedException, TerminationCondition\n", "from autogen_agentchat.messages import BaseAgentEvent, BaseChatMessage, StopMessage, ToolCallExecutionEvent\n", "from autogen_core import Component\n", "from pydantic import BaseModel\n", "from typing_extensions import Self\n", "\n", "\n", "class FunctionCallTerminationConfig(BaseModel):\n", " \"\"\"Configuration for the termination condition to allow for serialization\n", " and deserialization of the component.\n", " \"\"\"\n", "\n", " function_name: str\n", "\n", "\n", "class FunctionCallTermination(TerminationCondition, Component[FunctionCallTerminationConfig]):\n", " \"\"\"Terminate the conversation if a FunctionExecutionResult with a specific name is received.\"\"\"\n", "\n", " component_config_schema = FunctionCallTerminationConfig\n", " component_provider_override = \"autogen_agentchat.conditions.FunctionCallTermination\"\n", " \"\"\"The schema for the component configuration.\"\"\"\n", "\n", " def __init__(self, function_name: str) -> None:\n", " self._terminated = False\n", " self._function_name = function_name\n", "\n", " @property\n", " def terminated(self) -> bool:\n", " return self._terminated\n", "\n", " async def __call__(self, messages: Sequence[BaseAgentEvent | BaseChatMessage]) -> StopMessage | None:\n", " if self._terminated:\n", " raise TerminatedException(\"Termination condition has already been reached\")\n", " for message in messages:\n", " if isinstance(message, ToolCallExecutionEvent):\n", " for execution in message.content:\n", " if execution.name == self._function_name:\n", " self._terminated = True\n", " return StopMessage(\n", " content=f\"Function '{self._function_name}' was executed.\",\n", " source=\"FunctionCallTermination\",\n", " )\n", " return None\n", "\n", " async def reset(self) -> None:\n", " self._terminated = False\n", "\n", " def _to_config(self) -> FunctionCallTerminationConfig:\n", " return FunctionCallTerminationConfig(\n", " function_name=self._function_name,\n", " )\n", "\n", " @classmethod\n", " def _from_config(cls, config: FunctionCallTerminationConfig) -> Self:\n", " return cls(\n", " function_name=config.function_name,\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "让我们使用这个新的终止条件,在评论代理通过`approve`函数调用批准消息时结束对话。\n\n首先我们创建一个简单的函数,当评论代理批准消息时会被调用。\n" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "def approve() -> None:\n", " \"\"\"Approve the message when all feedbacks have been addressed.\"\"\"\n", " pass" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "然后我们创建代理。评论代理配备了`approve`工具。\n" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "from autogen_agentchat.agents import AssistantAgent\n", "from autogen_agentchat.teams import RoundRobinGroupChat\n", "from autogen_agentchat.ui import Console\n", "from autogen_ext.models.openai import OpenAIChatCompletionClient\n", "\n", "model_client = OpenAIChatCompletionClient(\n", " model=\"gpt-4o\",\n", " temperature=1,\n", " # api_key=\"sk-...\", # 可选,如果你设置了OPENAI_API_KEY环境变量\n", ")\n", "\n", "# 创建主代理。\n", "primary_agent = AssistantAgent(\n", " \"primary\",\n", " model_client=model_client,\n", " system_message=\"You are a helpful AI assistant.\",\n", ")\n", "\n", "# 创建带有approve函数作为工具的评论代理。\n", "critic_agent = AssistantAgent(\n", " \"critic\",\n", " model_client=model_client,\n", " tools=[approve], # 将审批函数注册为工具。\n", " system_message=\"Provide constructive feedback. Use the approve tool to approve when all feedbacks are addressed.\",\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "现在,我们创建终止条件和团队。\n我们运行团队执行诗歌创作任务。\n" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "---------- user ----------\n", "Write a unique, Haiku about the weather in Paris\n", "---------- primary ----------\n", "Raindrops gently fall, \n", "Cobblestones shine in dim light— \n", "Paris dreams in grey. \n", "---------- critic ----------\n", "This Haiku beautifully captures a melancholic yet romantic image of Paris in the rain. The use of sensory imagery like \"Raindrops gently fall\" and \"Cobblestones shine\" effectively paints a vivid picture. It could be interesting to experiment with more distinct seasonal elements of Paris, such as incorporating the Seine River or iconic landmarks in the context of the weather. Overall, it successfully conveys the atmosphere of Paris in subtle, poetic imagery.\n", "---------- primary ----------\n", "Thank you for your feedback! I’m glad you enjoyed the imagery. Here’s another Haiku that incorporates iconic Parisian elements:\n", "\n", "Eiffel stands in mist, \n", "Seine's ripple mirrors the sky— \n", "Spring whispers anew. \n", "---------- critic ----------\n", "[FunctionCall(id='call_QEWJZ873EG4UIEpsQHi1HsAu', arguments='{}', name='approve')]\n", "---------- critic ----------\n", "[FunctionExecutionResult(content='None', name='approve', call_id='call_QEWJZ873EG4UIEpsQHi1HsAu', is_error=False)]\n", "---------- critic ----------\n", "None\n" ] }, { "data": { "text/plain": [ "TaskResult(messages=[TextMessage(source='user', models_usage=None, metadata={}, content='Write a unique, Haiku about the weather in Paris', type='TextMessage'), TextMessage(source='primary', models_usage=RequestUsage(prompt_tokens=30, completion_tokens=23), metadata={}, content='Raindrops gently fall, \\nCobblestones shine in dim light— \\nParis dreams in grey. ', type='TextMessage'), TextMessage(source='critic', models_usage=RequestUsage(prompt_tokens=99, completion_tokens=90), metadata={}, content='This Haiku beautifully captures a melancholic yet romantic image of Paris in the rain. The use of sensory imagery like \"Raindrops gently fall\" and \"Cobblestones shine\" effectively paints a vivid picture. It could be interesting to experiment with more distinct seasonal elements of Paris, such as incorporating the Seine River or iconic landmarks in the context of the weather. Overall, it successfully conveys the atmosphere of Paris in subtle, poetic imagery.', type='TextMessage'), TextMessage(source='primary', models_usage=RequestUsage(prompt_tokens=152, completion_tokens=48), metadata={}, content=\"Thank you for your feedback! I’m glad you enjoyed the imagery. Here’s another Haiku that incorporates iconic Parisian elements:\\n\\nEiffel stands in mist, \\nSeine's ripple mirrors the sky— \\nSpring whispers anew. \", type='TextMessage'), ToolCallRequestEvent(source='critic', models_usage=RequestUsage(prompt_tokens=246, completion_tokens=11), metadata={}, content=[FunctionCall(id='call_QEWJZ873EG4UIEpsQHi1HsAu', arguments='{}', name='approve')], type='ToolCallRequestEvent'), ToolCallExecutionEvent(source='critic', models_usage=None, metadata={}, content=[FunctionExecutionResult(content='None', name='approve', call_id='call_QEWJZ873EG4UIEpsQHi1HsAu', is_error=False)], type='ToolCallExecutionEvent'), ToolCallSummaryMessage(source='critic', models_usage=None, metadata={}, content='None', type='ToolCallSummaryMessage')], stop_reason=\"Function 'approve' was executed.\")" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "function_call_termination = FunctionCallTermination(function_name=\"approve\")\n", "round_robin_team = RoundRobinGroupChat([primary_agent, critic_agent], termination_condition=function_call_termination)\n", "\n", "# 如果将此脚本作为独立脚本运行,请使用 asyncio.run(...)。\n", "await Console(round_robin_team.run_stream(task=\"Write a unique, Haiku about the weather in Paris\"))\n", "await model_client.close()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "可以看到,当评论代理通过 `approve` 函数调用批准消息时,对话就停止了。\n" ] } ], "metadata": { "kernelspec": { "display_name": ".venv", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.12.3" } }, "nbformat": 4, "nbformat_minor": 2 }