Skip to content

Commit

Permalink
Remove history reduction from agent. Add history reduction to agent g…
Browse files Browse the repository at this point in the history
…roup chat since history is maintained internally. Update samples to be more clear.
  • Loading branch information
moonbox3 committed Jan 28, 2025
1 parent 71e42f0 commit 92ebbed
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 129 deletions.
135 changes: 64 additions & 71 deletions python/samples/concepts/agents/chat_completion_history_reducer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion, OpenAIChatCompletion
from semantic_kernel.contents import (
AuthorRole,
ChatHistory,
ChatHistorySummarizationReducer,
ChatHistoryTruncationReducer,
ChatMessageContent,
)
from semantic_kernel.contents.history_reducer.chat_history_reducer import ChatHistoryReducer

#####################################################################
# The following sample demonstrates how to implement a chat history #
Expand Down Expand Up @@ -60,57 +60,30 @@ class HistoryReducerExample:
AGENT_NAME = "NumeroTranslator"
AGENT_INSTRUCTIONS = "Add one to the latest user number and spell it in Spanish without explanation."

def create_truncating_agent(self, reducer_msg_count: int, reducer_threshold: int) -> ChatCompletionAgent:
def create_chat_completion_agent(self, service_id: str) -> ChatCompletionAgent:
"""
Creates a ChatCompletionAgent with a truncation-based history reducer.
Creates a ChatCompletionAgent.
Args:
reducer_msg_count: Target number of messages to retain after truncation.
reducer_threshold: Threshold number of messages to trigger truncation.
service_id: The service ID for the chat completion service.
Returns:
A configured ChatCompletionAgent instance with truncation enabled.
A configured ChatCompletionAgent instance.
"""
return ChatCompletionAgent(
name=self.AGENT_NAME,
instructions=self.AGENT_INSTRUCTIONS,
kernel=_create_kernel_with_chat_completion("truncate_agent"),
history_reducer=ChatHistoryTruncationReducer(
target_count=reducer_msg_count, threshold_count=reducer_threshold
),
kernel=_create_kernel_with_chat_completion(service_id=service_id),
)

def create_summarizing_agent(self, reducer_msg_count: int, reducer_threshold: int) -> ChatCompletionAgent:
"""
Creates a ChatCompletionAgent with a summarization-based history reducer.
Args:
reducer_msg_count: Target number of messages to retain after summarization.
reducer_threshold: Threshold number of messages to trigger summarization.
Returns:
A configured ChatCompletionAgent instance with summarization enabled.
"""
service_id = "summarize_agent"
kernel = _create_kernel_with_chat_completion(service_id)

return ChatCompletionAgent(
name=self.AGENT_NAME,
instructions=self.AGENT_INSTRUCTIONS,
kernel=kernel,
history_reducer=ChatHistorySummarizationReducer(
service=kernel.get_service(service_id=service_id),
target_count=reducer_msg_count,
threshold_count=reducer_threshold,
),
)

async def invoke_agent(self, agent: ChatCompletionAgent, chat_history: ChatHistory, message_count: int):
async def invoke_agent(
self, agent: ChatCompletionAgent, chat_history_reducer: ChatHistoryReducer, message_count: int
):
"""Demonstrates agent invocation with direct history management and reduction.
Args:
agent: The ChatCompletionAgent to invoke.
chat_history: The chat history to use for the conversation.
chat_history_reducer: The chat history to use for the conversation.
message_count: The number of messages to simulate in the conversation.
"""

