다음을 통해 공유


방법: 에이전트 그룹 채팅을 사용하여 에이전트 공동 작업 조정

Warning

의미 체계 커널 에이전트 프레임워크는 아직 개발 중이며 변경될 수 있습니다.

개요

이 샘플에서는 에이전트 그룹 채팅을 사용하여 사용자가 제공한 콘텐츠를 검토하고 다시 쓰기 위해 작업하는 두 에이전트의 데이터 정렬을 조정하는 방법을 살펴봅니다. 각 에이전트에는 고유한 역할이 할당됩니다.

  • 검토자: 기록기에 대한 검토 및 지침을 제공합니다.
  • 기록기: 검토자 입력에 따라 사용자 콘텐츠를 업데이트합니다.

이 방법은 코딩 프로세스의 핵심 부분을 밝게 하기 위해 단계별로 세분화됩니다.

시작하기

기능 코딩을 계속하기 전에 개발 환경이 완전히 설정되고 구성되었는지 확인합니다.

먼저 콘솔 프로젝트를 만듭니다. 그런 다음 필요한 모든 종속성을 사용할 수 있도록 다음 패키지 참조를 포함합니다.

명령줄에서 패키지 종속성을 추가하려면 다음 명령을 사용합니다 dotnet .

dotnet add package Azure.Identity
dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Binder
dotnet add package Microsoft.Extensions.Configuration.UserSecrets
dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables
dotnet add package Microsoft.SemanticKernel.Connectors.AzureOpenAI
dotnet add package Microsoft.SemanticKernel.Agents.Core --prerelease

Visual StudioInclude prerelease.

프로젝트 파일(.csproj)에는 다음 PackageReference 정의가 포함되어야 합니다.

  <ItemGroup>
    <PackageReference Include="Azure.Identity" Version="<stable>" />
    <PackageReference Include="Microsoft.Extensions.Configuration" Version="<stable>" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="<stable>" />
    <PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="<stable>" />
    <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="<stable>" />
    <PackageReference Include="Microsoft.SemanticKernel.Agents.Core" Version="<latest>" />
    <PackageReference Include="Microsoft.SemanticKernel.Connectors.AzureOpenAI" Version="<latest>" />
  </ItemGroup>

에이전트 프레임워크실험적이며 경고 표시 안 함이 필요합니다. 프로젝트 파일.csproj()의 속성으로 이 문제를 해결할 수 있습니다.

  <PropertyGroup>
    <NoWarn>$(NoWarn);CA2007;IDE1006;SKEXP0001;SKEXP0110;OPENAI001</NoWarn>
  </PropertyGroup>
import asyncio
import os
import copy
import pyperclip # Install via pip

from semantic_kernel.agents import AgentGroupChat, ChatCompletionAgent
from semantic_kernel.agents.strategies.selection.kernel_function_selection_strategy import (
    KernelFunctionSelectionStrategy,
)
from semantic_kernel.agents.strategies.termination.kernel_function_termination_strategy import (
    KernelFunctionTerminationStrategy,
)
from semantic_kernel.connectors.ai.open_ai.services.azure_chat_completion 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.functions.kernel_function_from_prompt import KernelFunctionFromPrompt
from semantic_kernel.kernel import Kernel

에이전트는 현재 Java에서 사용할 수 없습니다.

구성

이 샘플에서는 원격 서비스에 연결하기 위해 구성 설정이 필요합니다. Open AI 또는 Azure Open AI에 대한 설정을 정의해야 합니다.

# Open AI
dotnet user-secrets set "OpenAISettings:ApiKey" "<api-key>"
dotnet user-secrets set "OpenAISettings:ChatModel" "gpt-4o"

# Azure Open AI
dotnet user-secrets set "AzureOpenAISettings:ApiKey" "<api-key>" # Not required if using token-credential
dotnet user-secrets set "AzureOpenAISettings:Endpoint" "<model-endpoint>"
dotnet user-secrets set "AzureOpenAISettings:ChatModelDeployment" "gpt-4o"

다음 클래스는 모든 에이전트 예제에서 사용됩니다. 적절한 기능을 보장하려면 프로젝트에 포함해야 합니다. 이 클래스는 다음 예제의 기본 구성 요소 역할을 합니다.

using System.Reflection;
using Microsoft.Extensions.Configuration;

namespace AgentsSample;

public class Settings
{
    private readonly IConfigurationRoot configRoot;

    private AzureOpenAISettings azureOpenAI;
    private OpenAISettings openAI;

    public AzureOpenAISettings AzureOpenAI => this.azureOpenAI ??= this.GetSettings<Settings.AzureOpenAISettings>();
    public OpenAISettings OpenAI => this.openAI ??= this.GetSettings<Settings.OpenAISettings>();

