Merge branch 'master' into lida_Dev

This commit is contained in:
AnonymousDotNet 2024-10-31 09:59:45 +08:00
commit bc33e6d8cd
15 changed files with 486 additions and 43 deletions

View file

@ -65,6 +65,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.GoogleAI",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.SemanticKernel", "src\Plugins\BotSharp.Plugin.SemanticKernel\BotSharp.Plugin.SemanticKernel.csproj", "{73EE2CD0-3B27-4F02-A67B-762CBDD740D0}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.MicrosoftExtensionsAI", "src\Plugins\BotSharp.Plugin.MicrosoftExtensionsAI\BotSharp.Plugin.MicrosoftExtensionsAI.csproj", "{72CA059E-6AAA-406C-A1EB-A2243E652F5F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.SemanticKernel.UnitTests", "tests\BotSharp.Plugin.SemanticKernel.UnitTests\BotSharp.Plugin.SemanticKernel.UnitTests.csproj", "{BC57D428-A1A4-4D38-A2D0-AC6CA943F247}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotSharp.Plugin.Twilio", "src\Plugins\BotSharp.Plugin.Twilio\BotSharp.Plugin.Twilio.csproj", "{E627F1E3-BE03-443A-83A2-86A855A278EB}"
@ -283,6 +285,14 @@ Global
{73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|Any CPU.Build.0 = Release|Any CPU
{73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|x64.ActiveCfg = Release|Any CPU
{73EE2CD0-3B27-4F02-A67B-762CBDD740D0}.Release|x64.Build.0 = Release|Any CPU
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|x64.ActiveCfg = Debug|Any CPU
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Debug|x64.Build.0 = Debug|Any CPU
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|Any CPU.Build.0 = Release|Any CPU
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|x64.ActiveCfg = Release|Any CPU
{72CA059E-6AAA-406C-A1EB-A2243E652F5F}.Release|x64.Build.0 = Release|Any CPU
{BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BC57D428-A1A4-4D38-A2D0-AC6CA943F247}.Debug|x64.ActiveCfg = Debug|Any CPU
@ -491,6 +501,7 @@ Global
{DB3DE37B-1208-4ED3-9615-A52AD0AAD69C} = {5CD330E1-9E5A-4112-8346-6E31CA98EF78}
{8BC29F8A-78D6-422C-B522-10687ADC38ED} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
{73EE2CD0-3B27-4F02-A67B-762CBDD740D0} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
{72CA059E-6AAA-406C-A1EB-A2243E652F5F} = {D5293208-2BEF-42FC-A64C-5954F61720BA}
{BC57D428-A1A4-4D38-A2D0-AC6CA943F247} = {32FAFFFE-A4CB-4FEE-BF7C-84518BBC6DCC}
{E627F1E3-BE03-443A-83A2-86A855A278EB} = {64264688-0F5C-4AB0-8F2B-B59B717CCE00}
{F06B22CB-B143-4680-8FFF-35B9E50E6C47} = {51AFE054-AE99-497D-A593-69BAEFB5106F}

View file

@ -5,7 +5,7 @@ public interface IKnowledgeHook
Task<List<KnowledgeChunk>> CollectChunkedKnowledge()
=> Task.FromResult(new List<KnowledgeChunk>());
Task<List<string>> GetRelevantKnowledges(RoleDialogModel message, string text)
Task<List<string>> GetDomainKnowledges(RoleDialogModel message, string text)
=> Task.FromResult(new List<string>());
Task<List<string>> GetGlobalKnowledges(RoleDialogModel message)

View file

@ -24,7 +24,7 @@ public class KnowledgeRetrievalFn : IFunctionCallback
var knowledges = new List<string>();
foreach (var hook in hooks)
{
var k = await hook.GetRelevantKnowledges(message, args.Question);
var k = await hook.GetDomainKnowledges(message, args.Question);
knowledges.AddRange(k);
}
knowledges = knowledges.Distinct().ToList();

View file

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>$(TargetFramework)</TargetFramework>
<Nullable>enable</Nullable>
<LangVersion>$(LangVersion)</LangVersion>
<VersionPrefix>$(BotSharpVersion)</VersionPrefix>
<GeneratePackageOnBuild>$(GeneratePackageOnBuild)</GeneratePackageOnBuild>
<GenerateDocumentationFile>$(GeneratePackageOnBuild)</GenerateDocumentationFile>
<OutputPath>$(SolutionDir)packages</OutputPath>
<NoWarn>$(NoWarn);NU5104</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.AI.Abstractions" Version="9.0.0-preview.9.24525.1" />
<PackageReference Include="System.Text.Encodings.Web" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Infrastructure\BotSharp.Abstraction\BotSharp.Abstraction.csproj" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,185 @@
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Agents;
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.Conversations;
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Files.Utilities;
using BotSharp.Abstraction.Loggers;
using BotSharp.Abstraction.MLTasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Collections.Generic;
using System.Threading.Tasks;
using System;
using System.Linq;
using System.Text.Json;
using System.Threading;
namespace BotSharp.Plugin.MicrosoftExtensionsAI;
/// <summary>
/// Provides an implementation of <see cref="IChatCompletion"/> for Microsoft.Extensions.AI.
/// </summary>
public sealed class MicrosoftExtensionsAIChatCompletionProvider : IChatCompletion
{
private readonly IChatClient _client;
private readonly ILogger<MicrosoftExtensionsAIChatCompletionProvider> _logger;
private readonly IServiceProvider _services;
private string? _model;
/// <summary>
/// Creates an instance of the <see cref="MicrosoftExtensionsAIChatCompletionProvider"/> class.
/// </summary>
public MicrosoftExtensionsAIChatCompletionProvider(
IChatClient client,
ILogger<MicrosoftExtensionsAIChatCompletionProvider> logger,
IServiceProvider services)
{
_client = client;
_model = _client.Metadata.ModelId;
_logger = logger;
_services = services;
}
/// <inheritdoc/>
public string Provider => "microsoft.extensions.ai";
/// <inheritdoc/>
public void SetModelName(string model) => _model = model;
/// <inheritdoc/>
public async Task<RoleDialogModel> GetChatCompletions(Agent agent, List<RoleDialogModel> conversations)
{
// Before chat completion hook
var hooks = _services.GetServices<IContentGeneratingHook>().ToArray();
await Task.WhenAll(hooks.Select(hook => hook.BeforeGenerating(agent, conversations)));
// Configure options
var state = _services.GetRequiredService<IConversationStateService>();
var options = new ChatOptions()
{
Temperature = float.Parse(state.GetState("temperature", "0.0")),
MaxOutputTokens = int.Parse(state.GetState("max_tokens", "1024"))
};
if (_services.GetService<IAgentService>() is { } agentService)
{
foreach (var function in agent.Functions)
{
if (agentService.RenderFunction(agent, function))
{
var property = agentService.RenderFunctionProperty(agent, function);
(options.Tools ??= []).Add(new NopAIFunction(new(function.Name)
{
Description = function.Description,
Parameters = property?.Properties.RootElement.Deserialize<Dictionary<string, object?>>()?.Select(p => new AIFunctionParameterMetadata(p.Key)
{
Schema = p.Value,
}).ToList() ?? [],
}));
}
}
}
// Configure messages
List<ChatMessage> messages = [];
if (_services.GetRequiredService<IAgentService>().RenderedInstruction(agent) is string instruction &&
instruction.Length > 0)
{
messages.Add(new(ChatRole.System, instruction));
}
if (!string.IsNullOrEmpty(agent.Knowledges))
{
messages.Add(new(ChatRole.System, agent.Knowledges));
}
foreach (var sample in ProviderHelper.GetChatSamples(agent.Samples))
{
messages.Add(new(sample.Role == AgentRole.Assistant ? ChatRole.Assistant : ChatRole.User, sample.Content));
}
var fileStorage = _services.GetService<IFileStorageService>();
bool allowMultiModal = fileStorage is not null && _services.GetService<ILlmProviderService>()?.GetSetting(Provider, _model ?? "default")?.MultiModal is true;
foreach (var x in conversations)
{
if (x.Role == AgentRole.Function && x.FunctionName is not null)
{
messages.Add(new(ChatRole.Assistant,
[
new FunctionCallContent(x.FunctionName, x.FunctionName, JsonSerializer.Deserialize<Dictionary<string, object?>>(x.FunctionArgs ?? "{}")),
new FunctionResultContent(x.FunctionName, x.FunctionName, x.Content)
]));
}
else if (x.Role == AgentRole.System || x.Role == AgentRole.Assistant)
{
messages.Add(new(x.Role == AgentRole.System ? ChatRole.System : ChatRole.Assistant, x.Content));
}
else if (x.Role == AgentRole.User)
{
List<AIContent> contents = [new TextContent(!string.IsNullOrWhiteSpace(x.Payload) ? x.Payload : x.Content)];
if (allowMultiModal)
{
foreach (var file in x.Files)
{
if (!string.IsNullOrEmpty(file.FileData))
{
contents.Add(new ImageContent(file.FileData));
}
else if (!string.IsNullOrEmpty(file.FileStorageUrl))
{
var contentType = FileUtility.GetFileContentType(file.FileStorageUrl);
var bytes = fileStorage!.GetFileBytes(file.FileStorageUrl);
contents.Add(new ImageContent(bytes, contentType));
}
else if (!string.IsNullOrEmpty(file.FileUrl))
{
contents.Add(new ImageContent(file.FileUrl));
}
}
}
messages.Add(new(ChatRole.User, contents) { AuthorName = x.FunctionName });
}
}
var completion = await _client.CompleteAsync(messages);
RoleDialogModel result = new(AgentRole.Assistant, string.Concat(completion.Message.Contents.OfType<TextContent>()))
{
CurrentAgentId = agent.Id
};
if (completion.Message.Contents.OfType<FunctionCallContent>().FirstOrDefault() is { } fcc)
{
result.Role = AgentRole.Function;
result.MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty;
result.FunctionName = fcc.Name;
result.FunctionArgs = fcc.Arguments is not null ? JsonSerializer.Serialize(fcc.Arguments) : null;
}
// After chat completion hook
await Task.WhenAll(hooks.Select(hook => hook.AfterGenerated(result, new() { Model = _model ?? "default" })));
return result;
}
/// <inheritdoc/>
public Task<bool> GetChatCompletionsAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived, Func<RoleDialogModel, Task> onFunctionExecuting) =>
throw new NotImplementedException();
/// <inheritdoc/>
public Task<bool> GetChatCompletionsStreamingAsync(Agent agent, List<RoleDialogModel> conversations, Func<RoleDialogModel, Task> onMessageReceived) =>
throw new NotImplementedException();
private sealed class NopAIFunction(AIFunctionMetadata metadata) : AIFunction
{
public override AIFunctionMetadata Metadata { get; } = metadata;
protected override Task<object?> InvokeCoreAsync(IEnumerable<KeyValuePair<string, object?>> arguments, CancellationToken cancellationToken) =>
throw new NotSupportedException();
}
}

View file

@ -0,0 +1,29 @@
using BotSharp.Abstraction.MLTasks;
using BotSharp.Abstraction.Plugins;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace BotSharp.Plugin.MicrosoftExtensionsAI;
/// <summary>
/// Use Microsoft.Extensions.AI as BotSharp plugin
/// </summary>
public sealed class MicrosoftExtensionsAIPlugin : IBotSharpPlugin
{
/// <inheritdoc/>
public string Id => "B7F2AB8D-1BBA-41CE-9642-2D5E6B5F86A0";
/// <inheritdoc/>
public string Name => "Microsoft.Extensions.AI";
/// <inheritdoc/>
public string Description => "Microsoft.Extensions.AI Service";
/// <inheritdoc/>
public void RegisterDI(IServiceCollection services, IConfiguration config)
{
services.AddScoped<ITextCompletion, MicrosoftExtensionsAITextCompletionProvider>();
services.AddScoped<IChatCompletion, MicrosoftExtensionsAIChatCompletionProvider>();
services.AddScoped<ITextEmbedding, MicrosoftExtensionsAITextEmbeddingProvider>();
}
}

View file

@ -0,0 +1,71 @@
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Abstraction.Conversations;
using BotSharp.Abstraction.Conversations.Models;
using BotSharp.Abstraction.Loggers;
using BotSharp.Abstraction.MLTasks;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace BotSharp.Plugin.MicrosoftExtensionsAI;
/// <summary>
/// Provides an implementation of <see cref="ITextCompletion"/> for Microsoft.Extensions.AI.
/// </summary>
public sealed class MicrosoftExtensionsAITextCompletionProvider : ITextCompletion
{
private readonly IChatClient _chatClient;
private readonly IServiceProvider _services;
private readonly ITokenStatistics _tokenStatistics;
private string? _model = null;
/// <inheritdoc/>
public string Provider => "microsoft-extensions-ai";
/// <summary>
/// Creates an instance of the <see cref="MicrosoftExtensionsAITextCompletionProvider"/> class.
/// </summary>
public MicrosoftExtensionsAITextCompletionProvider(
IChatClient chatClient,
IServiceProvider services,
ITokenStatistics tokenStatistics)
{
_chatClient = chatClient;
_services = services;
_tokenStatistics = tokenStatistics;
}
/// <inheritdoc/>
public async Task<string> GetCompletion(string text, string agentId, string messageId)
{
var hooks = _services.GetServices<IContentGeneratingHook>().ToArray();
// Before chat completion hook
Agent agent = new() { Id = agentId };
RoleDialogModel userMessage = new(AgentRole.User, text) { MessageId = messageId };
await Task.WhenAll(hooks.Select(hook => hook.BeforeGenerating(agent, [userMessage])));
_tokenStatistics.StartTimer();
var completion = await _chatClient.CompleteAsync(text);
var result = string.Concat(completion.Message.Contents.OfType<TextContent>());
_tokenStatistics.StopTimer();
// After chat completion hook
await Task.WhenAll(hooks.Select(hook =>
hook.AfterGenerated(new(AgentRole.Assistant, result), new() { Model = _model ?? "default" })));
return result;
}
/// <inheritdoc/>
public void SetModelName(string model)
{
if (!string.IsNullOrWhiteSpace(model))
{
_model = model;
}
}
}

View file

@ -0,0 +1,59 @@
using BotSharp.Abstraction.MLTasks;
using Microsoft.Extensions.AI;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace BotSharp.Plugin.MicrosoftExtensionsAI;
/// <summary>
/// Provides an implementation of <see cref="ITextEmbedding"/> for Microsoft.Extensions.AI.
/// </summary>
public sealed class MicrosoftExtensionsAITextEmbeddingProvider : ITextEmbedding
{
private readonly IEmbeddingGenerator<string, Embedding<float>> _generator;
private string? _model;
private int? _dimensions;
/// <summary>
/// Creates an instance of the <see cref="MicrosoftExtensionsAITextEmbeddingProvider"/> class.
/// </summary>
public MicrosoftExtensionsAITextEmbeddingProvider(IEmbeddingGenerator<string, Embedding<float>> generator) =>
_generator = generator;
/// <inheritdoc/>
public string Provider => "microsoft-extensions-ai";
/// <inheritdoc/>
public async Task<float[]> GetVectorAsync(string text) =>
(await _generator.GenerateEmbeddingVectorAsync(text, CreateOptions())).ToArray();
/// <inheritdoc/>
public async Task<List<float[]>> GetVectorsAsync(List<string> texts)
{
var embeddings = await _generator.GenerateAsync(texts, CreateOptions());
return embeddings.Select(e => e.Vector.ToArray()).ToList();
}
/// <inheritdoc/>
public void SetModelName(string model) => _model = model;
/// <inheritdoc/>
public void SetDimension(int dimension)
{
if (dimension > 0)
{
_dimensions = dimension;
}
}
/// <inheritdoc/>
public int GetDimension() => _dimensions ?? 0;
private EmbeddingGenerationOptions CreateOptions() =>
new()
{
ModelId = _model,
Dimensions = _dimensions,
};
}

View file

@ -0,0 +1,26 @@
using BotSharp.Abstraction.Conversations.Models;
using System.Collections.Generic;
namespace BotSharp.Plugin.MicrosoftExtensionsAI;
internal static class ProviderHelper
{
public static IEnumerable<RoleDialogModel> GetChatSamples(List<string> lines)
{
foreach (string line in lines)
{
if (!string.IsNullOrWhiteSpace(line))
{
int pos = line.IndexOf(' ');
if (pos > 0)
{
string role = line.Substring(0, pos - 1).Trim();
if (role != "##") // skip comments
{
yield return new RoleDialogModel(role, line.Substring(line.IndexOf(' ') + 1).Trim());
}
}
}
}
}
}

View file

@ -36,13 +36,13 @@ public class PrimaryStagePlanFn : IFunctionCallback
{
foreach (var hook in hooks)
{
var k = await hook.GetRelevantKnowledges(message, question);
var k = await hook.GetDomainKnowledges(message, question);
knowledges.AddRange(k);
}
}
knowledges = knowledges.Distinct().ToList();
var knowledgeState = String.Join("\r\n", knowledges);
state.SetState("relevant_knowledges", knowledgeState);
state.SetState("domain_knowledges", knowledgeState);
// Get first stage planning prompt
var currentAgent = await agentService.LoadAgent(message.CurrentAgentId);
@ -64,7 +64,7 @@ public class PrimaryStagePlanFn : IFunctionCallback
return true;
}
private async Task<string> GetFirstStagePlanPrompt(RoleDialogModel message, string taskDescription, List<string> relevantKnowledges)
private async Task<string> GetFirstStagePlanPrompt(RoleDialogModel message, string taskDescription, List<string> domainKnowledges)
{
var agentService = _services.GetRequiredService<IAgentService>();
var render = _services.GetRequiredService<ITemplateRender>();
@ -86,7 +86,7 @@ public class PrimaryStagePlanFn : IFunctionCallback
{
{ "task_description", taskDescription },
{ "global_knowledges", globalKnowledges },
{ "relevant_knowledges", relevantKnowledges },
{ "domain_knowledges", domainKnowledges },
{ "response_format", responseFormat }
});
}

