# Copyright (c) Microsoft. All rights reserved. import logging import sys import uuid from collections.abc import AsyncIterable, Callable from typing import TYPE_CHECKING, Any if sys.version_info >= (3, 12): from typing import override # pragma: no cover else: from typing_extensions import override # pragma: no cover from autogen import ConversableAgent from semantic_kernel.agents.agent import Agent, AgentResponseItem, AgentThread from semantic_kernel.contents.chat_history import ChatHistory from semantic_kernel.contents.chat_message_content import ChatMessageContent from semantic_kernel.contents.function_call_content import FunctionCallContent from semantic_kernel.contents.function_result_content import FunctionResultContent from semantic_kernel.contents.history_reducer.chat_history_reducer import ChatHistoryReducer from semantic_kernel.contents.text_content import TextContent from semantic_kernel.contents.utils.author_role import AuthorRole from semantic_kernel.exceptions.agent_exceptions import AgentInvokeException, AgentThreadOperationException from semantic_kernel.functions.kernel_arguments import KernelArguments from semantic_kernel.utils.feature_stage_decorator import experimental from semantic_kernel.utils.telemetry.agent_diagnostics.decorators import ( trace_agent_get_response, trace_agent_invocation, ) if TYPE_CHECKING: from autogen.cache import AbstractCache from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent from semantic_kernel.kernel import Kernel logger: logging.Logger = logging.getLogger(__name__) @experimental class AutoGenConversableAgentThread(AgentThread): """Azure AI Agent Thread class.""" def __init__(self, chat_history: ChatHistory | None = None, thread_id: str | None = None) -> None: """Initialize the AutoGenConversableAgentThread Thread. Args: chat_history: The chat history for the thread. If None, a new ChatHistory instance will be created. thread_id: The ID of the thread. If None, a new thread will be created. """ super().__init__() self._chat_history = chat_history or ChatHistory() self._id = thread_id @override async def _create(self) -> str: """Starts the thread and returns its ID.""" if not self._id: self._id = f"thread_{uuid.uuid4().hex}" return self._id @override async def _delete(self) -> None: """Ends the current thread.""" self._chat_history.clear() @override async def _on_new_message(self, new_message: str | ChatMessageContent) -> None: """Called when a new message has been contributed to the chat.""" if isinstance(new_message, str): new_message = ChatMessageContent(role=AuthorRole.USER, content=new_message) if ( not new_message.metadata or "thread_id" not in new_message.metadata or new_message.metadata["thread_id"] != self._id ): self._chat_history.add_message(new_message) async def get_messages(self) -> AsyncIterable[ChatMessageContent]: """Retrieve the current chat history. Returns: An async iterable of ChatMessageContent. """ if self._is_deleted: raise AgentThreadOperationException("Cannot retrieve chat history, since the thread has been deleted.") if self._id is None: await self.create() for message in self._chat_history.messages: yield message async def reduce(self) -> ChatHistory | None: """Reduce the chat history to a smaller size.""" if self._id is None: raise AgentThreadOperationException("Cannot reduce chat history, since the thread is not currently active.") if not isinstance(self._chat_history, ChatHistoryReducer): return None return await self._chat_history.reduce() @experimental class AutoGenConversableAgent(Agent): """A Semantic Kernel wrapper around an AutoGen 0.2 `ConversableAgent`. This allows one to use it as a Semantic Kernel `Agent`. Note: this agent abstraction does not currently allow for the use of AgentGroupChat within Semantic Kernel. """ conversable_agent: ConversableAgent def __init__(self, conversable_agent: ConversableAgent, **kwargs: Any) -> None: """Initialize the AutoGenConversableAgent. Args: conversable_agent: The existing AutoGen 0.2 ConversableAgent instance kwargs: Other Agent base class arguments (e.g. name, id, instructions) """ args: dict[str, Any] = { "name": conversable_agent.name, "description": conversable_agent.description, "instructions": conversable_agent.system_message, "conversable_agent": conversable_agent, } if kwargs: args.update(kwargs) super().__init__(**args) @trace_agent_get_response @override async def get_response( self, messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None, *, thread: AgentThread | None = None, **kwargs: Any, ) -> AgentResponseItem[ChatMessageContent]: """Get a response from the agent. Args: messages: The input chat message content either as a string, ChatMessageContent or a list of strings or ChatMessageContent. thread: The thread to use for the conversation. If None, a new thread will be created. kwargs: Additional keyword arguments Returns: An AgentResponseItem of type ChatMessageContent object with the response and the thread. """ thread = await self._ensure_thread_exists_with_messages( messages=messages, thread=thread, construct_thread=lambda: AutoGenConversableAgentThread(), expected_type=AutoGenConversableAgentThread, ) assert thread.id is not None # nosec reply = await self.conversable_agent.a_generate_reply( messages=[message.to_dict() async for message in thread.get_messages()], **kwargs, ) logger.info("Called AutoGenConversableAgent.a_generate_reply.") return await self._create_reply_content(reply, thread) @trace_agent_invocation @override async def invoke( self, messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None, *, thread: AgentThread | None = None, recipient: "AutoGenConversableAgent | None" = None, clear_history: bool = True, silent: bool = True, cache: "AbstractCache | None" = None, max_turns: int | None = None, summary_method: str | Callable | None = ConversableAgent.DEFAULT_SUMMARY_METHOD, summary_args: dict | None = None, **kwargs: Any, ) -> AsyncIterable[AgentResponseItem[ChatMessageContent]]: """A direct `invoke` method for the ConversableAgent. Args: messages: The input chat message content either as a string, ChatMessageContent or a list of strings or ChatMessageContent. thread: The thread to use for the conversation. If None, a new thread will be created. recipient: The recipient ConversableAgent to chat with clear_history: Whether to clear the chat history before starting. True by default. silent: Whether to suppress console output. True by default. cache: The cache to use for storing chat history max_turns: The maximum number of turns to chat for summary_method: The method to use for summarizing the chat summary_args: The arguments to pass to the summary method message: The initial message to send. If message is not provided, the agent will wait for the user to provide the first message. kwargs: Additional keyword arguments Yields: An AgentResponseItem of type ChatMessageContent object with the response and the thread. """ thread = await self._ensure_thread_exists_with_messages( messages=messages, thread=thread, construct_thread=lambda: AutoGenConversableAgentThread(), expected_type=AutoGenConversableAgentThread, ) assert thread.id is not None # nosec if summary_args is None: summary_args = {} if recipient is not None: if not isinstance(recipient, AutoGenConversableAgent): raise AgentInvokeException( f"Invalid recipient type: {type(recipient)}. " "Recipient must be an instance of AutoGenConversableAgent." ) messages = [message async for message in thread.get_messages()] chat_result = await self.conversable_agent.a_initiate_chat( recipient=recipient.conversable_agent, clear_history=clear_history, silent=silent, cache=cache, max_turns=max_turns, summary_method=summary_method, summary_args=summary_args, message=messages[-1].content, # type: ignore **kwargs, ) logger.info(f"Called AutoGenConversableAgent.a_initiate_chat with recipient: {recipient}.") for message in chat_result.chat_history: msg = AutoGenConversableAgent._to_chat_message_content(message) # type: ignore await thread.on_new_message(msg) yield AgentResponseItem( message=msg, thread=thread, ) else: reply = await self.conversable_agent.a_generate_reply( messages=[message.to_dict() async for message in thread.get_messages()], ) logger.info("Called AutoGenConversableAgent.a_generate_reply.") yield await self._create_reply_content(reply, thread) @override def invoke_stream( self, messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None, *, thread: AgentThread | None = None, kernel: "Kernel | None" = None, arguments: KernelArguments | None = None, **kwargs: Any, ) -> AsyncIterable[AgentResponseItem["StreamingChatMessageContent"]]: """Invoke the agent with a stream of messages.""" raise NotImplementedError("The AutoGenConversableAgent does not support streaming.") @staticmethod def _to_chat_message_content(message: dict[str, Any]) -> ChatMessageContent: """Translate an AutoGen message to a Semantic Kernel ChatMessageContent.""" items: list[TextContent | FunctionCallContent | FunctionResultContent] = [] role = AuthorRole(message.get("role")) name: str = message.get("name", "") content = message.get("content") if content is not None: text = TextContent(text=content) items.append(text) if role == AuthorRole.ASSISTANT: tool_calls = message.get("tool_calls") if tool_calls is not None: for tool_call in tool_calls: items.append( FunctionCallContent( id=tool_call.get("id"), function_name=tool_call.get("name"), arguments=tool_call.get("function").get("arguments"), ) ) if role == AuthorRole.TOOL: tool_responses = message.get("tool_responses") if tool_responses is not None: for tool_response in tool_responses: items.append( FunctionResultContent( id=tool_response.get("tool_call_id"), result=tool_response.get("content"), ) ) return ChatMessageContent(role=role, items=items, name=name) # type: ignore async def _create_reply_content( self, reply: str | dict[str, Any], thread: AgentThread ) -> AgentResponseItem[ChatMessageContent]: response: ChatMessageContent if isinstance(reply, str): response = ChatMessageContent(content=reply, role=AuthorRole.ASSISTANT) elif isinstance(reply, dict): response = ChatMessageContent(**reply) else: raise AgentInvokeException(f"Unexpected reply type from `a_generate_reply`: {type(reply)}") await thread.on_new_message(response) return AgentResponseItem( message=response, thread=thread, )