    public class OpenAISettings
    {
        public string ChatModel { get; set; } = string.Empty;
        public string ApiKey { get; set; } = string.Empty;
    }

    public class AzureOpenAISettings
    {
        public string ChatModelDeployment { get; set; } = string.Empty;
        public string Endpoint { get; set; } = string.Empty;
        public string ApiKey { get; set; } = string.Empty;
    }

    public TSettings GetSettings<TSettings>() =>
        this.configRoot.GetRequiredSection(typeof(TSettings).Name).Get<TSettings>()!;

    public Settings()
    {
        this.configRoot =
            new ConfigurationBuilder()
                .AddEnvironmentVariables()
                .AddUserSecrets(Assembly.GetExecutingAssembly(), optional: true)
                .Build();
    }
}

샘플 코드를 실행하는 적절한 구성을 시작하는 가장 빠른 방법은 프로젝트의 루트(스크립트가 실행되는 위치)에 파일을 만드는 .env 것입니다.

Azure OpenAI 또는 OpenAI에 대해 파일에서 다음 설정을 구성합니다 .env .

AZURE_OPENAI_API_KEY="..."
AZURE_OPENAI_ENDPOINT="https://..."
AZURE_OPENAI_CHAT_DEPLOYMENT_NAME="..."
AZURE_OPENAI_API_VERSION="..."

OPENAI_API_KEY="sk-..."
OPENAI_ORG_ID=""
OPENAI_CHAT_MODEL_ID=""

구성되면 각 AI 서비스 클래스는 필요한 변수를 선택하고 인스턴스화 중에 사용합니다.

에이전트는 현재 Java에서 사용할 수 없습니다.

코딩

이 샘플의 코딩 프로세스는 다음과 같습니다.

  1. 설치 - 설정 및 플러그 인 초기화
  2. 에이전트 정의 - 두 채팅 완료 에이전트 인스턴스(검토자 및 기록)를 만듭니다.
  3. 채팅 정의 - 에이전트 그룹 채팅 및 관련 전략을 만듭니다.
  4. 채팅 루프 - 사용자/에이전트 상호 작용을 구동하는 루프를 작성합니다.

전체 예제 코드는 최종 섹션에 제공됩니다. 전체 구현은 해당 섹션을 참조하세요.

설정

채팅 완료 에이전트를 만들기 전에 구성 설정, 플러그 인 및 커널을 초기화해야 합니다.

에이전트는 현재 Java에서 사용할 수 없습니다.

이제 .를 사용하여 인스턴스를 Kernel 초기화합니다 IChatCompletionService.

IKernelBuilder builder = Kernel.CreateBuilder();

builder.AddAzureOpenAIChatCompletion(
	settings.AzureOpenAI.ChatModelDeployment,
	settings.AzureOpenAI.Endpoint,
	new AzureCliCredential());

Kernel kernel = builder.Build();

커널 개체를 초기화합니다.

kernel = Kernel()

에이전트는 현재 Java에서 사용할 수 없습니다.

복제를 통해 두 번째 커널 인스턴스를 만들고 리빙이 업데이트된 콘텐츠를 클립보드에 배치할 수 있도록 하는 플러그 인을 추가해 보겠습니다.

Kernel toolKernel = kernel.Clone();
toolKernel.Plugins.AddFromType<ClipboardAccess>();
tool_kernel = copy.deepcopy(kernel)
tool_kernel.add_plugin(ClipboardAccess(), plugin_name="clipboard")

에이전트는 현재 Java에서 사용할 수 없습니다.

클립보드 플러그 인은 샘플의 일부로 정의될 수 있습니다.

private sealed class ClipboardAccess
{
    [KernelFunction]
    [Description("Copies the provided content to the clipboard.")]
    public static void SetClipboard(string content)
    {
        if (string.IsNullOrWhiteSpace(content))
        {
            return;
        }

        using Process clipProcess = Process.Start(
            new ProcessStartInfo
            {
                FileName = "clip",
                RedirectStandardInput = true,
                UseShellExecute = false,
            });

        clipProcess.StandardInput.Write(content);
        clipProcess.StandardInput.Close();
    }
}

참고: pyperclip이라는 Python 패키지를 활용하고 있습니다. pip를 사용하여 설치하세요.

class ClipboardAccess:
    @kernel_function
    def set_clipboard(content: str):
        if not content.strip():
            return

        pyperclip.copy(content)

에이전트는 현재 Java에서 사용할 수 없습니다.

에이전트 정의

에이전트 그룹 채팅const참조될 수 있도록 에이전트 이름을 선언해 보겠습니다.

