Skip to content

Commit 7bdd7f6

Browse files
authored
Add functional termination condition (#6398)
Use an expression for termination condition check. This works well especially with structured messages.
1 parent 519a04d commit 7bdd7f6

5 files changed

Lines changed: 135 additions & 3 deletions

File tree

python/packages/autogen-agentchat/src/autogen_agentchat/conditions/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from ._terminations import (
77
ExternalTermination,
8+
FunctionalTermination,
89
FunctionCallTermination,
910
HandoffTermination,
1011
MaxMessageTermination,
@@ -27,4 +28,5 @@
2728
"SourceMatchTermination",
2829
"TextMessageTermination",
2930
"FunctionCallTermination",
31+
"FunctionalTermination",
3032
]

python/packages/autogen-agentchat/src/autogen_agentchat/conditions/_terminations.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import asyncio
12
import time
2-
from typing import List, Sequence
3+
from typing import Awaitable, Callable, List, Sequence
34

45
from autogen_core import Component
56
from pydantic import BaseModel
@@ -154,6 +155,77 @@ def _from_config(cls, config: TextMentionTerminationConfig) -> Self:
154155
return cls(text=config.text)
155156

156157

158+
class FunctionalTermination(TerminationCondition):
159+
"""Terminate the conversation if an functional expression is met.
160+
161+
Args:
162+
func (Callable[[Sequence[BaseAgentEvent | BaseChatMessage]], bool] | Callable[[Sequence[BaseAgentEvent | BaseChatMessage]], Awaitable[bool]]): A function that takes a sequence of messages
163+
and returns True if the termination condition is met, False otherwise.
164+
The function can be a callable or an async callable.
165+
166+
Example:
167+
168+
.. code-block:: python
169+
170+
import asyncio
171+
from typing import Sequence
172+
173+
from autogen_agentchat.conditions import FunctionalTermination
174+
from autogen_agentchat.messages import BaseAgentEvent, BaseChatMessage, StopMessage
175+
176+
177+
def expression(messages: Sequence[BaseAgentEvent | BaseChatMessage]) -> bool:
178+
# Check if the last message is a stop message
179+
return isinstance(messages[-1], StopMessage)
180+
181+
182+
termination = FunctionalTermination(expression)
183+
184+
185+
async def run() -> None:
186+
messages = [
187+
StopMessage(source="agent1", content="Stop"),
188+
]
189+
result = await termination(messages)
190+
print(result)
191+
192+
193+
asyncio.run(run())
194+
195+
.. code-block:: text
196+
197+
StopMessage(source="FunctionalTermination", content="Functional termination condition met")
198+
199+
"""
200+
201+
def __init__(
202+
self,
203+
func: Callable[[Sequence[BaseAgentEvent | BaseChatMessage]], bool]
204+
| Callable[[Sequence[BaseAgentEvent | BaseChatMessage]], Awaitable[bool]],
205+
) -> None:
206+
self._func = func
207+
self._terminated = False
208+
209+
@property
210+
def terminated(self) -> bool:
211+
return self._terminated
212+
213+
async def __call__(self, messages: Sequence[BaseAgentEvent | BaseChatMessage]) -> StopMessage | None:
214+
if self._terminated:
215+
raise TerminatedException("Termination condition has already been reached")
216+
if asyncio.iscoroutinefunction(self._func):
217+
result = await self._func(messages)
218+
else:
219+
result = self._func(messages)
220+
if result is True:
221+
self._terminated = True
222+
return StopMessage(content="Functional termination condition met", source="FunctionalTermination")
223+
return None
224+
225+
async def reset(self) -> None:
226+
self._terminated = False
227+
228+
157229
class TokenUsageTerminationConfig(BaseModel):
158230
max_total_token: int | None
159231
max_prompt_token: int | None

python/packages/autogen-agentchat/tests/test_termination_condition.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import asyncio
2+
from typing import Sequence
23

34
import pytest
45
from autogen_agentchat.base import TerminatedException
56
from autogen_agentchat.conditions import (
67
ExternalTermination,
8+
FunctionalTermination,
79
FunctionCallTermination,
810
HandoffTermination,
911
MaxMessageTermination,
@@ -15,13 +17,17 @@
1517
TokenUsageTermination,
1618
)
1719
from autogen_agentchat.messages import (
20+
BaseAgentEvent,
21+
BaseChatMessage,
1822
HandoffMessage,
1923
StopMessage,
24+
StructuredMessage,
2025
TextMessage,
2126
ToolCallExecutionEvent,
2227
UserInputRequestedEvent,
2328
)
2429
from autogen_core.models import FunctionExecutionResult, RequestUsage
30+
from pydantic import BaseModel
2531

2632

2733
@pytest.mark.asyncio
@@ -375,3 +381,53 @@ async def test_function_call_termination() -> None:
375381
)
376382
assert not termination.terminated
377383
await termination.reset()
384+
385+
386+
@pytest.mark.asyncio
387+
async def test_functional_termination() -> None:
388+
async def async_termination_func(messages: Sequence[BaseAgentEvent | BaseChatMessage]) -> bool:
389+
if len(messages) < 1:
390+
return False
391+
if isinstance(messages[-1], TextMessage):
392+
return messages[-1].content == "stop"
393+
return False
394+
395+
termination = FunctionalTermination(async_termination_func)
396+
assert await termination([]) is None
397+
await termination.reset()
398+
399+
assert await termination([TextMessage(content="Hello", source="user")]) is None
400+
await termination.reset()
401+
402+
assert await termination([TextMessage(content="stop", source="user")]) is not None
403+
assert termination.terminated
404+
await termination.reset()
405+
406+
assert await termination([TextMessage(content="Hello", source="user")]) is None
407+
408+
class TestContentType(BaseModel):
409+
content: str
410+
data: str
411+
412+
def sync_termination_func(messages: Sequence[BaseAgentEvent | BaseChatMessage]) -> bool:
413+
if len(messages) < 1:
414+
return False
415+
last_message = messages[-1]
416+
if isinstance(last_message, StructuredMessage) and isinstance(last_message.content, TestContentType): # type: ignore[reportUnknownMemberType]
417+
return last_message.content.data == "stop"
418+
return False
419+
420+
termination = FunctionalTermination(sync_termination_func)
421+
assert await termination([]) is None
422+
await termination.reset()
423+
assert await termination([TextMessage(content="Hello", source="user")]) is None
424+
await termination.reset()
425+
assert (
426+
await termination(
427+
[StructuredMessage[TestContentType](content=TestContentType(content="1", data="stop"), source="user")]
428+
)
429+
is not None
430+
)
431+
assert termination.terminated
432+
await termination.reset()
433+
assert await termination([TextMessage(content="Hello", source="user")]) is None

