From 489db97f5824c47d8a693b1210302f7e92971baa Mon Sep 17 00:00:00 2001 From: Victor Dibia Date: Thu, 9 Jan 2025 21:58:23 -0800 Subject: [PATCH 1/8] make termination condition declarative --- .../autogen_agentchat/base/_termination.py | 85 ++++++-- .../conditions/_terminations.py | 181 ++++++++++++++++-- 2 files changed, 237 insertions(+), 29 deletions(-) diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py b/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py index 8975c75aad12..46a8243252ab 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py @@ -1,14 +1,19 @@ import asyncio from abc import ABC, abstractmethod -from typing import List, Sequence +from typing import Any, List, Sequence +from typing_extensions import Self + +from pydantic import BaseModel from ..messages import AgentEvent, ChatMessage, StopMessage +from autogen_core import Component, ComponentModel, ComponentLoader -class TerminatedException(BaseException): ... +class TerminatedException(BaseException): + ... -class TerminationCondition(ABC): +class TerminationCondition(ABC, Component[Any]): """A stateful condition that determines when a conversation should be terminated. A termination condition is a callable that takes a sequence of ChatMessage objects @@ -43,6 +48,8 @@ async def main() -> None: asyncio.run(main()) """ + component_type = "termination" + @property @abstractmethod def terminated(self) -> bool: @@ -79,7 +86,15 @@ def __or__(self, other: "TerminationCondition") -> "TerminationCondition": return _OrTerminationCondition(self, other) -class _AndTerminationCondition(TerminationCondition): +class AndTerminationConditionConfig(BaseModel): + conditions: List[ComponentModel] + + +class _AndTerminationCondition(TerminationCondition, Component[AndTerminationConditionConfig]): + + component_config_schema = AndTerminationConditionConfig + component_type = "termination" + def __init__(self, *conditions: TerminationCondition) -> None: self._conditions = conditions self._stop_messages: List[StopMessage] = [] @@ -90,7 +105,8 @@ def terminated(self) -> bool: async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self.terminated: - raise TerminatedException("Termination condition has already been reached.") + raise TerminatedException( + "Termination condition has already been reached.") # Check all remaining conditions. stop_messages = await asyncio.gather( *[condition(messages) for condition in self._conditions if not condition.terminated] @@ -102,8 +118,10 @@ async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMe if any(stop_message is None for stop_message in stop_messages): # If any remaining condition has not reached termination, it is not terminated. return None - content = ", ".join(stop_message.content for stop_message in self._stop_messages) - source = ", ".join(stop_message.source for stop_message in self._stop_messages) + content = ", ".join( + stop_message.content for stop_message in self._stop_messages) + source = ", ".join( + stop_message.source for stop_message in self._stop_messages) return StopMessage(content=content, source=source) async def reset(self) -> None: @@ -111,8 +129,33 @@ async def reset(self) -> None: await condition.reset() self._stop_messages.clear() + def _to_config(self) -> AndTerminationConditionConfig: + """Convert the AND termination condition to a config.""" + return AndTerminationConditionConfig( + conditions=[condition.dump_component() + for condition in self._conditions] + ) + + @classmethod + def _from_config(cls, config: AndTerminationConditionConfig) -> Self: + """Create an AND termination condition from a config.""" + conditions = [ + ComponentLoader.load_component( + condition_model, TerminationCondition) + for condition_model in config.conditions + ] + return cls(*conditions) + + +class OrTerminationConditionConfig(BaseModel): + conditions: List[ComponentModel] + """List of termination conditions where any one being satisfied is sufficient.""" + + +class _OrTerminationCondition(TerminationCondition, Component[OrTerminationConditionConfig]): + component_config_schema = OrTerminationConditionConfig + component_type = "termination" -class _OrTerminationCondition(TerminationCondition): def __init__(self, *conditions: TerminationCondition) -> None: self._conditions = conditions @@ -122,14 +165,34 @@ def terminated(self) -> bool: async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self.terminated: - raise RuntimeError("Termination condition has already been reached") + raise RuntimeError( + "Termination condition has already been reached") stop_messages = await asyncio.gather(*[condition(messages) for condition in self._conditions]) if any(stop_message is not None for stop_message in stop_messages): - content = ", ".join(stop_message.content for stop_message in stop_messages if stop_message is not None) - source = ", ".join(stop_message.source for stop_message in stop_messages if stop_message is not None) + content = ", ".join( + stop_message.content for stop_message in stop_messages if stop_message is not None) + source = ", ".join( + stop_message.source for stop_message in stop_messages if stop_message is not None) return StopMessage(content=content, source=source) return None async def reset(self) -> None: for condition in self._conditions: await condition.reset() + + def _to_config(self) -> OrTerminationConditionConfig: + """Convert the OR termination condition to a config.""" + return OrTerminationConditionConfig( + conditions=[condition.dump_component() + for condition in self._conditions] + ) + + @classmethod + def _from_config(cls, config: OrTerminationConditionConfig) -> Self: + """Create an OR termination condition from a config.""" + conditions = [ + ComponentLoader.load_component( + condition_model, TerminationCondition) + for condition_model in config.conditions + ] + return cls(*conditions) diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/conditions/_terminations.py b/python/packages/autogen-agentchat/src/autogen_agentchat/conditions/_terminations.py index c472d7e323a5..d67293c223f0 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/conditions/_terminations.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/conditions/_terminations.py @@ -1,13 +1,25 @@ import time from typing import List, Sequence +from pydantic import BaseModel + from ..base import TerminatedException, TerminationCondition from ..messages import AgentEvent, ChatMessage, HandoffMessage, MultiModalMessage, StopMessage +from autogen_core import Component +from typing_extensions import Self + + +class StopMessageTerminationConfig(BaseModel): + pass -class StopMessageTermination(TerminationCondition): +class StopMessageTermination(TerminationCondition, Component[StopMessageTerminationConfig]): """Terminate the conversation if a StopMessage is received.""" + component_type = "termination" + component_config_schema = StopMessageTerminationConfig + component_provider_override = "autogen_agentchat.conditions.StopMessageTermination" + def __init__(self) -> None: self._terminated = False @@ -17,7 +29,8 @@ def terminated(self) -> bool: async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self._terminated: - raise TerminatedException("Termination condition has already been reached") + raise TerminatedException( + "Termination condition has already been reached") for message in messages: if isinstance(message, StopMessage): self._terminated = True @@ -27,14 +40,29 @@ async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMe async def reset(self) -> None: self._terminated = False + def _to_config(self) -> StopMessageTerminationConfig: + return StopMessageTerminationConfig() + + @classmethod + def _from_config(cls, config: StopMessageTerminationConfig) -> Self: + return cls() + -class MaxMessageTermination(TerminationCondition): +class MaxMessageTerminationConfig(BaseModel): + max_messages: int + + +class MaxMessageTermination(TerminationCondition, Component[MaxMessageTerminationConfig]): """Terminate the conversation after a maximum number of messages have been exchanged. Args: max_messages: The maximum number of messages allowed in the conversation. """ + component_type = "termination" + component_config_schema = MaxMessageTerminationConfig + component_provider_override = "autogen_agentchat.conditions.MaxMessageTermination" + def __init__(self, max_messages: int) -> None: self._max_messages = max_messages self._message_count = 0 @@ -45,7 +73,8 @@ def terminated(self) -> bool: async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self.terminated: - raise TerminatedException("Termination condition has already been reached") + raise TerminatedException( + "Termination condition has already been reached") self._message_count += len(messages) if self._message_count >= self._max_messages: return StopMessage( @@ -57,14 +86,30 @@ async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMe async def reset(self) -> None: self._message_count = 0 + def _to_config(self) -> MaxMessageTerminationConfig: + return MaxMessageTerminationConfig(max_messages=self._max_messages) + + @classmethod + def _from_config(cls, config: MaxMessageTerminationConfig) -> Self: + return cls(max_messages=config.max_messages) + -class TextMentionTermination(TerminationCondition): +class TextMentionTerminationConfig(BaseModel): + text: str + + +class TextMentionTermination(TerminationCondition, Component[TextMentionTerminationConfig]): """Terminate the conversation if a specific text is mentioned. + Args: text: The text to look for in the messages. """ + component_type = "termination" + component_config_schema = TextMentionTerminationConfig + component_provider_override = "autogen_agentchat.conditions.TextMentionTermination" + def __init__(self, text: str) -> None: self._text = text self._terminated = False @@ -75,7 +120,8 @@ def terminated(self) -> bool: async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self._terminated: - raise TerminatedException("Termination condition has already been reached") + raise TerminatedException( + "Termination condition has already been reached") for message in messages: if isinstance(message.content, str) and self._text in message.content: self._terminated = True @@ -90,8 +136,21 @@ async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMe async def reset(self) -> None: self._terminated = False + def _to_config(self) -> TextMentionTerminationConfig: + return TextMentionTerminationConfig(text=self._text) + + @classmethod + def _from_config(cls, config: TextMentionTerminationConfig) -> Self: + return cls(text=config.text) + -class TokenUsageTermination(TerminationCondition): +class TokenUsageTerminationConfig(BaseModel): + max_total_token: int | None + max_prompt_token: int | None + max_completion_token: int | None + + +class TokenUsageTermination(TerminationCondition, Component[TokenUsageTerminationConfig]): """Terminate the conversation if a token usage limit is reached. Args: @@ -103,6 +162,10 @@ class TokenUsageTermination(TerminationCondition): ValueError: If none of max_total_token, max_prompt_token, or max_completion_token is provided. """ + component_type = "termination" + component_config_schema = TokenUsageTerminationConfig + component_provider_override = "autogen_agentchat.conditions.TokenUsageTermination" + def __init__( self, max_total_token: int | None = None, @@ -123,19 +186,22 @@ def __init__( @property def terminated(self) -> bool: return ( - (self._max_total_token is not None and self._total_token_count >= self._max_total_token) + (self._max_total_token is not None and self._total_token_count >= + self._max_total_token) or (self._max_prompt_token is not None and self._prompt_token_count >= self._max_prompt_token) or (self._max_completion_token is not None and self._completion_token_count >= self._max_completion_token) ) async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self.terminated: - raise TerminatedException("Termination condition has already been reached") + raise TerminatedException( + "Termination condition has already been reached") for message in messages: if message.models_usage is not None: self._prompt_token_count += message.models_usage.prompt_tokens self._completion_token_count += message.models_usage.completion_tokens - self._total_token_count += message.models_usage.prompt_tokens + message.models_usage.completion_tokens + self._total_token_count += message.models_usage.prompt_tokens + \ + message.models_usage.completion_tokens if self.terminated: content = f"Token usage limit reached, total token count: {self._total_token_count}, prompt token count: {self._prompt_token_count}, completion token count: {self._completion_token_count}." return StopMessage(content=content, source="TokenUsageTermination") @@ -146,8 +212,27 @@ async def reset(self) -> None: self._prompt_token_count = 0 self._completion_token_count = 0 + def _to_config(self) -> TokenUsageTerminationConfig: + return TokenUsageTerminationConfig( + max_total_token=self._max_total_token, + max_prompt_token=self._max_prompt_token, + max_completion_token=self._max_completion_token + ) + + @classmethod + def _from_config(cls, config: TokenUsageTerminationConfig) -> Self: + return cls( + max_total_token=config.max_total_token, + max_prompt_token=config.max_prompt_token, + max_completion_token=config.max_completion_token + ) + + +class HandoffTerminationConfig(BaseModel): + target: str -class HandoffTermination(TerminationCondition): + +class HandoffTermination(TerminationCondition, Component[HandoffTerminationConfig]): """Terminate the conversation if a :class:`~autogen_agentchat.messages.HandoffMessage` with the given target is received. @@ -155,6 +240,10 @@ class HandoffTermination(TerminationCondition): target (str): The target of the handoff message. """ + component_type = "termination" + component_config_schema = HandoffTerminationConfig + component_provider_override = "autogen_agentchat.conditions.HandoffTermination" + def __init__(self, target: str) -> None: self._terminated = False self._target = target @@ -165,7 +254,8 @@ def terminated(self) -> bool: async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self._terminated: - raise TerminatedException("Termination condition has already been reached") + raise TerminatedException( + "Termination condition has already been reached") for message in messages: if isinstance(message, HandoffMessage) and message.target == self._target: self._terminated = True @@ -177,14 +267,29 @@ async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMe async def reset(self) -> None: self._terminated = False + def _to_config(self) -> HandoffTerminationConfig: + return HandoffTerminationConfig(target=self._target) + + @classmethod + def _from_config(cls, config: HandoffTerminationConfig) -> Self: + return cls(target=config.target) -class TimeoutTermination(TerminationCondition): + +class TimeoutTerminationConfig(BaseModel): + timeout_seconds: float + + +class TimeoutTermination(TerminationCondition, Component[TimeoutTerminationConfig]): """Terminate the conversation after a specified duration has passed. Args: timeout_seconds: The maximum duration in seconds before terminating the conversation. """ + component_type = "termination" + component_config_schema = TimeoutTerminationConfig + component_provider_override = "autogen_agentchat.conditions.TimeoutTermination" + def __init__(self, timeout_seconds: float) -> None: self._timeout_seconds = timeout_seconds self._start_time = time.monotonic() @@ -196,7 +301,8 @@ def terminated(self) -> bool: async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self._terminated: - raise TerminatedException("Termination condition has already been reached") + raise TerminatedException( + "Termination condition has already been reached") if (time.monotonic() - self._start_time) >= self._timeout_seconds: self._terminated = True @@ -209,8 +315,19 @@ async def reset(self) -> None: self._start_time = time.monotonic() self._terminated = False + def _to_config(self) -> TimeoutTerminationConfig: + return TimeoutTerminationConfig(timeout_seconds=self._timeout_seconds) + + @classmethod + def _from_config(cls, config: TimeoutTerminationConfig) -> Self: + return cls(timeout_seconds=config.timeout_seconds) -class ExternalTermination(TerminationCondition): + +class ExternalTerminationConfig(BaseModel): + pass + + +class ExternalTermination(TerminationCondition, Component[ExternalTerminationConfig]): """A termination condition that is externally controlled by calling the :meth:`set` method. @@ -230,6 +347,10 @@ class ExternalTermination(TerminationCondition): """ + component_type = "termination" + component_config_schema = ExternalTerminationConfig + component_provider_override = "autogen_agentchat.conditions.ExternalTermination" + def __init__(self) -> None: self._terminated = False self._setted = False @@ -244,7 +365,8 @@ def set(self) -> None: async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self._terminated: - raise TerminatedException("Termination condition has already been reached") + raise TerminatedException( + "Termination condition has already been reached") if self._setted: self._terminated = True return StopMessage(content="External termination requested", source="ExternalTermination") @@ -254,8 +376,19 @@ async def reset(self) -> None: self._terminated = False self._setted = False + def _to_config(self) -> ExternalTerminationConfig: + return ExternalTerminationConfig() + + @classmethod + def _from_config(cls, config: ExternalTerminationConfig) -> Self: + return cls() + -class SourceMatchTermination(TerminationCondition): +class SourceMatchTerminationConfig(BaseModel): + sources: List[str] + + +class SourceMatchTermination(TerminationCondition, Component[SourceMatchTerminationConfig]): """Terminate the conversation after a specific source responds. Args: @@ -265,6 +398,10 @@ class SourceMatchTermination(TerminationCondition): TerminatedException: If the termination condition has already been reached. """ + component_type = "termination" + component_config_schema = SourceMatchTerminationConfig + component_provider_override = "autogen_agentchat.conditions.SourceMatchTermination" + def __init__(self, sources: List[str]) -> None: self._sources = sources self._terminated = False @@ -275,7 +412,8 @@ def terminated(self) -> bool: async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self._terminated: - raise TerminatedException("Termination condition has already been reached") + raise TerminatedException( + "Termination condition has already been reached") if not messages: return None for message in messages: @@ -286,3 +424,10 @@ async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMe async def reset(self) -> None: self._terminated = False + + def _to_config(self) -> SourceMatchTerminationConfig: + return SourceMatchTerminationConfig(sources=self._sources) + + @classmethod + def _from_config(cls, config: SourceMatchTerminationConfig) -> Self: + return cls(sources=config.sources) From 003012fc4d721d75325ce5a1ef76a853062ee644 Mon Sep 17 00:00:00 2001 From: Victor Dibia Date: Fri, 10 Jan 2025 16:34:10 -0800 Subject: [PATCH 2/8] make all term conditions declarative --- .../autogen_agentchat/base/_termination.py | 59 +++++++------------ .../conditions/_terminations.py | 34 ++++------- 2 files changed, 32 insertions(+), 61 deletions(-) diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py b/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py index 46a8243252ab..7da86f47de31 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py @@ -6,11 +6,10 @@ from pydantic import BaseModel from ..messages import AgentEvent, ChatMessage, StopMessage -from autogen_core import Component, ComponentModel, ComponentLoader +from autogen_core import Component, ComponentModel -class TerminatedException(BaseException): - ... +class TerminatedException(BaseException): ... class TerminationCondition(ABC, Component[Any]): @@ -49,6 +48,7 @@ async def main() -> None: """ component_type = "termination" + # component_config_schema = BaseModel # type: ignore @property @abstractmethod @@ -79,21 +79,21 @@ async def reset(self) -> None: def __and__(self, other: "TerminationCondition") -> "TerminationCondition": """Combine two termination conditions with an AND operation.""" - return _AndTerminationCondition(self, other) + return AndTerminationCondition(self, other) def __or__(self, other: "TerminationCondition") -> "TerminationCondition": """Combine two termination conditions with an OR operation.""" - return _OrTerminationCondition(self, other) + return OrTerminationCondition(self, other) class AndTerminationConditionConfig(BaseModel): - conditions: List[ComponentModel] - + conditions: List[ComponentModel] -class _AndTerminationCondition(TerminationCondition, Component[AndTerminationConditionConfig]): +class AndTerminationCondition(TerminationCondition, Component[AndTerminationConditionConfig]): component_config_schema = AndTerminationConditionConfig component_type = "termination" + component_provider_override = "autogen_agentchat.base.AndTerminationCondition" def __init__(self, *conditions: TerminationCondition) -> None: self._conditions = conditions @@ -105,8 +105,7 @@ def terminated(self) -> bool: async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self.terminated: - raise TerminatedException( - "Termination condition has already been reached.") + raise TerminatedException("Termination condition has already been reached.") # Check all remaining conditions. stop_messages = await asyncio.gather( *[condition(messages) for condition in self._conditions if not condition.terminated] @@ -118,10 +117,8 @@ async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMe if any(stop_message is None for stop_message in stop_messages): # If any remaining condition has not reached termination, it is not terminated. return None - content = ", ".join( - stop_message.content for stop_message in self._stop_messages) - source = ", ".join( - stop_message.source for stop_message in self._stop_messages) + content = ", ".join(stop_message.content for stop_message in self._stop_messages) + source = ", ".join(stop_message.source for stop_message in self._stop_messages) return StopMessage(content=content, source=source) async def reset(self) -> None: @@ -131,19 +128,12 @@ async def reset(self) -> None: def _to_config(self) -> AndTerminationConditionConfig: """Convert the AND termination condition to a config.""" - return AndTerminationConditionConfig( - conditions=[condition.dump_component() - for condition in self._conditions] - ) + return AndTerminationConditionConfig(conditions=[condition.dump_component() for condition in self._conditions]) @classmethod def _from_config(cls, config: AndTerminationConditionConfig) -> Self: """Create an AND termination condition from a config.""" - conditions = [ - ComponentLoader.load_component( - condition_model, TerminationCondition) - for condition_model in config.conditions - ] + conditions = [TerminationCondition.load_component(condition_model) for condition_model in config.conditions] return cls(*conditions) @@ -152,9 +142,10 @@ class OrTerminationConditionConfig(BaseModel): """List of termination conditions where any one being satisfied is sufficient.""" -class _OrTerminationCondition(TerminationCondition, Component[OrTerminationConditionConfig]): +class OrTerminationCondition(TerminationCondition, Component[OrTerminationConditionConfig]): component_config_schema = OrTerminationConditionConfig component_type = "termination" + component_provider_override = "autogen_agentchat.base.OrTerminationCondition" def __init__(self, *conditions: TerminationCondition) -> None: self._conditions = conditions @@ -165,14 +156,11 @@ def terminated(self) -> bool: async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self.terminated: - raise RuntimeError( - "Termination condition has already been reached") + raise RuntimeError("Termination condition has already been reached") stop_messages = await asyncio.gather(*[condition(messages) for condition in self._conditions]) if any(stop_message is not None for stop_message in stop_messages): - content = ", ".join( - stop_message.content for stop_message in stop_messages if stop_message is not None) - source = ", ".join( - stop_message.source for stop_message in stop_messages if stop_message is not None) + content = ", ".join(stop_message.content for stop_message in stop_messages if stop_message is not None) + source = ", ".join(stop_message.source for stop_message in stop_messages if stop_message is not None) return StopMessage(content=content, source=source) return None @@ -182,17 +170,10 @@ async def reset(self) -> None: def _to_config(self) -> OrTerminationConditionConfig: """Convert the OR termination condition to a config.""" - return OrTerminationConditionConfig( - conditions=[condition.dump_component() - for condition in self._conditions] - ) + return OrTerminationConditionConfig(conditions=[condition.dump_component() for condition in self._conditions]) @classmethod def _from_config(cls, config: OrTerminationConditionConfig) -> Self: """Create an OR termination condition from a config.""" - conditions = [ - ComponentLoader.load_component( - condition_model, TerminationCondition) - for condition_model in config.conditions - ] + conditions = [TerminationCondition.load_component(condition_model) for condition_model in config.conditions] return cls(*conditions) diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/conditions/_terminations.py b/python/packages/autogen-agentchat/src/autogen_agentchat/conditions/_terminations.py index d67293c223f0..27635257b9f2 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/conditions/_terminations.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/conditions/_terminations.py @@ -29,8 +29,7 @@ def terminated(self) -> bool: async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self._terminated: - raise TerminatedException( - "Termination condition has already been reached") + raise TerminatedException("Termination condition has already been reached") for message in messages: if isinstance(message, StopMessage): self._terminated = True @@ -73,8 +72,7 @@ def terminated(self) -> bool: async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self.terminated: - raise TerminatedException( - "Termination condition has already been reached") + raise TerminatedException("Termination condition has already been reached") self._message_count += len(messages) if self._message_count >= self._max_messages: return StopMessage( @@ -120,8 +118,7 @@ def terminated(self) -> bool: async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self._terminated: - raise TerminatedException( - "Termination condition has already been reached") + raise TerminatedException("Termination condition has already been reached") for message in messages: if isinstance(message.content, str) and self._text in message.content: self._terminated = True @@ -186,22 +183,19 @@ def __init__( @property def terminated(self) -> bool: return ( - (self._max_total_token is not None and self._total_token_count >= - self._max_total_token) + (self._max_total_token is not None and self._total_token_count >= self._max_total_token) or (self._max_prompt_token is not None and self._prompt_token_count >= self._max_prompt_token) or (self._max_completion_token is not None and self._completion_token_count >= self._max_completion_token) ) async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self.terminated: - raise TerminatedException( - "Termination condition has already been reached") + raise TerminatedException("Termination condition has already been reached") for message in messages: if message.models_usage is not None: self._prompt_token_count += message.models_usage.prompt_tokens self._completion_token_count += message.models_usage.completion_tokens - self._total_token_count += message.models_usage.prompt_tokens + \ - message.models_usage.completion_tokens + self._total_token_count += message.models_usage.prompt_tokens + message.models_usage.completion_tokens if self.terminated: content = f"Token usage limit reached, total token count: {self._total_token_count}, prompt token count: {self._prompt_token_count}, completion token count: {self._completion_token_count}." return StopMessage(content=content, source="TokenUsageTermination") @@ -216,7 +210,7 @@ def _to_config(self) -> TokenUsageTerminationConfig: return TokenUsageTerminationConfig( max_total_token=self._max_total_token, max_prompt_token=self._max_prompt_token, - max_completion_token=self._max_completion_token + max_completion_token=self._max_completion_token, ) @classmethod @@ -224,7 +218,7 @@ def _from_config(cls, config: TokenUsageTerminationConfig) -> Self: return cls( max_total_token=config.max_total_token, max_prompt_token=config.max_prompt_token, - max_completion_token=config.max_completion_token + max_completion_token=config.max_completion_token, ) @@ -254,8 +248,7 @@ def terminated(self) -> bool: async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self._terminated: - raise TerminatedException( - "Termination condition has already been reached") + raise TerminatedException("Termination condition has already been reached") for message in messages: if isinstance(message, HandoffMessage) and message.target == self._target: self._terminated = True @@ -301,8 +294,7 @@ def terminated(self) -> bool: async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self._terminated: - raise TerminatedException( - "Termination condition has already been reached") + raise TerminatedException("Termination condition has already been reached") if (time.monotonic() - self._start_time) >= self._timeout_seconds: self._terminated = True @@ -365,8 +357,7 @@ def set(self) -> None: async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self._terminated: - raise TerminatedException( - "Termination condition has already been reached") + raise TerminatedException("Termination condition has already been reached") if self._setted: self._terminated = True return StopMessage(content="External termination requested", source="ExternalTermination") @@ -412,8 +403,7 @@ def terminated(self) -> bool: async def __call__(self, messages: Sequence[AgentEvent | ChatMessage]) -> StopMessage | None: if self._terminated: - raise TerminatedException( - "Termination condition has already been reached") + raise TerminatedException("Termination condition has already been reached") if not messages: return None for message in messages: From b0d5eee874ae21a9c991a4e6eb0177cd8fb477cd Mon Sep 17 00:00:00 2001 From: Victor Dibia Date: Fri, 10 Jan 2025 16:34:31 -0800 Subject: [PATCH 3/8] make And/OrTermination top level objects in base --- .../autogen-agentchat/src/autogen_agentchat/base/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/base/__init__.py b/python/packages/autogen-agentchat/src/autogen_agentchat/base/__init__.py index 5815adc35fa9..44d70886ea73 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/base/__init__.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/base/__init__.py @@ -2,7 +2,7 @@ from ._handoff import Handoff from ._task import TaskResult, TaskRunner from ._team import Team -from ._termination import TerminatedException, TerminationCondition +from ._termination import TerminatedException, TerminationCondition, AndTerminationCondition, OrTerminationCondition __all__ = [ "ChatAgent", @@ -10,6 +10,8 @@ "Team", "TerminatedException", "TerminationCondition", + "AndTerminationCondition", + "OrTerminationCondition", "TaskResult", "TaskRunner", "Handoff", From 84e376d5236f324c1045641022a812b0a26d439f Mon Sep 17 00:00:00 2001 From: Victor Dibia Date: Fri, 10 Jan 2025 16:34:42 -0800 Subject: [PATCH 4/8] add basic tests --- .../tests/test_declarative_components.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 python/packages/autogen-agentchat/tests/test_declarative_components.py diff --git a/python/packages/autogen-agentchat/tests/test_declarative_components.py b/python/packages/autogen-agentchat/tests/test_declarative_components.py new file mode 100644 index 000000000000..fe6029e06afd --- /dev/null +++ b/python/packages/autogen-agentchat/tests/test_declarative_components.py @@ -0,0 +1,34 @@ +import pytest +from autogen_agentchat.conditions import StopMessageTermination, MaxMessageTermination +from autogen_agentchat.messages import StopMessage, TextMessage +from autogen_core import ComponentLoader + + +@pytest.mark.asyncio +async def test_termination_declarative() -> None: + """Test that termination conditions can be declared and serialized properly.""" + # Create basic termination conditions + max_term = MaxMessageTermination(5) + stop_term = StopMessageTermination() + + # Test basic serialization/deserialization + max_config = max_term.dump_component() + assert max_config.provider == "autogen_agentchat.conditions.MaxMessageTermination" + assert max_config.config["max_messages"] == 5 + loaded_max = ComponentLoader.load_component(max_config) + assert isinstance(loaded_max, MaxMessageTermination) + assert loaded_max._max_messages == 5 + + # Test composition and complex serialization + or_term = max_term | stop_term + or_config = or_term.dump_component() + assert or_config.provider == "autogen_agentchat.base.OrTerminationCondition" + assert len(or_config.config["conditions"]) == 2 + + # Test behavior after deserialization + loaded_or = ComponentLoader.load_component(or_config) + messages = [StopMessage(content="stop", source="test")] + result = await loaded_or(messages) + assert isinstance(result, StopMessage) + await loaded_or.reset() + assert not loaded_or.terminated From a9a52ff88a5b07c84f893b8aa24d4a45d0cbad0e Mon Sep 17 00:00:00 2001 From: Victor Dibia Date: Fri, 10 Jan 2025 16:34:50 -0800 Subject: [PATCH 5/8] add tutorial notebook --- .../tutorial/declarative.ipynb | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/declarative.ipynb diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/declarative.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/declarative.ipynb new file mode 100644 index 000000000000..b471a34bcf49 --- /dev/null +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/declarative.ipynb @@ -0,0 +1,127 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Declarative Components \n", + "\n", + "AutoGen provides a declarative component configuration class that defines behaviours for being able to export and import components in a declarative format. This is useful for debugging, visualizing, and even for sharing your work with others. In this notebook, we will demonstrate how to export a declarative representation of a multiagent team in the form of a JSON file. \n", + "\n", + "\n", + "```{note}\n", + "This is work in progress\n", + "``` \n", + "\n", + "We will be implementing declarative support for the following components:\n", + "\n", + "- [X] Termination conditions \n", + "- [ ] Tools \n", + "- [ ] Agents \n", + "- [ ] Teams \n", + "\n", + "\n", + "### Termination Condition Example \n", + "\n", + "In the example below, we will define termination conditions (a part of an agent team) in python, export this to a dictionary/json and also demonstrate how the termination condition object can be loaded from the dictionary/json. \n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from autogen_agentchat.conditions import StopMessageTermination, MaxMessageTermination \n", + "\n", + "max_termination = MaxMessageTermination(5)\n", + "stop_termination = StopMessageTermination() " + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "provider='autogen_agentchat.conditions.MaxMessageTermination' component_type='termination' version=1 component_version=1 description=None config={'max_messages': 5}\n" + ] + } + ], + "source": [ + "print(max_termination.dump_component())" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'provider': 'autogen_agentchat.conditions.MaxMessageTermination', 'component_type': 'termination', 'version': 1, 'component_version': 1, 'description': None, 'config': {'max_messages': 5}}\n" + ] + } + ], + "source": [ + "print(max_termination.dump_component().model_dump())" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/var/folders/wg/hgs_dt8n5lbd3gx3pq7k6lym0000gn/T/ipykernel_86784/1252547633.py:2: UserWarning: Internal module name used in provider string. This is not recommended and may cause issues in the future. Silence this warning by setting component_provider_override to this value.\n", + " or_termination.dump_component()\n" + ] + }, + { + "data": { + "text/plain": [ + "ComponentModel(provider='autogen_agentchat.base._termination._OrTerminationCondition', component_type='termination', version=1, component_version=1, description=None, config={'conditions': [{'provider': 'autogen_agentchat.conditions.MaxMessageTermination', 'component_type': 'termination', 'version': 1, 'component_version': 1, 'config': {'max_messages': 5}}, {'provider': 'autogen_agentchat.conditions.StopMessageTermination', 'component_type': 'termination', 'version': 1, 'component_version': 1, 'config': {}}]})" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "or_termination = max_termination | stop_termination\n", + "or_termination.dump_component() " + ] + } + ], + "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.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From fb65bad16a18a9c4062e2f14b2e98a9f1a164e34 Mon Sep 17 00:00:00 2001 From: Victor Dibia Date: Fri, 10 Jan 2025 18:22:28 -0800 Subject: [PATCH 6/8] update tests and formatting --- .../src/autogen_agentchat/base/__init__.py | 2 +- .../autogen_agentchat/base/_termination.py | 4 +- .../conditions/_terminations.py | 4 +- .../tests/test_declarative_components.py | 39 +++++++++++++------ .../user-guide/agentchat-user-guide/index.md | 1 + .../tutorial/declarative.ipynb | 34 ++++++++-------- 6 files changed, 51 insertions(+), 33 deletions(-) diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/base/__init__.py b/python/packages/autogen-agentchat/src/autogen_agentchat/base/__init__.py index 44d70886ea73..dcb0a24c3e79 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/base/__init__.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/base/__init__.py @@ -2,7 +2,7 @@ from ._handoff import Handoff from ._task import TaskResult, TaskRunner from ._team import Team -from ._termination import TerminatedException, TerminationCondition, AndTerminationCondition, OrTerminationCondition +from ._termination import AndTerminationCondition, OrTerminationCondition, TerminatedException, TerminationCondition __all__ = [ "ChatAgent", diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py b/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py index 7da86f47de31..f154db9f37d1 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py @@ -1,12 +1,12 @@ import asyncio from abc import ABC, abstractmethod from typing import Any, List, Sequence -from typing_extensions import Self +from autogen_core import Component, ComponentModel from pydantic import BaseModel +from typing_extensions import Self from ..messages import AgentEvent, ChatMessage, StopMessage -from autogen_core import Component, ComponentModel class TerminatedException(BaseException): ... diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/conditions/_terminations.py b/python/packages/autogen-agentchat/src/autogen_agentchat/conditions/_terminations.py index 27635257b9f2..d824815aeb1f 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/conditions/_terminations.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/conditions/_terminations.py @@ -1,12 +1,12 @@ import time from typing import List, Sequence +from autogen_core import Component from pydantic import BaseModel +from typing_extensions import Self from ..base import TerminatedException, TerminationCondition from ..messages import AgentEvent, ChatMessage, HandoffMessage, MultiModalMessage, StopMessage -from autogen_core import Component -from typing_extensions import Self class StopMessageTerminationConfig(BaseModel): diff --git a/python/packages/autogen-agentchat/tests/test_declarative_components.py b/python/packages/autogen-agentchat/tests/test_declarative_components.py index fe6029e06afd..0bf0aa1bfa10 100644 --- a/python/packages/autogen-agentchat/tests/test_declarative_components.py +++ b/python/packages/autogen-agentchat/tests/test_declarative_components.py @@ -1,7 +1,8 @@ import pytest -from autogen_agentchat.conditions import StopMessageTermination, MaxMessageTermination -from autogen_agentchat.messages import StopMessage, TextMessage -from autogen_core import ComponentLoader +from autogen_agentchat.base import OrTerminationCondition +from autogen_agentchat.conditions import MaxMessageTermination, StopMessageTermination +from autogen_agentchat.messages import StopMessage +from autogen_core import ComponentLoader, ComponentModel @pytest.mark.asyncio @@ -11,13 +12,19 @@ async def test_termination_declarative() -> None: max_term = MaxMessageTermination(5) stop_term = StopMessageTermination() - # Test basic serialization/deserialization + # Test basic serialization max_config = max_term.dump_component() + assert isinstance(max_config, ComponentModel) assert max_config.provider == "autogen_agentchat.conditions.MaxMessageTermination" - assert max_config.config["max_messages"] == 5 - loaded_max = ComponentLoader.load_component(max_config) + assert max_config.config.get("max_messages") == 5 + + # Test basic deserialization + loaded_max = ComponentLoader.load_component(max_config, MaxMessageTermination) assert isinstance(loaded_max, MaxMessageTermination) - assert loaded_max._max_messages == 5 + # Use public interface to verify state + messages = [StopMessage(content="msg", source="test") for _ in range(5)] + result = await loaded_max(messages) + assert isinstance(result, StopMessage) # Test composition and complex serialization or_term = max_term | stop_term @@ -25,10 +32,20 @@ async def test_termination_declarative() -> None: assert or_config.provider == "autogen_agentchat.base.OrTerminationCondition" assert len(or_config.config["conditions"]) == 2 - # Test behavior after deserialization - loaded_or = ComponentLoader.load_component(or_config) - messages = [StopMessage(content="stop", source="test")] - result = await loaded_or(messages) + # Verify nested conditions are correctly serialized + conditions = or_config.config["conditions"] + assert conditions[0]["provider"] == "autogen_agentchat.conditions.MaxMessageTermination" + assert conditions[1]["provider"] == "autogen_agentchat.conditions.StopMessageTermination" + + # Test behavior of loaded composite condition + loaded_or = OrTerminationCondition.load_component(or_config) + + # Test with stop message + stop_messages = [StopMessage(content="stop", source="test")] + result = await loaded_or(stop_messages) assert isinstance(result, StopMessage) + assert loaded_or.terminated + + # Test reset functionality await loaded_or.reset() assert not loaded_or.terminated diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/index.md b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/index.md index 9754bd8ca5ed..7a764c4c7340 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/index.md +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/index.md @@ -91,6 +91,7 @@ tutorial/human-in-the-loop tutorial/termination tutorial/custom-agents tutorial/state +tutorial/declarative ``` ```{toctree} diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/declarative.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/declarative.ipynb index b471a34bcf49..7682f624c590 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/declarative.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/declarative.ipynb @@ -15,10 +15,10 @@ "\n", "We will be implementing declarative support for the following components:\n", "\n", - "- [X] Termination conditions \n", - "- [ ] Tools \n", - "- [ ] Agents \n", - "- [ ] Teams \n", + "- Termination conditions ✔️\n", + "- Tools \n", + "- Agents \n", + "- Teams \n", "\n", "\n", "### Termination Condition Example \n", @@ -31,12 +31,20 @@ "cell_type": "code", "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + ":106: UserWarning: Class variable 'component_config_schema' must be defined in TerminationCondition to be a valid component\n" + ] + } + ], "source": [ - "from autogen_agentchat.conditions import StopMessageTermination, MaxMessageTermination \n", + "from autogen_agentchat.conditions import MaxMessageTermination, StopMessageTermination\n", "\n", "max_termination = MaxMessageTermination(5)\n", - "stop_termination = StopMessageTermination() " + "stop_termination = StopMessageTermination()" ] }, { @@ -78,18 +86,10 @@ "execution_count": 4, "metadata": {}, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/var/folders/wg/hgs_dt8n5lbd3gx3pq7k6lym0000gn/T/ipykernel_86784/1252547633.py:2: UserWarning: Internal module name used in provider string. This is not recommended and may cause issues in the future. Silence this warning by setting component_provider_override to this value.\n", - " or_termination.dump_component()\n" - ] - }, { "data": { "text/plain": [ - "ComponentModel(provider='autogen_agentchat.base._termination._OrTerminationCondition', component_type='termination', version=1, component_version=1, description=None, config={'conditions': [{'provider': 'autogen_agentchat.conditions.MaxMessageTermination', 'component_type': 'termination', 'version': 1, 'component_version': 1, 'config': {'max_messages': 5}}, {'provider': 'autogen_agentchat.conditions.StopMessageTermination', 'component_type': 'termination', 'version': 1, 'component_version': 1, 'config': {}}]})" + "ComponentModel(provider='autogen_agentchat.base.OrTerminationCondition', component_type='termination', version=1, component_version=1, description=None, config={'conditions': [{'provider': 'autogen_agentchat.conditions.MaxMessageTermination', 'component_type': 'termination', 'version': 1, 'component_version': 1, 'config': {'max_messages': 5}}, {'provider': 'autogen_agentchat.conditions.StopMessageTermination', 'component_type': 'termination', 'version': 1, 'component_version': 1, 'config': {}}]})" ] }, "execution_count": 4, @@ -99,7 +99,7 @@ ], "source": [ "or_termination = max_termination | stop_termination\n", - "or_termination.dump_component() " + "or_termination.dump_component()" ] } ], From f431b73e95c80dddf2737ef29f079fcb25bb0b0a Mon Sep 17 00:00:00 2001 From: Victor Dibia Date: Fri, 10 Jan 2025 18:46:51 -0800 Subject: [PATCH 7/8] update tests --- .../tests/test_declarative_components.py | 105 ++++++++++++------ 1 file changed, 74 insertions(+), 31 deletions(-) diff --git a/python/packages/autogen-agentchat/tests/test_declarative_components.py b/python/packages/autogen-agentchat/tests/test_declarative_components.py index 0bf0aa1bfa10..35cf54f86416 100644 --- a/python/packages/autogen-agentchat/tests/test_declarative_components.py +++ b/python/packages/autogen-agentchat/tests/test_declarative_components.py @@ -1,7 +1,15 @@ import pytest -from autogen_agentchat.base import OrTerminationCondition -from autogen_agentchat.conditions import MaxMessageTermination, StopMessageTermination -from autogen_agentchat.messages import StopMessage +from autogen_agentchat.base import AndTerminationCondition +from autogen_agentchat.conditions import ( + ExternalTermination, + HandoffTermination, + MaxMessageTermination, + SourceMatchTermination, + StopMessageTermination, + TextMentionTermination, + TimeoutTermination, + TokenUsageTermination, +) from autogen_core import ComponentLoader, ComponentModel @@ -11,6 +19,12 @@ async def test_termination_declarative() -> None: # Create basic termination conditions max_term = MaxMessageTermination(5) stop_term = StopMessageTermination() + text_term = TextMentionTermination("stop") + token_term = TokenUsageTermination(max_total_token=100, max_prompt_token=50, max_completion_token=100) + handoff_term = HandoffTermination(target="human") + timeout_term = TimeoutTermination(timeout_seconds=30) + external_term = ExternalTermination() + source_term = SourceMatchTermination(sources=["human"]) # Test basic serialization max_config = max_term.dump_component() @@ -18,34 +32,63 @@ async def test_termination_declarative() -> None: assert max_config.provider == "autogen_agentchat.conditions.MaxMessageTermination" assert max_config.config.get("max_messages") == 5 + # Test serialization of new conditions + text_config = text_term.dump_component() + assert text_config.provider == "autogen_agentchat.conditions.TextMentionTermination" + assert text_config.config.get("text") == "stop" + + token_config = token_term.dump_component() + assert token_config.provider == "autogen_agentchat.conditions.TokenUsageTermination" + assert token_config.config.get("max_total_token") == 100 + + handoff_config = handoff_term.dump_component() + assert handoff_config.provider == "autogen_agentchat.conditions.HandoffTermination" + assert handoff_config.config.get("target") == "human" + + timeout_config = timeout_term.dump_component() + assert timeout_config.provider == "autogen_agentchat.conditions.TimeoutTermination" + assert timeout_config.config.get("timeout_seconds") == 30 + + external_config = external_term.dump_component() + assert external_config.provider == "autogen_agentchat.conditions.ExternalTermination" + + source_config = source_term.dump_component() + assert source_config.provider == "autogen_agentchat.conditions.SourceMatchTermination" + assert source_config.config.get("sources") == ["human"] + # Test basic deserialization loaded_max = ComponentLoader.load_component(max_config, MaxMessageTermination) assert isinstance(loaded_max, MaxMessageTermination) - # Use public interface to verify state - messages = [StopMessage(content="msg", source="test") for _ in range(5)] - result = await loaded_max(messages) - assert isinstance(result, StopMessage) - - # Test composition and complex serialization - or_term = max_term | stop_term - or_config = or_term.dump_component() - assert or_config.provider == "autogen_agentchat.base.OrTerminationCondition" - assert len(or_config.config["conditions"]) == 2 - - # Verify nested conditions are correctly serialized - conditions = or_config.config["conditions"] - assert conditions[0]["provider"] == "autogen_agentchat.conditions.MaxMessageTermination" - assert conditions[1]["provider"] == "autogen_agentchat.conditions.StopMessageTermination" - - # Test behavior of loaded composite condition - loaded_or = OrTerminationCondition.load_component(or_config) - - # Test with stop message - stop_messages = [StopMessage(content="stop", source="test")] - result = await loaded_or(stop_messages) - assert isinstance(result, StopMessage) - assert loaded_or.terminated - - # Test reset functionality - await loaded_or.reset() - assert not loaded_or.terminated + + # Test deserialization of new conditions + loaded_text = ComponentLoader.load_component(text_config, TextMentionTermination) + assert isinstance(loaded_text, TextMentionTermination) + + loaded_token = ComponentLoader.load_component(token_config, TokenUsageTermination) + assert isinstance(loaded_token, TokenUsageTermination) + + loaded_handoff = ComponentLoader.load_component(handoff_config, HandoffTermination) + assert isinstance(loaded_handoff, HandoffTermination) + + loaded_timeout = ComponentLoader.load_component(timeout_config, TimeoutTermination) + assert isinstance(loaded_timeout, TimeoutTermination) + + loaded_external = ComponentLoader.load_component(external_config, ExternalTermination) + assert isinstance(loaded_external, ExternalTermination) + + loaded_source = ComponentLoader.load_component(source_config, SourceMatchTermination) + assert isinstance(loaded_source, SourceMatchTermination) + + # Test composition with new conditions + composite_term = (max_term | stop_term) & (token_term | handoff_term) + composite_config = composite_term.dump_component() + + assert composite_config.provider == "autogen_agentchat.base.AndTerminationCondition" + conditions = composite_config.config["conditions"] + assert len(conditions) == 2 + assert conditions[0]["provider"] == "autogen_agentchat.base.OrTerminationCondition" + assert conditions[1]["provider"] == "autogen_agentchat.base.OrTerminationCondition" + + # Test loading complex composition + loaded_composite = ComponentLoader.load_component(composite_config) + assert isinstance(loaded_composite, AndTerminationCondition) From 9cc873d35186f8428a6eeacadfde09ed1bad0c7e Mon Sep 17 00:00:00 2001 From: Victor Dibia Date: Mon, 13 Jan 2025 15:42:17 -0800 Subject: [PATCH 8/8] update declarative config with updated api. --- .../src/autogen_agentchat/base/_termination.py | 6 +++--- .../agentchat-user-guide/tutorial/declarative.ipynb | 12 ++---------- 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py b/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py index f154db9f37d1..dcefa5a04111 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/base/_termination.py @@ -1,8 +1,8 @@ import asyncio from abc import ABC, abstractmethod -from typing import Any, List, Sequence +from typing import List, Sequence -from autogen_core import Component, ComponentModel +from autogen_core import Component, ComponentBase, ComponentModel from pydantic import BaseModel from typing_extensions import Self @@ -12,7 +12,7 @@ class TerminatedException(BaseException): ... -class TerminationCondition(ABC, Component[Any]): +class TerminationCondition(ABC, ComponentBase[BaseModel]): """A stateful condition that determines when a conversation should be terminated. A termination condition is a callable that takes a sequence of ChatMessage objects diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/declarative.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/declarative.ipynb index 7682f624c590..274135c1155c 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/declarative.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/declarative.ipynb @@ -6,7 +6,7 @@ "source": [ "# Declarative Components \n", "\n", - "AutoGen provides a declarative component configuration class that defines behaviours for being able to export and import components in a declarative format. This is useful for debugging, visualizing, and even for sharing your work with others. In this notebook, we will demonstrate how to export a declarative representation of a multiagent team in the form of a JSON file. \n", + "AutoGen provides a declarative {py:class}`~autogen_core.Component` configuration class that defines behaviours for declarative import/export. This is useful for debugging, visualizing, and even for sharing your work with others. In this notebook, we will demonstrate how to export a declarative representation of a multiagent team in the form of a JSON file. \n", "\n", "\n", "```{note}\n", @@ -31,15 +31,7 @@ "cell_type": "code", "execution_count": 1, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - ":106: UserWarning: Class variable 'component_config_schema' must be defined in TerminationCondition to be a valid component\n" - ] - } - ], + "outputs": [], "source": [ "from autogen_agentchat.conditions import MaxMessageTermination, StopMessageTermination\n", "\n",