const string ReviewerName = "Reviewer";
const string WriterName = "Writer";
REVIEWER_NAME = "Reviewer"
COPYWRITER_NAME = "Writer"

에이전트는 현재 Java에서 사용할 수 없습니다.

검토자 에이전트를 정의하면 방법: 채팅 완료 에이전트에서 탐색한 패턴이 사용됩니다.

여기서 검토자는 사용자 입력에 응답하고, 기록기 에이전트에 방향을 제공하고, 기록기 에이전트의 결과를 확인하는 역할을 부여합니다.

ChatCompletionAgent agentReviewer =
    new()
    {
        Name = ReviewerName,
        Instructions =
            """
            Your responsiblity is to review and identify how to improve user provided content.
            If the user has providing input or direction for content already provided, specify how to address this input.
            Never directly perform the correction or provide example.
            Once the content has been updated in a subsequent response, you will review the content again until satisfactory.
            Always copy satisfactory content to the clipboard using available tools and inform user.

            RULES:
            - Only identify suggestions that are specific and actionable.
            - Verify previous suggestions have been addressed.
            - Never repeat previous suggestions.
            """,
        Kernel = toolKernel,
        Arguments =
            new KernelArguments(
                new AzureOpenAIPromptExecutionSettings() 
                { 
                    FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() 
                })
    };
agent_reviewer = ChatCompletionAgent(
    service_id=REVIEWER_NAME,
    kernel=_create_kernel_with_chat_completion(REVIEWER_NAME),
    name=REVIEWER_NAME,
    instructions="""
        Your responsiblity is to review and identify how to improve user provided content.
        If the user has providing input or direction for content already provided, specify how to 
        address this input.
        Never directly perform the correction or provide example.
        Once the content has been updated in a subsequent response, you will review the content 
        again until satisfactory.
        Always copy satisfactory content to the clipboard using available tools and inform user.

        RULES:
        - Only identify suggestions that are specific and actionable.
        - Verify previous suggestions have been addressed.
        - Never repeat previous suggestions.
        """,
)

에이전트는 현재 Java에서 사용할 수 없습니다.

기록기 에이전트는 비슷하지만 플러그 인으로 구성되지 않으므로 실행 설정 사양이 필요하지 않습니다.

여기서 작성기에는 단일 용도 작업이 지정되고, 방향을 따르고, 콘텐츠를 다시 작성합니다.

ChatCompletionAgent agentWriter =
    new()
    {
        Name = WriterName,
        Instructions =
            """
            Your sole responsiblity is to rewrite content according to review suggestions.

            - Always apply all review direction.
            - Always revise the content in its entirety without explanation.
            - Never address the user.
            """,
        Kernel = kernel,
    };
agent_writer = ChatCompletionAgent(
    service_id=COPYWRITER_NAME,
    kernel=_create_kernel_with_chat_completion(COPYWRITER_NAME),
    name=COPYWRITER_NAME,
    instructions="""
        Your sole responsiblity is to rewrite content according to review suggestions.

        - Always apply all review direction.
        - Always revise the content in its entirety without explanation.
        - Never address the user.
        """,
)

에이전트는 현재 Java에서 사용할 수 없습니다.

채팅 정의

에이전트 그룹 채팅정의하려면 에이전트 턴을 선택하고 채팅 루프를 종료할 시기를 결정하는 전략을 고려해야 합니다. 이러한 두 가지 고려 사항에 대해 커널 프롬프트 함수정의합니다.

에이전트 선택에 대해 가장 먼저 추론하는 방법은 다음과 같습니다.

사용 AgentGroupChat.CreatePromptFunctionForStrategy 은 HTML이 메시지 매개 변수를 인코딩하지 않도록 하는 편리한 메커니즘을 제공합니다.

KernelFunction selectionFunction =
    AgentGroupChat.CreatePromptFunctionForStrategy(
        $$$"""
        Examine the provided RESPONSE and choose the next participant.
        State only the name of the chosen participant without explanation.
        Never choose the participant named in the RESPONSE.

        Choose only from these participants:
        - {{{ReviewerName}}}
        - {{{WriterName}}}

        Always follow these rules when choosing the next participant:
        - If RESPONSE is user input, it is {{{ReviewerName}}}'s turn.
        - If RESPONSE is by {{{ReviewerName}}}, it is {{{WriterName}}}'s turn.
        - If RESPONSE is by {{{WriterName}}}, it is {{{ReviewerName}}}'s turn.

        RESPONSE:
        {{$lastmessage}}
        """,
        safeParameterNames: "lastmessage");
