-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Python: Introduce the single, non-group chat completion agent #7116
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
Merged
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
32d2101
Add unit tests for single SK chat completion agent.
moonbox3 135e075
Clean up comments
moonbox3 ee5fd29
Merge branch 'main' into single_sk_agent
moonbox3 403488f
Improve docstrings. Add step2 concept sample.
moonbox3 397b484
Fix streaming unit test.
moonbox3 05c0884
Merge main to branch
moonbox3 44f1fd0
PR feedback
moonbox3 9998e5e
Leverage Pydantic class inits
moonbox3 9edd6da
Make the kernel an attribute of agent base.
moonbox3 6a71a9b
Add streaming concept message to chat history.
moonbox3 dba9860
Improve unit test coverage. Fix order of streaming chat history aggre…
moonbox3 c4dcd61
Fix ruff
moonbox3 6bcecf3
Merge branch 'main' into single_sk_agent
moonbox3 b13e0b1
Fix exception logging
moonbox3 e26fe13
PR feedback
moonbox3 e5e5e02
Address PR feedback
moonbox3 2d2c5a5
Restructure agent classes to make it cleaner.
moonbox3 8fd6af7
Fix test invoke for chat history channel for Windows platform
moonbox3 1aa12aa
Merge branch 'main' into single_sk_agent
moonbox3 255b456
Adjust how we add the chat content
moonbox3 8ed486c
Cleaning up code
moonbox3 d062dea
Merge branch 'main' into single_sk_agent
moonbox3 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # Semantic Kernel Agents - Getting Started | ||
|
|
||
| This project contains a step by step guide to get started with _Semantic Kernel Agents_ in Python. | ||
|
|
||
|
|
||
| #### PyPI: | ||
| - For the use of agents, the minimum allowed Semantic Kernel pypi version is 1.3 # TODO Update | ||
|
|
||
| #### Source | ||
| - [Semantic Kernel Agent Framework](../../../semantic_kernel/agents/) | ||
|
|
||
| ## Examples | ||
|
|
||
| The getting started with agents examples include: | ||
|
|
||
| Example|Description | ||
| ---|--- | ||
| [step1_agent](../agents/step1_agent.py)|How to create and use an agent. | ||
| [step2_plugins](../agents/step2_plugins.py)|How to associate plugins with an agent. | ||
|
|
||
| ## Configuring the Kernel | ||
|
|
||
| Similar to the Semantic Kernel Python concept samples, it is necessary to configure the secrets | ||
| and keys used by the kernel. See the follow "Configuring the Kernel" [guide](../README.md#configuring-the-kernel) for | ||
| more information. | ||
|
|
||
| ## Running Concept Samples | ||
|
|
||
| Concept samples can be run in an IDE or via the command line. After setting up the required api key | ||
| for your AI connector, the samples run without any extra command line arguments. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| # Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| import asyncio | ||
| from functools import reduce | ||
|
|
||
| from semantic_kernel.agents.chat_completion_agent import ChatCompletionAgent | ||
| from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion | ||
| from semantic_kernel.contents.chat_history import ChatHistory | ||
| from semantic_kernel.contents.utils.author_role import AuthorRole | ||
| from semantic_kernel.kernel import Kernel | ||
|
|
||
| ################################################################### | ||
| # The following sample demonstrates how to create a simple, # | ||
| # non-group agent that repeats the user message in the voice # | ||
| # of a pirate and then ends with a parrot sound. # | ||
| ################################################################### | ||
|
|
||
| # To toggle streaming or non-streaming mode, change the following boolean | ||
| streaming = True | ||
|
|
||
| # Define the agent name and instructions | ||
| PARROT_NAME = "Parrot" | ||
| PARROT_INSTRUCTIONS = "Repeat the user message in the voice of a pirate and then end with a parrot sound." | ||
|
|
||
|
|
||
| async def invoke_agent(agent: ChatCompletionAgent, input: str, chat: ChatHistory): | ||
| """Invoke the agent with the user input.""" | ||
| chat.add_user_message(input) | ||
|
|
||
| print(f"# {AuthorRole.USER}: '{input}'") | ||
|
|
||
| if streaming: | ||
| contents = [] | ||
| content_name = "" | ||
| async for content in agent.invoke_stream(chat): | ||
| content_name = content.name | ||
|
moonbox3 marked this conversation as resolved.
|
||
| contents.append(content) | ||
| streaming_chat_message = reduce(lambda first, second: first + second, contents) | ||
| print(f"# {content.role} - {content_name or '*'}: '{streaming_chat_message}'") | ||
| chat.add_message(content) | ||
| else: | ||
| async for content in agent.invoke(chat): | ||
| print(f"# {content.role} - {content.name or '*'}: '{content.content}'") | ||
|
crickman marked this conversation as resolved.
|
||
| chat.add_message(content) | ||
|
|
||
|
|
||
| async def main(): | ||
| # Create the instance of the Kernel | ||
| kernel = Kernel() | ||
|
|
||
| # Add the OpenAIChatCompletion AI Service to the Kernel | ||
| kernel.add_service(AzureChatCompletion(service_id="agent")) | ||
|
|
||
| # Create the agent | ||
| agent = ChatCompletionAgent(service_id="agent", kernel=kernel, name=PARROT_NAME, instructions=PARROT_INSTRUCTIONS) | ||
|
|
||
| # Define the chat history | ||
| chat = ChatHistory() | ||
|
|
||
| # Respond to user input | ||
| await invoke_agent(agent, "Fortune favors the bold.", chat) | ||
| await invoke_agent(agent, "I came, I saw, I conquered.", chat) | ||
| await invoke_agent(agent, "Practice makes perfect.", chat) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| asyncio.run(main()) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| # Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| import asyncio | ||
| from typing import Annotated | ||
|
|
||
| from semantic_kernel.agents.chat_completion_agent import ChatCompletionAgent | ||
| from semantic_kernel.connectors.ai.function_choice_behavior import FunctionChoiceBehavior | ||
| from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion | ||
| from semantic_kernel.contents.chat_history import ChatHistory | ||
| 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 a simple, # | ||
| # non-group agent that utilizes plugins defined as part of # | ||
| # the Kernel. # | ||
| ################################################################### | ||
|
|
||
| # This sample allows for a streaming response verus a non-streaming response | ||
| streaming = True | ||
|
|
||
| # Define the agent name and instructions | ||
| HOST_NAME = "Host" | ||
| HOST_INSTRUCTIONS = "Answer questions about the menu." | ||
|
|
||
|
|
||
| # Define a sample plugin for the sample | ||
| 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" | ||
|
|
||
|
|
||
| # A helper method to invoke the agent with the user input | ||
| async def invoke_agent(agent: ChatCompletionAgent, input: str, chat: ChatHistory) -> None: | ||
| """Invoke the agent with the user input.""" | ||
| chat.add_user_message(input) | ||
|
|
||
| print(f"# {AuthorRole.USER}: '{input}'") | ||
|
|
||
| if streaming: | ||
| contents = [] | ||
| content_name = "" | ||
| async for content in agent.invoke_stream(chat): | ||
| content_name = content.name | ||
| contents.append(content) | ||
| message_content = "".join([content.content for content in contents]) | ||
| print(f"# {content.role} - {content_name or '*'}: '{message_content}'") | ||
| chat.add_assistant_message(message_content) | ||
| else: | ||
| async for content in agent.invoke(chat): | ||
| print(f"# {content.role} - {content.name or '*'}: '{content.content}'") | ||
| chat.add_message(content) | ||
|
|
||
|
|
||
| async def main(): | ||
| # Create the instance of the Kernel | ||
| kernel = Kernel() | ||
|
|
||
| # Add the OpenAIChatCompletion AI Service to the Kernel | ||
| service_id = "agent" | ||
| kernel.add_service(AzureChatCompletion(service_id=service_id)) | ||
|
|
||
| settings = kernel.get_prompt_execution_settings_from_service_id(service_id=service_id) | ||
| # Configure the function choice behavior to auto invoke kernel functions | ||
| settings.function_choice_behavior = FunctionChoiceBehavior.Auto() | ||
|
|
||
| kernel.add_plugin(plugin=MenuPlugin(), plugin_name="menu") | ||
|
|
||
| # Create the agent | ||
| agent = ChatCompletionAgent( | ||
| service_id="agent", kernel=kernel, name=HOST_NAME, instructions=HOST_INSTRUCTIONS, execution_settings=settings | ||
| ) | ||
|
|
||
| # Define the chat history | ||
| chat = ChatHistory() | ||
|
|
||
| # Respond to user input | ||
| await invoke_agent(agent, "Hello", chat) | ||
| await invoke_agent(agent, "What is the special soup?", chat) | ||
| await invoke_agent(agent, "What is the special drink?", chat) | ||
| await invoke_agent(agent, "Thank you", chat) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| asyncio.run(main()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| from semantic_kernel.agents.chat_completion_agent import ChatCompletionAgent | ||
|
|
||
| __all__ = [ | ||
|
moonbox3 marked this conversation as resolved.
|
||
| "ChatCompletionAgent", | ||
| ] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| import uuid | ||
| from abc import ABC | ||
| from typing import ClassVar | ||
|
|
||
| from pydantic import Field | ||
|
|
||
| from semantic_kernel.agents.agent_channel import AgentChannel | ||
| from semantic_kernel.kernel import Kernel | ||
| from semantic_kernel.kernel_pydantic import KernelBaseModel | ||
| from semantic_kernel.utils.experimental_decorator import experimental_class | ||
|
|
||
|
|
||
| @experimental_class | ||
| class Agent(ABC, KernelBaseModel): | ||
|
moonbox3 marked this conversation as resolved.
moonbox3 marked this conversation as resolved.
|
||
| """Base abstraction for all Semantic Kernel agents. | ||
|
|
||
| An agent instance may participate in one or more conversations. | ||
| A conversation may include one or more agents. | ||
| In addition to identity and descriptive meta-data, an Agent | ||
| must define its communication protocol, or AgentChannel. | ||
|
|
||
| Attributes: | ||
| name: The name of the agent (optional). | ||
| description: The description of the agent (optional). | ||
| id: The unique identifier of the agent (optional). If no id is provided, | ||
| a new UUID will be generated. | ||
| instructions: The instructions for the agent (optional | ||
| """ | ||
|
|
||
| id: str = Field(default_factory=lambda: str(uuid.uuid4())) | ||
| description: str | None = None | ||
| name: str | None = None | ||
| instructions: str | None = None | ||
| kernel: Kernel = Field(default_factory=Kernel) | ||
| channel_type: ClassVar[type[AgentChannel] | None] = None | ||
|
|
||
| def get_channel_keys(self) -> list[str]: | ||
| """Get the channel keys. | ||
|
|
||
| Returns: | ||
| A list of channel keys. | ||
| """ | ||
| if not self.channel_type: | ||
| raise NotImplementedError("Unable to get channel keys. Channel type not configured.") | ||
| return [self.channel_type.__name__] | ||
|
|
||
| def create_channel(self) -> AgentChannel: | ||
| """Create a channel. | ||
|
|
||
| Returns: | ||
| An instance of AgentChannel. | ||
| """ | ||
| if not self.channel_type: | ||
| raise NotImplementedError("Unable to create channel. Channel type not configured.") | ||
| return self.channel_type() | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| # Copyright (c) Microsoft. All rights reserved. | ||
|
|
||
| from abc import ABC, abstractmethod | ||
| from collections.abc import AsyncIterable | ||
| from typing import TYPE_CHECKING | ||
|
|
||
| from semantic_kernel.utils.experimental_decorator import experimental_class | ||
|
|
||
| if TYPE_CHECKING: | ||
| from semantic_kernel.agents.agent import Agent | ||
| from semantic_kernel.contents.chat_message_content import ChatMessageContent | ||
|
|
||
|
|
||
| @experimental_class | ||
| class AgentChannel(ABC): | ||
| """Defines the communication protocol for a particular Agent type. | ||
|
|
||
| An agent provides it own AgentChannel via CreateChannel. | ||
| """ | ||
|
|
||
| @abstractmethod | ||
| async def receive( | ||
| self, | ||
| history: list["ChatMessageContent"], | ||
| ) -> None: | ||
| """Receive the conversation messages. | ||
|
|
||
| Used when joining a conversation and also during each agent interaction. | ||
|
|
||
| Args: | ||
| history: The history of messages in the conversation. | ||
| """ | ||
| ... | ||
|
|
||
| @abstractmethod | ||
| def invoke( | ||
| self, | ||
| agent: "Agent", | ||
| ) -> AsyncIterable["ChatMessageContent"]: | ||
| """Perform a discrete incremental interaction between a single Agent and AgentChat. | ||
|
|
||
| Args: | ||
| agent: The agent to interact with. | ||
|
|
||
| Returns: | ||
| An async iterable of ChatMessageContent. | ||
| """ | ||
| ... | ||
|
|
||
| @abstractmethod | ||
| def get_history( | ||
| self, | ||
| ) -> AsyncIterable["ChatMessageContent"]: | ||
| """Retrieve the message history specific to this channel. | ||
|
|
||
| Returns: | ||
| An async iterable of ChatMessageContent. | ||
| """ | ||
| ... |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.