From ac53961bc8b6545824dbe900d497e72b4079e8d1 Mon Sep 17 00:00:00 2001 From: Leonardo Pinheiro Date: Sat, 23 Nov 2024 02:29:39 +1000 Subject: [PATCH 1/4] Delete autogen-ext refactor deprecations (#4305) * delete files and update dependencies * add explicit config exports * ignore mypy error on nb --------- Co-authored-by: Leonardo Pinheiro Co-authored-by: Jack Gerrits --- .../tutorial/termination.ipynb | 2 +- .../cookbook/local-llms-ollama-litellm.ipynb | 8 +- .../design-patterns/mixture-of-agents.ipynb | 3 +- .../design-patterns/multi-agent-debate.ipynb | 4 +- .../core-user-guide/framework/tools.ipynb | 2 +- .../autogen-core/samples/common/utils.py | 3 +- .../samples/distributed-group-chat/_types.py | 2 +- .../samples/distributed-group-chat/_utils.py | 2 +- .../components/models/__init__.py | 30 - .../components/models/_openai_client.py | 901 ------------------ .../components/models/config/__init__.py | 52 - .../autogen-core/tests/test_tool_agent.py | 127 ++- .../packages/autogen-core/tests/test_tools.py | 24 - .../aca_dynamic_sessions/__init__.py | 21 - .../code_executor/docker_executor/__init__.py | 11 - .../src/autogen_ext/models/__init__.py | 9 +- .../models/_openai/config/__init__.py | 3 + .../autogen_ext/tools/langchain/__init__.py | 7 - .../tests/models/test_openai_model_client.py | 51 +- 19 files changed, 131 insertions(+), 1131 deletions(-) delete mode 100644 python/packages/autogen-core/src/autogen_core/components/models/_openai_client.py delete mode 100644 python/packages/autogen-core/src/autogen_core/components/models/config/__init__.py delete mode 100644 python/packages/autogen-ext/src/autogen_ext/code_executor/aca_dynamic_sessions/__init__.py delete mode 100644 python/packages/autogen-ext/src/autogen_ext/code_executor/docker_executor/__init__.py delete mode 100644 python/packages/autogen-ext/src/autogen_ext/tools/langchain/__init__.py diff --git a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/termination.ipynb b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/termination.ipynb index e1094249128..4a1cfe42cf6 100644 --- a/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/termination.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/agentchat-user-guide/tutorial/termination.ipynb @@ -39,7 +39,7 @@ "from autogen_agentchat.logging import ConsoleLogHandler\n", "from autogen_agentchat.task import MaxMessageTermination, TextMentionTermination\n", "from autogen_agentchat.teams import RoundRobinGroupChat\n", - "from autogen_core.components.models import OpenAIChatCompletionClient\n", + "from autogen_ext.models import OpenAIChatCompletionClient\n", "\n", "logger = logging.getLogger(EVENT_LOGGER_NAME)\n", "logger.addHandler(ConsoleLogHandler())\n", diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/local-llms-ollama-litellm.ipynb b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/local-llms-ollama-litellm.ipynb index a90cb440d6c..80fde2b7101 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/local-llms-ollama-litellm.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/cookbook/local-llms-ollama-litellm.ipynb @@ -46,10 +46,10 @@ "from autogen_core.components.models import (\n", " AssistantMessage,\n", " ChatCompletionClient,\n", - " OpenAIChatCompletionClient,\n", " SystemMessage,\n", " UserMessage,\n", - ")" + ")\n", + "from autogen_ext.models import OpenAIChatCompletionClient" ] }, { @@ -65,7 +65,7 @@ "metadata": {}, "outputs": [], "source": [ - "def get_model_client() -> OpenAIChatCompletionClient:\n", + "def get_model_client() -> OpenAIChatCompletionClient: # type: ignore\n", " \"Mimic OpenAI API using Local LLM Server.\"\n", " return OpenAIChatCompletionClient(\n", " model=\"gpt-4o\", # Need to use one of the OpenAI models as a placeholder for now.\n", @@ -233,7 +233,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.9" + "version": "3.12.7" } }, "nbformat": 4, diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/mixture-of-agents.ipynb b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/mixture-of-agents.ipynb index dff7d18bd42..61b8b62bc22 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/mixture-of-agents.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/mixture-of-agents.ipynb @@ -41,7 +41,8 @@ "from autogen_core.application import SingleThreadedAgentRuntime\n", "from autogen_core.base import AgentId, MessageContext\n", "from autogen_core.components import RoutedAgent, message_handler\n", - "from autogen_core.components.models import ChatCompletionClient, OpenAIChatCompletionClient, SystemMessage, UserMessage" + "from autogen_core.components.models import ChatCompletionClient, SystemMessage, UserMessage\n", + "from autogen_ext.models import OpenAIChatCompletionClient" ] }, { diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/multi-agent-debate.ipynb b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/multi-agent-debate.ipynb index 3120363dd23..72b65368791 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/multi-agent-debate.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/design-patterns/multi-agent-debate.ipynb @@ -50,10 +50,10 @@ " AssistantMessage,\n", " ChatCompletionClient,\n", " LLMMessage,\n", - " OpenAIChatCompletionClient,\n", " SystemMessage,\n", " UserMessage,\n", - ")" + ")\n", + "from autogen_ext.models import OpenAIChatCompletionClient" ] }, { diff --git a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/tools.ipynb b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/tools.ipynb index 183d878e4c8..ff24095e8b5 100644 --- a/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/tools.ipynb +++ b/python/packages/autogen-core/docs/src/user-guide/core-user-guide/framework/tools.ipynb @@ -161,12 +161,12 @@ "from autogen_core.components.models import (\n", " ChatCompletionClient,\n", " LLMMessage,\n", - " OpenAIChatCompletionClient,\n", " SystemMessage,\n", " UserMessage,\n", ")\n", "from autogen_core.components.tool_agent import ToolAgent, tool_agent_caller_loop\n", "from autogen_core.components.tools import FunctionTool, Tool, ToolSchema\n", + "from autogen_ext.models import OpenAIChatCompletionClient\n", "\n", "\n", "@dataclass\n", diff --git a/python/packages/autogen-core/samples/common/utils.py b/python/packages/autogen-core/samples/common/utils.py index 4e77ac33232..0765ceec561 100644 --- a/python/packages/autogen-core/samples/common/utils.py +++ b/python/packages/autogen-core/samples/common/utils.py @@ -3,14 +3,13 @@ from autogen_core.components.models import ( AssistantMessage, - AzureOpenAIChatCompletionClient, ChatCompletionClient, FunctionExecutionResult, FunctionExecutionResultMessage, LLMMessage, - OpenAIChatCompletionClient, UserMessage, ) +from autogen_ext.models import AzureOpenAIChatCompletionClient, OpenAIChatCompletionClient from azure.identity import DefaultAzureCredential, get_bearer_token_provider from typing_extensions import Literal diff --git a/python/packages/autogen-core/samples/distributed-group-chat/_types.py b/python/packages/autogen-core/samples/distributed-group-chat/_types.py index 178446ca8c6..0e05d941c1f 100644 --- a/python/packages/autogen-core/samples/distributed-group-chat/_types.py +++ b/python/packages/autogen-core/samples/distributed-group-chat/_types.py @@ -4,7 +4,7 @@ from autogen_core.components.models import ( LLMMessage, ) -from autogen_core.components.models.config import AzureOpenAIClientConfiguration +from autogen_ext.models import AzureOpenAIClientConfiguration from pydantic import BaseModel diff --git a/python/packages/autogen-core/samples/distributed-group-chat/_utils.py b/python/packages/autogen-core/samples/distributed-group-chat/_utils.py index 2c4b768e49d..431a94319fc 100644 --- a/python/packages/autogen-core/samples/distributed-group-chat/_utils.py +++ b/python/packages/autogen-core/samples/distributed-group-chat/_utils.py @@ -5,7 +5,7 @@ import yaml from _types import AppConfig from autogen_core.base import MessageSerializer, try_get_known_serializers_for_type -from autogen_core.components.models.config import AzureOpenAIClientConfiguration +from autogen_ext.models import AzureOpenAIClientConfiguration from azure.identity import DefaultAzureCredential, get_bearer_token_provider diff --git a/python/packages/autogen-core/src/autogen_core/components/models/__init__.py b/python/packages/autogen-core/src/autogen_core/components/models/__init__.py index f57c82289dd..9b12aa702ed 100644 --- a/python/packages/autogen-core/src/autogen_core/components/models/__init__.py +++ b/python/packages/autogen-core/src/autogen_core/components/models/__init__.py @@ -1,7 +1,3 @@ -import importlib -import warnings -from typing import TYPE_CHECKING, Any - from ._model_client import ChatCompletionClient, ModelCapabilities from ._types import ( AssistantMessage, @@ -17,13 +13,7 @@ UserMessage, ) -if TYPE_CHECKING: - from ._openai_client import AzureOpenAIChatCompletionClient, OpenAIChatCompletionClient - - __all__ = [ - "AzureOpenAIChatCompletionClient", - "OpenAIChatCompletionClient", "ModelCapabilities", "ChatCompletionClient", "SystemMessage", @@ -38,23 +28,3 @@ "TopLogprob", "ChatCompletionTokenLogprob", ] - - -def __getattr__(name: str) -> Any: - deprecated_classes = { - "AzureOpenAIChatCompletionClient": "autogen_ext.models.AzureOpenAIChatCompletionClient", - "OpenAIChatCompletionClient": "autogen_ext.modelsChatCompletionClient", - } - if name in deprecated_classes: - warnings.warn( - f"{name} moved to autogen_ext. " f"Please import it from {deprecated_classes[name]}.", - FutureWarning, - stacklevel=2, - ) - # Dynamically import the class from the current module - module = importlib.import_module("._openai_client", __name__) - attr = getattr(module, name) - # Cache the attribute in the module's global namespace - globals()[name] = attr - return attr - raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/python/packages/autogen-core/src/autogen_core/components/models/_openai_client.py b/python/packages/autogen-core/src/autogen_core/components/models/_openai_client.py deleted file mode 100644 index 8ce8ddff2cb..00000000000 --- a/python/packages/autogen-core/src/autogen_core/components/models/_openai_client.py +++ /dev/null @@ -1,901 +0,0 @@ -import asyncio -import inspect -import json -import logging -import math -import re -import warnings -from asyncio import Task -from typing import ( - Any, - AsyncGenerator, - Dict, - List, - Mapping, - Optional, - Sequence, - Set, - Type, - Union, - cast, -) - -import tiktoken -from openai import AsyncAzureOpenAI, AsyncOpenAI -from openai.types.chat import ( - ChatCompletion, - ChatCompletionAssistantMessageParam, - ChatCompletionContentPartParam, - ChatCompletionContentPartTextParam, - ChatCompletionMessageParam, - ChatCompletionMessageToolCallParam, - ChatCompletionRole, - ChatCompletionSystemMessageParam, - ChatCompletionToolMessageParam, - ChatCompletionToolParam, - ChatCompletionUserMessageParam, - ParsedChatCompletion, - ParsedChoice, - completion_create_params, -) -from openai.types.chat.chat_completion import Choice -from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice -from openai.types.shared_params import FunctionDefinition, FunctionParameters -from pydantic import BaseModel -from typing_extensions import Unpack - -from ...application.logging import EVENT_LOGGER_NAME, TRACE_LOGGER_NAME -from ...application.logging.events import LLMCallEvent -from ...base import CancellationToken -from .. import ( - FunctionCall, - Image, -) -from ..tools import Tool, ToolSchema -from . import _model_info -from ._model_client import ChatCompletionClient, ModelCapabilities -from ._types import ( - AssistantMessage, - ChatCompletionTokenLogprob, - CreateResult, - FunctionExecutionResultMessage, - LLMMessage, - RequestUsage, - SystemMessage, - TopLogprob, - UserMessage, -) -from .config import AzureOpenAIClientConfiguration, OpenAIClientConfiguration - -logger = logging.getLogger(EVENT_LOGGER_NAME) -trace_logger = logging.getLogger(TRACE_LOGGER_NAME) - -openai_init_kwargs = set(inspect.getfullargspec(AsyncOpenAI.__init__).kwonlyargs) -aopenai_init_kwargs = set(inspect.getfullargspec(AsyncAzureOpenAI.__init__).kwonlyargs) - -create_kwargs = set(completion_create_params.CompletionCreateParamsBase.__annotations__.keys()) | set( - ("timeout", "stream") -) -# Only single choice allowed -disallowed_create_args = set(["stream", "messages", "function_call", "functions", "n"]) -required_create_args: Set[str] = set(["model"]) - - -def _azure_openai_client_from_config(config: Mapping[str, Any]) -> AsyncAzureOpenAI: - # Take a copy - copied_config = dict(config).copy() - - # Do some fixups - copied_config["azure_deployment"] = copied_config.get("azure_deployment", config.get("model")) - if copied_config["azure_deployment"] is not None: - copied_config["azure_deployment"] = copied_config["azure_deployment"].replace(".", "") - copied_config["azure_endpoint"] = copied_config.get("azure_endpoint", copied_config.pop("base_url", None)) - - # Shave down the config to just the AzureOpenAIChatCompletionClient kwargs - azure_config = {k: v for k, v in copied_config.items() if k in aopenai_init_kwargs} - return AsyncAzureOpenAI(**azure_config) - - -def _openai_client_from_config(config: Mapping[str, Any]) -> AsyncOpenAI: - # Shave down the config to just the OpenAI kwargs - openai_config = {k: v for k, v in config.items() if k in openai_init_kwargs} - return AsyncOpenAI(**openai_config) - - -def _create_args_from_config(config: Mapping[str, Any]) -> Dict[str, Any]: - create_args = {k: v for k, v in config.items() if k in create_kwargs} - create_args_keys = set(create_args.keys()) - if not required_create_args.issubset(create_args_keys): - raise ValueError(f"Required create args are missing: {required_create_args - create_args_keys}") - if disallowed_create_args.intersection(create_args_keys): - raise ValueError(f"Disallowed create args are present: {disallowed_create_args.intersection(create_args_keys)}") - return create_args - - -# TODO check types -# oai_system_message_schema = type2schema(ChatCompletionSystemMessageParam) -# oai_user_message_schema = type2schema(ChatCompletionUserMessageParam) -# oai_assistant_message_schema = type2schema(ChatCompletionAssistantMessageParam) -# oai_tool_message_schema = type2schema(ChatCompletionToolMessageParam) - - -def type_to_role(message: LLMMessage) -> ChatCompletionRole: - if isinstance(message, SystemMessage): - return "system" - elif isinstance(message, UserMessage): - return "user" - elif isinstance(message, AssistantMessage): - return "assistant" - else: - return "tool" - - -def user_message_to_oai(message: UserMessage) -> ChatCompletionUserMessageParam: - assert_valid_name(message.source) - if isinstance(message.content, str): - return ChatCompletionUserMessageParam( - content=message.content, - role="user", - name=message.source, - ) - else: - parts: List[ChatCompletionContentPartParam] = [] - for part in message.content: - if isinstance(part, str): - oai_part = ChatCompletionContentPartTextParam( - text=part, - type="text", - ) - parts.append(oai_part) - elif isinstance(part, Image): - # TODO: support url based images - # TODO: support specifying details - parts.append(part.to_openai_format()) - else: - raise ValueError(f"Unknown content type: {part}") - return ChatCompletionUserMessageParam( - content=parts, - role="user", - name=message.source, - ) - - -def system_message_to_oai(message: SystemMessage) -> ChatCompletionSystemMessageParam: - return ChatCompletionSystemMessageParam( - content=message.content, - role="system", - ) - - -def func_call_to_oai(message: FunctionCall) -> ChatCompletionMessageToolCallParam: - return ChatCompletionMessageToolCallParam( - id=message.id, - function={ - "arguments": message.arguments, - "name": message.name, - }, - type="function", - ) - - -def tool_message_to_oai( - message: FunctionExecutionResultMessage, -) -> Sequence[ChatCompletionToolMessageParam]: - return [ - ChatCompletionToolMessageParam(content=x.content, role="tool", tool_call_id=x.call_id) for x in message.content - ] - - -def assistant_message_to_oai( - message: AssistantMessage, -) -> ChatCompletionAssistantMessageParam: - assert_valid_name(message.source) - if isinstance(message.content, list): - return ChatCompletionAssistantMessageParam( - tool_calls=[func_call_to_oai(x) for x in message.content], - role="assistant", - name=message.source, - ) - else: - return ChatCompletionAssistantMessageParam( - content=message.content, - role="assistant", - name=message.source, - ) - - -def to_oai_type(message: LLMMessage) -> Sequence[ChatCompletionMessageParam]: - if isinstance(message, SystemMessage): - return [system_message_to_oai(message)] - elif isinstance(message, UserMessage): - return [user_message_to_oai(message)] - elif isinstance(message, AssistantMessage): - return [assistant_message_to_oai(message)] - else: - return tool_message_to_oai(message) - - -def calculate_vision_tokens(image: Image, detail: str = "auto") -> int: - MAX_LONG_EDGE = 2048 - BASE_TOKEN_COUNT = 85 - TOKENS_PER_TILE = 170 - MAX_SHORT_EDGE = 768 - TILE_SIZE = 512 - - if detail == "low": - return BASE_TOKEN_COUNT - - width, height = image.image.size - - # Scale down to fit within a MAX_LONG_EDGE x MAX_LONG_EDGE square if necessary - - if width > MAX_LONG_EDGE or height > MAX_LONG_EDGE: - aspect_ratio = width / height - if aspect_ratio > 1: - # Width is greater than height - width = MAX_LONG_EDGE - height = int(MAX_LONG_EDGE / aspect_ratio) - else: - # Height is greater than or equal to width - height = MAX_LONG_EDGE - width = int(MAX_LONG_EDGE * aspect_ratio) - - # Resize such that the shortest side is MAX_SHORT_EDGE if both dimensions exceed MAX_SHORT_EDGE - aspect_ratio = width / height - if width > MAX_SHORT_EDGE and height > MAX_SHORT_EDGE: - if aspect_ratio > 1: - # Width is greater than height - height = MAX_SHORT_EDGE - width = int(MAX_SHORT_EDGE * aspect_ratio) - else: - # Height is greater than or equal to width - width = MAX_SHORT_EDGE - height = int(MAX_SHORT_EDGE / aspect_ratio) - - # Calculate the number of tiles based on TILE_SIZE - - tiles_width = math.ceil(width / TILE_SIZE) - tiles_height = math.ceil(height / TILE_SIZE) - total_tiles = tiles_width * tiles_height - # Calculate the total tokens based on the number of tiles and the base token count - - total_tokens = BASE_TOKEN_COUNT + TOKENS_PER_TILE * total_tiles - - return total_tokens - - -def _add_usage(usage1: RequestUsage, usage2: RequestUsage) -> RequestUsage: - return RequestUsage( - prompt_tokens=usage1.prompt_tokens + usage2.prompt_tokens, - completion_tokens=usage1.completion_tokens + usage2.completion_tokens, - ) - - -def convert_tools( - tools: Sequence[Tool | ToolSchema], -) -> List[ChatCompletionToolParam]: - result: List[ChatCompletionToolParam] = [] - for tool in tools: - if isinstance(tool, Tool): - tool_schema = tool.schema - else: - assert isinstance(tool, dict) - tool_schema = tool - - result.append( - ChatCompletionToolParam( - type="function", - function=FunctionDefinition( - name=tool_schema["name"], - description=(tool_schema["description"] if "description" in tool_schema else ""), - parameters=( - cast(FunctionParameters, tool_schema["parameters"]) if "parameters" in tool_schema else {} - ), - ), - ) - ) - # Check if all tools have valid names. - for tool_param in result: - assert_valid_name(tool_param["function"]["name"]) - return result - - -def normalize_name(name: str) -> str: - """ - LLMs sometimes ask functions while ignoring their own format requirements, this function should be used to replace invalid characters with "_". - - Prefer _assert_valid_name for validating user configuration or input - """ - return re.sub(r"[^a-zA-Z0-9_-]", "_", name)[:64] - - -def assert_valid_name(name: str) -> str: - """ - Ensure that configured names are valid, raises ValueError if not. - - For munging LLM responses use _normalize_name to ensure LLM specified names don't break the API. - """ - if not re.match(r"^[a-zA-Z0-9_-]+$", name): - raise ValueError(f"Invalid name: {name}. Only letters, numbers, '_' and '-' are allowed.") - if len(name) > 64: - raise ValueError(f"Invalid name: {name}. Name must be less than 64 characters.") - return name - - -class BaseOpenAIChatCompletionClient(ChatCompletionClient): - def __init__( - self, - client: Union[AsyncOpenAI, AsyncAzureOpenAI], - create_args: Dict[str, Any], - model_capabilities: Optional[ModelCapabilities] = None, - ): - self._client = client - if model_capabilities is None and isinstance(client, AsyncAzureOpenAI): - raise ValueError("AzureOpenAIChatCompletionClient requires explicit model capabilities") - elif model_capabilities is None: - self._model_capabilities = _model_info.get_capabilities(create_args["model"]) - else: - self._model_capabilities = model_capabilities - - self._resolved_model: Optional[str] = None - if "model" in create_args: - self._resolved_model = _model_info.resolve_model(create_args["model"]) - - if ( - "response_format" in create_args - and create_args["response_format"]["type"] == "json_object" - and not self._model_capabilities["json_output"] - ): - raise ValueError("Model does not support JSON output") - - self._create_args = create_args - self._total_usage = RequestUsage(prompt_tokens=0, completion_tokens=0) - self._actual_usage = RequestUsage(prompt_tokens=0, completion_tokens=0) - - @classmethod - def create_from_config(cls, config: Dict[str, Any]) -> ChatCompletionClient: - return OpenAIChatCompletionClient(**config) - - async def create( - self, - messages: Sequence[LLMMessage], - tools: Sequence[Tool | ToolSchema] = [], - json_output: Optional[bool] = None, - extra_create_args: Mapping[str, Any] = {}, - cancellation_token: Optional[CancellationToken] = None, - ) -> CreateResult: - # Make sure all extra_create_args are valid - extra_create_args_keys = set(extra_create_args.keys()) - if not create_kwargs.issuperset(extra_create_args_keys): - raise ValueError(f"Extra create args are invalid: {extra_create_args_keys - create_kwargs}") - - # Copy the create args and overwrite anything in extra_create_args - create_args = self._create_args.copy() - create_args.update(extra_create_args) - - # Declare use_beta_client - use_beta_client: bool = False - response_format_value: Optional[Type[BaseModel]] = None - - if "response_format" in create_args: - value = create_args["response_format"] - # If value is a Pydantic model class, use the beta client - if isinstance(value, type) and issubclass(value, BaseModel): - response_format_value = value - use_beta_client = True - else: - # response_format_value is not a Pydantic model class - use_beta_client = False - response_format_value = None - - # Remove 'response_format' from create_args to prevent passing it twice - create_args_no_response_format = {k: v for k, v in create_args.items() if k != "response_format"} - - # TODO: allow custom handling. - # For now we raise an error if images are present and vision is not supported - if self.capabilities["vision"] is False: - for message in messages: - if isinstance(message, UserMessage): - if isinstance(message.content, list) and any(isinstance(x, Image) for x in message.content): - raise ValueError("Model does not support vision and image was provided") - - if json_output is not None: - if self.capabilities["json_output"] is False and json_output is True: - raise ValueError("Model does not support JSON output") - - if json_output is True: - create_args["response_format"] = {"type": "json_object"} - else: - create_args["response_format"] = {"type": "text"} - - if self.capabilities["json_output"] is False and json_output is True: - raise ValueError("Model does not support JSON output") - - oai_messages_nested = [to_oai_type(m) for m in messages] - oai_messages = [item for sublist in oai_messages_nested for item in sublist] - - if self.capabilities["function_calling"] is False and len(tools) > 0: - raise ValueError("Model does not support function calling") - future: Union[Task[ParsedChatCompletion[BaseModel]], Task[ChatCompletion]] - if len(tools) > 0: - converted_tools = convert_tools(tools) - if use_beta_client: - # Pass response_format_value if it's not None - if response_format_value is not None: - future = asyncio.ensure_future( - self._client.beta.chat.completions.parse( - messages=oai_messages, - tools=converted_tools, - response_format=response_format_value, - **create_args_no_response_format, - ) - ) - else: - future = asyncio.ensure_future( - self._client.beta.chat.completions.parse( - messages=oai_messages, - tools=converted_tools, - **create_args_no_response_format, - ) - ) - else: - future = asyncio.ensure_future( - self._client.chat.completions.create( - messages=oai_messages, - stream=False, - tools=converted_tools, - **create_args, - ) - ) - else: - if use_beta_client: - if response_format_value is not None: - future = asyncio.ensure_future( - self._client.beta.chat.completions.parse( - messages=oai_messages, - response_format=response_format_value, - **create_args_no_response_format, - ) - ) - else: - future = asyncio.ensure_future( - self._client.beta.chat.completions.parse( - messages=oai_messages, - **create_args_no_response_format, - ) - ) - else: - future = asyncio.ensure_future( - self._client.chat.completions.create( - messages=oai_messages, - stream=False, - **create_args, - ) - ) - - if cancellation_token is not None: - cancellation_token.link_future(future) - result: Union[ParsedChatCompletion[BaseModel], ChatCompletion] = await future - if use_beta_client: - result = cast(ParsedChatCompletion[Any], result) - - if result.usage is not None: - logger.info( - LLMCallEvent( - prompt_tokens=result.usage.prompt_tokens, - completion_tokens=result.usage.completion_tokens, - ) - ) - - usage = RequestUsage( - # TODO backup token counting - prompt_tokens=result.usage.prompt_tokens if result.usage is not None else 0, - completion_tokens=(result.usage.completion_tokens if result.usage is not None else 0), - ) - - if self._resolved_model is not None: - if self._resolved_model != result.model: - warnings.warn( - f"Resolved model mismatch: {self._resolved_model} != {result.model}. Model mapping may be incorrect.", - stacklevel=2, - ) - - # Limited to a single choice currently. - choice: Union[ParsedChoice[Any], ParsedChoice[BaseModel], Choice] = result.choices[0] - if choice.finish_reason == "function_call": - raise ValueError("Function calls are not supported in this context") - - content: Union[str, List[FunctionCall]] - if choice.finish_reason == "tool_calls": - assert choice.message.tool_calls is not None - assert choice.message.function_call is None - - # NOTE: If OAI response type changes, this will need to be updated - content = [ - FunctionCall( - id=x.id, - arguments=x.function.arguments, - name=normalize_name(x.function.name), - ) - for x in choice.message.tool_calls - ] - finish_reason = "function_calls" - else: - finish_reason = choice.finish_reason - content = choice.message.content or "" - logprobs: Optional[List[ChatCompletionTokenLogprob]] = None - if choice.logprobs and choice.logprobs.content: - logprobs = [ - ChatCompletionTokenLogprob( - token=x.token, - logprob=x.logprob, - top_logprobs=[TopLogprob(logprob=y.logprob, bytes=y.bytes) for y in x.top_logprobs], - bytes=x.bytes, - ) - for x in choice.logprobs.content - ] - response = CreateResult( - finish_reason=finish_reason, # type: ignore - content=content, - usage=usage, - cached=False, - logprobs=logprobs, - ) - - _add_usage(self._actual_usage, usage) - _add_usage(self._total_usage, usage) - - # TODO - why is this cast needed? - return response - - async def create_stream( - self, - messages: Sequence[LLMMessage], - tools: Sequence[Tool | ToolSchema] = [], - json_output: Optional[bool] = None, - extra_create_args: Mapping[str, Any] = {}, - cancellation_token: Optional[CancellationToken] = None, - ) -> AsyncGenerator[Union[str, CreateResult], None]: - """ - Creates an AsyncGenerator that will yield a stream of chat completions based on the provided messages and tools. - - Args: - messages (Sequence[LLMMessage]): A sequence of messages to be processed. - tools (Sequence[Tool | ToolSchema], optional): A sequence of tools to be used in the completion. Defaults to `[]`. - json_output (Optional[bool], optional): If True, the output will be in JSON format. Defaults to None. - extra_create_args (Mapping[str, Any], optional): Additional arguments for the creation process. Default to `{}`. - cancellation_token (Optional[CancellationToken], optional): A token to cancel the operation. Defaults to None. - - Yields: - AsyncGenerator[Union[str, CreateResult], None]: A generator yielding the completion results as they are produced. - - In streaming, the default behaviour is not return token usage counts. See: [OpenAI API reference for possible args](https://platform.openai.com/docs/api-reference/chat/create). - However `extra_create_args={"stream_options": {"include_usage": True}}` will (if supported by the accessed API) - return a final chunk with usage set to a RequestUsage object having prompt and completion token counts, - all preceding chunks will have usage as None. See: [stream_options](https://platform.openai.com/docs/api-reference/chat/create#chat-create-stream_options). - - Other examples of OPENAI supported arguments that can be included in `extra_create_args`: - - `temperature` (float): Controls the randomness of the output. Higher values (e.g., 0.8) make the output more random, while lower values (e.g., 0.2) make it more focused and deterministic. - - `max_tokens` (int): The maximum number of tokens to generate in the completion. - - `top_p` (float): An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. - - `frequency_penalty` (float): A value between -2.0 and 2.0 that penalizes new tokens based on their existing frequency in the text so far, decreasing the likelihood of repeated phrases. - - `presence_penalty` (float): A value between -2.0 and 2.0 that penalizes new tokens based on whether they appear in the text so far, encouraging the model to talk about new topics. - """ - # Make sure all extra_create_args are valid - extra_create_args_keys = set(extra_create_args.keys()) - if not create_kwargs.issuperset(extra_create_args_keys): - raise ValueError(f"Extra create args are invalid: {extra_create_args_keys - create_kwargs}") - - # Copy the create args and overwrite anything in extra_create_args - create_args = self._create_args.copy() - create_args.update(extra_create_args) - - oai_messages_nested = [to_oai_type(m) for m in messages] - oai_messages = [item for sublist in oai_messages_nested for item in sublist] - - # TODO: allow custom handling. - # For now we raise an error if images are present and vision is not supported - if self.capabilities["vision"] is False: - for message in messages: - if isinstance(message, UserMessage): - if isinstance(message.content, list) and any(isinstance(x, Image) for x in message.content): - raise ValueError("Model does not support vision and image was provided") - - if json_output is not None: - if self.capabilities["json_output"] is False and json_output is True: - raise ValueError("Model does not support JSON output") - - if json_output is True: - create_args["response_format"] = {"type": "json_object"} - else: - create_args["response_format"] = {"type": "text"} - - if len(tools) > 0: - converted_tools = convert_tools(tools) - stream_future = asyncio.ensure_future( - self._client.chat.completions.create( - messages=oai_messages, - stream=True, - tools=converted_tools, - **create_args, - ) - ) - else: - stream_future = asyncio.ensure_future( - self._client.chat.completions.create(messages=oai_messages, stream=True, **create_args) - ) - if cancellation_token is not None: - cancellation_token.link_future(stream_future) - stream = await stream_future - choice: Union[ParsedChoice[Any], ParsedChoice[BaseModel], ChunkChoice] = cast(ChunkChoice, None) - chunk = None - stop_reason = None - maybe_model = None - content_deltas: List[str] = [] - full_tool_calls: Dict[int, FunctionCall] = {} - completion_tokens = 0 - logprobs: Optional[List[ChatCompletionTokenLogprob]] = None - while True: - try: - chunk_future = asyncio.ensure_future(anext(stream)) - if cancellation_token is not None: - cancellation_token.link_future(chunk_future) - chunk = await chunk_future - - # to process usage chunk in streaming situations - # add stream_options={"include_usage": True} in the initialization of OpenAIChatCompletionClient(...) - # However the different api's - # OPENAI api usage chunk produces no choices so need to check if there is a choice - # liteLLM api usage chunk does produce choices - choice = ( - chunk.choices[0] - if len(chunk.choices) > 0 - else choice - if chunk.usage is not None and stop_reason is not None - else cast(ChunkChoice, None) - ) - - # for liteLLM chunk usage, do the following hack keeping the pervious chunk.stop_reason (if set). - # set the stop_reason for the usage chunk to the prior stop_reason - stop_reason = choice.finish_reason if chunk.usage is None and stop_reason is None else stop_reason - maybe_model = chunk.model - # First try get content - if choice.delta.content is not None: - content_deltas.append(choice.delta.content) - if len(choice.delta.content) > 0: - yield choice.delta.content - continue - - # Otherwise, get tool calls - if choice.delta.tool_calls is not None: - for tool_call_chunk in choice.delta.tool_calls: - idx = tool_call_chunk.index - if idx not in full_tool_calls: - # We ignore the type hint here because we want to fill in type when the delta provides it - full_tool_calls[idx] = FunctionCall(id="", arguments="", name="") - - if tool_call_chunk.id is not None: - full_tool_calls[idx].id += tool_call_chunk.id - - if tool_call_chunk.function is not None: - if tool_call_chunk.function.name is not None: - full_tool_calls[idx].name += tool_call_chunk.function.name - if tool_call_chunk.function.arguments is not None: - full_tool_calls[idx].arguments += tool_call_chunk.function.arguments - if choice.logprobs and choice.logprobs.content: - logprobs = [ - ChatCompletionTokenLogprob( - token=x.token, - logprob=x.logprob, - top_logprobs=[TopLogprob(logprob=y.logprob, bytes=y.bytes) for y in x.top_logprobs], - bytes=x.bytes, - ) - for x in choice.logprobs.content - ] - - except StopAsyncIteration: - break - - model = maybe_model or create_args["model"] - model = model.replace("gpt-35", "gpt-3.5") # hack for Azure API - - if chunk and chunk.usage: - prompt_tokens = chunk.usage.prompt_tokens - else: - prompt_tokens = 0 - - if stop_reason is None: - raise ValueError("No stop reason found") - - content: Union[str, List[FunctionCall]] - if len(content_deltas) > 1: - content = "".join(content_deltas) - if chunk and chunk.usage: - completion_tokens = chunk.usage.completion_tokens - else: - completion_tokens = 0 - else: - completion_tokens = 0 - # TODO: fix assumption that dict values were added in order and actually order by int index - # for tool_call in full_tool_calls.values(): - # # value = json.dumps(tool_call) - # # completion_tokens += count_token(value, model=model) - # completion_tokens += 0 - content = list(full_tool_calls.values()) - - usage = RequestUsage( - prompt_tokens=prompt_tokens, - completion_tokens=completion_tokens, - ) - if stop_reason == "function_call": - raise ValueError("Function calls are not supported in this context") - if stop_reason == "tool_calls": - stop_reason = "function_calls" - - result = CreateResult( - finish_reason=stop_reason, # type: ignore - content=content, - usage=usage, - cached=False, - logprobs=logprobs, - ) - - _add_usage(self._actual_usage, usage) - _add_usage(self._total_usage, usage) - - yield result - - def actual_usage(self) -> RequestUsage: - return self._actual_usage - - def total_usage(self) -> RequestUsage: - return self._total_usage - - def count_tokens(self, messages: Sequence[LLMMessage], tools: Sequence[Tool | ToolSchema] = []) -> int: - model = self._create_args["model"] - try: - encoding = tiktoken.encoding_for_model(model) - except KeyError: - trace_logger.warning(f"Model {model} not found. Using cl100k_base encoding.") - encoding = tiktoken.get_encoding("cl100k_base") - tokens_per_message = 3 - tokens_per_name = 1 - num_tokens = 0 - - # Message tokens. - for message in messages: - num_tokens += tokens_per_message - oai_message = to_oai_type(message) - for oai_message_part in oai_message: - for key, value in oai_message_part.items(): - if value is None: - continue - - if isinstance(message, UserMessage) and isinstance(value, list): - typed_message_value = cast(List[ChatCompletionContentPartParam], value) - - assert len(typed_message_value) == len( - message.content - ), "Mismatch in message content and typed message value" - - # We need image properties that are only in the original message - for part, content_part in zip(typed_message_value, message.content, strict=False): - if isinstance(content_part, Image): - # TODO: add detail parameter - num_tokens += calculate_vision_tokens(content_part) - elif isinstance(part, str): - num_tokens += len(encoding.encode(part)) - else: - try: - serialized_part = json.dumps(part) - num_tokens += len(encoding.encode(serialized_part)) - except TypeError: - trace_logger.warning(f"Could not convert {part} to string, skipping.") - else: - if not isinstance(value, str): - try: - value = json.dumps(value) - except TypeError: - trace_logger.warning(f"Could not convert {value} to string, skipping.") - continue - num_tokens += len(encoding.encode(value)) - if key == "name": - num_tokens += tokens_per_name - num_tokens += 3 # every reply is primed with <|start|>assistant<|message|> - - # Tool tokens. - oai_tools = convert_tools(tools) - for tool in oai_tools: - function = tool["function"] - tool_tokens = len(encoding.encode(function["name"])) - if "description" in function: - tool_tokens += len(encoding.encode(function["description"])) - tool_tokens -= 2 - if "parameters" in function: - parameters = function["parameters"] - if "properties" in parameters: - assert isinstance(parameters["properties"], dict) - for propertiesKey in parameters["properties"]: # pyright: ignore - assert isinstance(propertiesKey, str) - tool_tokens += len(encoding.encode(propertiesKey)) - v = parameters["properties"][propertiesKey] # pyright: ignore - for field in v: # pyright: ignore - if field == "type": - tool_tokens += 2 - tool_tokens += len(encoding.encode(v["type"])) # pyright: ignore - elif field == "description": - tool_tokens += 2 - tool_tokens += len(encoding.encode(v["description"])) # pyright: ignore - elif field == "enum": - tool_tokens -= 3 - for o in v["enum"]: # pyright: ignore - tool_tokens += 3 - tool_tokens += len(encoding.encode(o)) # pyright: ignore - else: - trace_logger.warning(f"Not supported field {field}") - tool_tokens += 11 - if len(parameters["properties"]) == 0: # pyright: ignore - tool_tokens -= 2 - num_tokens += tool_tokens - num_tokens += 12 - return num_tokens - - def remaining_tokens(self, messages: Sequence[LLMMessage], tools: Sequence[Tool | ToolSchema] = []) -> int: - token_limit = _model_info.get_token_limit(self._create_args["model"]) - return token_limit - self.count_tokens(messages, tools) - - @property - def capabilities(self) -> ModelCapabilities: - return self._model_capabilities - - -class OpenAIChatCompletionClient(BaseOpenAIChatCompletionClient): - def __init__(self, **kwargs: Unpack[OpenAIClientConfiguration]): - if "model" not in kwargs: - raise ValueError("model is required for OpenAIChatCompletionClient") - - model_capabilities: Optional[ModelCapabilities] = None - copied_args = dict(kwargs).copy() - if "model_capabilities" in kwargs: - model_capabilities = kwargs["model_capabilities"] - del copied_args["model_capabilities"] - - client = _openai_client_from_config(copied_args) - create_args = _create_args_from_config(copied_args) - self._raw_config = copied_args - super().__init__(client, create_args, model_capabilities) - - def __getstate__(self) -> Dict[str, Any]: - state = self.__dict__.copy() - state["_client"] = None - return state - - def __setstate__(self, state: Dict[str, Any]) -> None: - self.__dict__.update(state) - self._client = _openai_client_from_config(state["_raw_config"]) - - -class AzureOpenAIChatCompletionClient(BaseOpenAIChatCompletionClient): - def __init__(self, **kwargs: Unpack[AzureOpenAIClientConfiguration]): - if "model" not in kwargs: - raise ValueError("model is required for OpenAIChatCompletionClient") - - model_capabilities: Optional[ModelCapabilities] = None - copied_args = dict(kwargs).copy() - if "model_capabilities" in kwargs: - model_capabilities = kwargs["model_capabilities"] - del copied_args["model_capabilities"] - - client = _azure_openai_client_from_config(copied_args) - create_args = _create_args_from_config(copied_args) - self._raw_config = copied_args - super().__init__(client, create_args, model_capabilities) - - def __getstate__(self) -> Dict[str, Any]: - state = self.__dict__.copy() - state["_client"] = None - return state - - def __setstate__(self, state: Dict[str, Any]) -> None: - self.__dict__.update(state) - self._client = _azure_openai_client_from_config(state["_raw_config"]) diff --git a/python/packages/autogen-core/src/autogen_core/components/models/config/__init__.py b/python/packages/autogen-core/src/autogen_core/components/models/config/__init__.py deleted file mode 100644 index d1edcf8c62f..00000000000 --- a/python/packages/autogen-core/src/autogen_core/components/models/config/__init__.py +++ /dev/null @@ -1,52 +0,0 @@ -from typing import Awaitable, Callable, Dict, List, Literal, Optional, Union - -from typing_extensions import Required, TypedDict - -from .._model_client import ModelCapabilities - - -class ResponseFormat(TypedDict): - type: Literal["text", "json_object"] - - -class CreateArguments(TypedDict, total=False): - frequency_penalty: Optional[float] - logit_bias: Optional[Dict[str, int]] - max_tokens: Optional[int] - n: Optional[int] - presence_penalty: Optional[float] - response_format: ResponseFormat - seed: Optional[int] - stop: Union[Optional[str], List[str]] - temperature: Optional[float] - top_p: Optional[float] - user: str - - -AsyncAzureADTokenProvider = Callable[[], Union[str, Awaitable[str]]] - - -class BaseOpenAIClientConfiguration(CreateArguments, total=False): - model: str - api_key: str - timeout: Union[float, None] - max_retries: int - - -# See OpenAI docs for explanation of these parameters -class OpenAIClientConfiguration(BaseOpenAIClientConfiguration, total=False): - organization: str - base_url: str - # Not required - model_capabilities: ModelCapabilities - - -class AzureOpenAIClientConfiguration(BaseOpenAIClientConfiguration, total=False): - # Azure specific - azure_endpoint: Required[str] - azure_deployment: str - api_version: Required[str] - azure_ad_token: str - azure_ad_token_provider: AsyncAzureADTokenProvider - # Must be provided - model_capabilities: Required[ModelCapabilities] diff --git a/python/packages/autogen-core/tests/test_tool_agent.py b/python/packages/autogen-core/tests/test_tool_agent.py index 322fdf6b794..6184e9c78c8 100644 --- a/python/packages/autogen-core/tests/test_tool_agent.py +++ b/python/packages/autogen-core/tests/test_tool_agent.py @@ -1,6 +1,6 @@ import asyncio import json -from typing import Any, AsyncGenerator, List +from typing import Any, AsyncGenerator, List, Mapping, Optional, Sequence, Union import pytest from autogen_core.application import SingleThreadedAgentRuntime @@ -8,9 +8,13 @@ from autogen_core.components import FunctionCall from autogen_core.components.models import ( AssistantMessage, + ChatCompletionClient, + CreateResult, FunctionExecutionResult, FunctionExecutionResultMessage, - OpenAIChatCompletionClient, + LLMMessage, + ModelCapabilities, + RequestUsage, UserMessage, ) from autogen_core.components.tool_agent import ( @@ -20,13 +24,7 @@ ToolNotFoundException, tool_agent_caller_loop, ) -from autogen_core.components.tools import FunctionTool, Tool -from openai.resources.chat.completions import AsyncCompletions -from openai.types.chat.chat_completion import ChatCompletion, Choice -from openai.types.chat.chat_completion_chunk import ChatCompletionChunk -from openai.types.chat.chat_completion_message import ChatCompletionMessage -from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall, Function -from openai.types.completion_usage import CompletionUsage +from autogen_core.components.tools import FunctionTool, Tool, ToolSchema def _pass_function(input: str) -> str: @@ -42,60 +40,6 @@ async def _async_sleep_function(input: str) -> str: return "pass" -class _MockChatCompletion: - def __init__(self, model: str = "gpt-4o") -> None: - self._saved_chat_completions: List[ChatCompletion] = [ - ChatCompletion( - id="id1", - choices=[ - Choice( - finish_reason="tool_calls", - index=0, - message=ChatCompletionMessage( - content=None, - tool_calls=[ - ChatCompletionMessageToolCall( - id="1", - type="function", - function=Function( - name="pass", - arguments=json.dumps({"input": "pass"}), - ), - ) - ], - role="assistant", - ), - ) - ], - created=0, - model=model, - object="chat.completion", - usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0), - ), - ChatCompletion( - id="id2", - choices=[ - Choice( - finish_reason="stop", index=0, message=ChatCompletionMessage(content="Hello", role="assistant") - ) - ], - created=0, - model=model, - object="chat.completion", - usage=CompletionUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0), - ), - ] - self._curr_index = 0 - - async def mock_create( - self, *args: Any, **kwargs: Any - ) -> ChatCompletion | AsyncGenerator[ChatCompletionChunk, None]: - await asyncio.sleep(0.1) - completion = self._saved_chat_completions[self._curr_index] - self._curr_index += 1 - return completion - - @pytest.mark.asyncio async def test_tool_agent() -> None: runtime = SingleThreadedAgentRuntime() @@ -144,10 +88,59 @@ async def test_tool_agent() -> None: @pytest.mark.asyncio -async def test_caller_loop(monkeypatch: pytest.MonkeyPatch) -> None: - mock = _MockChatCompletion(model="gpt-4o-2024-05-13") - monkeypatch.setattr(AsyncCompletions, "create", mock.mock_create) - client = OpenAIChatCompletionClient(model="gpt-4o-2024-05-13", api_key="api_key") +async def test_caller_loop() -> None: + class MockChatCompletionClient(ChatCompletionClient): + async def create( + self, + messages: Sequence[LLMMessage], + tools: Sequence[Tool | ToolSchema] = [], + json_output: Optional[bool] = None, + extra_create_args: Mapping[str, Any] = {}, + cancellation_token: Optional[CancellationToken] = None, + ) -> CreateResult: + if len(messages) == 1: + return CreateResult( + content=[FunctionCall(id="1", name="pass", arguments=json.dumps({"input": "test"}))], + finish_reason="stop", + usage=RequestUsage(prompt_tokens=0, completion_tokens=0), + cached=False, + logprobs=None, + ) + return CreateResult( + content="Done", + finish_reason="stop", + usage=RequestUsage(prompt_tokens=0, completion_tokens=0), + cached=False, + logprobs=None, + ) + + def create_stream( + self, + messages: Sequence[LLMMessage], + tools: Sequence[Tool | ToolSchema] = [], + json_output: Optional[bool] = None, + extra_create_args: Mapping[str, Any] = {}, + cancellation_token: Optional[CancellationToken] = None, + ) -> AsyncGenerator[Union[str, CreateResult], None]: + raise NotImplementedError() + + def actual_usage(self) -> RequestUsage: + return RequestUsage(prompt_tokens=0, completion_tokens=0) + + def total_usage(self) -> RequestUsage: + return RequestUsage(prompt_tokens=0, completion_tokens=0) + + def count_tokens(self, messages: Sequence[LLMMessage], tools: Sequence[Tool | ToolSchema] = []) -> int: + return 0 + + def remaining_tokens(self, messages: Sequence[LLMMessage], tools: Sequence[Tool | ToolSchema] = []) -> int: + return 0 + + @property + def capabilities(self) -> ModelCapabilities: + return ModelCapabilities(vision=False, function_calling=True, json_output=False) + + client = MockChatCompletionClient() tools: List[Tool] = [FunctionTool(_pass_function, name="pass", description="Pass function")] runtime = SingleThreadedAgentRuntime() await runtime.register( diff --git a/python/packages/autogen-core/tests/test_tools.py b/python/packages/autogen-core/tests/test_tools.py index 70ec0846970..27a89748c65 100644 --- a/python/packages/autogen-core/tests/test_tools.py +++ b/python/packages/autogen-core/tests/test_tools.py @@ -4,7 +4,6 @@ import pytest from autogen_core.base import CancellationToken from autogen_core.components._function_utils import get_typed_signature -from autogen_core.components.models._openai_client import convert_tools from autogen_core.components.tools import BaseTool, FunctionTool from autogen_core.components.tools._base import ToolSchema from pydantic import BaseModel, Field, model_serializer @@ -323,29 +322,6 @@ def my_function(arg: int) -> int: assert tool.return_value_as_string(result) == "5" -def test_convert_tools_accepts_both_func_tool_and_schema() -> None: - def my_function(arg: str, other: Annotated[int, "int arg"], nonrequired: int = 5) -> MyResult: - return MyResult(result="test") - - tool = FunctionTool(my_function, description="Function tool.") - schema = tool.schema - - converted_tool_schema = convert_tools([tool, schema]) - - assert len(converted_tool_schema) == 2 - assert converted_tool_schema[0] == converted_tool_schema[1] - - -def test_convert_tools_accepts_both_tool_and_schema() -> None: - tool = MyTool() - schema = tool.schema - - converted_tool_schema = convert_tools([tool, schema]) - - assert len(converted_tool_schema) == 2 - assert converted_tool_schema[0] == converted_tool_schema[1] - - @pytest.mark.asyncio async def test_func_tool_return_list() -> None: def my_function() -> List[int]: diff --git a/python/packages/autogen-ext/src/autogen_ext/code_executor/aca_dynamic_sessions/__init__.py b/python/packages/autogen-ext/src/autogen_ext/code_executor/aca_dynamic_sessions/__init__.py deleted file mode 100644 index 009997c41ab..00000000000 --- a/python/packages/autogen-ext/src/autogen_ext/code_executor/aca_dynamic_sessions/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -import warnings -from typing import Any - -from ...code_executors import ACADynamicSessionsCodeExecutor - - -class AzureContainerCodeExecutor(ACADynamicSessionsCodeExecutor): - """AzureContainerCodeExecutor has been renamed and moved to autogen_ext.code_executors.ACADynamicSessionsCodeExecutor""" - - def __init__(self, *args: Any, **kwargs: Any) -> None: - warnings.warn( - "AzureContainerCodeExecutor has been renamed and moved to autogen_ext.code_executors.ACADynamicSessionsCodeExecutor", - DeprecationWarning, - stacklevel=2, - ) - super().__init__(*args, **kwargs) - - -__all__ = [ - "AzureContainerCodeExecutor", -] diff --git a/python/packages/autogen-ext/src/autogen_ext/code_executor/docker_executor/__init__.py b/python/packages/autogen-ext/src/autogen_ext/code_executor/docker_executor/__init__.py deleted file mode 100644 index 66719114300..00000000000 --- a/python/packages/autogen-ext/src/autogen_ext/code_executor/docker_executor/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -import warnings - -from ...code_executors import DockerCommandLineCodeExecutor - -warnings.warn( - "DockerCommandLineCodeExecutor moved to autogen_ext.code_executors.DockerCommandLineCodeExecutor", - DeprecationWarning, - stacklevel=2, -) - -__all__ = ["DockerCommandLineCodeExecutor"] diff --git a/python/packages/autogen-ext/src/autogen_ext/models/__init__.py b/python/packages/autogen-ext/src/autogen_ext/models/__init__.py index d39c1d9bf24..80533f80575 100644 --- a/python/packages/autogen-ext/src/autogen_ext/models/__init__.py +++ b/python/packages/autogen-ext/src/autogen_ext/models/__init__.py @@ -2,6 +2,13 @@ AzureOpenAIChatCompletionClient, OpenAIChatCompletionClient, ) +from ._openai.config import AzureOpenAIClientConfiguration, OpenAIClientConfiguration from ._reply_chat_completion_client import ReplayChatCompletionClient -__all__ = ["AzureOpenAIChatCompletionClient", "OpenAIChatCompletionClient", "ReplayChatCompletionClient"] +__all__ = [ + "AzureOpenAIClientConfiguration", + "AzureOpenAIChatCompletionClient", + "OpenAIClientConfiguration", + "OpenAIChatCompletionClient", + "ReplayChatCompletionClient", +] diff --git a/python/packages/autogen-ext/src/autogen_ext/models/_openai/config/__init__.py b/python/packages/autogen-ext/src/autogen_ext/models/_openai/config/__init__.py index b6729a70d11..53abfcc5879 100644 --- a/python/packages/autogen-ext/src/autogen_ext/models/_openai/config/__init__.py +++ b/python/packages/autogen-ext/src/autogen_ext/models/_openai/config/__init__.py @@ -49,3 +49,6 @@ class AzureOpenAIClientConfiguration(BaseOpenAIClientConfiguration, total=False) azure_ad_token_provider: AsyncAzureADTokenProvider # Must be provided model_capabilities: Required[ModelCapabilities] + + +__all__ = ["AzureOpenAIClientConfiguration", "OpenAIClientConfiguration"] diff --git a/python/packages/autogen-ext/src/autogen_ext/tools/langchain/__init__.py b/python/packages/autogen-ext/src/autogen_ext/tools/langchain/__init__.py deleted file mode 100644 index 4d401fc7ef1..00000000000 --- a/python/packages/autogen-ext/src/autogen_ext/tools/langchain/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -import warnings - -from ...tools import LangChainToolAdapter - -warnings.warn("LangChainToolAdapter moved to autogen_ext.tools.LangChainToolAdapter", DeprecationWarning, stacklevel=2) - -__all__ = ["LangChainToolAdapter"] diff --git a/python/packages/autogen-ext/tests/models/test_openai_model_client.py b/python/packages/autogen-ext/tests/models/test_openai_model_client.py index a51e33c0234..cee3be5835b 100644 --- a/python/packages/autogen-ext/tests/models/test_openai_model_client.py +++ b/python/packages/autogen-ext/tests/models/test_openai_model_client.py @@ -1,5 +1,5 @@ import asyncio -from typing import Any, AsyncGenerator, List, Tuple +from typing import Annotated, Any, AsyncGenerator, List, Tuple from unittest.mock import MagicMock import pytest @@ -15,17 +15,25 @@ SystemMessage, UserMessage, ) -from autogen_core.components.tools import FunctionTool +from autogen_core.components.tools import BaseTool, FunctionTool from autogen_ext.models import AzureOpenAIChatCompletionClient, OpenAIChatCompletionClient from autogen_ext.models._openai._model_info import resolve_model -from autogen_ext.models._openai._openai_client import calculate_vision_tokens +from autogen_ext.models._openai._openai_client import calculate_vision_tokens, convert_tools from openai.resources.chat.completions import AsyncCompletions from openai.types.chat.chat_completion import ChatCompletion, Choice from openai.types.chat.chat_completion_chunk import ChatCompletionChunk, ChoiceDelta from openai.types.chat.chat_completion_chunk import Choice as ChunkChoice from openai.types.chat.chat_completion_message import ChatCompletionMessage from openai.types.completion_usage import CompletionUsage -from pydantic import BaseModel +from pydantic import BaseModel, Field + + +class MyResult(BaseModel): + result: str = Field(description="The other description.") + + +class MyArgs(BaseModel): + query: str = Field(description="The description.") class MockChunkDefinition(BaseModel): @@ -302,3 +310,38 @@ def test_openai_count_image_tokens(mock_size: Tuple[int, int], expected_num_toke # Directly call calculate_vision_tokens and check the result calculated_tokens = calculate_vision_tokens(mock_image, detail="auto") assert calculated_tokens == expected_num_tokens + + +def test_convert_tools_accepts_both_func_tool_and_schema() -> None: + def my_function(arg: str, other: Annotated[int, "int arg"], nonrequired: int = 5) -> MyResult: + return MyResult(result="test") + + tool = FunctionTool(my_function, description="Function tool.") + schema = tool.schema + + converted_tool_schema = convert_tools([tool, schema]) + + assert len(converted_tool_schema) == 2 + assert converted_tool_schema[0] == converted_tool_schema[1] + + +def test_convert_tools_accepts_both_tool_and_schema() -> None: + class MyTool(BaseTool[MyArgs, MyResult]): + def __init__(self) -> None: + super().__init__( + args_type=MyArgs, + return_type=MyResult, + name="TestTool", + description="Description of test tool.", + ) + + async def run(self, args: MyArgs, cancellation_token: CancellationToken) -> MyResult: + return MyResult(result="value") + + tool = MyTool() + schema = tool.schema + + converted_tool_schema = convert_tools([tool, schema]) + + assert len(converted_tool_schema) == 2 + assert converted_tool_schema[0] == converted_tool_schema[1] From 1e0b254d0aa011712328ac7f40b7788e37e63510 Mon Sep 17 00:00:00 2001 From: gagb Date: Fri, 22 Nov 2024 14:06:04 -0500 Subject: [PATCH 2/4] Update README.md with link to clarifications statement (#4318) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 71509427170..b5bda7de8e0 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ # AutoGen > [!IMPORTANT] -> +> - (11/14/24) ⚠️ In response to a number of asks to clarify and distinguish between official AutoGen and its forks that created confusion, we issued a [clarification statement](https://github.com/microsoft/autogen/discussions/4217). > - (10/13/24) Interested in the standard AutoGen as a prior user? Find it at the actively-maintained *AutoGen* [0.2 branch](https://github.com/microsoft/autogen/tree/0.2) and `autogen-agentchat~=0.2` PyPi package. > - (10/02/24) [AutoGen 0.4](https://microsoft.github.io/autogen/dev) is a from-the-ground-up rewrite of AutoGen. Learn more about the history, goals and future at [this blog post](https://microsoft.github.io/autogen/blog). We’re excited to work with the community to gather feedback, refine, and improve the project before we officially release 0.4. This is a big change, so AutoGen 0.2 is still available, maintained, and developed in the [0.2 branch](https://github.com/microsoft/autogen/tree/0.2). From 8f4d8c89c3cceaf18540f1b4904ada13bc120393 Mon Sep 17 00:00:00 2001 From: Xiaoyun Zhang Date: Fri, 22 Nov 2024 13:51:08 -0800 Subject: [PATCH 3/4] .NET add roleplay tool call orchestrator in AutoGen.OpenAI (#4323) * add roleplay tool call orchestrator * add chinese business workflow test * update --- .../src/AutoGen.OpenAI/AutoGen.OpenAI.csproj | 1 + .../RolePlayToolCallOrchestrator.cs | 133 +++++++++ .../RolePlayToolCallOrchestratorTests.cs | 269 ++++++++++++++++++ 3 files changed, 403 insertions(+) create mode 100644 dotnet/src/AutoGen.OpenAI/Orchestrator/RolePlayToolCallOrchestrator.cs create mode 100644 dotnet/test/AutoGen.OpenAI.Tests/RolePlayToolCallOrchestratorTests.cs diff --git a/dotnet/src/AutoGen.OpenAI/AutoGen.OpenAI.csproj b/dotnet/src/AutoGen.OpenAI/AutoGen.OpenAI.csproj index 7f00b63be86..70c0f2b0d1c 100644 --- a/dotnet/src/AutoGen.OpenAI/AutoGen.OpenAI.csproj +++ b/dotnet/src/AutoGen.OpenAI/AutoGen.OpenAI.csproj @@ -18,6 +18,7 @@ + diff --git a/dotnet/src/AutoGen.OpenAI/Orchestrator/RolePlayToolCallOrchestrator.cs b/dotnet/src/AutoGen.OpenAI/Orchestrator/RolePlayToolCallOrchestrator.cs new file mode 100644 index 00000000000..f088e1748e6 --- /dev/null +++ b/dotnet/src/AutoGen.OpenAI/Orchestrator/RolePlayToolCallOrchestrator.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// RolePlayToolCallOrchestrator.cs + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using AutoGen.OpenAI.Extension; +using OpenAI.Chat; + +namespace AutoGen.OpenAI.Orchestrator; + +/// +/// Orchestrating group chat using role play tool call +/// +public partial class RolePlayToolCallOrchestrator : IOrchestrator +{ + public readonly ChatClient chatClient; + private readonly Graph? workflow; + + public RolePlayToolCallOrchestrator(ChatClient chatClient, Graph? workflow = null) + { + this.chatClient = chatClient; + this.workflow = workflow; + } + + public async Task GetNextSpeakerAsync( + OrchestrationContext context, + CancellationToken cancellationToken = default) + { + var candidates = context.Candidates.ToList(); + + if (candidates.Count == 0) + { + return null; + } + + if (candidates.Count == 1) + { + return candidates.First(); + } + + // if there's a workflow + // and the next available agent from the workflow is in the group chat + // then return the next agent from the workflow + if (this.workflow != null) + { + var lastMessage = context.ChatHistory.LastOrDefault(); + if (lastMessage == null) + { + return null; + } + var currentSpeaker = candidates.First(candidates => candidates.Name == lastMessage.From); + var nextAgents = await this.workflow.TransitToNextAvailableAgentsAsync(currentSpeaker, context.ChatHistory, cancellationToken); + nextAgents = nextAgents.Where(nextAgent => candidates.Any(candidate => candidate.Name == nextAgent.Name)); + candidates = nextAgents.ToList(); + if (!candidates.Any()) + { + return null; + } + + if (candidates is { Count: 1 }) + { + return candidates.First(); + } + } + + // In this case, since there are more than one available agents from the workflow for the next speaker + // We need to invoke LLM to select the next speaker via select next speaker function + + var chatHistoryStringBuilder = new StringBuilder(); + foreach (var message in context.ChatHistory) + { + var chatHistoryPrompt = $"{message.From}: {message.GetContent()}"; + + chatHistoryStringBuilder.AppendLine(chatHistoryPrompt); + } + + var chatHistory = chatHistoryStringBuilder.ToString(); + + var prompt = $""" + # Task: Select the next speaker + + You are in a role-play game. Carefully read the conversation history and select the next speaker from the available roles. + + # Conversation + {chatHistory} + + # Available roles + - {string.Join(",", candidates.Select(candidate => candidate.Name))} + + Select the next speaker from the available roles and provide a reason for your selection. + """; + + // enforce the next speaker to be selected by the LLM + var option = new ChatCompletionOptions + { + ToolChoice = ChatToolChoice.CreateFunctionChoice(this.SelectNextSpeakerFunctionContract.Name), + }; + + option.Tools.Add(this.SelectNextSpeakerFunctionContract.ToChatTool()); + var toolCallMiddleware = new FunctionCallMiddleware( + functions: [this.SelectNextSpeakerFunctionContract], + functionMap: new Dictionary>> + { + [this.SelectNextSpeakerFunctionContract.Name] = this.SelectNextSpeakerWrapper, + }); + + var selectAgent = new OpenAIChatAgent( + chatClient, + "admin", + option) + .RegisterMessageConnector() + .RegisterMiddleware(toolCallMiddleware); + + var reply = await selectAgent.SendAsync(prompt); + + var nextSpeaker = candidates.FirstOrDefault(candidate => candidate.Name == reply.GetContent()); + + return nextSpeaker; + } + + /// + /// Select the next speaker by name and reason + /// + [Function] + public async Task SelectNextSpeaker(string name, string reason) + { + return name; + } +} diff --git a/dotnet/test/AutoGen.OpenAI.Tests/RolePlayToolCallOrchestratorTests.cs b/dotnet/test/AutoGen.OpenAI.Tests/RolePlayToolCallOrchestratorTests.cs new file mode 100644 index 00000000000..807bf41e947 --- /dev/null +++ b/dotnet/test/AutoGen.OpenAI.Tests/RolePlayToolCallOrchestratorTests.cs @@ -0,0 +1,269 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// RolePlayToolCallOrchestratorTests.cs + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using AutoGen.OpenAI.Orchestrator; +using AutoGen.Tests; +using Azure.AI.OpenAI; +using FluentAssertions; +using Moq; +using OpenAI; +using OpenAI.Chat; +using Xunit; + +namespace AutoGen.OpenAI.Tests; + +public class RolePlayToolCallOrchestratorTests +{ + [Fact] + public async Task ItReturnNullWhenNoCandidateIsAvailableAsync() + { + var chatClient = Mock.Of(); + var orchestrator = new RolePlayToolCallOrchestrator(chatClient); + var context = new OrchestrationContext + { + Candidates = [], + ChatHistory = [], + }; + + var speaker = await orchestrator.GetNextSpeakerAsync(context); + speaker.Should().BeNull(); + } + + [Fact] + public async Task ItReturnCandidateWhenOnlyOneCandidateIsAvailableAsync() + { + var chatClient = Mock.Of(); + var alice = new EchoAgent("Alice"); + var orchestrator = new RolePlayToolCallOrchestrator(chatClient); + var context = new OrchestrationContext + { + Candidates = [alice], + ChatHistory = [], + }; + + var speaker = await orchestrator.GetNextSpeakerAsync(context); + speaker.Should().Be(alice); + } + + [Fact] + public async Task ItSelectNextSpeakerFromWorkflowIfProvided() + { + var workflow = new Graph(); + var alice = new EchoAgent("Alice"); + var bob = new EchoAgent("Bob"); + var charlie = new EchoAgent("Charlie"); + workflow.AddTransition(Transition.Create(alice, bob)); + workflow.AddTransition(Transition.Create(bob, charlie)); + workflow.AddTransition(Transition.Create(charlie, alice)); + + var client = Mock.Of(); + var orchestrator = new RolePlayToolCallOrchestrator(client, workflow); + var context = new OrchestrationContext + { + Candidates = [alice, bob, charlie], + ChatHistory = + [ + new TextMessage(Role.User, "Hello, Bob", from: "Alice"), + ], + }; + + var speaker = await orchestrator.GetNextSpeakerAsync(context); + speaker.Should().Be(bob); + } + + [Fact] + public async Task ItReturnNullIfNoAvailableAgentFromWorkflowAsync() + { + var workflow = new Graph(); + var alice = new EchoAgent("Alice"); + var bob = new EchoAgent("Bob"); + workflow.AddTransition(Transition.Create(alice, bob)); + + var client = Mock.Of(); + var orchestrator = new RolePlayToolCallOrchestrator(client, workflow); + var context = new OrchestrationContext + { + Candidates = [alice, bob], + ChatHistory = + [ + new TextMessage(Role.User, "Hello, Alice", from: "Bob"), + ], + }; + + var speaker = await orchestrator.GetNextSpeakerAsync(context); + speaker.Should().BeNull(); + } + + [ApiKeyFact("AZURE_OPENAI_API_KEY", "AZURE_OPENAI_ENDPOINT", "AZURE_OPENAI_DEPLOY_NAME")] + public async Task GPT_3_5_CoderReviewerRunnerTestAsync() + { + var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT") ?? throw new Exception("Please set AZURE_OPENAI_ENDPOINT environment variable."); + var key = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY") ?? throw new Exception("Please set AZURE_OPENAI_API_KEY environment variable."); + var deployName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOY_NAME") ?? throw new Exception("Please set AZURE_OPENAI_DEPLOY_NAME environment variable."); + var openaiClient = new AzureOpenAIClient(new Uri(endpoint), new System.ClientModel.ApiKeyCredential(key)); + var chatClient = openaiClient.GetChatClient(deployName); + + await BusinessWorkflowTest(chatClient); + await CoderReviewerRunnerTestAsync(chatClient); + } + + [ApiKeyFact("OPENAI_API_KEY")] + public async Task GPT_4o_CoderReviewerRunnerTestAsync() + { + var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set"); + var model = "gpt-4o"; + var openaiClient = new OpenAIClient(apiKey); + var chatClient = openaiClient.GetChatClient(model); + + await BusinessWorkflowTest(chatClient); + await CoderReviewerRunnerTestAsync(chatClient); + } + + [ApiKeyFact("OPENAI_API_KEY")] + public async Task GPT_4o_mini_CoderReviewerRunnerTestAsync() + { + var apiKey = Environment.GetEnvironmentVariable("OPENAI_API_KEY") ?? throw new InvalidOperationException("OPENAI_API_KEY is not set"); + var model = "gpt-4o-mini"; + var openaiClient = new OpenAIClient(apiKey); + var chatClient = openaiClient.GetChatClient(model); + + await BusinessWorkflowTest(chatClient); + await CoderReviewerRunnerTestAsync(chatClient); + } + + /// + /// This test is to mimic the conversation among coder, reviewer and runner. + /// The coder will write the code, the reviewer will review the code, and the runner will run the code. + /// + /// + /// + private async Task CoderReviewerRunnerTestAsync(ChatClient client) + { + var coder = new EchoAgent("Coder"); + var reviewer = new EchoAgent("Reviewer"); + var runner = new EchoAgent("Runner"); + var user = new EchoAgent("User"); + var initializeMessage = new List + { + new TextMessage(Role.User, "Hello, I am user, I will provide the coding task, please write the code first, then review and run it", from: "User"), + new TextMessage(Role.User, "Hello, I am coder, I will write the code", from: "Coder"), + new TextMessage(Role.User, "Hello, I am reviewer, I will review the code", from: "Reviewer"), + new TextMessage(Role.User, "Hello, I am runner, I will run the code", from: "Runner"), + new TextMessage(Role.User, "how to print 'hello world' using C#", from: user.Name), + }; + + var chatHistory = new List() + { + new TextMessage(Role.User, """ + ```csharp + Console.WriteLine("Hello World"); + ``` + """, from: coder.Name), + new TextMessage(Role.User, "The code looks good", from: reviewer.Name), + new TextMessage(Role.User, "The code runs successfully, the output is 'Hello World'", from: runner.Name), + }; + + var orchestrator = new RolePlayToolCallOrchestrator(client); + foreach (var message in chatHistory) + { + var context = new OrchestrationContext + { + Candidates = [coder, reviewer, runner, user], + ChatHistory = initializeMessage, + }; + + var speaker = await orchestrator.GetNextSpeakerAsync(context); + speaker!.Name.Should().Be(message.From); + initializeMessage.Add(message); + } + + // the last next speaker should be the user + var lastSpeaker = await orchestrator.GetNextSpeakerAsync(new OrchestrationContext + { + Candidates = [coder, reviewer, runner, user], + ChatHistory = initializeMessage, + }); + + lastSpeaker!.Name.Should().Be(user.Name); + } + + // test if the tool call orchestrator still run business workflow when the conversation is not in English + private async Task BusinessWorkflowTest(ChatClient client) + { + var ceo = new EchoAgent("乙方首席执行官"); + var pm = new EchoAgent("乙方项目经理"); + var dev = new EchoAgent("乙方开发人员"); + var user = new EchoAgent("甲方"); + var initializeMessage = new List + { + new TextMessage(Role.User, "你好,我是你们的甲方", from: user.Name), + new TextMessage(Role.User, "你好,我是乙方首席执行官,我将负责对接甲方和给项目经理及开发人员分配任务", from: ceo.Name), + new TextMessage(Role.User, "你好,我是乙方项目经理,我将负责项目的进度和质量", from: pm.Name), + new TextMessage(Role.User, "你好,我是乙方开发人员 我将负责项目的具体开发", from: dev.Name), + new TextMessage(Role.User, "开发一个淘宝,预算1W", from: user.Name), + }; + + var workflow = new Graph(); + workflow.AddTransition(Transition.Create(ceo, pm)); + workflow.AddTransition(Transition.Create(ceo, dev)); + workflow.AddTransition(Transition.Create(pm, ceo)); + workflow.AddTransition(Transition.Create(dev, ceo)); + workflow.AddTransition(Transition.Create(user, ceo)); + workflow.AddTransition(Transition.Create(ceo, user)); + + var chatHistory = new List() + { + new TextMessage(Role.User, """ + 项目经理,如何使用1W预算开发一个淘宝 + """, from: ceo.Name), + new TextMessage(Role.User, """ + 对于1万预算开发淘宝类网站,以下是关键建议: + 技术选择: + - 使用开源电商系统节省成本, 选择便宜但稳定的云服务器和域名,预算2000元/年 + - 核心功能优先 + - 人员安排: + - 找1位全栈开发,负责系统搭建(6000元) + - 1位兼职UI设计(2000元) + - 进度规划: + - 基础功能1个月完成,后续根据运营情况逐步优化。 + """, from: pm.Name), + new TextMessage(Role.User, "好的,开发人员,请根据项目经理的规划开始开发", from: ceo.Name), + new TextMessage(Role.User, """ + 好的,已开发完毕 + ```html + + ``` + """, from: dev.Name), + new TextMessage(Role.User, "好的,项目已完成,甲方请付款", from: ceo.Name), + }; + + var orchestrator = new RolePlayToolCallOrchestrator(client, workflow); + + foreach (var message in chatHistory) + { + var context = new OrchestrationContext + { + Candidates = [ceo, pm, dev, user], + ChatHistory = initializeMessage, + }; + + var speaker = await orchestrator.GetNextSpeakerAsync(context); + speaker!.Name.Should().Be(message.From); + initializeMessage.Add(message); + } + + // the last next speaker should be the user + var lastSpeaker = await orchestrator.GetNextSpeakerAsync(new OrchestrationContext + { + Candidates = [ceo, pm, dev, user], + ChatHistory = initializeMessage, + }); + + lastSpeaker!.Name.Should().Be(user.Name); + } +} From 0b5eaf1240e05f0ef05595c92c0d33a9850d0dc4 Mon Sep 17 00:00:00 2001 From: Thai Nguyen Date: Sat, 23 Nov 2024 22:07:21 +0700 Subject: [PATCH 4/4] Agent name termination (#4123) --- .../src/autogen_agentchat/task/__init__.py | 2 ++ .../autogen_agentchat/task/_terminations.py | 35 ++++++++++++++++++- .../tests/test_termination_condition.py | 25 +++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/task/__init__.py b/python/packages/autogen-agentchat/src/autogen_agentchat/task/__init__.py index dd7b6265ad4..e1e6766338d 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/task/__init__.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/task/__init__.py @@ -7,6 +7,7 @@ TextMentionTermination, TimeoutTermination, TokenUsageTermination, + SourceMatchTermination, ) __all__ = [ @@ -17,5 +18,6 @@ "HandoffTermination", "TimeoutTermination", "ExternalTermination", + "SourceMatchTermination", "Console", ] diff --git a/python/packages/autogen-agentchat/src/autogen_agentchat/task/_terminations.py b/python/packages/autogen-agentchat/src/autogen_agentchat/task/_terminations.py index f8d79cef285..81cb5cca7d6 100644 --- a/python/packages/autogen-agentchat/src/autogen_agentchat/task/_terminations.py +++ b/python/packages/autogen-agentchat/src/autogen_agentchat/task/_terminations.py @@ -1,5 +1,5 @@ import time -from typing import Sequence +from typing import Sequence, List from ..base import TerminatedException, TerminationCondition from ..messages import AgentMessage, HandoffMessage, MultiModalMessage, StopMessage, TextMessage @@ -251,3 +251,36 @@ async def __call__(self, messages: Sequence[AgentMessage]) -> StopMessage | None async def reset(self) -> None: self._terminated = False self._setted = False + + +class SourceMatchTermination(TerminationCondition): + """Terminate the conversation after a specific source responds. + + Args: + sources (List[str]): List of source names to terminate the conversation. + + Raises: + TerminatedException: If the termination condition has already been reached. + """ + + def __init__(self, sources: List[str]) -> None: + self._sources = sources + self._terminated = False + + @property + def terminated(self) -> bool: + return self._terminated + + async def __call__(self, messages: Sequence[AgentMessage]) -> StopMessage | None: + if self._terminated: + raise TerminatedException("Termination condition has already been reached") + if not messages: + return None + for message in messages: + if message.source in self._sources: + self._terminated = True + return StopMessage(content=f"'{message.source}' answered", source="SourceMatchTermination") + return None + + async def reset(self) -> None: + self._terminated = False diff --git a/python/packages/autogen-agentchat/tests/test_termination_condition.py b/python/packages/autogen-agentchat/tests/test_termination_condition.py index c09e0e1c14a..f4aa5d2a720 100644 --- a/python/packages/autogen-agentchat/tests/test_termination_condition.py +++ b/python/packages/autogen-agentchat/tests/test_termination_condition.py @@ -1,6 +1,7 @@ import asyncio import pytest +from autogen_agentchat.base import TerminatedException from autogen_agentchat.messages import HandoffMessage, StopMessage, TextMessage from autogen_agentchat.task import ( ExternalTermination, @@ -10,6 +11,7 @@ TextMentionTermination, TimeoutTermination, TokenUsageTermination, + SourceMatchTermination, ) from autogen_core.components.models import RequestUsage @@ -242,3 +244,26 @@ async def test_external_termination() -> None: await termination.reset() assert await termination([]) is None + + +@pytest.mark.asyncio +async def test_source_match_termination() -> None: + termination = SourceMatchTermination(sources=["Assistant"]) + assert await termination([]) is None + + continue_messages = [TextMessage(content="Hello", source="agent"), TextMessage(content="Hello", source="user")] + assert await termination(continue_messages) is None + + terminate_messages = [ + TextMessage(content="Hello", source="agent"), + TextMessage(content="Hello", source="Assistant"), + TextMessage(content="Hello", source="user"), + ] + result = await termination(terminate_messages) + assert isinstance(result, StopMessage) + assert termination.terminated + + with pytest.raises(TerminatedException): + await termination([]) + await termination.reset() + assert not termination.terminated