Public Access
Add tray rules and identities editor
PR and Push Build/Test / build-and-test (push) Successful in 7m0s
PR and Push Build/Test / build-and-test (push) Successful in 7m0s
This commit is contained in:
@@ -0,0 +1,239 @@
|
||||
using MeetingAssistant.Summary;
|
||||
using MeetingAssistant.Speakers;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace MeetingAssistant.Workflow;
|
||||
|
||||
#pragma warning disable MAAI001
|
||||
public sealed class WorkflowRulesEditorChatPipeline : IWorkflowRulesEditorChatPipeline
|
||||
{
|
||||
private readonly MeetingAssistantOptions options;
|
||||
private readonly ILoggerFactory loggerFactory;
|
||||
private readonly ILogger<WorkflowRulesEditorChatPipeline> logger;
|
||||
private readonly IWorkflowRulesEditorInstructionBuilder instructionBuilder;
|
||||
private readonly IDbContextFactory<SpeakerIdentityDbContext> speakerIdentityDbContextFactory;
|
||||
private readonly IWorkflowRulesEditorSamplePlaybackQueue samplePlaybackQueue;
|
||||
|
||||
public WorkflowRulesEditorChatPipeline(
|
||||
IOptions<MeetingAssistantOptions> options,
|
||||
ILoggerFactory loggerFactory,
|
||||
ILogger<WorkflowRulesEditorChatPipeline> logger,
|
||||
IWorkflowRulesEditorInstructionBuilder instructionBuilder,
|
||||
IDbContextFactory<SpeakerIdentityDbContext> speakerIdentityDbContextFactory,
|
||||
IWorkflowRulesEditorSamplePlaybackQueue samplePlaybackQueue)
|
||||
{
|
||||
this.options = options.Value;
|
||||
this.loggerFactory = loggerFactory;
|
||||
this.logger = logger;
|
||||
this.instructionBuilder = instructionBuilder;
|
||||
this.speakerIdentityDbContextFactory = speakerIdentityDbContextFactory;
|
||||
this.samplePlaybackQueue = samplePlaybackQueue;
|
||||
}
|
||||
|
||||
public async Task<WorkflowRulesEditorChatResult> SendAsync(
|
||||
IReadOnlyList<WorkflowRulesEditorChatMessage> conversation,
|
||||
string userMessage,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(userMessage))
|
||||
{
|
||||
return new WorkflowRulesEditorChatResult("", conversation);
|
||||
}
|
||||
|
||||
var agentOptions = options.WorkflowRulesEditor.ToEffectiveAgentOptions(options.Agent);
|
||||
var key = ResolveApiKey(agentOptions, "workflow rules editor");
|
||||
var tools = new WorkflowRulesEditorTools(
|
||||
options,
|
||||
speakerIdentityDbContextFactory,
|
||||
samplePlaybackQueue);
|
||||
var messages = conversation
|
||||
.Select(ToChatMessage)
|
||||
.Append(new ChatMessage(ChatRole.User, userMessage.Trim()))
|
||||
.ToList();
|
||||
var instructions = await instructionBuilder.BuildAsync(options, cancellationToken);
|
||||
|
||||
using var compactionSummaryClient = new LiteLlmResponsesChatClient(
|
||||
new Uri(agentOptions.Endpoint),
|
||||
key,
|
||||
agentOptions.Model,
|
||||
agentOptions.EnableThinking,
|
||||
ToReasoningEffortValue(agentOptions.ReasoningEffort),
|
||||
agentOptions.ReconnectionAttempts,
|
||||
agentOptions.ReconnectionDelay,
|
||||
compactionOptions: null,
|
||||
logger,
|
||||
firstRequestIsUser: false);
|
||||
var compactionOptions = CreateCompactionOptions(agentOptions, compactionSummaryClient);
|
||||
using var chatClient = new LiteLlmResponsesChatClient(
|
||||
new Uri(agentOptions.Endpoint),
|
||||
key,
|
||||
agentOptions.Model,
|
||||
agentOptions.EnableThinking,
|
||||
ToReasoningEffortValue(agentOptions.ReasoningEffort),
|
||||
agentOptions.ReconnectionAttempts,
|
||||
agentOptions.ReconnectionDelay,
|
||||
compactionOptions,
|
||||
logger,
|
||||
firstRequestIsUser: true);
|
||||
var functionClient = chatClient
|
||||
.AsBuilder()
|
||||
.UseFunctionInvocation(loggerFactory)
|
||||
.Build();
|
||||
|
||||
var response = await functionClient.GetResponseAsync(
|
||||
messages,
|
||||
CreateChatOptions(agentOptions, tools, instructions),
|
||||
cancellationToken);
|
||||
var responseText = string.IsNullOrWhiteSpace(response.Text)
|
||||
? "(No response text returned.)"
|
||||
: response.Text.Trim();
|
||||
var nextConversation = conversation
|
||||
.Append(new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.User, userMessage.Trim()))
|
||||
.Append(new WorkflowRulesEditorChatMessage(WorkflowRulesEditorChatRole.Agent, responseText))
|
||||
.ToList();
|
||||
return new WorkflowRulesEditorChatResult(responseText, nextConversation);
|
||||
}
|
||||
|
||||
private static ChatMessage ToChatMessage(WorkflowRulesEditorChatMessage message)
|
||||
{
|
||||
return new ChatMessage(
|
||||
message.Role == WorkflowRulesEditorChatRole.Agent ? ChatRole.Assistant : ChatRole.User,
|
||||
message.Content);
|
||||
}
|
||||
|
||||
private static ChatOptions CreateChatOptions(
|
||||
AgentOptions options,
|
||||
WorkflowRulesEditorTools tools,
|
||||
string instructions)
|
||||
{
|
||||
return new ChatOptions
|
||||
{
|
||||
ModelId = options.Model,
|
||||
Instructions = instructions,
|
||||
MaxOutputTokens = options.MaxOutputTokens,
|
||||
AllowMultipleToolCalls = true,
|
||||
ToolMode = ChatToolMode.Auto,
|
||||
Tools =
|
||||
[
|
||||
AIFunctionFactory.Create(
|
||||
tools.ReadRules,
|
||||
"read_rules",
|
||||
"Read the configured workflow rules YAML file. With no line arguments, read the whole file. With from and to, read that clamped inclusive 1-based line range."),
|
||||
AIFunctionFactory.Create(
|
||||
tools.WriteRules,
|
||||
"write_rules",
|
||||
"Overwrite the configured workflow rules YAML file after validating that it parses as a workflow rules document."),
|
||||
AIFunctionFactory.Create(
|
||||
tools.Search,
|
||||
"search",
|
||||
"Search the configured workflow rules YAML file using ripgrep-style syntax. The search is scoped to this single file."),
|
||||
AIFunctionFactory.Create(
|
||||
tools.SearchIdentities,
|
||||
"search_identities",
|
||||
"Search or list speaker identities. Optional query matches canonical names, aliases, and candidate names. Returns JSON summaries."),
|
||||
AIFunctionFactory.Create(
|
||||
tools.ReadIdentity,
|
||||
"read_identity",
|
||||
"Read one speaker identity by numeric identity id, including aliases, candidate names, references, and sample metadata."),
|
||||
AIFunctionFactory.Create(
|
||||
tools.CreateIdentity,
|
||||
"create_identity",
|
||||
"Create a speaker identity with optional canonicalName, aliases, and candidateNames. Returns the created identity JSON."),
|
||||
AIFunctionFactory.Create(
|
||||
tools.UpdateIdentity,
|
||||
"update_identity",
|
||||
"Replace a speaker identity's canonicalName, aliases, and candidateNames by numeric identity id. Omitted aliases/candidateNames become empty."),
|
||||
AIFunctionFactory.Create(
|
||||
tools.DeleteIdentity,
|
||||
"delete_identity",
|
||||
"Delete a speaker identity and all linked aliases, candidate names, references, and samples by numeric identity id."),
|
||||
AIFunctionFactory.Create(
|
||||
tools.MergeIdentities,
|
||||
"merge_identities",
|
||||
"Merge sourceIdentityId into targetIdentityId, preserving target canonical name, adding source names as aliases, moving references and samples, then deleting the source."),
|
||||
AIFunctionFactory.Create(
|
||||
tools.ListIdentitySamples,
|
||||
"list_identity_samples",
|
||||
"List audio samples for a speaker identity by numeric identity id. Returns sample ids and metadata, not audio bytes."),
|
||||
AIFunctionFactory.Create(
|
||||
tools.ReadIdentitySample,
|
||||
"read_identity_sample",
|
||||
"Read one audio sample by numeric sample id. Returns metadata and base64 WAV bytes."),
|
||||
AIFunctionFactory.Create(
|
||||
tools.DeleteIdentitySample,
|
||||
"delete_identity_sample",
|
||||
"Delete one audio sample by numeric sample id."),
|
||||
AIFunctionFactory.Create(
|
||||
tools.QueuePlayIdentitySample,
|
||||
"queue_play_identity_sample",
|
||||
"Queue one audio sample by numeric sample id for local playback. This is asynchronous and does not block until playback finishes.")
|
||||
],
|
||||
Reasoning = options.EnableThinking
|
||||
? new ReasoningOptions { Effort = ToReasoningEffort(options.ReasoningEffort) }
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
private static LiteLlmResponsesCompactionOptions CreateCompactionOptions(
|
||||
AgentOptions options,
|
||||
IChatClient? summaryClient)
|
||||
{
|
||||
return new LiteLlmResponsesCompactionOptions
|
||||
{
|
||||
Enabled = options.EnableCompaction,
|
||||
ContextWindowTokens = options.ContextWindowTokens,
|
||||
MaxOutputTokens = options.MaxOutputTokens,
|
||||
RemainingRatio = options.CompactionRemainingRatio,
|
||||
CompactPath = options.ResponsesCompactPath,
|
||||
FallbackStrategy = OpenAiMeetingSummaryAgentPipeline.CreateFallbackCompactionStrategyForTests(
|
||||
options.ContextWindowTokens,
|
||||
options.MaxOutputTokens,
|
||||
options.CompactionRemainingRatio,
|
||||
summaryClient)
|
||||
};
|
||||
}
|
||||
|
||||
private static string ResolveApiKey(AgentOptions options, string agentName)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(options.Key))
|
||||
{
|
||||
return options.Key;
|
||||
}
|
||||
|
||||
var value = Environment.GetEnvironmentVariable(options.KeyEnv);
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(
|
||||
$"No {agentName} API key configured. Set MeetingAssistant:WorkflowRulesEditor:Key, MeetingAssistant:Agent:Key, or environment variable '{options.KeyEnv}'.");
|
||||
}
|
||||
|
||||
private static string ToReasoningEffortValue(ReasoningEffortOption effort)
|
||||
{
|
||||
return effort switch
|
||||
{
|
||||
ReasoningEffortOption.None => "none",
|
||||
ReasoningEffortOption.Low => "low",
|
||||
ReasoningEffortOption.High => "high",
|
||||
ReasoningEffortOption.ExtraHigh => "xhigh",
|
||||
_ => "medium"
|
||||
};
|
||||
}
|
||||
|
||||
private static ReasoningEffort ToReasoningEffort(ReasoningEffortOption effort)
|
||||
{
|
||||
return effort switch
|
||||
{
|
||||
ReasoningEffortOption.None => ReasoningEffort.None,
|
||||
ReasoningEffortOption.Low => ReasoningEffort.Low,
|
||||
ReasoningEffortOption.High => ReasoningEffort.High,
|
||||
ReasoningEffortOption.ExtraHigh => ReasoningEffort.ExtraHigh,
|
||||
_ => ReasoningEffort.Medium
|
||||
};
|
||||
}
|
||||
}
|
||||
#pragma warning restore MAAI001
|
||||
Reference in New Issue
Block a user