chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,197 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
import logging
import threading
from collections.abc import AsyncIterable
from pydantic import Field, PrivateAttr
from semantic_kernel.agents import Agent
from semantic_kernel.agents.channels.agent_channel import AgentChannel
from semantic_kernel.agents.group_chat.agent_chat_utils import KeyEncoder
from semantic_kernel.agents.group_chat.broadcast_queue import BroadcastQueue, ChannelReference
from semantic_kernel.contents.chat_history import ChatHistory
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions.agent_exceptions import AgentChatException
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.utils.feature_stage_decorator import experimental
logger: logging.Logger = logging.getLogger(__name__)
@experimental
class AgentChat(KernelBaseModel):
"""A base class chat interface for agents."""
broadcast_queue: BroadcastQueue = Field(default_factory=BroadcastQueue)
agent_channels: dict[str, AgentChannel] = Field(default_factory=dict)
channel_map: dict[Agent, str] = Field(default_factory=dict)
history: ChatHistory = Field(default_factory=ChatHistory)
_lock: threading.Lock = PrivateAttr(default_factory=threading.Lock)
_is_active: bool = False
@property
def is_active(self) -> bool:
"""Indicates whether the agent is currently active."""
return self._is_active
def set_activity_or_throw(self):
"""Set the activity signal or throw an exception if another agent is active."""
with self._lock:
if self._is_active:
raise Exception("Unable to proceed while another agent is active.")
self._is_active = True
def clear_activity_signal(self):
"""Clear the activity signal."""
with self._lock:
self._is_active = False
def invoke(self, agent: Agent | None = None, is_joining: bool = True) -> AsyncIterable[ChatMessageContent]:
"""Invoke the agent asynchronously."""
raise NotImplementedError("Subclasses should implement this method")
async def get_messages_in_descending_order(self) -> AsyncIterable[ChatMessageContent]:
"""Get messages in descending order asynchronously."""
for index in range(len(self.history.messages) - 1, -1, -1):
yield self.history.messages[index]
await asyncio.sleep(0) # Yield control to the event loop
async def get_chat_messages(self, agent: "Agent | None" = None) -> AsyncIterable[ChatMessageContent]:
"""Get chat messages asynchronously."""
self.set_activity_or_throw()
logger.info("Getting chat messages")
messages: AsyncIterable[ChatMessageContent] | None = None
try:
if agent is None:
messages = self.get_messages_in_descending_order()
else:
channel_key = self._get_agent_hash(agent)
channel = await self._synchronize_channel(channel_key)
if channel is not None:
messages = channel.get_history()
if messages is not None:
async for message in messages:
yield message
finally:
self.clear_activity_signal()
async def _synchronize_channel(self, channel_key: str) -> AgentChannel | None:
"""Synchronize a channel."""
channel = self.agent_channels.get(channel_key, None)
if channel:
await self.broadcast_queue.ensure_synchronized(ChannelReference(channel=channel, hash=channel_key))
return channel
def _get_agent_hash(self, agent: Agent):
"""Get the hash of an agent."""
hash_value = self.channel_map.get(agent, None)
if hash_value is None:
hash_value = KeyEncoder.generate_hash(agent.get_channel_keys())
self.channel_map[agent] = hash_value
return hash_value
async def add_chat_message(self, message: str | ChatMessageContent) -> None:
"""Add a chat message."""
if isinstance(message, str):
message = ChatMessageContent(role=AuthorRole.USER, content=message)
await self.add_chat_messages([message])
async def add_chat_messages(self, messages: list[ChatMessageContent]) -> None:
"""Add chat messages."""
self.set_activity_or_throw()
for message in messages:
if message.role == AuthorRole.SYSTEM:
error_message = "System messages cannot be added to the chat history."
logger.error(error_message)
raise AgentChatException(error_message)
logger.info(f"Adding `{len(messages)}` agent chat messages")
try:
self.history.messages.extend(messages)
# Broadcast message to other channels (in parallel)
# Note: Able to queue messages without synchronizing channels.
channel_refs = [ChannelReference(channel=channel, hash=key) for key, channel in self.agent_channels.items()]
await self.broadcast_queue.enqueue(channel_refs, messages)
finally:
self.clear_activity_signal()
async def _get_or_create_channel(self, agent: Agent) -> AgentChannel:
"""Get or create a channel."""
channel_key = self._get_agent_hash(agent)
channel = await self._synchronize_channel(channel_key)
if channel is None:
channel = await agent.create_channel()
self.agent_channels[channel_key] = channel
if len(self.history.messages) > 0:
await channel.receive(self.history.messages)
return channel
async def invoke_agent(self, agent: Agent) -> AsyncIterable[ChatMessageContent]:
"""Invoke an agent asynchronously."""
self.set_activity_or_throw()
logger.info(f"Invoking agent {agent.name}")
try:
channel: AgentChannel = await self._get_or_create_channel(agent)
messages: list[ChatMessageContent] = []
async for is_visible, message in channel.invoke(agent):
messages.append(message)
self.history.messages.append(message)
if is_visible:
yield message
# Broadcast message to other channels (in parallel)
# Note: Able to queue messages without synchronizing channels.
channel_refs = [
ChannelReference(channel=ch, hash=key) for key, ch in self.agent_channels.items() if ch != channel
]
await self.broadcast_queue.enqueue(channel_refs, messages)
finally:
self.clear_activity_signal()
async def invoke_agent_stream(self, agent: Agent) -> AsyncIterable[ChatMessageContent]:
"""Invoke an agent stream asynchronously."""
self.set_activity_or_throw()
logger.info(f"Invoking agent {agent.name}")
try:
channel: AgentChannel = await self._get_or_create_channel(agent)
messages: list[ChatMessageContent] = []
async for message in channel.invoke_stream(agent, messages):
yield message
for message in messages:
self.history.messages.append(message)
# Broadcast message to other channels (in parallel)
# Note: Able to queue messages without synchronizing channels.
channel_refs = [
ChannelReference(channel=ch, hash=key) for key, ch in self.agent_channels.items() if ch != channel
]
await self.broadcast_queue.enqueue(channel_refs, messages)
finally:
self.clear_activity_signal()
async def reset(self) -> None:
"""Reset the agent chat."""
self.set_activity_or_throw()
try:
await asyncio.gather(*(channel.reset() for channel in self.agent_channels.values()))
self.agent_channels.clear()
self.channel_map.clear()
self.history.messages.clear()
finally:
self.clear_activity_signal()
@@ -0,0 +1,33 @@
# Copyright (c) Microsoft. All rights reserved.
import base64
import hashlib
from collections.abc import Iterable
from semantic_kernel.exceptions.agent_exceptions import AgentExecutionException
from semantic_kernel.utils.feature_stage_decorator import experimental
@experimental
class KeyEncoder:
"""A class for encoding keys."""
@staticmethod
def generate_hash(keys: Iterable[str]) -> str:
"""Generate a hash from a list of keys.
Args:
keys: A list of keys to generate the hash from.
Returns:
str: The generated hash.
Raises:
AgentExecutionException: If the keys are empty
"""
if not keys:
raise AgentExecutionException("Channel Keys must not be empty. Unable to generate channel hash.")
joined_keys = ":".join(keys)
buffer = joined_keys.encode("utf-8")
sha256_hash = hashlib.sha256(buffer).digest()
return base64.b64encode(sha256_hash).decode("utf-8")
@@ -0,0 +1,228 @@
# Copyright (c) Microsoft. All rights reserved.
import logging
from collections.abc import AsyncIterable
from copy import deepcopy
from typing import TYPE_CHECKING, Any, cast
from pydantic import Field
from semantic_kernel.agents import Agent, AgentChat
from semantic_kernel.agents.strategies import (
DefaultTerminationStrategy,
SequentialSelectionStrategy,
)
from semantic_kernel.agents.strategies.selection.selection_strategy import SelectionStrategy
from semantic_kernel.agents.strategies.termination.termination_strategy import TerminationStrategy
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.history_reducer.chat_history_reducer import ChatHistoryReducer
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.exceptions.agent_exceptions import AgentChatException
from semantic_kernel.utils.feature_stage_decorator import experimental
if TYPE_CHECKING:
from semantic_kernel.contents.chat_history import ChatHistory
logger: logging.Logger = logging.getLogger(__name__)
@experimental
class AgentGroupChat(AgentChat):
"""An agent chat that supports multi-turn interactions."""
agent_ids: set[str]
agents: list[Agent] = Field(default_factory=list)
is_complete: bool = False
termination_strategy: TerminationStrategy = Field(
default_factory=DefaultTerminationStrategy,
description="The termination strategy to use. The default strategy never terminates and has a max iterations of 5.", # noqa: E501
)
selection_strategy: SelectionStrategy = Field(default_factory=SequentialSelectionStrategy)
def __init__(
self,
agents: list[Agent] | None = None,
termination_strategy: TerminationStrategy | None = None,
selection_strategy: SelectionStrategy | None = None,
chat_history: "ChatHistory | None" = None,
) -> None:
"""Initialize a new instance of AgentGroupChat.
Args:
agents: The agents to add to the group chat.
termination_strategy: The termination strategy to use.
selection_strategy: The selection strategy
chat_history: The chat history.
"""
agent_ids = {agent.id for agent in agents} if agents else set()
if agents is None:
agents = []
args: dict[str, Any] = {
"agents": agents,
"agent_ids": agent_ids,
}
if termination_strategy is not None:
args["termination_strategy"] = termination_strategy
if selection_strategy is not None:
args["selection_strategy"] = selection_strategy
if chat_history is not None:
args["history"] = chat_history
super().__init__(**args)
def add_agent(self, agent: Agent) -> None:
"""Add an agent to the group chat.
Args:
agent: The agent to add.
"""
if agent.id not in self.agent_ids:
self.agent_ids.add(agent.id)
self.agents.append(agent)
async def invoke_single_turn(self, agent: Agent) -> AsyncIterable[ChatMessageContent]:
"""Invoke the agent chat for a single turn.
Args:
agent: The agent to invoke.
Yields:
The chat message.
"""
async for message in self.invoke(agent, is_joining=True):
if message.role == AuthorRole.ASSISTANT:
task = self.termination_strategy.should_terminate(agent, self.history.messages)
self.is_complete = await task
yield message
async def invoke_stream_single_turn(self, agent: Agent) -> AsyncIterable[ChatMessageContent]:
"""Invoke the agent chat for a single turn.
Args:
agent: The agent to invoke.
Yields:
The chat message.
"""
async for message in self.invoke_stream(agent, is_joining=True):
yield message
self.is_complete = await self.termination_strategy.should_terminate(agent, self.history.messages)
async def invoke(self, agent: Agent | None = None, is_joining: bool = True) -> AsyncIterable[ChatMessageContent]:
"""Invoke the agent chat asynchronously.
Handles both group interactions and single agent interactions based on the provided arguments.
Args:
agent: The agent to invoke. If not provided, the method processes all agents in the chat.
is_joining: Controls whether the agent joins the chat. Defaults to True.
Yields:
The chat message.
"""
if agent is not None:
if is_joining:
self.add_agent(agent)
async for message in super().invoke_agent(agent):
if message.role == AuthorRole.ASSISTANT:
task = self.termination_strategy.should_terminate(agent, self.history.messages)
self.is_complete = await task
yield message
return
if not self.agents:
raise AgentChatException("No agents are available")
if self.is_complete:
if not self.termination_strategy.automatic_reset:
raise AgentChatException("Chat is already complete")
self.is_complete = False
for _ in range(self.termination_strategy.maximum_iterations):
try:
selected_agent = await self.selection_strategy.next(self.agents, self.history.messages)
except Exception as ex:
logger.error(f"Failed to select agent: {ex}")
raise AgentChatException("Failed to select agent") from ex
async for message in super().invoke_agent(selected_agent):
if message.role == AuthorRole.ASSISTANT:
task = self.termination_strategy.should_terminate(selected_agent, self.history.messages)
self.is_complete = await task
yield message
if self.is_complete:
break
async def invoke_stream(
self, agent: Agent | None = None, is_joining: bool = True
) -> AsyncIterable[ChatMessageContent]:
"""Invoke the agent chat stream asynchronously.
Handles both group interactions and single agent interactions based on the provided arguments.
Args:
agent: The agent to invoke. If not provided, the method processes all agents in the chat.
is_joining: Controls whether the agent joins the chat. Defaults to True.
Yields:
The chat message.
"""
if agent is not None:
if is_joining:
self.add_agent(agent)
async for message in super().invoke_agent_stream(agent):
if message.role == AuthorRole.ASSISTANT:
task = self.termination_strategy.should_terminate(agent, self.history.messages)
self.is_complete = await task
yield message
return
if not self.agents:
raise AgentChatException("No agents are available")
if self.is_complete:
if not self.termination_strategy.automatic_reset:
raise AgentChatException("Chat is already complete")
self.is_complete = False
for _ in range(self.termination_strategy.maximum_iterations):
try:
selected_agent = await self.selection_strategy.next(self.agents, self.history.messages)
except Exception as ex:
logger.error(f"Failed to select agent: {ex}")
raise AgentChatException("Failed to select agent") from ex
async for message in super().invoke_agent_stream(selected_agent):
yield message
self.is_complete = await self.termination_strategy.should_terminate(selected_agent, self.history.messages)
if self.is_complete:
break
async def reduce_history(self) -> bool:
"""Perform the reduction on the provided history, returning True if reduction occurred."""
if not isinstance(self.history, ChatHistoryReducer):
return False
result = await self.history.reduce()
if result is None:
return False
reducer = cast(ChatHistoryReducer, result)
reduced_history = deepcopy(reducer.messages)
await self.reset()
await self.add_chat_messages(reduced_history)
return True
@@ -0,0 +1,129 @@
# Copyright (c) Microsoft. All rights reserved.
import asyncio
from collections import deque
from dataclasses import dataclass, field
from typing import Any
from pydantic import Field, SkipValidation, ValidationError, model_validator
from semantic_kernel.agents.channels.agent_channel import AgentChannel
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.utils.feature_stage_decorator import experimental
@experimental
class QueueReference(KernelBaseModel):
"""Utility class to associate a queue with its specific lock."""
queue: deque = Field(default_factory=deque)
queue_lock: SkipValidation[asyncio.Lock] = Field(default_factory=asyncio.Lock, exclude=True)
receive_task: SkipValidation[asyncio.Task | None] = None
receive_failure: Exception | None = None
@property
def is_empty(self):
"""Check if the queue is empty."""
return len(self.queue) == 0
@model_validator(mode="before")
def validate_receive_task(cls, values: Any):
"""Validate the receive task."""
if isinstance(values, dict):
receive_task = values.get("receive_task")
if receive_task is not None and not isinstance(receive_task, asyncio.Task):
raise ValidationError("receive_task must be an instance of asyncio.Task or None")
return values
@experimental
@dataclass
class ChannelReference:
"""Tracks a channel along with its hashed key."""
hash: str
channel: AgentChannel = field(default_factory=AgentChannel)
@experimental
class BroadcastQueue(KernelBaseModel):
"""A queue for broadcasting messages to listeners."""
queues: dict[str, QueueReference] = Field(default_factory=dict)
block_duration: float = 0.1
async def enqueue(self, channel_refs: list[ChannelReference], messages: list[ChatMessageContent]) -> None:
"""Enqueue a set of messages for a given channel.
Args:
channel_refs: The channel references.
messages: The messages to broadcast.
"""
for channel_ref in channel_refs:
if channel_ref.hash not in self.queues:
self.queues[channel_ref.hash] = QueueReference()
queue_ref = self.queues[channel_ref.hash]
async with queue_ref.queue_lock:
queue_ref.queue.append(messages)
if not queue_ref.receive_task or queue_ref.receive_task.done():
queue_ref.receive_task = asyncio.create_task(self.receive(channel_ref, queue_ref))
async def ensure_synchronized(self, channel_ref: ChannelReference) -> None:
"""Blocks until a channel-queue is not in a receive state to ensure that channel history is complete.
Args:
channel_ref: The channel reference.
"""
if channel_ref.hash not in self.queues:
return
queue_ref = self.queues[channel_ref.hash]
while True:
async with queue_ref.queue_lock:
is_empty = queue_ref.is_empty
if queue_ref.receive_failure is not None:
failure = queue_ref.receive_failure
queue_ref.receive_failure = None
raise Exception(
f"Unexpected failure broadcasting to channel: {type(channel_ref.channel)}, failure: {failure}"
) from failure
if not is_empty and (not queue_ref.receive_task or queue_ref.receive_task.done()):
queue_ref.receive_task = asyncio.create_task(self.receive(channel_ref, queue_ref))
if is_empty:
break
await asyncio.sleep(self.block_duration)
async def receive(self, channel_ref: ChannelReference, queue_ref: QueueReference) -> None:
"""Processes the specified queue with the provided channel, until the queue is empty.
Args:
channel_ref: The channel reference.
queue_ref: The queue reference.
"""
while True:
async with queue_ref.queue_lock:
if queue_ref.is_empty:
break
messages = queue_ref.queue[0]
try:
await channel_ref.channel.receive(messages)
except Exception as e:
queue_ref.receive_failure = e
async with queue_ref.queue_lock:
if not queue_ref.is_empty:
queue_ref.queue.popleft()
if queue_ref.receive_failure is not None or queue_ref.is_empty:
break