selection_function = KernelFunctionFromPrompt(
    function_name="selection",
    prompt=f"""
    Determine which participant takes the next turn in a conversation based on the the most recent participant.
    State only the name of the participant to take the next turn.
    No participant should take more than one turn in a row.

    Choose only from these participants:
    - {REVIEWER_NAME}
    - {COPYWRITER_NAME}

    Always follow these rules when selecting the next participant:
    - After user input, it is {COPYWRITER_NAME}'s turn.
    - After {COPYWRITER_NAME} replies, it is {REVIEWER_NAME}'s turn.
    - After {REVIEWER_NAME} provides feedback, it is {COPYWRITER_NAME}'s turn.

    History:
    {{{{$history}}}}
    """,
)

에이전트는 현재 Java에서 사용할 수 없습니다.

두 번째는 채팅 루프를 종료할 시기를 평가합니다.

const string TerminationToken = "yes";

KernelFunction terminationFunction =
    AgentGroupChat.CreatePromptFunctionForStrategy(
        $$$"""
        Examine the RESPONSE and determine whether the content has been deemed satisfactory.
        If content is satisfactory, respond with a single word without explanation: {{{TerminationToken}}}.
        If specific suggestions are being provided, it is not satisfactory.
        If no correction is suggested, it is satisfactory.

        RESPONSE:
        {{$lastmessage}}
        """,
        safeParameterNames: "lastmessage");
TERMINATION_KEYWORD = "yes"

termination_function = KernelFunctionFromPrompt(
    function_name="termination",
    prompt=f"""
        Examine the RESPONSE and determine whether the content has been deemed satisfactory.
        If content is satisfactory, respond with a single word without explanation: {TERMINATION_KEYWORD}.
        If specific suggestions are being provided, it is not satisfactory.
        If no correction is suggested, it is satisfactory.

        RESPONSE:
        {{{{$history}}}}
        """,
)

에이전트는 현재 Java에서 사용할 수 없습니다.

이러한 두 전략 모두 최신 채팅 메시지에 대한 지식만 필요합니다. 이렇게 하면 토큰 사용량이 줄어들고 성능이 향상됩니다.

ChatHistoryTruncationReducer historyReducer = new(1);
**ChatHistoryReducer is coming soon to Python.**

에이전트는 현재 Java에서 사용할 수 없습니다.

마지막으로 에이전트 그룹 채팅 정의에서 모든 항목을 함께 가져올 준비가 완료되었습니다.

만들기 AgentGroupChat 에는 다음이 포함됩니다.

  1. 생성자에 두 에이전트를 모두 포함합니다.
  2. 이전에 정의한 KernelFunctionSelectionStrategy 인스턴스 및 KernelFunction 사용을 정의 Kernel 합니다.
  3. 이전에 정의한 KernelFunctionTerminationStrategy 인스턴스 및 KernelFunction 사용을 정의 Kernel 합니다.

각 전략은 결과를 구문 분석해야 합니다 KernelFunction .

AgentGroupChat chat =
    new(agentReviewer, agentWriter)
    {
        ExecutionSettings = new AgentGroupChatSettings
        {
            SelectionStrategy =
                new KernelFunctionSelectionStrategy(selectionFunction, kernel)
                {
                    // Always start with the editor agent.
                    InitialAgent = agentReviewer,
                    // Save tokens by only including the final response
                    HistoryReducer = historyReducer,
                    // The prompt variable name for the history argument.
                    HistoryVariableName = "lastmessage",
                    // Returns the entire result value as a string.
                    ResultParser = (result) => result.GetValue<string>() ?? agentReviewer.Name
                },
            TerminationStrategy =
                new KernelFunctionTerminationStrategy(terminationFunction, kernel)
                {
                    // Only evaluate for editor's response
                    Agents = [agentReviewer],
                    // Save tokens by only including the final response
                    HistoryReducer = historyReducer,
                    // The prompt variable name for the history argument.
                    HistoryVariableName = "lastmessage",
                    // Limit total number of turns
                    MaximumIterations = 12,
                    // Customer result parser to determine if the response is "yes"
                    ResultParser = (result) => result.GetValue<string>()?.Contains(TerminationToken, StringComparison.OrdinalIgnoreCase) ?? false
                }
        }
    };

Console.WriteLine("Ready!");

만들기 AgentGroupChat 에는 다음이 포함됩니다.

  1. 생성자에 두 에이전트를 모두 포함합니다.
  2. 이전에 정의한 KernelFunctionSelectionStrategy 인스턴스 및 KernelFunction 사용을 정의 Kernel 합니다.
  3. 이전에 정의한 KernelFunctionTerminationStrategy 인스턴스 및 KernelFunction 사용을 정의 Kernel 합니다.

