Skip to content

Optimize the selection NextSpeaker mechanism of RolePlayOrchestrator … #6688

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

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 99 additions & 11 deletions dotnet/src/AutoGen.Core/Orchestrator/RolePlayOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using AutoGen.Core.Orchestrator;

namespace AutoGen.Core;

Expand Down Expand Up @@ -65,12 +67,20 @@ public RolePlayOrchestrator(IAgent admin, Graph? workflow = null)
var agentNames = candidates.Select(candidate => candidate.Name);
var rolePlayMessage = new TextMessage(Role.User,
content: $@"You are in a role play game. Carefully read the conversation history and carry on the conversation.
The available roles are:
{string.Join(",", agentNames)}
Each message will start with 'From name:', e.g:
From {agentNames.First()}:
//your message//.");
## Available Speaker Names
Copy link
Collaborator

@LittleLittleCloud LittleLittleCloud Jun 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe adding a class-level summary on top?

/// <summary>
/// This orchestrator uses a robust two-step strategy to select the next speaker in a roleplay conversation:
/// 1. It first prompts the LLM to select the next speaker from the list of valid candidate names, requiring output in a strict JSON format.
/// 2. If the LLM's chosen name does not exactly match any candidate (e.g., due to hallucination, abbreviation, or formatting issues), 
///    the orchestrator issues a second prompt to the LLM, instructing it to map the provided name to the closest valid candidate name from the original list.
/// This approach ensures that the selected speaker always corresponds to an authorized candidate and guards against LLM output errors.
/// </summary>

{string.Join($"{Environment.NewLine}", agentNames.Select(z => $"- {z}"))}
## Output Role
Each message will use the strickly JSON format with a '//finish' suffix:
{{""Speaker"":""<From Speaker Name>"", ""Message"":""<Chat Message>""}}//finish
e,g:
{{""Speaker"":""{agentNames.First()}"", ""Message"":""Hi, I'm {agentNames.First()}.""}}//finish
Note:
1. ""Speaker"" must be one of the most suitable names in ""Available Speaker names"". You cannot create it yourself, nor can you merge two names, it must be 100% exactly equal.
2. You have to output clean JSON result, no other words are allowed.");

var chatHistoryWithName = this.ProcessConversationsForRolePlay(context.ChatHistory);
var messages = new IMessage[] { rolePlayMessage }.Concat(chatHistoryWithName);
Expand All @@ -80,23 +90,101 @@ public RolePlayOrchestrator(IAgent admin, Graph? workflow = null)
options: new GenerateReplyOptions
{
Temperature = 0,
MaxToken = 128,
StopSequence = [":"],
MaxToken = 1024,
StopSequence = ["//finish"],
Functions = null,
},
cancellationToken: cancellationToken);

var name = response.GetContent() ?? throw new ArgumentException("No name is returned.");
var responseMessageStr = response.GetContent() ?? throw new ArgumentException("No name is returned.");

RolePlayOrchestratorResponse? responseMessage;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

var responseMessage = JsonSerializer.Deserialize<RolePlayOrchestratorResponse>(responseMessageStr) ?? throw new InvalidOperationException("Incorrect RolePlayOrchestratorResponse JSON format.");

try
{
responseMessage = JsonSerializer.Deserialize<RolePlayOrchestratorResponse>(responseMessageStr);
if (responseMessage == null)
{
throw new Exception("Incorrect RolePlayOrchestratorResponse JSON format.");
}
}
catch
{
throw;
}

// remove From
name = name!.Substring(5);
var candidate = candidates.FirstOrDefault(x => x.Name!.ToLower() == name.ToLower());
var name = responseMessage.Speaker;
var candidate = candidates.FirstOrDefault(x => x.Name!.ToUpper() == name!.ToUpper());

if (candidate != null)
{
return candidate;
}

//Regain the correct name
var regainMessage = new TextMessage(Role.User,
content: @$"Choose a name that is closest to the meaning from ""Available Speaker Names"" by ""Input Name"".
## Example
### Available Speaker Names
- Sales Manager
- General Manager Assistant
- Chief Financial Officer
### Input Name
CFO
### Outout Name
{{""Speaker"":""Chief Financial Officer"", ""Message"":""""}}//finish
## Task
Output Name must be one of the name in the following ""Available Speaker Names"" without any change.
Note:
1. ""Speaker"" must be one of the most suitable names in ""Available Speaker Names"". You cannot create it yourself, nor can you merge two names, it must be 100% exactly equal.
2. You have to output clean JSON result,no other words are allowed.
### Speaker List
{string.Join($"{Environment.NewLine}", agentNames.Select(z => $"- {z}"))}
### Input Name
{name}
### Output Name");

var regainResponse = await this.admin.GenerateReplyAsync(
messages: new[] { regainMessage },
options: new GenerateReplyOptions
{
Temperature = 0,
MaxToken = 1024,
StopSequence = ["//finish"],
Functions = null,
},
cancellationToken: cancellationToken);

RolePlayOrchestratorResponse? regainResponseMessage;
var regainNameStr = regainResponse.GetContent() ?? throw new ArgumentException("No name is returned.");
try
{
regainResponseMessage = JsonSerializer.Deserialize<RolePlayOrchestratorResponse>(regainNameStr);
if (regainResponseMessage == null)
{
throw new Exception("Incorrect RolePlayOrchestratorResponse JSON format.");
}
}
catch
{
throw;
}

var reaginCandidate = candidates.FirstOrDefault(x => x.Name!.ToUpper() == regainResponseMessage.Speaker!.ToUpper());

if (reaginCandidate != null)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reaginCandidate -> regainCadidate

{
return reaginCandidate;
}

var errorMessage = $"The response from admin is {name}, which is either not in the candidates list or not in the correct format.";
throw new ArgumentException(errorMessage);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// RolePlayOrchestratorResponse.cs

namespace AutoGen.Core.Orchestrator;
internal class RolePlayOrchestratorResponse

Check failure on line 5 in dotnet/src/AutoGen.Core/Orchestrator/RolePlayOrchestratorResponse.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Type 'RolePlayOrchestratorResponse' can be sealed because it has no subtypes in its containing assembly and is not externally visible (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852)

Check failure on line 5 in dotnet/src/AutoGen.Core/Orchestrator/RolePlayOrchestratorResponse.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Type 'RolePlayOrchestratorResponse' can be sealed because it has no subtypes in its containing assembly and is not externally visible (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852)

Check failure on line 5 in dotnet/src/AutoGen.Core/Orchestrator/RolePlayOrchestratorResponse.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Type 'RolePlayOrchestratorResponse' can be sealed because it has no subtypes in its containing assembly and is not externally visible (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852)

Check failure on line 5 in dotnet/src/AutoGen.Core/Orchestrator/RolePlayOrchestratorResponse.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Type 'RolePlayOrchestratorResponse' can be sealed because it has no subtypes in its containing assembly and is not externally visible (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1852)

Check failure on line 5 in dotnet/src/AutoGen.Core/Orchestrator/RolePlayOrchestratorResponse.cs

View workflow job for this annotation

GitHub Actions / Dotnet Build & Test (ubuntu-latest, 3.11)

Type 'RolePlayOrchestratorResponse' can be sealed because it has no subtypes in its containing assembly and is not externally visible

Check failure on line 5 in dotnet/src/AutoGen.Core/Orchestrator/RolePlayOrchestratorResponse.cs

View workflow job for this annotation

GitHub Actions / Dotnet Build & Test (ubuntu-latest, 3.11)

Type 'RolePlayOrchestratorResponse' can be sealed because it has no subtypes in its containing assembly and is not externally visible

Check failure on line 5 in dotnet/src/AutoGen.Core/Orchestrator/RolePlayOrchestratorResponse.cs

View workflow job for this annotation

GitHub Actions / Dotnet Build & Test (macos-latest, 3.11)

Type 'RolePlayOrchestratorResponse' can be sealed because it has no subtypes in its containing assembly and is not externally visible

Check failure on line 5 in dotnet/src/AutoGen.Core/Orchestrator/RolePlayOrchestratorResponse.cs

View workflow job for this annotation

GitHub Actions / Dotnet Build & Test (macos-latest, 3.11)

Type 'RolePlayOrchestratorResponse' can be sealed because it has no subtypes in its containing assembly and is not externally visible
{
internal string? Speaker { get; set; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The access modifier of property doesn't have to be also internal here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public is also fine.

internal string? Message { get; set; }
}
Loading