Expand All @@ -119,35 +92,37 @@ async def invoke_agent(self, agent: ChatCompletionAgent, chat_history: ChatHisto
# The user sends 1, 3, 5, etc., and the agent responds with 2, 4, 6, etc. (in Spanish)
for index in range(1, message_count + 1, 2):
# Provide user input
chat_history.add_user_message(str(index))
chat_history_reducer.add_user_message(str(index))
print(f"# User: '{index}'")

# Try history reduction
if is_reduced := await agent.reduce_history(chat_history):
print(f"@ History reduced to {len(chat_history.messages)} messages.")
if is_reduced := await chat_history_reducer.reduce():
print(f"@ History reduced to {len(chat_history_reducer.messages)} messages.")

# Invoke the agent and display its response
async for response in agent.invoke(chat_history):
chat_history.add_message(response)
async for response in agent.invoke(chat_history_reducer):
chat_history_reducer.add_message(response)
print(f"# {response.role} - {response.name}: '{response.content}'")

print(f"@ Message Count: {len(chat_history.messages)}\n")
print(f"@ Message Count: {len(chat_history_reducer.messages)}\n")

# If history was reduced, and the agent uses `ChatHistorySummarizationReducer`,
# print summaries as it will contain the __summary__ metadata key.
if is_reduced and isinstance(agent.history_reducer, ChatHistorySummarizationReducer):
self._print_summaries(chat_history.messages)
if is_reduced and isinstance(chat_history_reducer, ChatHistorySummarizationReducer):
self._print_summaries(chat_history_reducer.messages)

async def invoke_chat(self, agent: ChatCompletionAgent, message_count: int):
async def invoke_chat(
self, agent: ChatCompletionAgent, chat_history_reducer: ChatHistoryReducer, message_count: int
):
"""
Demonstrates agent invocation within a group chat.
Args:
agent: The ChatCompletionAgent to invoke.
chat_history_reducer: The chat history to use for the conversation.
message_count: The number of messages to simulate in the conversation.
"""
chat = AgentGroupChat() # Initialize a new group chat
last_history_count = 0
chat = AgentGroupChat(chat_history=chat_history_reducer) # Initialize a new group chat with the history reducer

# The index is incremented by 2 because the agent is told to:
# "Add one to the latest user number and spell it in Spanish without explanation."
Expand All @@ -157,6 +132,10 @@ async def invoke_chat(self, agent: ChatCompletionAgent, message_count: int):
await chat.add_chat_message(ChatMessageContent(role=AuthorRole.USER, content=str(index)))
print(f"# User: '{index}'")

# Try history reduction
if is_reduced := await chat.reduce_history():
print(f"@ History reduced to {len(chat_history_reducer.messages)} messages.")

# Invoke the agent and display its response
async for message in chat.invoke(agent):
print(f"# {message.role} - {message.name or '*'}: '{message.content}'")
Expand All @@ -168,11 +147,9 @@ async def invoke_chat(self, agent: ChatCompletionAgent, message_count: int):
print(f"@ Message Count: {len(msgs)}\n")

# Check for reduction in message count and print summaries
if len(msgs) < last_history_count and isinstance(agent.history_reducer, ChatHistorySummarizationReducer):
if is_reduced and isinstance(chat_history_reducer, ChatHistorySummarizationReducer):
self._print_summaries(msgs)

last_history_count = len(msgs)

def _print_summaries(self, messages: list[ChatMessageContent]):
"""
Prints summaries from the front of the message list.
Expand Down Expand Up @@ -216,31 +193,47 @@ async def main():
# especially for sensitive interactions like API function calls or complex responses.
reducer_msg_count = 10
reducer_threshold = 10
# create both agents with the same configuration
trunc_agent = example.create_truncating_agent(
reducer_msg_count=reducer_msg_count, reducer_threshold=reducer_threshold
)
sum_agent = example.create_summarizing_agent(
reducer_msg_count=reducer_msg_count, reducer_threshold=reducer_threshold

truncation_reducer = ChatHistoryTruncationReducer(target_count=reducer_msg_count, threshold_count=reducer_threshold)

kernel = _create_kernel_with_chat_completion(service_id="summary")
summarization_reducer = ChatHistorySummarizationReducer(
service=kernel.get_service("summary"), target_count=reducer_msg_count, threshold_count=reducer_threshold
)

# Demonstrate truncation-based reduction
print("===TruncatedAgentReduction Demo===")
await example.invoke_agent(trunc_agent, chat_history=ChatHistory(), message_count=50)
# Demonstrate truncation-based reduction for a single agent
print("===Single Agent Truncated Chat History Reduction Demo===")
await example.invoke_agent(
agent=example.create_chat_completion_agent("truncation_agent"),
chat_history_reducer=truncation_reducer,
message_count=50,
)

# # Demonstrate group chat with truncation
print("\n===TruncatedChatReduction Demo===")
trunc_agent.history_reducer.clear()
await example.invoke_chat(trunc_agent, message_count=50)
# # Demonstrate group chat with a truncation reducer
print("\n===Group Agent Chat Truncated Chat History Reduction Demo===")
truncation_reducer.clear()
await example.invoke_chat(
agent=example.create_chat_completion_agent(service_id="truncation_chat"),
chat_history_reducer=truncation_reducer,
message_count=50,
)

# Demonstrate summarization-based reduction
print("\n===SummarizedAgentReduction Demo===")
await example.invoke_agent(sum_agent, chat_history=ChatHistory(), message_count=50)
# Demonstrate summarization-based reduction for a single agent
print("\n===Single Agent Summarized Chat History Reduction Demo===")
await example.invoke_agent(
agent=example.create_chat_completion_agent(service_id="summary"),
chat_history_reducer=summarization_reducer,
message_count=50,
)

# Demonstrate group chat with summarization
print("\n===SummarizedChatReduction Demo===")
sum_agent.history_reducer.clear()
await example.invoke_chat(sum_agent, message_count=50)
# Demonstrate group chat with a summarization reducer
print("\n===Group Agent Chat Summarized Chat History Reduction Demo===")
summarization_reducer.clear()
await example.invoke_chat(
agent=example.create_chat_completion_agent(service_id="summary"),
chat_history_reducer=summarization_reducer,
message_count=50,
)


# Interaction between reducer_msg_count and reducer_threshold:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@
# The purpose of this sample is to demonstrate how to use a kernel function and use a chat history reducer.
# To build a basic chatbot, it is sufficient to use a ChatCompletionService with a chat history directly.

# Toggle this flag to view the chat history summary after a reduction was performed.
view_chat_history_summary_after_reduction = True

# You can select from the following chat completion services:
# - Services.OPENAI
# - Services.AZURE_OPENAI
Expand Down Expand Up @@ -122,7 +125,8 @@ async def chat() -> bool:
print("\n\nExiting chat...")
return False

await summarization_reducer.reduce()
if is_reduced := await summarization_reducer.reduce():
print(f"@ History reduced to {len(summarization_reducer.messages)} messages.")

kernel_arguments = KernelArguments(
settings=request_settings,
Expand All @@ -136,6 +140,15 @@ async def chat() -> bool:
summarization_reducer.add_user_message(user_input)
summarization_reducer.add_message(answer.value[0])

if view_chat_history_summary_after_reduction and is_reduced:
for msg in summarization_reducer.messages:
if msg.metadata and msg.metadata.get("__summary__"):
print("*" * 60)
print(f"Chat History Reduction Summary: {msg.content}")
print("*" * 60)
break
print("\n")

return True


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
# The purpose of this sample is to demonstrate how to use a kernel function and use a chat history reducer.
# To build a basic chatbot, it is sufficient to use a ChatCompletionService with a chat history directly.

# Toggle this flag to view the chat history summary after a reduction was performed.
view_chat_history_summary_after_reduction = True

# You can select from the following chat completion services:
# - Services.OPENAI
# - Services.AZURE_OPENAI
Expand Down Expand Up @@ -136,7 +139,8 @@ async def chat() -> bool:
print("\n\nExiting chat...")
return False

await summarization_reducer.reduce()
if is_reduced := await summarization_reducer.reduce():
print(f"@ History reduced to {len(summarization_reducer.messages)} messages.")

kernel_arguments = KernelArguments(
settings=request_settings,
Expand Down Expand Up @@ -169,17 +173,26 @@ async def chat() -> bool:
frc.append(item)

for i, item in enumerate(fcc):
summarization_reducer.add_assistant_message_list([item])
summarization_reducer.add_assistant_message([item])
processed_fccs.add(item.id)
# Safely check if there's a matching FunctionResultContent
if i < len(frc):
assert fcc[i].id == frc[i].id # nosec
summarization_reducer.add_tool_message_list([frc[i]])
summarization_reducer.add_tool_message([frc[i]])
processed_frcs.add(item.id)

# Since this example is showing how to include FunctionCallContent and FunctionResultContent
# in the summary, we need to add them to the chat history and also to the processed sets.

if view_chat_history_summary_after_reduction and is_reduced:
for msg in summarization_reducer.messages:
if msg.metadata and msg.metadata.get("__summary__"):
print("*" * 60)
print(f"Chat History Reduction Summary: {msg.content}")
print("*" * 60)
break
print("\n")

return True


Expand Down
43 changes: 1 addition & 42 deletions python/semantic_kernel/agents/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,17 @@
import logging
import uuid
from collections.abc import Iterable
from typing import TYPE_CHECKING, ClassVar
from typing import ClassVar

from pydantic import Field

from semantic_kernel.agents.channels.agent_channel import AgentChannel
from semantic_kernel.contents.history_reducer.chat_history_reducer import ChatHistoryReducer
from semantic_kernel.kernel import Kernel
from semantic_kernel.kernel_pydantic import KernelBaseModel
from semantic_kernel.utils.experimental_decorator import experimental_class
from semantic_kernel.utils.naming import generate_random_ascii_name
from semantic_kernel.utils.validation import AGENT_NAME_REGEX

if TYPE_CHECKING:
from semantic_kernel.contents.chat_history import ChatHistory

logger: logging.Logger = logging.getLogger(__name__)


Expand All @@ -44,39 +40,6 @@ class Agent(KernelBaseModel):
instructions: str | None = None
kernel: Kernel = Field(default_factory=Kernel)
channel_type: ClassVar[type[AgentChannel] | None] = None
history_reducer: ChatHistoryReducer | None = None

async def reduce_history(self, history: "ChatHistory") -> bool:
"""Perform the reduction on the provided history, returning True if reduction occurred.
If the history_reducer is not set, this method will return False.
If the history_reducer is the same object as the history, then it will just call `history.reduce()`,
you could also do that manually in this case.
Otherwise the settings of the history_reducer are used and applied
to the history provided and the history_reducer will be used to reduce the history.
Args:
history: The history to reduce.
Returns:
True if the history was reduced, False otherwise.
The provided history will have been updated if True.
"""
if self.history_reducer is None:
return False

if self.history_reducer is history:
logger.info("You're reducing the same object, you can call `history.reduce()` instead.")
initial_len = len(self.history_reducer)
await self.history_reducer.reduce()
return len(self.history_reducer) < initial_len

self.history_reducer.replace(history)
new_messages = await self.history_reducer.reduce()
if new_messages is None:
return False
history.replace(new_messages)
return True

def get_channel_keys(self) -> Iterable[str]:
"""Get the channel keys.
Expand All @@ -88,10 +51,6 @@ def get_channel_keys(self) -> Iterable[str]:
raise NotImplementedError("Unable to get channel keys. Channel type not configured.")
yield self.channel_type.__name__

if self.history_reducer is not None:
yield self.history_reducer.__class__.__name__
yield str(self.history_reducer.__hash__)

async def create_channel(self) -> AgentChannel:
"""Create a channel.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,6 @@ async def invoke(
f"Invalid channel binding for agent with id: `{id}` with name: ({type(agent).__name__})"
)

# pre-process history reduction
await agent.reduce_history(self)

message_count = len(self.messages)
mutated_history = set()
message_queue: Deque[ChatMessageContent] = deque()
Expand Down Expand Up @@ -122,9 +119,6 @@ async def invoke_stream(
f"Invalid channel binding for agent with id: `{id}` with name: ({type(agent).__name__})"
)

# pre-process history reduction
await agent.reduce_history(self)

message_count = len(self.messages)

async for response_message in agent.invoke_stream(self):
Expand Down
Loading

0 comments on commit 92ebbed

Please sign in to comment.