chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
### Understanding Semantic Kernel AI Connectors
|
||||
|
||||
AI Connectors in Semantic Kernel are components that facilitate communication between the Kernel's core functionalities and various AI services. They abstract the intricate details of service-specific protocols, allowing developers to seamlessly interact with AI services for tasks like text generation, chat interactions, and more.
|
||||
|
||||
### Using AI Connectors in Semantic Kernel
|
||||
|
||||
Developers utilize AI connectors to connect their applications to different AI services efficiently. The connectors manage the requests and responses, providing a streamlined way to leverage the power of these AI services without needing to handle the specific communication protocols each service requires.
|
||||
|
||||
### Creating Custom AI Connectors in Semantic Kernel
|
||||
|
||||
To create a custom AI connector in Semantic Kernel, one must extend the base classes provided, such as `ChatCompletionClientBase` and `AIServiceClientBase`. Below is a guide and example for implementing a mock AI connector:
|
||||
|
||||
#### Step-by-Step Walkthrough
|
||||
|
||||
1. **Understand the Base Classes**: The foundational classes `ChatCompletionClientBase` and `AIServiceClientBase` provide necessary methods and structures for creating chat-based AI connectors.
|
||||
|
||||
2. **Implementing the Connector**: Here's a mock implementation example illustrating how to implement a connector without real service dependencies, ensuring compatibility with Pydantic's expectations within the framework:
|
||||
|
||||
```python
|
||||
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
|
||||
|
||||
class MockAIChatCompletionService(ChatCompletionClientBase):
|
||||
def __init__(self, ai_model_id: str):
|
||||
super().__init__(ai_model_id=ai_model_id)
|
||||
|
||||
async def _inner_get_chat_message_contents(self, chat_history, settings):
|
||||
# Mock implementation: returns dummy chat message content for demonstration.
|
||||
return [{"role": "assistant", "content": "Mock response based on your history."}]
|
||||
|
||||
def service_url(self):
|
||||
return "http://mock-ai-service.com"
|
||||
```
|
||||
|
||||
### Usage Example
|
||||
|
||||
The following example demonstrates how to integrate and use the `MockAIChatCompletionService` in an application:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from semantic_kernel.contents.chat_history import ChatHistory
|
||||
from semantic_kernel.connectors.ai.prompt_execution_settings import PromptExecutionSettings
|
||||
|
||||
async def main():
|
||||
chat_history = ChatHistory(messages=[{"role": "user", "content": "Hello"}])
|
||||
settings = PromptExecutionSettings(model="mock-model")
|
||||
|
||||
service = MockAIChatCompletionService(ai_model_id="mock-model")
|
||||
|
||||
response = await service.get_chat_message_contents(chat_history, settings)
|
||||
print(response)
|
||||
|
||||
# Run the main function
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
### Conclusion
|
||||
|
||||
By following the revised guide and understanding the base class functionalities, developers can effectively create custom connectors within Semantic Kernel. This structured approach enhances integration with various AI services while ensuring alignment with the framework's architectural expectations. Custom connectors offer flexibility, allowing developers to adjust implementations to meet specific service needs, such as additional logging, authentication, or modifications tailored to specific protocols. This guide provides a strong foundation upon which more complex and service-specific extensions can be built, promoting robust and scalable AI service integration.
|
||||
@@ -0,0 +1,117 @@
|
||||
# Document Generator
|
||||
|
||||
This sample app demonstrates how to create technical documents for a codebase using AI. More specifically, it uses the agent framework offered by **Semantic Kernel** to ochestrate multiple agents to create a technical document.
|
||||
|
||||
This sample app also provides telemetry to monitor the agents, making it easier to observe the inner workings of the agents.
|
||||
|
||||
To learn more about agents, please refer to this introduction [video](https://learn.microsoft.com/en-us/shows/generative-ai-for-beginners/ai-agents-generative-ai-for-beginners).
|
||||
To learn more about the Semantic Kernel Agent Framework, please refer to the [Semantic Kernel documentation](https://learn.microsoft.com/en-us/semantic-kernel/frameworks/agent/agent-architecture?pivots=programming-language-python).
|
||||
|
||||
> Note: This sample app cannot guarantee to generate a perfect technical document each time due to the stochastic nature of the AI model. Please a version of the document generated by the app in [GENERATED_DOCUMENT.md](GENERATED_DOCUMENT.md).
|
||||
|
||||
## Design
|
||||
|
||||
### Tools/Plugins
|
||||
|
||||
- **Code Execution Plugin**: This plugin offers a sandbox environment to execute Python snippets. It returns the output of the program or errors if any.
|
||||
- **Repository File Plugin**: This plugin allows the AI to retrieve files from the Semantic Kernel repository.
|
||||
- **User Input Plugin**: This plugin allows the AI to present content to the user and receive feedback.
|
||||
|
||||
### Agents
|
||||
|
||||
- **Content Creation Agent**: This agent is responsible for creating the content of the document. This agent has access to the **Repository File Plugin** to read source files it deems necessary for reference.
|
||||
- **Code Validation Agent**: This agent is responsible for validating the code snippets in the document. This agent has access to the **Code Execution Plugin** to execute the code snippets.
|
||||
- **User Agent**: This agent is responsible for interacting with the user. This agent has access to the **User Input Plugin** to present content to the user and receive feedback.
|
||||
|
||||
### Agent Selection Strategy
|
||||
|
||||
### Termination Strategy
|
||||
|
||||
## Prerequisites
|
||||
|
||||
1. Azure OpenAI
|
||||
2. Azure Application Insights
|
||||
|
||||
## Additional packages
|
||||
|
||||
- `AICodeSandbox` - for executing AI generated code in a sandbox environment
|
||||
|
||||
```bash
|
||||
pip install ai-code-sandbox
|
||||
```
|
||||
|
||||
> You must also have `docker` installed and running on your machine. Follow the instructions [here](https://docs.docker.com/get-started/introduction/get-docker-desktop/) to install docker for your platform. Images will be pulled during runtime if not already present. Containers will be created and destroyed during code execution.
|
||||
|
||||
## Running the app
|
||||
|
||||
### Step 1: Set up the environment
|
||||
|
||||
The Document Generator demo currently supports two different AI Services. If integrating into your own code, other AI services can be used. See Semantic Kernel's Learn Site [page](https://learn.microsoft.com/en-us/semantic-kernel/concepts/ai-services/chat-completion/?tabs=csharp-AzureOpenAI%2Cpython-AzureOpenAI%2Cjava-AzureOpenAI&pivots=programming-language-python) about other available Chat Completion services.
|
||||
|
||||
#### OpenAI
|
||||
|
||||
Make sure you have the following environment variables set:
|
||||
|
||||
```bash
|
||||
OPENAI_CHAT_MODEL_ID=<model-id>
|
||||
OPENAI_API_KEY=<your-key>
|
||||
```
|
||||
|
||||
> gpt-4o-2024-08-06 was used to generate [GENERATED_DOCUMENT.md](GENERATED_DOCUMENT.md).
|
||||
> Feel free to use other models from OpenAI or other providers. When you use models from another provider, make sure to update the chat completion services accordingly.
|
||||
|
||||
#### Azure OpenAI
|
||||
|
||||
```bash
|
||||
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME=<deployment-name>
|
||||
AZURE_OPENAI_ENDPOINT=<endpoint> # in the form of `https://<resource>.openai.azure.com/`
|
||||
AZURE_OPENAI_API_KEY=<api-key> # only required if using api key auth
|
||||
AZURE_OPENAI_API_VERSION=<api-version> # optional, defaults to the latest Azure OpenAI GA API version of `2024-10-21` if not provided
|
||||
```
|
||||
|
||||
```bash
|
||||
### Step 2: Run the app
|
||||
|
||||
```bash
|
||||
python ./main.py
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```bash
|
||||
==== ContentCreationAgent just responded ====
|
||||
==== CodeValidationAgent just responded ====
|
||||
==== ContentCreationAgent just responded ====
|
||||
...
|
||||
```
|
||||
|
||||
## Customization
|
||||
|
||||
Since this is a sample app that demonstrates the creation of a technical document on Semantic Kernel AI connectors, you can customize the app to suit your needs. You can try different tasks, add more agents, tune existing agents, change the agent selection strategy, or modify the termination strategy.
|
||||
|
||||
- To try a different task, modify the `TASK` prompt in `main.py`.
|
||||
- To add more agents, create a new agent under `agents/` and add it to the `agents` list in `main.py`.
|
||||
- To tune existing agents, modify the `INSTRUCTION` prompt in the agent's source code.
|
||||
- To change the agent selection strategy, modify `custom_selection_strategy.py`.
|
||||
- To change the termination strategy, modify `custom_termination_strategy.py`.
|
||||
|
||||
## Optional: Monitoring the agents
|
||||
|
||||
When you see the final document generated by the app, what you see is actually the creation of multiple agents working together. You may wonder, how did the agents work together to create the document? What was the sequence of actions taken by the agents? How did the agents interact with each other? To answer these questions, you need to **observe** the agents.
|
||||
|
||||
Semantic Kernel by default instruments all the LLM calls. However, for agents there is no default instrumentation. This sample app shows how one can extend the Semantic Kernel agent to add instrumentation.
|
||||
|
||||
> There are currently no standards on what information needs to be captured for agents as the concept of agents is still relatively new. At the time of writing, the Semantic Convention for agents is still in the draft stage: <https://github.com/open-telemetry/semantic-conventions/issues/1732>
|
||||
|
||||
To monitor the agents, set the following environment variables:
|
||||
|
||||
```env
|
||||
AZURE_APP_INSIGHTS_CONNECTION_STRING=<your-connection-string>
|
||||
|
||||
SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS=true
|
||||
SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE=true
|
||||
```
|
||||
|
||||
Follow this guide to inspect the telemetry data: <https://learn.microsoft.com/en-us/semantic-kernel/concepts/enterprise-readiness/observability/telemetry-with-app-insights?tabs=Powershell&pivots=programming-language-python#inspect-telemetry-data>
|
||||
|
||||
Or follow this guide to visualize the telemetry data on Azure AI Foundry: <https://learn.microsoft.com/en-us/semantic-kernel/concepts/enterprise-readiness/observability/telemetry-with-azure-ai-foundry-tracing#visualize-traces-on-azure-ai-foundry-tracing-ui-1>
|
||||
@@ -0,0 +1,67 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import sys
|
||||
from collections.abc import AsyncIterable, Awaitable, 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 samples.demos.document_generator.agents.custom_agent_base import CustomAgentBase, Services
|
||||
from samples.demos.document_generator.plugins.code_execution_plugin import CodeExecutionPlugin
|
||||
from semantic_kernel.contents import ChatMessageContent
|
||||
from semantic_kernel.functions import KernelArguments
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.agents.agent import AgentResponseItem, AgentThread
|
||||
from semantic_kernel.kernel import Kernel
|
||||
|
||||
INSTRUCTION = """
|
||||
You are a code validation agent in a collaborative document creation chat.
|
||||
|
||||
Your task is to validate Python code in the latest document draft and summarize any errors.
|
||||
Follow the instructions in the document to assemble the code snippets into a single Python script.
|
||||
If the snippets in the document are from multiple scripts, you need to modify them to work together as a single script.
|
||||
Execute the code to validate it. If there are errors, summarize the error messages.
|
||||
|
||||
Do not try to fix the errors.
|
||||
"""
|
||||
|
||||
DESCRIPTION = """
|
||||
Select me to validate the Python code in the latest document draft.
|
||||
"""
|
||||
|
||||
|
||||
class CodeValidationAgent(CustomAgentBase):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
service=self._create_ai_service(Services.AZURE_OPENAI),
|
||||
plugins=[CodeExecutionPlugin()],
|
||||
name="CodeValidationAgent",
|
||||
instructions=INSTRUCTION.strip(),
|
||||
description=DESCRIPTION.strip(),
|
||||
)
|
||||
|
||||
@override
|
||||
async def invoke(
|
||||
self,
|
||||
*,
|
||||
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
|
||||
thread: "AgentThread | None" = None,
|
||||
on_intermediate_message: Callable[[ChatMessageContent], Awaitable[None]] | None = None,
|
||||
arguments: KernelArguments | None = None,
|
||||
kernel: "Kernel | None" = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable["AgentResponseItem[ChatMessageContent]"]:
|
||||
async for response in super().invoke(
|
||||
messages=messages,
|
||||
thread=thread,
|
||||
on_intermediate_message=on_intermediate_message,
|
||||
arguments=arguments,
|
||||
kernel=kernel,
|
||||
additional_user_message="Now validate the Python code in the latest document draft and summarize any errors.", # noqa: E501
|
||||
**kwargs,
|
||||
):
|
||||
yield response
|
||||
@@ -0,0 +1,64 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import sys
|
||||
from collections.abc import AsyncIterable, Awaitable, 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 samples.demos.document_generator.agents.custom_agent_base import CustomAgentBase, Services
|
||||
from samples.demos.document_generator.plugins.repo_file_plugin import RepoFilePlugin
|
||||
from semantic_kernel.contents import ChatMessageContent
|
||||
from semantic_kernel.functions import KernelArguments
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.agents import AgentResponseItem, AgentThread
|
||||
from semantic_kernel.kernel import Kernel
|
||||
|
||||
INSTRUCTION = """
|
||||
You are part of a chat with multiple agents focused on creating technical content.
|
||||
|
||||
Your task is to generate informative and engaging technical content,
|
||||
including code snippets to explain concepts or demonstrate features.
|
||||
Incorporate feedback by providing the updated full content with changes.
|
||||
"""
|
||||
|
||||
DESCRIPTION = """
|
||||
Select me to generate new content or to revise existing content.
|
||||
"""
|
||||
|
||||
|
||||
class ContentCreationAgent(CustomAgentBase):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
service=self._create_ai_service(Services.AZURE_OPENAI),
|
||||
plugins=[RepoFilePlugin()],
|
||||
name="ContentCreationAgent",
|
||||
instructions=INSTRUCTION.strip(),
|
||||
description=DESCRIPTION.strip(),
|
||||
)
|
||||
|
||||
@override
|
||||
async def invoke(
|
||||
self,
|
||||
*,
|
||||
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
|
||||
thread: "AgentThread | None" = None,
|
||||
on_intermediate_message: Callable[[ChatMessageContent], Awaitable[None]] | None = None,
|
||||
arguments: KernelArguments | None = None,
|
||||
kernel: "Kernel | None" = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable["AgentResponseItem[ChatMessageContent]"]:
|
||||
async for response in super().invoke(
|
||||
messages=messages,
|
||||
thread=thread,
|
||||
on_intermediate_message=on_intermediate_message,
|
||||
arguments=arguments,
|
||||
kernel=kernel,
|
||||
additional_user_message="Now generate new content or revise existing content to incorporate feedback.",
|
||||
**kwargs,
|
||||
):
|
||||
yield response
|
||||
@@ -0,0 +1,122 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import sys
|
||||
from abc import ABC
|
||||
from collections.abc import AsyncIterable, Awaitable, Callable
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
|
||||
from semantic_kernel.contents.utils.author_role import AuthorRole
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import override # pragma: no cover
|
||||
else:
|
||||
from typing_extensions import override # pragma: no cover
|
||||
|
||||
from semantic_kernel.agents import ChatCompletionAgent
|
||||
from semantic_kernel.contents import ChatMessageContent
|
||||
from semantic_kernel.functions import KernelArguments
|
||||
from semantic_kernel.kernel import Kernel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.agents import AgentResponseItem, AgentThread
|
||||
|
||||
|
||||
class Services(str, Enum):
|
||||
"""Enum for supported chat completion services.
|
||||
|
||||
For service specific settings, refer to this documentation:
|
||||
https://learn.microsoft.com/en-us/semantic-kernel/concepts/ai-services/chat-completion
|
||||
"""
|
||||
|
||||
OPENAI = "openai"
|
||||
AZURE_OPENAI = "azure_openai"
|
||||
|
||||
|
||||
class CustomAgentBase(ChatCompletionAgent, ABC):
|
||||
def _create_ai_service(
|
||||
self, service: Services = Services.AZURE_OPENAI, instruction_role: Literal["system", "developer"] = "system"
|
||||
) -> ChatCompletionClientBase:
|
||||
"""Create an AI service for the agent.
|
||||
|
||||
Note: if using Azure OpenAI, ensure the following environment variables are present in your .env file:
|
||||
- AZURE_OPENAI_CHAT_DEPLOYMENT_NAME
|
||||
- AZURE_OPENAI_ENDPOINT
|
||||
- AZURE_OPENAI_API_KEY (if using Azure OpenAI API key authentication)
|
||||
- AZURE_OPENAI_API_VERSION
|
||||
|
||||
If using OpenAI, ensure the following environment variables are present in your .env file:
|
||||
- OPENAI_API_KEY
|
||||
- OPENAI_CHAT_MODEL_ID
|
||||
|
||||
Args:
|
||||
service (Services): The AI service to use.
|
||||
instruction_role (str): The role of the instruction in the chat completion request.
|
||||
Can be either "system" or "developer". Defaults to "system".
|
||||
|
||||
Returns:
|
||||
ChatCompletionClientBase: The AI service instance.
|
||||
|
||||
"""
|
||||
|
||||
match service:
|
||||
case Services.AZURE_OPENAI:
|
||||
from azure.identity import AzureCliCredential
|
||||
|
||||
from semantic_kernel.connectors.ai.open_ai import AzureChatCompletion
|
||||
|
||||
return AzureChatCompletion(instruction_role=instruction_role, credential=AzureCliCredential())
|
||||
case Services.OPENAI:
|
||||
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
|
||||
|
||||
return OpenAIChatCompletion(instruction_role=instruction_role)
|
||||
case _:
|
||||
raise ValueError(
|
||||
f"Unsupported service: {service}. Supported services are: {', '.join([s.value for s in Services])}"
|
||||
)
|
||||
|
||||
@override
|
||||
async def invoke(
|
||||
self,
|
||||
*,
|
||||
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
|
||||
thread: "AgentThread | None" = None,
|
||||
on_intermediate_message: Callable[[ChatMessageContent], Awaitable[None]] | None = None,
|
||||
arguments: KernelArguments | None = None,
|
||||
kernel: "Kernel | None" = None,
|
||||
additional_user_message: str | None = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable["AgentResponseItem[ChatMessageContent]"]:
|
||||
normalized_messages = self._normalize_messages(messages)
|
||||
|
||||
if additional_user_message:
|
||||
normalized_messages.append(ChatMessageContent(role=AuthorRole.USER, content=additional_user_message))
|
||||
|
||||
# Filter out empty or function-only messages to avoid polluting context
|
||||
messages_to_pass = [m for m in normalized_messages if m.content]
|
||||
|
||||
async for response in super().invoke(
|
||||
messages=messages_to_pass, # type: ignore
|
||||
thread=thread,
|
||||
on_intermediate_message=on_intermediate_message,
|
||||
arguments=arguments,
|
||||
kernel=kernel,
|
||||
**kwargs,
|
||||
):
|
||||
yield response
|
||||
|
||||
def _normalize_messages(
|
||||
self, messages: str | ChatMessageContent | list[str | ChatMessageContent] | None
|
||||
) -> list[ChatMessageContent]:
|
||||
if messages is None:
|
||||
return []
|
||||
if isinstance(messages, (str, ChatMessageContent)):
|
||||
messages = [messages]
|
||||
normalized: list[ChatMessageContent] = []
|
||||
for msg in messages:
|
||||
if isinstance(msg, str):
|
||||
normalized.append(ChatMessageContent(role=AuthorRole.USER, content=msg))
|
||||
else:
|
||||
normalized.append(msg)
|
||||
return normalized
|
||||
@@ -0,0 +1,65 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import sys
|
||||
from collections.abc import AsyncIterable, Awaitable, 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 samples.demos.document_generator.agents.custom_agent_base import CustomAgentBase, Services
|
||||
from samples.demos.document_generator.plugins.user_plugin import UserPlugin
|
||||
from semantic_kernel.contents import ChatMessageContent
|
||||
from semantic_kernel.functions import KernelArguments
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.agents.agent import AgentResponseItem, AgentThread
|
||||
from semantic_kernel.kernel import Kernel
|
||||
|
||||
INSTRUCTION = """
|
||||
You are part of a chat with multiple agents working on a document.
|
||||
|
||||
Your task is to summarize the user's feedback on the latest draft from the author agent.
|
||||
Present the draft to the user and summarize their feedback.
|
||||
|
||||
Do not try to address the user's feedback in this chat.
|
||||
"""
|
||||
|
||||
DESCRIPTION = """
|
||||
Select me if you want to ask the user to review the latest draft for publication.
|
||||
"""
|
||||
|
||||
|
||||
class UserAgent(CustomAgentBase):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
service=self._create_ai_service(Services.AZURE_OPENAI),
|
||||
plugins=[UserPlugin()],
|
||||
name="UserAgent",
|
||||
instructions=INSTRUCTION.strip(),
|
||||
description=DESCRIPTION.strip(),
|
||||
)
|
||||
|
||||
@override
|
||||
async def invoke(
|
||||
self,
|
||||
*,
|
||||
messages: str | ChatMessageContent | list[str | ChatMessageContent] | None = None,
|
||||
thread: "AgentThread | None" = None,
|
||||
on_intermediate_message: Callable[[ChatMessageContent], Awaitable[None]] | None = None,
|
||||
arguments: KernelArguments | None = None,
|
||||
kernel: "Kernel | None" = None,
|
||||
**kwargs: Any,
|
||||
) -> AsyncIterable["AgentResponseItem[ChatMessageContent]"]:
|
||||
async for response in super().invoke(
|
||||
messages=messages,
|
||||
thread=thread,
|
||||
on_intermediate_message=on_intermediate_message,
|
||||
arguments=arguments,
|
||||
kernel=kernel,
|
||||
additional_user_message="Now present the latest draft to the user for feedback and summarize their feedback.", # noqa: E501
|
||||
**kwargs,
|
||||
):
|
||||
yield response
|
||||
@@ -0,0 +1,100 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import TYPE_CHECKING, ClassVar
|
||||
|
||||
from opentelemetry import trace
|
||||
from pydantic import Field
|
||||
|
||||
from semantic_kernel.agents.strategies.selection.selection_strategy import SelectionStrategy
|
||||
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
|
||||
from semantic_kernel.connectors.ai.open_ai import (
|
||||
AzureChatPromptExecutionSettings,
|
||||
OpenAIChatCompletion,
|
||||
)
|
||||
from semantic_kernel.contents import ChatHistory
|
||||
from semantic_kernel.utils.feature_stage_decorator import experimental
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.agents import Agent
|
||||
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
||||
|
||||
NEWLINE = "\n"
|
||||
|
||||
|
||||
@experimental
|
||||
class CustomSelectionStrategy(SelectionStrategy):
|
||||
"""A selection strategy that selects the next agent intelligently."""
|
||||
|
||||
NUM_OF_RETRIES: ClassVar[int] = 3
|
||||
|
||||
chat_completion_service: ChatCompletionClientBase = Field(default_factory=lambda: OpenAIChatCompletion())
|
||||
|
||||
async def next(self, agents: list["Agent"], history: list["ChatMessageContent"]) -> "Agent":
|
||||
"""Select the next agent to interact with.
|
||||
|
||||
Args:
|
||||
agents: The list of agents to select from.
|
||||
history: The history of messages in the conversation.
|
||||
|
||||
Returns:
|
||||
The next agent to interact with.
|
||||
"""
|
||||
if len(agents) == 0:
|
||||
raise ValueError("No agents to select from")
|
||||
|
||||
tracer = trace.get_tracer(__name__)
|
||||
with tracer.start_as_current_span("selection_strategy"):
|
||||
chat_history = ChatHistory(system_message=self.get_system_message(agents).strip())
|
||||
|
||||
for message in history:
|
||||
content = message.content
|
||||
# We don't want to add messages whose text content is empty.
|
||||
# Those messages are likely messages from function calls and function results.
|
||||
if content:
|
||||
chat_history.add_message(message)
|
||||
|
||||
chat_history.add_user_message("Now follow the rules and select the next agent by typing the agent's index.")
|
||||
|
||||
for _ in range(self.NUM_OF_RETRIES):
|
||||
completion = await self.chat_completion_service.get_chat_message_content(
|
||||
chat_history,
|
||||
AzureChatPromptExecutionSettings(),
|
||||
)
|
||||
|
||||
if completion is None:
|
||||
continue
|
||||
|
||||
try:
|
||||
return agents[int(completion.content)]
|
||||
except ValueError as ex:
|
||||
chat_history.add_message(completion)
|
||||
chat_history.add_user_message(str(ex))
|
||||
chat_history.add_user_message(f"You must only say a number between 0 and {len(agents) - 1}.")
|
||||
|
||||
raise ValueError("Failed to select an agent since the model did not return a valid index")
|
||||
|
||||
def get_system_message(self, agents: list["Agent"]) -> str:
|
||||
return f"""
|
||||
You are in a multi-agent chat to create a document.
|
||||
Each message in the chat history contains the agent's name and the message content.
|
||||
|
||||
Initially, the chat history may be empty.
|
||||
|
||||
Here are the agents with their indices, names, and descriptions:
|
||||
{NEWLINE.join(f"[{index}] {agent.name}:{NEWLINE}{agent.description}" for index, agent in enumerate(agents))}
|
||||
|
||||
Your task is to select the next agent based on the conversation history.
|
||||
|
||||
The conversation must follow these steps:
|
||||
1. The content creation agent writes a draft.
|
||||
2. The code validation agent checks the code in the draft.
|
||||
3. The content creation agent updates the draft based on the feedback.
|
||||
4. The code validation agent checks the updated code.
|
||||
...
|
||||
If the code validation agent approves the code, the user agent can ask the user for final feedback.
|
||||
N: The user agent provides feedback.
|
||||
(If the feedback is not positive, the conversation goes back to the content creation agent.)
|
||||
|
||||
Respond with a single number between 0 and {len(agents) - 1}, representing the agent's index.
|
||||
Only return the index as an integer.
|
||||
"""
|
||||
@@ -0,0 +1,91 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import TYPE_CHECKING, ClassVar
|
||||
|
||||
from opentelemetry import trace
|
||||
from pydantic import Field
|
||||
|
||||
from semantic_kernel.agents.strategies import TerminationStrategy
|
||||
from semantic_kernel.connectors.ai.chat_completion_client_base import ChatCompletionClientBase
|
||||
from semantic_kernel.connectors.ai.open_ai import (
|
||||
AzureChatPromptExecutionSettings,
|
||||
OpenAIChatCompletion,
|
||||
)
|
||||
from semantic_kernel.contents import ChatHistory
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from semantic_kernel.agents.agent import Agent
|
||||
from semantic_kernel.contents.chat_message_content import ChatMessageContent
|
||||
|
||||
|
||||
TERMINATE_TRUE_KEYWORD = "yes"
|
||||
TERMINATE_FALSE_KEYWORD = "no"
|
||||
|
||||
NEWLINE = "\n"
|
||||
|
||||
|
||||
class CustomTerminationStrategy(TerminationStrategy):
|
||||
NUM_OF_RETRIES: ClassVar[int] = 3
|
||||
|
||||
maximum_iterations: int = 20
|
||||
chat_completion_service: ChatCompletionClientBase = Field(default_factory=lambda: OpenAIChatCompletion())
|
||||
|
||||
async def should_agent_terminate(self, agent: "Agent", history: list["ChatMessageContent"]) -> bool:
|
||||
"""Check if the agent should terminate.
|
||||
|
||||
Args:
|
||||
agent: The agent to check.
|
||||
history: The history of messages in the conversation.
|
||||
"""
|
||||
tracer = trace.get_tracer(__name__)
|
||||
with tracer.start_as_current_span("terminate_strategy"):
|
||||
chat_history = ChatHistory(system_message=self.get_system_message().strip())
|
||||
|
||||
for message in history:
|
||||
content = message.content
|
||||
# We don't want to add messages whose text content is empty.
|
||||
# Those messages are likely messages from function calls and function results.
|
||||
if content:
|
||||
chat_history.add_message(message)
|
||||
|
||||
chat_history.add_user_message(
|
||||
"Is the latest content approved by all agents? "
|
||||
f"Answer with '{TERMINATE_TRUE_KEYWORD}' or '{TERMINATE_FALSE_KEYWORD}'."
|
||||
)
|
||||
|
||||
for _ in range(self.NUM_OF_RETRIES):
|
||||
completion = await self.chat_completion_service.get_chat_message_content(
|
||||
chat_history,
|
||||
AzureChatPromptExecutionSettings(),
|
||||
)
|
||||
|
||||
if not completion:
|
||||
continue
|
||||
|
||||
if TERMINATE_FALSE_KEYWORD in completion.content.lower():
|
||||
return False
|
||||
if TERMINATE_TRUE_KEYWORD in completion.content.lower():
|
||||
return True
|
||||
|
||||
chat_history.add_message(completion)
|
||||
chat_history.add_user_message(
|
||||
f"You must only say either '{TERMINATE_TRUE_KEYWORD}' or '{TERMINATE_FALSE_KEYWORD}'."
|
||||
)
|
||||
|
||||
raise ValueError(
|
||||
"Failed to determine if the agent should terminate because the model did not return a valid response."
|
||||
)
|
||||
|
||||
def get_system_message(self) -> str:
|
||||
return f"""
|
||||
You are in a chat with multiple agents collaborating to create a document.
|
||||
Each message in the chat history contains the agent's name and the message content.
|
||||
|
||||
The chat history may start empty as no agents have spoken yet.
|
||||
|
||||
Here are the agents with their indices, names, and descriptions:
|
||||
{NEWLINE.join(f"[{index}] {agent.name}:{NEWLINE}{agent.description}" for index, agent in enumerate(self.agents))}
|
||||
|
||||
Your task is NOT to continue the conversation. Determine if the latest content is approved by all agents.
|
||||
If approved, say "{TERMINATE_TRUE_KEYWORD}". Otherwise, say "{TERMINATE_FALSE_KEYWORD}".
|
||||
"""
|
||||
@@ -0,0 +1,138 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.sdk.resources import Resource
|
||||
from opentelemetry.semconv.resource import ResourceAttributes
|
||||
|
||||
from samples.demos.document_generator.agents.code_validation_agent import CodeValidationAgent
|
||||
from samples.demos.document_generator.agents.content_creation_agent import ContentCreationAgent
|
||||
from samples.demos.document_generator.agents.user_agent import UserAgent
|
||||
from samples.demos.document_generator.custom_selection_strategy import CustomSelectionStrategy
|
||||
from samples.demos.document_generator.custom_termination_strategy import CustomTerminationStrategy
|
||||
from semantic_kernel.agents import AgentGroupChat
|
||||
from semantic_kernel.contents import AuthorRole, ChatMessageContent
|
||||
|
||||
"""
|
||||
Note: This sample use the `AgentGroupChat` feature of Semantic Kernel, which is
|
||||
no longer maintained. For a replacement, consider using the `GroupChatOrchestration`.
|
||||
Read more about the `GroupChatOrchestration` here:
|
||||
https://learn.microsoft.com/semantic-kernel/frameworks/agent/agent-orchestration/group-chat?pivots=programming-language-python
|
||||
Here is a migration guide from `AgentGroupChat` to `GroupChatOrchestration`:
|
||||
https://learn.microsoft.com/semantic-kernel/support/migration/group-chat-orchestration-migration-guide?pivots=programming-language-python
|
||||
"""
|
||||
|
||||
TASK = """
|
||||
Create a blog post to share technical details about the Semantic Kernel AI connectors.
|
||||
The content of the blog post should include the following:
|
||||
1. What are AI connectors in Semantic Kernel?
|
||||
2. How do people use AI connectors in Semantic Kernel?
|
||||
3. How do devs create custom AI connectors in Semantic Kernel?
|
||||
- Include a walk through of creating a custom AI connector.
|
||||
The connector may not connect to a real service, but should demonstrate the process.
|
||||
- Include a sample on how to use the connector.
|
||||
- If a reader follows the walk through and the sample, they should be able to run the connector.
|
||||
|
||||
|
||||
Here is the file that contains the source code for the base class of the AI connectors:
|
||||
semantic_kernel/connectors/ai/chat_completion_client_base.py
|
||||
semantic_kernel/services/ai_service_client_base.py
|
||||
|
||||
Here are some files containing the source code that may be useful:
|
||||
semantic_kernel/connectors/ai/ollama/services/ollama_chat_completion.py
|
||||
semantic_kernel/connectors/ai/open_ai/services/open_ai_chat_completion_base.py
|
||||
semantic_kernel/contents/chat_history.py
|
||||
|
||||
If you want to reference the implementations of other AI connectors, you can find them under the following directory:
|
||||
semantic_kernel/connectors/ai
|
||||
"""
|
||||
|
||||
load_dotenv()
|
||||
AZURE_APP_INSIGHTS_CONNECTION_STRING = os.getenv("AZURE_APP_INSIGHTS_CONNECTION_STRING")
|
||||
|
||||
resource = Resource.create({ResourceAttributes.SERVICE_NAME: "Document Generator"})
|
||||
|
||||
|
||||
def set_up_tracing():
|
||||
from azure.monitor.opentelemetry.exporter import AzureMonitorTraceExporter
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||
from opentelemetry.trace import set_tracer_provider
|
||||
|
||||
# Initialize a trace provider for the application. This is a factory for creating tracers.
|
||||
tracer_provider = TracerProvider(resource=resource)
|
||||
tracer_provider.add_span_processor(
|
||||
BatchSpanProcessor(AzureMonitorTraceExporter(connection_string=AZURE_APP_INSIGHTS_CONNECTION_STRING))
|
||||
)
|
||||
# Sets the global default tracer provider
|
||||
set_tracer_provider(tracer_provider)
|
||||
|
||||
|
||||
def set_up_logging():
|
||||
from azure.monitor.opentelemetry.exporter import AzureMonitorLogExporter
|
||||
from opentelemetry._logs import set_logger_provider
|
||||
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
|
||||
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
|
||||
|
||||
# Create and set a global logger provider for the application.
|
||||
logger_provider = LoggerProvider(resource=resource)
|
||||
logger_provider.add_log_record_processor(
|
||||
BatchLogRecordProcessor(AzureMonitorLogExporter(connection_string=AZURE_APP_INSIGHTS_CONNECTION_STRING))
|
||||
)
|
||||
# Sets the global default logger provider
|
||||
set_logger_provider(logger_provider)
|
||||
|
||||
# Create a logging handler to write logging records, in OTLP format, to the exporter.
|
||||
handler = LoggingHandler()
|
||||
# Attach the handler to the root logger. `getLogger()` with no arguments returns the root logger.
|
||||
# Events from all child loggers will be processed by this handler.
|
||||
logger = logging.getLogger()
|
||||
logger.addHandler(handler)
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
async def main():
|
||||
if AZURE_APP_INSIGHTS_CONNECTION_STRING:
|
||||
set_up_tracing()
|
||||
set_up_logging()
|
||||
|
||||
tracer = trace.get_tracer(__name__)
|
||||
with tracer.start_as_current_span("main"):
|
||||
agents = [
|
||||
ContentCreationAgent(),
|
||||
UserAgent(),
|
||||
CodeValidationAgent(),
|
||||
]
|
||||
|
||||
group_chat = AgentGroupChat(
|
||||
agents=agents,
|
||||
termination_strategy=CustomTerminationStrategy(agents=agents),
|
||||
selection_strategy=CustomSelectionStrategy(),
|
||||
)
|
||||
await group_chat.add_chat_message(
|
||||
ChatMessageContent(
|
||||
role=AuthorRole.USER,
|
||||
content=TASK.strip(),
|
||||
)
|
||||
)
|
||||
|
||||
async for response in group_chat.invoke():
|
||||
print(f"==== {response.name} just responded ====")
|
||||
# print(response.content)
|
||||
|
||||
content_history: list[ChatMessageContent] = []
|
||||
async for message in group_chat.get_chat_messages(agent=agents[0]):
|
||||
if message.name == agents[0].name:
|
||||
# The chat history contains responses from other agents.
|
||||
content_history.append(message)
|
||||
# The chat history is in descending order.
|
||||
print("Final content:")
|
||||
print(content_history[0].content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,26 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from ai_code_sandbox import AICodeSandbox
|
||||
|
||||
from semantic_kernel.functions import kernel_function
|
||||
|
||||
|
||||
class CodeExecutionPlugin:
|
||||
"""A plugin that runs Python code snippets."""
|
||||
|
||||
@kernel_function(description="Run a Python code snippet. You can assume all the necessary packages are installed.")
|
||||
def run(
|
||||
self, code: Annotated[str, "The Python code snippet."]
|
||||
) -> Annotated[str, "Returns the output of the code."]:
|
||||
"""Run a Python code snippet."""
|
||||
sandbox: AICodeSandbox = AICodeSandbox(
|
||||
custom_image="python:3.12-slim",
|
||||
packages=["semantic_kernel"],
|
||||
)
|
||||
|
||||
try:
|
||||
return sandbox.run_code(code)
|
||||
finally:
|
||||
sandbox.close()
|
||||
@@ -0,0 +1,51 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import os
|
||||
from typing import Annotated
|
||||
|
||||
from semantic_kernel.functions import kernel_function
|
||||
|
||||
|
||||
class RepoFilePlugin:
|
||||
"""A plugin that reads files from this repository.
|
||||
|
||||
This plugin assumes that the code is run within the Semantic Kernel repository.
|
||||
"""
|
||||
|
||||
@kernel_function(description="Read a file given a relative path to the root of the repository.")
|
||||
def read_file_by_path(
|
||||
self, path: Annotated[str, "The relative path to the file."]
|
||||
) -> Annotated[str, "Returns the file content."]:
|
||||
path = os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", path)
|
||||
|
||||
try:
|
||||
with open(path) as file:
|
||||
return file.read()
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError(f"File {path} not found in repository.")
|
||||
|
||||
@kernel_function(
|
||||
description="Read a file given the name of the file. Function will search for the file in the repository."
|
||||
)
|
||||
def read_file_by_name(
|
||||
self, file_name: Annotated[str, "The name of the file."]
|
||||
) -> Annotated[str, "Returns the file content."]:
|
||||
path = os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")
|
||||
for root, dirs, files in os.walk(path):
|
||||
if file_name in files:
|
||||
print(f"Found file {file_name} in {root}.")
|
||||
with open(os.path.join(root, file_name)) as file:
|
||||
return file.read()
|
||||
raise FileNotFoundError(f"File {file_name} not found in repository.")
|
||||
|
||||
@kernel_function(description="List all files or subdirectories in a directory.")
|
||||
def list_directory(
|
||||
self, path: Annotated[str, "Path of a directory relative to the root of the repository."]
|
||||
) -> Annotated[str, "Returns a list of files and subdirectories as a string."]:
|
||||
path = os.path.join(os.path.dirname(__file__), "..", "..", "..", "..", path)
|
||||
try:
|
||||
files = os.listdir(path)
|
||||
# Join the list of files into a single string
|
||||
return "\n".join(files)
|
||||
except FileNotFoundError:
|
||||
raise FileNotFoundError(f"Directory {path} not found in repository.")
|
||||
@@ -0,0 +1,16 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from semantic_kernel.functions import kernel_function
|
||||
|
||||
|
||||
class UserPlugin:
|
||||
"""A plugin that interacts with the user."""
|
||||
|
||||
@kernel_function(description="Present the content to user and request feedback.")
|
||||
def request_user_feedback(
|
||||
self, content: Annotated[str, "The content to present and request feedback on."]
|
||||
) -> Annotated[str, "The feedback provided by the user."]:
|
||||
"""Request user feedback on the content."""
|
||||
return input(f"Please provide feedback on the content:\n\n{content}\n\n> ")
|
||||
Reference in New Issue
Block a user