Files
wehub-resource-sync b957a53def
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
chore: import upstream snapshot with attribution
2026-07-13 13:21:23 +08:00

322 lines
13 KiB
Python

# 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,
)