각 전략은 결과를 구문 분석해야 합니다 KernelFunction .

chat = AgentGroupChat(
    agents=[agent_writer, agent_reviewer],
    selection_strategy=KernelFunctionSelectionStrategy(
        function=selection_function,
        kernel=_create_kernel_with_chat_completion("selection"),
        result_parser=lambda result: str(result.value[0]) if result.value is not None else COPYWRITER_NAME,
        agent_variable_name="agents",
        history_variable_name="history",
    ),
    termination_strategy=KernelFunctionTerminationStrategy(
        agents=[agent_reviewer],
        function=termination_function,
        kernel=_create_kernel_with_chat_completion("termination"),
        result_parser=lambda result: TERMINATION_KEYWORD in str(result.value[0]).lower(),
        history_variable_name="history",
        maximum_iterations=10,
    ),
)

에이전트는 현재 Java에서 사용할 수 없습니다.

채팅 루프

마침내 사용자와 에이전트 그룹 채팅 간의 상호 작용을 조정할 수 있습니다. 먼저 빈 루프를 만듭니다.

참고: 다른 예제와 달리 외부 기록 또는 스레드 는 관리되지 않습니다. 에이전트 그룹 채팅 은 내부적으로 대화 기록을 관리합니다.

bool isComplete = false;
do
{

} while (!isComplete);
is_complete: bool = False
while not is_complete:
    # operational logic

에이전트는 현재 Java에서 사용할 수 없습니다.

이제 이전 루프 내에서 사용자 입력을 캡처해 보겠습니다. 이 경우 다음과 같습니다.

  • 빈 입력은 무시됩니다.
  • 용어 EXIT 는 대화가 완료되었음을 나타냅니다.
  • 이 용어 RESET 는 에이전트 그룹 채팅 기록을 지웁니다.
  • 시작하는 @ 모든 용어는 콘텐츠가 입력으로 제공되는 파일 경로로 처리됩니다.
  • 유효한 입력이 에이전트 그룹 Chaty사용자 메시지로 추가됩니다.
Console.WriteLine();
Console.Write("> ");
string input = Console.ReadLine();
if (string.IsNullOrWhiteSpace(input))
{
    continue;
}
input = input.Trim();
if (input.Equals("EXIT", StringComparison.OrdinalIgnoreCase))
{
    isComplete = true;
    break;
}

if (input.Equals("RESET", StringComparison.OrdinalIgnoreCase))
{
    await chat.ResetAsync();
    Console.WriteLine("[Converation has been reset]");
    continue;
}

if (input.StartsWith("@", StringComparison.Ordinal) && input.Length > 1)
{
    string filePath = input.Substring(1);
    try
    {
        if (!File.Exists(filePath))
        {
            Console.WriteLine($"Unable to access file: {filePath}");
            continue;
        }
        input = File.ReadAllText(filePath);
    }
    catch (Exception)
    {
        Console.WriteLine($"Unable to access file: {filePath}");
        continue;
    }
}

chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input));
user_input = input("User:> ")
if not user_input:
    continue

if user_input.lower() == "exit":
    is_complete = True
    break

if user_input.lower() == "reset":
    await chat.reset()
    print("[Conversation has been reset]")
    continue

if user_input.startswith("@") and len(input) > 1:
    file_path = input[1:]
    try:
        if not os.path.exists(file_path):
            print(f"Unable to access file: {file_path}")
            continue
        with open(file_path) as file:
            user_input = file.read()
    except Exception:
        print(f"Unable to access file: {file_path}")
        continue

await chat.add_chat_message(ChatMessageContent(role=AuthorRole.USER, content=user_input))

에이전트는 현재 Java에서 사용할 수 없습니다.

사용자 입력에 대한 응답으로 에이전트 협업을 초기화하고 에이전트 응답을 표시하려면 에이전트 그룹 채팅을 호출합니다. 그러나 먼저 이전 호출에서 완료 상태를 다시 설정해야 합니다.

참고: 대화 루프가 충돌하지 않도록 서비스 오류가 catch되고 표시됩니다.

chat.IsComplete = false;

try
{
    await foreach (ChatMessageContent response in chat.InvokeAsync())
    {
        Console.WriteLine();
        Console.WriteLine($"{response.AuthorName.ToUpperInvariant()}:{Environment.NewLine}{response.Content}");
    }
}
catch (HttpOperationException exception)
{
    Console.WriteLine(exception.Message);
    if (exception.InnerException != null)
    {
        Console.WriteLine(exception.InnerException.Message);
        if (exception.InnerException.Data.Count > 0)
        {
            Console.WriteLine(JsonSerializer.Serialize(exception.InnerException.Data, new JsonSerializerOptions() { WriteIndented = true }));
        }
    }
}
chat.is_complete = False
async for response in chat.invoke():
    print(f"# {response.role} - {response.name or '*'}: '{response.content}'")

