Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python: Fix agent group chat bug related to function calling in ChatCompletionAgent #8330

Merged
merged 2 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions python/samples/concepts/agents/mixed_chat_agents_plugins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Copyright (c) Microsoft. All rights reserved.

import asyncio
from typing import Annotated

from semantic_kernel.agents import AgentGroupChat, ChatCompletionAgent
from semantic_kernel.agents.open_ai import OpenAIAssistantAgent
from semantic_kernel.agents.strategies.termination.termination_strategy import TerminationStrategy
from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
from semantic_kernel.contents.chat_message_content import ChatMessageContent
from semantic_kernel.contents.utils.author_role import AuthorRole
from semantic_kernel.functions.kernel_function_decorator import kernel_function
from semantic_kernel.kernel import Kernel

#####################################################################
# The following sample demonstrates how to create an OpenAI #
# assistant using either Azure OpenAI or OpenAI, a chat completion #
# agent and have them participate in a group chat to work towards #
# the user's requirement. The ChatCompletionAgent uses a plugin #
# that is part of the agent group chat. #
#####################################################################


class ApprovalTerminationStrategy(TerminationStrategy):
"""A strategy for determining when an agent should terminate."""

async def should_agent_terminate(self, agent, history):
"""Check if the agent should terminate."""
return "approved" in history[-1].content.lower()


REVIEWER_NAME = "ArtDirector"
REVIEWER_INSTRUCTIONS = """
You are an art director who has opinions about copywriting born of a love for David Ogilvy.
The goal is to determine if the given copy is acceptable to print.
If so, state that it is approved. Only include the word "approved" if it is so.
If not, provide insight on how to refine suggested copy without example.
You should always tie the conversation back to the food specials offered by the plugin.
"""

COPYWRITER_NAME = "CopyWriter"
COPYWRITER_INSTRUCTIONS = """
You are a copywriter with ten years of experience and are known for brevity and a dry humor.
The goal is to refine and decide on the single best copy as an expert in the field.
Only provide a single proposal per response.
You're laser focused on the goal at hand.
Don't waste time with chit chat.
Consider suggestions when refining an idea.
"""


class MenuPlugin:
"""A sample Menu Plugin used for the concept sample."""

@kernel_function(description="Provides a list of specials from the menu.")
def get_specials(self) -> Annotated[str, "Returns the specials from the menu."]:
return """
Special Soup: Clam Chowder
Special Salad: Cobb Salad
Special Drink: Chai Tea
"""

@kernel_function(description="Provides the price of the requested menu item.")
def get_item_price(
self, menu_item: Annotated[str, "The name of the menu item."]
) -> Annotated[str, "Returns the price of the menu item."]:
return "$9.99"


def _create_kernel_with_chat_completion(service_id: str) -> Kernel:
kernel = Kernel()
kernel.add_service(AzureChatCompletion(service_id=service_id))
kernel.add_plugin(plugin=MenuPlugin(), plugin_name="menu")
return kernel


async def main():
try:
kernel = _create_kernel_with_chat_completion("artdirector")
settings = kernel.get_prompt_execution_settings_from_service_id(service_id="artdirector")
# Configure the function choice behavior to auto invoke kernel functions
settings.function_choice_behavior = FunctionChoiceBehavior.Auto()
agent_reviewer = ChatCompletionAgent(
service_id="artdirector",
kernel=kernel,
name=REVIEWER_NAME,
instructions=REVIEWER_INSTRUCTIONS,
execution_settings=settings,
)

agent_writer = await OpenAIAssistantAgent.create(
service_id="copywriter",
kernel=Kernel(),
name=COPYWRITER_NAME,
instructions=COPYWRITER_INSTRUCTIONS,
)

chat = AgentGroupChat(
agents=[agent_writer, agent_reviewer],
termination_strategy=ApprovalTerminationStrategy(agents=[agent_reviewer], maximum_iterations=10),
)

input = "Write copy based on the food specials."

await chat.add_chat_message(ChatMessageContent(role=AuthorRole.USER, content=input))
print(f"# {AuthorRole.USER}: '{input}'")

async for content in chat.invoke():
print(f"# {content.role} - {content.name or '*'}: '{content.content}'")

print(f"# IS COMPLETE: {chat.is_complete}")
finally:
await agent_writer.delete()


if __name__ == "__main__":
asyncio.run(main())
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from collections.abc import AsyncIterable
from typing import TYPE_CHECKING, Any

from semantic_kernel.contents.function_call_content import FunctionCallContent

if sys.version_info >= (3, 12):
from typing import override # pragma: no cover
else:
Expand Down Expand Up @@ -36,6 +38,8 @@ async def receive(self, history: list["ChatMessageContent"]) -> None:
history: The conversation messages.
"""
for message in history:
if any(isinstance(item, FunctionCallContent) for item in message.items):
moonbox3 marked this conversation as resolved.
Show resolved Hide resolved
continue
await create_chat_message(self.client, self.thread_id, message)

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ async def create_chat_message(
Returns:
Message: The message.
"""
if message.role.value not in allowed_message_roles:
if message.role.value not in allowed_message_roles and message.role != AuthorRole.TOOL:
raise AgentExecutionException(
f"Invalid message role `{message.role.value}`. Allowed roles are {allowed_message_roles}."
)
Expand All @@ -56,7 +56,7 @@ async def create_chat_message(

return await client.beta.threads.messages.create(
thread_id=thread_id,
role=message.role.value, # type: ignore
role="assistant" if message.role == AuthorRole.TOOL else message.role.value, # type: ignore
content=message_contents, # type: ignore
)

Expand All @@ -78,6 +78,8 @@ def get_message_contents(message: "ChatMessageContent") -> list[dict[str, Any]]:
"type": "image_file",
"image_file": {"file_id": content.file_id},
})
elif isinstance(content, FunctionResultContent):
contents.append({"type": "text", "text": content.result})
return contents


Expand Down
4 changes: 2 additions & 2 deletions python/tests/unit/agents/test_open_ai_assistant_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -810,9 +810,9 @@ async def test_add_chat_message(
async def test_add_chat_message_invalid_role(
azure_openai_assistant_agent, mock_chat_message_content, openai_unit_test_env
):
mock_chat_message_content.role = AuthorRole.TOOL
mock_chat_message_content.role = AuthorRole.SYSTEM

with pytest.raises(AgentExecutionException, match="Invalid message role `tool`"):
with pytest.raises(AgentExecutionException, match="Invalid message role `system`"):
await azure_openai_assistant_agent.add_chat_message("test_thread_id", mock_chat_message_content)


Expand Down
Loading