chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
## AutoGen Conversable Agent (v0.2.X)
|
||||
|
||||
Semantic Kernel Python supports running AutoGen Conversable Agents provided in the 0.2.X package.
|
||||
|
||||
### Limitations
|
||||
|
||||
Currently, there are some limitations to note:
|
||||
|
||||
- AutoGen Conversable Agents in Semantic Kernel run asynchronously and do not support streaming of agent inputs or responses.
|
||||
- The `AutoGenConversableAgent` in Semantic Kernel Python cannot be configured as part of a Semantic Kernel `AgentGroupChat`. As we progress towards GA for our agent group chat patterns, we will explore ways to integrate AutoGen agents into a Semantic Kernel group chat scenario.
|
||||
|
||||
### Installation
|
||||
|
||||
Install the `semantic-kernel` package with the `autogen` extra:
|
||||
|
||||
```bash
|
||||
pip install semantic-kernel[autogen]
|
||||
```
|
||||
|
||||
For an example of how to integrate an AutoGen Conversable Agent using the Semantic Kernel Agent abstraction, please refer to [`autogen_conversable_agent_simple_convo.py`](../../../samples/concepts/agents/autogen_conversable_agent/autogen_conversable_agent_simple_convo.py).
|
||||
@@ -0,0 +1,321 @@
|
||||
# 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,
|
||||
)
|
||||
Reference in New Issue
Block a user