if chat.is_complete:
    is_complete = True
    break

에이전트는 현재 Java에서 사용할 수 없습니다.

최종

모든 단계를 함께 가져오면 이 예제의 최종 코드가 있습니다. 전체 구현은 아래에 제공됩니다.

다음과 같은 제안된 입력을 사용해 보세요.

  1. 안녕
  2. {"message: "안녕 세상"}
  3. {"message": "안녕하세요 세계"}
  4. Semantic Kernel(SK)은 개발자가 NLP(자연어 처리) 및 기계 학습 모델을 포함하는 복잡한 AI 워크플로를 빌드하고 오케스트레이션할 수 있는 오픈 소스 SDK입니다. 의미 체계 검색, 텍스트 요약 및 대화 시스템과 같은 AI 기능을 애플리케이션에 통합하기 위한 유연한 플랫폼을 제공합니다. SK를 사용하면 다양한 AI 서비스와 모델을 쉽게 결합하고, 관계를 정의하고, 상호 작용을 오케스트레이션할 수 있습니다.
  5. 이 문장을 두 개의 단락으로 나누십시오.
  6. 감사합니다
  7. @.\WomensSuffrage.txt
  8. 괜찮긴 한데, 내 대학 교수에게도 괜찮을까?
// Copyright (c) Microsoft. All rights reserved.

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Azure.Identity;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Chat;
using Microsoft.SemanticKernel.Agents.History;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.AzureOpenAI;

namespace AgentsSample;

public static class Program
{
    public static async Task Main()
    {
        // Load configuration from environment variables or user secrets.
        Settings settings = new();

        Console.WriteLine("Creating kernel...");
        IKernelBuilder builder = Kernel.CreateBuilder();

        builder.AddAzureOpenAIChatCompletion(
            settings.AzureOpenAI.ChatModelDeployment,
            settings.AzureOpenAI.Endpoint,
            new AzureCliCredential());

        Kernel kernel = builder.Build();

        Kernel toolKernel = kernel.Clone();
        toolKernel.Plugins.AddFromType<ClipboardAccess>();


        Console.WriteLine("Defining agents...");

        const string ReviewerName = "Reviewer";
        const string WriterName = "Writer";

        ChatCompletionAgent agentReviewer =
            new()
            {
                Name = ReviewerName,
                Instructions =
                    """
                    Your responsiblity is to review and identify how to improve user provided content.
                    If the user has providing input or direction for content already provided, specify how to address this input.
                    Never directly perform the correction or provide example.
                    Once the content has been updated in a subsequent response, you will review the content again until satisfactory.
                    Always copy satisfactory content to the clipboard using available tools and inform user.

                    RULES:
                    - Only identify suggestions that are specific and actionable.
                    - Verify previous suggestions have been addressed.
                    - Never repeat previous suggestions.
                    """,
                Kernel = toolKernel,
                Arguments = new KernelArguments(new AzureOpenAIPromptExecutionSettings() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() })
            };

        ChatCompletionAgent agentWriter =
            new()
            {
                Name = WriterName,
                Instructions =
                    """
                    Your sole responsiblity is to rewrite content according to review suggestions.

                    - Always apply all review direction.
                    - Always revise the content in its entirety without explanation.
                    - Never address the user.
                    """,
                Kernel = kernel,
            };

        KernelFunction selectionFunction =
            AgentGroupChat.CreatePromptFunctionForStrategy(
                $$$"""
                Examine the provided RESPONSE and choose the next participant.
                State only the name of the chosen participant without explanation.
                Never choose the participant named in the RESPONSE.

                Choose only from these participants:
                - {{{ReviewerName}}}
                - {{{WriterName}}}

                Always follow these rules when choosing the next participant:
                - If RESPONSE is user input, it is {{{ReviewerName}}}'s turn.
                - If RESPONSE is by {{{ReviewerName}}}, it is {{{WriterName}}}'s turn.
                - If RESPONSE is by {{{WriterName}}}, it is {{{ReviewerName}}}'s turn.

                RESPONSE:
                {{$lastmessage}}
                """,
                safeParameterNames: "lastmessage");

        const string TerminationToken = "yes";

        KernelFunction terminationFunction =
            AgentGroupChat.CreatePromptFunctionForStrategy(
                $$$"""
                Examine the RESPONSE and determine whether the content has been deemed satisfactory.
                If content is satisfactory, respond with a single word without explanation: {{{TerminationToken}}}.
                If specific suggestions are being provided, it is not satisfactory.
                If no correction is suggested, it is satisfactory.

                RESPONSE:
                {{$lastmessage}}
                """,
                safeParameterNames: "lastmessage");

        ChatHistoryTruncationReducer historyReducer = new(1);

        AgentGroupChat chat =
            new(agentReviewer, agentWriter)
            {
                ExecutionSettings = new AgentGroupChatSettings
                {
                    SelectionStrategy =
                        new KernelFunctionSelectionStrategy(selectionFunction, kernel)
                        {
                            // Always start with the editor agent.
                            InitialAgent = agentReviewer,
                            // Save tokens by only including the final response
                            HistoryReducer = historyReducer,
                            // The prompt variable name for the history argument.
                            HistoryVariableName = "lastmessage",
                            // Returns the entire result value as a string.
                            ResultParser = (result) => result.GetValue<string>() ?? agentReviewer.Name
                        },
                    TerminationStrategy =
                        new KernelFunctionTerminationStrategy(terminationFunction, kernel)
                        {
                            // Only evaluate for editor's response
                            Agents = [agentReviewer],
                            // Save tokens by only including the final response
                            HistoryReducer = historyReducer,
                            // The prompt variable name for the history argument.
                            HistoryVariableName = "lastmessage",
                            // Limit total number of turns
                            MaximumIterations = 12,
                            // Customer result parser to determine if the response is "yes"
                            ResultParser = (result) => result.GetValue<string>()?.Contains(TerminationToken, StringComparison.OrdinalIgnoreCase) ?? false
                        }
                }
            };

        Console.WriteLine("Ready!");

        bool isComplete = false;
        do
        {
            Console.WriteLine();
            Console.Write("> ");
            string input = Console.ReadLine();
            if (string.IsNullOrWhiteSpace(input))
            {
                continue;
            }
            input = input.Trim();
            if (input.Equals("EXIT", StringComparison.OrdinalIgnoreCase))
            {
                isComplete = true;
                break;
            }

            if (input.Equals("RESET", StringComparison.OrdinalIgnoreCase))
            {
                await chat.ResetAsync();
                Console.WriteLine("[Converation has been reset]");
                continue;
            }

            if (input.StartsWith("@", StringComparison.Ordinal) && input.Length > 1)
            {
                string filePath = input.Substring(1);
                try
                {
                    if (!File.Exists(filePath))
                    {
                        Console.WriteLine($"Unable to access file: {filePath}");
                        continue;
                    }
                    input = File.ReadAllText(filePath);
                }
                catch (Exception)
                {
                    Console.WriteLine($"Unable to access file: {filePath}");
                    continue;
                }
            }

            chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input));

            chat.IsComplete = false;

            try
            {
                await foreach (ChatMessageContent response in chat.InvokeAsync())
                {
                    Console.WriteLine();
                    Console.WriteLine($"{response.AuthorName.ToUpperInvariant()}:{Environment.NewLine}{response.Content}");
                }
            }
            catch (HttpOperationException exception)
            {
                Console.WriteLine(exception.Message);
                if (exception.InnerException != null)
                {
                    Console.WriteLine(exception.InnerException.Message);
                    if (exception.InnerException.Data.Count > 0)
                    {
                        Console.WriteLine(JsonSerializer.Serialize(exception.InnerException.Data, new JsonSerializerOptions() { WriteIndented = true }));
                    }
                }
            }
        } while (!isComplete);
    }

    private sealed class ClipboardAccess
    {
        [KernelFunction]
        [Description("Copies the provided content to the clipboard.")]
        public static void SetClipboard(string content)
        {
            if (string.IsNullOrWhiteSpace(content))
            {
                return;
            }

            using Process clipProcess = Process.Start(
                new ProcessStartInfo
                {
                    FileName = "clip",
                    RedirectStandardInput = true,
                    UseShellExecute = false,
                });

            clipProcess.StandardInput.Write(content);
            clipProcess.StandardInput.Close();
        }
    }
}