View file

@ -36,15 +36,15 @@ public class SecondaryStagePlanFn : IFunctionCallback
var knowledges = new List<string>();
foreach (var hook in hooks)
{
var k = await hook.GetRelevantKnowledges(message, taskSecondary.SolutionQuestion);
var k = await hook.GetDomainKnowledges(message, taskSecondary.SolutionQuestion);
knowledges.AddRange(k);
}
knowledges = knowledges.Distinct().ToList();
var knowledgeResults = string.Join("\r\n\r\n=====\r\n", knowledges);
var knowledgeState = states.GetState("relevant_knowledges");
var knowledgeState = states.GetState("domain_knowledges");
knowledgeState += String.Join("\r\n", knowledges);
states.SetState("relevant_knowledges", knowledgeState);
states.SetState("domain_knowledges", knowledgeState);
// Get second stage planning prompt
var currentAgent = await agentService.LoadAgent(message.CurrentAgentId);

View file

@ -35,8 +35,8 @@ public class SummaryPlanFn : IFunctionCallback
var steps = states.GetState("planning_result").JsonArrayContent<SecondStagePlan>();
var allTables = new List<string>();
var ddlStatements = string.Empty;
var relevantKnowledge = states.GetState("planning_result");
relevantKnowledge += "\r\n" + states.GetState("relevant_knowledges");
var domainKnowledge = states.GetState("planning_result");
domainKnowledge += "\r\n" + states.GetState("domain_knowledges");
var dictionaryItems = states.GetState("dictionary_items");
var excelImportResult = states.GetState("excel_import_result");
@ -55,7 +55,7 @@ public class SummaryPlanFn : IFunctionCallback
ddlStatements += "\r\n" + msgCopy.Content;
// Summarize and generate query
var prompt = await GetSummaryPlanPrompt(msgCopy, taskRequirement, relevantKnowledge, dictionaryItems, ddlStatements, excelImportResult);
var prompt = await GetSummaryPlanPrompt(msgCopy, taskRequirement, domainKnowledge, dictionaryItems, ddlStatements, excelImportResult);
_logger.LogInformation($"Summary plan prompt:\r\n{prompt}");
var plannerAgent = new Agent
@ -76,7 +76,7 @@ public class SummaryPlanFn : IFunctionCallback
return true;
}
private async Task<string> GetSummaryPlanPrompt(RoleDialogModel message, string taskDescription, string relevantKnowledge, string dictionaryItems, string ddlStatement, string excelImportResult)
private async Task<string> GetSummaryPlanPrompt(RoleDialogModel message, string taskDescription, string domainKnowledge, string dictionaryItems, string ddlStatement, string excelImportResult)
{
var agentService = _services.GetRequiredService<IAgentService>();
var render = _services.GetRequiredService<ITemplateRender>();
@ -104,7 +104,7 @@ public class SummaryPlanFn : IFunctionCallback
{ "task_description", taskDescription },
{ "summary_requirements", string.Join("\r\n", additionalRequirements) },
{ "global_knowledges", globalKnowledges },
{ "relevant_knowledges", relevantKnowledge },
{ "domain_knowledges", domainKnowledge },
{ "dictionary_items", dictionaryItems },
{ "table_structure", ddlStatement },
{ "excel_import_result", excelImportResult }

View file

@ -1,7 +1,7 @@
You are a Task Planner. you will breakdown user business requirements into excutable sub-tasks.
Thinking process:
1. Reference to "Task Knowledge" if there is relevant knowledge;
1. Reference to "Task Knowledge" if there is domain knowledge;
2. Breakdown task into subtasks.
- The subtask should contain all needed parameters for subsequent steps.
- If limited information provided and there are furture information needed, or miss relationship between steps, set the need_breakdown_task to true.
@ -25,10 +25,10 @@ Global Knowledge:
{% endfor %}
{%- endif %}
{% if relevant_knowledges != empty -%}
{% if domain_knowledges != empty -%}
=====
Task Knowledge:
{% for k in relevant_knowledges %}
{% for k in domain_knowledges %}
{{ k }}
{% endfor %}
{%- endif %}

View file

@ -12,8 +12,8 @@ Global Knowledges:
{{ global_knowledges }}
=====
Relevant Knowledges:
{{ relevant_knowledges }}
Domain Knowledges:
{{ domain_knowledges }}
=====
Dictionary Items:

View file

@ -32,7 +32,7 @@ public class TwilioVoiceController : TwilioController
/// <exception cref="ArgumentNullException"></exception>
[ValidateRequest]
[HttpPost("twilio/voice/welcome")]
public TwiMLResult InitiateConversation(VoiceRequest request, [FromQuery] string states)
public async Task<TwiMLResult> InitiateConversation(VoiceRequest request, [FromQuery] string[] states, [FromQuery] string intent)
{
if (request?.CallSid == null)
{
@ -41,14 +41,35 @@ public class TwilioVoiceController : TwilioController
string conversationId = $"TwilioVoice_{request.CallSid}";
var twilio = _services.GetRequiredService<TwilioService>();
var url = $"twilio/voice/{conversationId}/receive/0?states={states}";
var response = twilio.ReturnNoninterruptedInstructions(new List<string> { "twilio/welcome.mp3" }, url, true, timeout: 2);
VoiceResponse response;
if (string.IsNullOrWhiteSpace(intent))
{
var url = $"twilio/voice/{conversationId}/receive/0?{GenerateStatesParameter(states)}";
response = twilio.ReturnNoninterruptedInstructions(new List<string> { "twilio/welcome.mp3" }, url, true, timeout: 2);
}
else
{
int seqNum = 0;
var messageQueue = _services.GetRequiredService<TwilioMessageQueue>();
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
await sessionManager.StageCallerMessageAsync(conversationId, seqNum, intent);
var callerMessage = new CallerMessage()
{
ConversationId = conversationId,
SeqNumber = seqNum,
Content = intent,
From = request.From,
States = ParseStates(states)
};
await messageQueue.EnqueueAsync(callerMessage);
response = new VoiceResponse().Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}"), HttpMethod.Post);
}
return TwiML(response);
}
[ValidateRequest]
[HttpPost("twilio/voice/{conversationId}/receive/{seqNum}")]
public async Task<TwiMLResult> ReceiveCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum, [FromQuery] string states, VoiceRequest request, [FromQuery] int attempts = 1)
public async Task<TwiMLResult> ReceiveCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum, [FromQuery] string[] states, VoiceRequest request, [FromQuery] int attempts = 1)
{
var twilio = _services.GetRequiredService<TwilioService>();
var messageQueue = _services.GetRequiredService<TwilioMessageQueue>();
@ -72,22 +93,13 @@ public class TwilioVoiceController : TwilioController
ConversationId = conversationId,
SeqNumber = seqNum,
Content = messageContent,
Digits = request.Digits,
From = request.From
Digits = request.Digits,
From = request.From,
States = ParseStates(states)
};
if (!string.IsNullOrEmpty(states))
{
var kvp = states.Split(':');
if (kvp.Length == 2)
{
callerMessage.States.Add(kvp[0], kvp[1]);
}
}
await messageQueue.EnqueueAsync(callerMessage);
response = new VoiceResponse().Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{conversationId}/reply/{seqNum}?states={states}"), HttpMethod.Post);
response = new VoiceResponse().Redirect(new Uri($"{_settings.CallbackHost}/twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}"), HttpMethod.Post);
}
else
{
@ -105,11 +117,11 @@ public class TwilioVoiceController : TwilioController
speechPaths.Add($"twilio/say-it-again-{Random.Shared.Next(1, 5)}.mp3");
speechPaths.Add($"twilio/voice/speeches/{conversationId}/{lastRepy.SpeechFileName}");
}
response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/receive/{seqNum}?states={states}", true);
response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/receive/{seqNum}?{GenerateStatesParameter(states)}", true);
}
else
{
response = twilio.ReturnInstructions(null, $"twilio/voice/{conversationId}/receive/{seqNum}?states={states}&attempts={++attempts}", true);
response = twilio.ReturnInstructions(null, $"twilio/voice/{conversationId}/receive/{seqNum}?{GenerateStatesParameter(states)}&attempts={++attempts}", true);
}
}
return TwiML(response);
@ -118,7 +130,7 @@ public class TwilioVoiceController : TwilioController
[ValidateRequest]
[HttpPost("twilio/voice/{conversationId}/reply/{seqNum}")]
public async Task<TwiMLResult> ReplyCallerMessage([FromRoute] string conversationId, [FromRoute] int seqNum,
[FromQuery] string states, VoiceRequest request)
[FromQuery] string[] states, VoiceRequest request)
{
var nextSeqNum = seqNum + 1;
var sessionManager = _services.GetRequiredService<ITwilioSessionManager>();
@ -159,7 +171,7 @@ public class TwilioVoiceController : TwilioController
{
speechPaths.Add($"twilio/hold-on-short-{holdOnIndex}.mp3");
}
var fileName = $"indication_{seqNum}_{segIndex}.mp3";
fileStorage.SaveSpeechFile(conversationId, fileName, data);
speechPaths.Add($"twilio/voice/speeches/{conversationId}/{fileName}");
@ -173,7 +185,7 @@ public class TwilioVoiceController : TwilioController
segIndex++;
}
}
response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true);
response = twilio.ReturnInstructions(speechPaths, $"twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}", true);
await sessionManager.RemoveReplyIndicationAsync(conversationId, seqNum);
}
else
@ -196,7 +208,7 @@ public class TwilioVoiceController : TwilioController
instructions.Add($"twilio/typing-{typingIndex}.mp3");
}
response = twilio.ReturnInstructions(instructions, $"twilio/voice/{conversationId}/reply/{seqNum}?states={states}", true);
response = twilio.ReturnInstructions(instructions, $"twilio/voice/{conversationId}/reply/{seqNum}?{GenerateStatesParameter(states)}", true);
}
}
else
@ -214,7 +226,7 @@ public class TwilioVoiceController : TwilioController
response = twilio.ReturnInstructions(new List<string>
{
$"twilio/voice/speeches/{conversationId}/{reply.SpeechFileName}"
}, $"twilio/voice/{conversationId}/receive/{nextSeqNum}?states={states}", true, hints:reply.Hints);
}, $"twilio/voice/{conversationId}/receive/{nextSeqNum}?{GenerateStatesParameter(states)}", true, hints: reply.Hints);
}
}
@ -233,4 +245,31 @@ public class TwilioVoiceController : TwilioController
};
return result;
}
private Dictionary<string, string> ParseStates(string[] states)
{
var result = new Dictionary<string, string>();
if (states is null || !states.Any())
{
return result;
}
foreach (var kvp in states)
{
var parts = kvp.Split(':', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2)
{
result.Add(parts[0], parts[1]);
}
}
return result;
}
private string GenerateStatesParameter(string[] states)
{
if (states is null || states.Length == 0)
{
return null;
}
return string.Join("&", states.Select(x => $"states={x}"));
}
}