python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/teams.ipynb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"- {py:class}`~autogen_agentchat.teams.RoundRobinGroupChat`: A team that runs a group chat with participants taking turns in a round-robin fashion (covered on this page). [Tutorial](#creating-a-team) \n",
1717
"- {py:class}`~autogen_agentchat.teams.SelectorGroupChat`: A team that selects the next speaker using a ChatCompletion model after each message. [Tutorial](../selector-group-chat.ipynb)\n",
1818
"- {py:class}`~autogen_agentchat.teams.MagenticOneGroupChat`: A generalist multi-agent system for solving open-ended web and file-based tasks across a variety of domains. [Tutorial](../magentic-one.md) \n",
19+
"- {py:class}`~autogen_agentchat.teams.Swarm`: A team that uses {py:class}`~autogen_agentchat.messages.HandoffMessage` to signal transitions between agents. [Tutorial](../swarm.ipynb)\n",
1920
"\n",
2021
"```{note}\n",
2122
"\n",

python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/termination.ipynb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@
4040
"7. {py:class}`~autogen_agentchat.conditions.ExternalTermination`: Enables programmatic control of termination from outside the run. This is useful for UI integration (e.g., \"Stop\" buttons in chat interfaces).\n",
4141
"8. {py:class}`~autogen_agentchat.conditions.StopMessageTermination`: Stops when a {py:class}`~autogen_agentchat.messages.StopMessage` is produced by an agent.\n",
4242
"9. {py:class}`~autogen_agentchat.conditions.TextMessageTermination`: Stops when a {py:class}`~autogen_agentchat.messages.TextMessage` is produced by an agent.\n",
43-
"10. {py:class}`~autogen_agentchat.conditions.FunctionCallTermination`: Stops when a {py:class}`~autogen_agentchat.messages.ToolCallExecutionEvent` containing a {py:class}`~autogen_core.models.FunctionExecutionResult` with a matching name is produced by an agent."
43+
"10. {py:class}`~autogen_agentchat.conditions.FunctionCallTermination`: Stops when a {py:class}`~autogen_agentchat.messages.ToolCallExecutionEvent` containing a {py:class}`~autogen_core.models.FunctionExecutionResult` with a matching name is produced by an agent.\n",
44+
"11. {py:class}`~autogen_agentchat.conditions.FunctionalTermination`: Stop when a function expression is evaluated to `True` on the last delta sequence of messages. This is useful for quickly create custom termination conditions that are not covered by the built-in ones."
4445
]
4546
},
4647
{
@@ -510,7 +511,7 @@
510511
"name": "python",
511512
"nbconvert_exporter": "python",
512513
"pygments_lexer": "ipython3",
513-
"version": "3.12.9"
514+
"version": "3.12.3"
514515
}
515516
},
516517
"nbformat": 4,

0 commit comments

Comments
 (0)