# Copyright (c) Microsoft. All rights reserved.

import asyncio
import os

from semantic_kernel.agents import AgentGroupChat, ChatCompletionAgent
from semantic_kernel.agents.strategies.selection.kernel_function_selection_strategy import (
    KernelFunctionSelectionStrategy,
)
from semantic_kernel.agents.strategies.termination.kernel_function_termination_strategy import (
    KernelFunctionTerminationStrategy,
)
from semantic_kernel.connectors.ai.open_ai.services.azure_chat_completion 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.functions.kernel_function_from_prompt import KernelFunctionFromPrompt
from semantic_kernel.kernel import Kernel

###################################################################
# The following sample demonstrates how to create a simple,       #
# agent group chat that utilizes a Reviewer Chat Completion       #
# Agent along with a Writer Chat Completion Agent to              #
# complete a user's task.                                         #
###################################################################


class ClipboardAccess:
    @kernel_function
    def set_clipboard(content: str):
        if not content.strip():
            return

        pyperclip.copy(content)


REVIEWER_NAME = "Reviewer"
COPYWRITER_NAME = "Writer"


def _create_kernel_with_chat_completion(service_id: str) -> Kernel:
    kernel = Kernel()
    kernel.add_service(AzureChatCompletion(service_id=service_id))
    return kernel


async def main():
    agent_reviewer = ChatCompletionAgent(
        service_id=REVIEWER_NAME,
        kernel=_create_kernel_with_chat_completion(REVIEWER_NAME),
        name=REVIEWER_NAME,
        instructions="""
            Your responsiblity is to review and identify how to improve user provided content.
            If the user has providing input or direction for content already provided, specify how to 
            address this input.
            Never directly perform the correction or provide example.
            Once the content has been updated in a subsequent response, you will review the content 
            again until satisfactory.
            Always copy satisfactory content to the clipboard using available tools and inform user.

            RULES:
            - Only identify suggestions that are specific and actionable.
            - Verify previous suggestions have been addressed.
            - Never repeat previous suggestions.
            """,
    )

    agent_writer = ChatCompletionAgent(
        service_id=COPYWRITER_NAME,
        kernel=_create_kernel_with_chat_completion(COPYWRITER_NAME),
        name=COPYWRITER_NAME,
        instructions="""
            Your sole responsiblity is to rewrite content according to review suggestions.

            - Always apply all review direction.
            - Always revise the content in its entirety without explanation.
            - Never address the user.
            """,
    )

    selection_function = KernelFunctionFromPrompt(
        function_name="selection",
        prompt=f"""
        Determine which participant takes the next turn in a conversation based on the the most recent participant.
        State only the name of the participant to take the next turn.
        No participant should take more than one turn in a row.

        Choose only from these participants:
        - {REVIEWER_NAME}
        - {COPYWRITER_NAME}

        Always follow these rules when selecting the next participant:
        - After user input, it is {COPYWRITER_NAME}'s turn.
        - After {COPYWRITER_NAME} replies, it is {REVIEWER_NAME}'s turn.
        - After {REVIEWER_NAME} provides feedback, it is {COPYWRITER_NAME}'s turn.

        History:
        {{{{$history}}}}
        """,
    )

    TERMINATION_KEYWORD = "yes"

    termination_function = KernelFunctionFromPrompt(
        function_name="termination",
        prompt=f"""
            Examine the RESPONSE and determine whether the content has been deemed satisfactory.
            If content is satisfactory, respond with a single word without explanation: {TERMINATION_KEYWORD}.
            If specific suggestions are being provided, it is not satisfactory.
            If no correction is suggested, it is satisfactory.

            RESPONSE:
            {{{{$history}}}}
            """,
    )

    chat = AgentGroupChat(
        agents=[agent_writer, agent_reviewer],
        selection_strategy=KernelFunctionSelectionStrategy(
            function=selection_function,
            kernel=_create_kernel_with_chat_completion("selection"),
            result_parser=lambda result: str(result.value[0]) if result.value is not None else COPYWRITER_NAME,
            agent_variable_name="agents",
            history_variable_name="history",
        ),
        termination_strategy=KernelFunctionTerminationStrategy(
            agents=[agent_reviewer],
            function=termination_function,
            kernel=_create_kernel_with_chat_completion("termination"),
            result_parser=lambda result: TERMINATION_KEYWORD in str(result.value[0]).lower(),
            history_variable_name="history",
            maximum_iterations=10,
        ),
    )

    is_complete: bool = False
    while not is_complete:
        user_input = input("User:> ")
        if not user_input:
            continue

        if user_input.lower() == "exit":
            is_complete = True
            break

        if user_input.lower() == "reset":
            await chat.reset()
            print("[Conversation has been reset]")
            continue

        if user_input.startswith("@") and len(input) > 1:
            file_path = input[1:]
            try:
                if not os.path.exists(file_path):
                    print(f"Unable to access file: {file_path}")
                    continue
                with open(file_path) as file:
                    user_input = file.read()
            except Exception:
                print(f"Unable to access file: {file_path}")
                continue

        await chat.add_chat_message(ChatMessageContent(role=AuthorRole.USER, content=user_input))

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

        if chat.is_complete:
            is_complete = True
            break


if __name__ == "__main__":
    asyncio.run(main())

에이전트는 현재 Java에서 사용할 수 없습니다.