Merge branch 'SciSharp:master' into master

This commit is contained in:
Haiping 2025-01-16 19:48:29 -06:00 committed by GitHub
commit a0935627a6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 179 additions and 60 deletions

View file

@ -1,6 +0,0 @@
namespace BotSharp.Abstraction.Agents;
public interface IAgentRuleHook
{
void AddRules(List<AgentRule> rules);
}

View file

@ -5,8 +5,9 @@ public class ConversationChannel
public const string WebChat = "webchat";
public const string OpenAPI = "openapi";
public const string Phone = "phone";
public const string SMS = "sms";
public const string Messenger = "messenger";
public const string Email = "email";
public const string Cron = "cron";
public const string Crontab = "crontab";
public const string Database = "database";
}

View file

@ -146,6 +146,7 @@ public class RoleDialogModel : ITrackableMessage
FunctionArgs = source.FunctionArgs,
FunctionName = source.FunctionName,
ToolCallId = source.ToolCallId,
Indication = source.Indication,
PostbackFunctionName = source.PostbackFunctionName,
RichContent = source.RichContent,
Payload = source.Payload,

View file

@ -23,6 +23,9 @@ public class CrontabItem : ScheduleTaskArgs
[JsonPropertyName("expire_seconds")]
public int ExpireSeconds { get; set; } = 60;
[JsonPropertyName("last_execution_time")]
public DateTime? LastExecutionTime { get; set; }
[JsonPropertyName("created_time")]
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;

View file

@ -9,7 +9,7 @@ public interface IFunctionCallback
/// </summary>
string Indication => string.Empty;
Task<string> GetIndication(RoleDialogModel message) => Task.FromResult(Indication);
Task<string> GetIndication(RoleDialogModel message) => Task.FromResult(message.Indication ?? Indication);
Task<bool> Execute(RoleDialogModel message);
}

View file

@ -2,5 +2,12 @@ namespace BotSharp.Core.Crontab.Abstraction;
public interface ICrontabHook
{
Task OnCronTriggered(CrontabItem item);
Task OnCronTriggered(CrontabItem item)
=> Task.CompletedTask;
Task OnTaskExecuting(CrontabItem item)
=> Task.CompletedTask;
Task OnTaskExecuted(CrontabItem item)
=> Task.CompletedTask;
}

View file

@ -0,0 +1,9 @@
namespace BotSharp.Core.Crontab.Abstraction;
/// <summary>
/// Provide a cron source for the crontab service.
/// </summary>
public interface ICrontabSource
{
CrontabItem GetCrontabItem();
}

View file

@ -39,7 +39,17 @@ public class CrontabService : ICrontabService
{
var repo = _services.GetRequiredService<IBotSharpRepository>();
var crontable = repo.GetCrontabItems(CrontabItemFilter.Empty());
return crontable.Items.ToList();
// Add fixed crontab items from cronsources
var fixedCrantabItems = crontable.Items.ToList();
var cronsources = _services.GetServices<ICrontabSource>();
foreach (var source in cronsources)
{
var item = source.GetCrontabItem();
fixedCrantabItems.Add(source.GetCrontabItem());
}
return fixedCrantabItems;
}
public async Task ScheduledTimeArrived(CrontabItem item)
@ -47,8 +57,10 @@ public class CrontabService : ICrontabService
_logger.LogDebug($"ScheduledTimeArrived {item}");
await HookEmitter.Emit<ICrontabHook>(_services, async hook =>
await hook.OnCronTriggered(item)
);
await Task.Delay(1000 * 10);
{
await hook.OnTaskExecuting(item);
await hook.OnCronTriggered(item);
await hook.OnTaskExecuted(item);
});
}
}

View file

@ -24,9 +24,9 @@ public class CrontabWatcher : BackgroundService
{
var locker = scope.ServiceProvider.GetRequiredService<IDistributedLocker>();
/*while (!stoppingToken.IsCancellationRequested)
while (!stoppingToken.IsCancellationRequested)
{
var delay = Task.Delay(1000, stoppingToken);
var delay = Task.Delay(1000 * 10, stoppingToken);
await locker.LockAsync("CrontabWatcher", async () =>
{
@ -34,7 +34,7 @@ public class CrontabWatcher : BackgroundService
});
await delay;
}*/
}
_logger.LogWarning("Crontab Watcher background service is stopped.");
}
@ -58,10 +58,24 @@ public class CrontabWatcher : BackgroundService
// Get the current time
var currentTime = DateTime.UtcNow;
// Get the last occurrence from the schedule
var lastOccurrence = GetLastOccurrence(schedule);
// Get the next occurrence from the schedule
var nextOccurrence = schedule.GetNextOccurrence(currentTime.AddSeconds(-1));
// Check if the current time matches the schedule
// Get the previous occurrence from the execution log
var previousOccurrence = item.LastExecutionTime;
// First check if this occurrence was already triggered
if (previousOccurrence.HasValue &&
previousOccurrence.Value >= lastOccurrence &&
previousOccurrence.Value < nextOccurrence.AddSeconds(1))
{
continue;
}
// Then check if the current time matches the schedule
bool matches = currentTime >= nextOccurrence && currentTime < nextOccurrence.AddSeconds(1);
if (matches)
@ -72,9 +86,21 @@ public class CrontabWatcher : BackgroundService
}
catch (Exception ex)
{
_logger.LogWarning($"Error when running cron task ({item.ConversationId}, {item.Title}, {item.Cron}): {ex.Message}\r\n{ex.InnerException}");
_logger.LogError($"Error when running cron task ({item.Title}, {item.Cron}): {ex.Message}");
continue;
}
}
}
private DateTime GetLastOccurrence(CrontabSchedule schedule)
{
var nextOccurrence = schedule.GetNextOccurrence(DateTime.UtcNow);
var afterNextOccurrence = schedule.GetNextOccurrence(nextOccurrence);
var interval = afterNextOccurrence - nextOccurrence;
if (interval.TotalMinutes < 10)
{
throw new ArgumentException("The minimum interval must be at least 10 minutes.");
}
return nextOccurrence - interval;
}
}

View file

@ -45,6 +45,8 @@ public class RuleEngine : IRuleEngine
{
var conv = await convService.NewConversation(new Conversation
{
Channel = trigger.Channel,
Title = data,
AgentId = agent.Id
});
@ -52,7 +54,7 @@ public class RuleEngine : IRuleEngine
var states = new List<MessageState>
{
new("channel", ConversationChannel.Database),
new("channel", trigger.Channel),
new("channel_id", trigger.EntityId)
};
convService.SetConversationId(conv.Id, states);

View file

@ -0,0 +1,5 @@
namespace BotSharp.Core.Rules.Triggers;
public interface IRuleConfig
{
}

View file

@ -111,7 +111,7 @@ public partial class AgentService
parameterDef.Properties = JsonSerializer.Deserialize<JsonDocument>(clonedRoot.ToString());
parameterDef.Required = required;
return parameterDef; ;
return parameterDef;
}
public string RenderedTemplate(Agent agent, string templateName)

View file

@ -43,6 +43,7 @@ public partial class RoutingService
message.ToolCallId = response.ToolCallId;
message.FunctionName = response.FunctionName;
message.FunctionArgs = response.FunctionArgs;
message.Indication = response.Indication;
message.CurrentAgentId = agent.Id;
await InvokeFunction(message, dialogs);

View file

@ -52,7 +52,7 @@ public class RateLimitConversationHook : ConversationHookBase
var channel = states.GetState("channel");
// Check the number of conversations
if (channel != ConversationChannel.Phone && channel != ConversationChannel.Email)
if (channel != ConversationChannel.Phone && channel != ConversationChannel.Email && channel != ConversationChannel.Database)
{
var user = _services.GetRequiredService<IUserIdentity>();
var convService = _services.GetRequiredService<IConversationService>();

View file

@ -41,7 +41,7 @@ public class VerboseLogHook : IContentGeneratingHook
var agent = await agentService.LoadAgent(message.CurrentAgentId);
var log = message.Role == AgentRole.Function ?
$"[{agent?.Name}]: {message.FunctionName}({message.FunctionArgs})" :
$"[{agent?.Name}]: {message.Indication} {message.FunctionName}({message.FunctionArgs})" :
$"[{agent?.Name}]: {message.Content}" + $" <== [msg_id: {message.MessageId}]";
_logger.LogInformation(tokenStats.Prompt);

View file

@ -47,6 +47,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BotSharp.Core.Rules\BotSharp.Core.Rules.csproj" />
<ProjectReference Include="..\BotSharp.Core\BotSharp.Core.csproj" />
</ItemGroup>

View file

@ -160,16 +160,4 @@ public class AgentController : ControllerBase
}
return utilities.Where(x => !string.IsNullOrWhiteSpace(x.Name)).OrderBy(x => x.Name).ToList();
}
[HttpGet("/agent/rule/options")]
public IEnumerable<AgentRule> GetAgentRuleOptions()
{
var rules = new List<AgentRule>();
var hooks = _services.GetServices<IAgentRuleHook>();
foreach (var hook in hooks)
{
hook.AddRules(rules);
}
return rules.Where(x => !string.IsNullOrWhiteSpace(x.TriggerName)).OrderBy(x => x.TriggerName).ToList();
}
}

View file

@ -0,0 +1,33 @@
using BotSharp.Abstraction.Agents.Models;
using BotSharp.Core.Rules.Triggers;
namespace BotSharp.OpenAPI.Controllers;
[Authorize]
[ApiController]
public class RulesController
{
private readonly IServiceProvider _services;
public RulesController(
IServiceProvider services)
{
_services = services;
}
[HttpGet("/rule/triggers")]
public IEnumerable<AgentRule> GetRuleTriggers()
{
var triggers = _services.GetServices<IRuleTrigger>();
return triggers.Select(x => new AgentRule
{
TriggerName = x.GetType().Name
}).OrderBy(x => x.TriggerName).ToList();
}
[HttpGet("/rule/formalization")]
public async Task<string> GetFormalizedRuleDefinition([FromBody] AgentRule rule)
{
return "{}";
}
}

View file

@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Anthropic.SDK" Version="4.3.0" />
<PackageReference Include="Anthropic.SDK" Version="4.4.2" />
</ItemGroup>
<ItemGroup>

View file

@ -1,7 +1,6 @@
using Anthropic.SDK.Common;
using BotSharp.Abstraction.Conversations;
using BotSharp.Abstraction.MLTasks.Settings;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
@ -48,15 +47,16 @@ public class ChatCompletionProvider : IChatCompletion
if (response.StopReason == "tool_use")
{
var content = response.Content.OfType<TextContent>().FirstOrDefault();
var toolResult = response.Content.OfType<ToolUseContent>().First();
responseMessage = new RoleDialogModel(AgentRole.Function, response.FirstMessage?.Text ?? string.Empty)
responseMessage = new RoleDialogModel(AgentRole.Function, content?.Text ?? string.Empty)
{
CurrentAgentId = agent.Id,
MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty,
ToolCallId = toolResult.Id,
FunctionName = toolResult.Name,
FunctionArgs = JsonSerializer.Serialize(toolResult.Input)
FunctionArgs = JsonSerializer.Serialize(toolResult.Input),
};
}
else
@ -161,7 +161,7 @@ public class ChatCompletionProvider : IChatCompletion
new ToolResultContent()
{
ToolUseId = conv.ToolCallId,
Content = conv.Content
Content = [new TextContent() { Text = conv.Content }]
}
}
});

View file

@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.AI.OpenAI" Version="2.0.0" />
<PackageReference Include="Azure.AI.OpenAI" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
</ItemGroup>

View file

@ -160,6 +160,7 @@ public class ChatCompletionProvider : IChatCompletion
var funcContextIn = new RoleDialogModel(AgentRole.Function, text)
{
CurrentAgentId = agent.Id,
MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty,
FunctionName = toolCall?.FunctionName,
FunctionArgs = toolCall?.FunctionArguments?.ToString()
};

View file

@ -116,7 +116,8 @@ public class StreamingLogHook : ConversationHookBase, IContentGeneratingHook, IR
var agent = await _agentService.LoadAgent(message.CurrentAgentId);
message.FunctionArgs = message.FunctionArgs ?? "{}";
var args = message.FunctionArgs.FormatJson();
var log = $"{message.FunctionName} <u>executing</u>\r\n```json\r\n{args}\r\n```";
var log = $"*{message.Indication.Replace("\r", string.Empty).Replace("\n", string.Empty)}* \r\n\r\n **{message.FunctionName}**()";
log += args.Length > 5 ? $" \r\n```json\r\n{args}\r\n```" : string.Empty;
var input = new ContentLogInputModel(conversationId, message)
{

View file

@ -1,6 +1,6 @@
{
"name": "util-file-read_image",
"description": "If the user's request is related to analyzing images, you can call this function to analyze images.",
"description": "If the user's request is related to describing or analyzing images, you can call this function to analyze images.",
"parameters": {
"type": "object",
"properties": {
@ -10,7 +10,7 @@
},
"image_urls": {
"type": "array",
"description": "The image, photo or picture urls that user requests for analysis. They typically start with 'http' or 'https'. If user doesn't include any url, then leave this array empty. Please remove any duplicated urls",
"description": "The image, photo or picture urls that user requests for analysis. They typically start with 'http' or 'https'. If user doesn't include any url, then leave this array empty. Please remove any duplicated urls. Do not make up any urls.",
"items": {
"type": "string",
"description": "The image, photo or picture url that user requests for analysis. It typically starts with http or https."

View file

@ -1 +1,2 @@
Please call function util-file-read_image if user wants to describe an image or images.
Please call function util-file-read_image if user wants to describe an image or images.
You can also call function util-file-read_image to access the image or images that user uploaded.

View file

@ -1,6 +1,7 @@
using BotSharp.Abstraction.Agents;
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Abstraction.Loggers;
using Google.Protobuf.WellKnownTypes;
using Microsoft.Extensions.Logging;
using Mscc.GenerativeAI;
@ -125,17 +126,19 @@ public class GeminiChatCompletionProvider : IChatCompletion
if (!agentService.RenderFunction(agent, function)) continue;
var def = agentService.RenderFunctionProperty(agent, function);
var props = JsonSerializer.Serialize(def?.Properties);
var parameters = !string.IsNullOrWhiteSpace(props) && props != "{}" ? new Schema()
{
Type = ParameterType.Object,
Properties = JsonSerializer.Deserialize<dynamic>(props),
Required = def?.Required ?? []
} : null;
funcDeclarations.Add(new FunctionDeclaration
{
Name = function.Name,
Description = function.Description,
Parameters = new()
{
Type = ParameterType.Object,
Properties = def.Properties,
Required = def.Required
}
Parameters = parameters
});
funcPrompts.Add($"{function.Name}: {function.Description} {def}");

View file

@ -14,6 +14,8 @@ public class CrontabItemDocument : MongoBase
public int ExecutionCount { get; set; }
public int MaxExecutionCount { get; set; }
public int ExpireSeconds { get; set; }
public DateTime? LastExecutionTime { get; set; }
public bool LessThan60Seconds { get; set; } = false;
public IEnumerable<CronTaskMongoElement> Tasks { get; set; } = [];
public DateTime CreatedTime { get; set; } = DateTime.UtcNow;
@ -31,6 +33,8 @@ public class CrontabItemDocument : MongoBase
ExecutionCount = item.ExecutionCount,
MaxExecutionCount = item.MaxExecutionCount,
ExpireSeconds = item.ExpireSeconds,
LastExecutionTime = item.LastExecutionTime,
LessThan60Seconds = item.LessThan60Seconds,
Tasks = item.Tasks?.Select(x => CronTaskMongoElement.ToDomainElement(x))?.ToArray() ?? [],
CreatedTime = item.CreatedTime
};
@ -50,6 +54,8 @@ public class CrontabItemDocument : MongoBase
ExecutionCount = item.ExecutionCount,
MaxExecutionCount = item.MaxExecutionCount,
ExpireSeconds = item.ExpireSeconds,
LastExecutionTime = item.LastExecutionTime,
LessThan60Seconds = item.LessThan60Seconds,
Tasks = item.Tasks?.Select(x => CronTaskMongoElement.ToMongoElement(x))?.ToList() ?? [],
CreatedTime = item.CreatedTime
};

View file

@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenAI" Version="2.0.0" />
<PackageReference Include="OpenAI" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
</ItemGroup>

View file

@ -138,6 +138,7 @@ public class ChatCompletionProvider : IChatCompletion
var funcContextIn = new RoleDialogModel(AgentRole.Function, text)
{
CurrentAgentId = agent.Id,
MessageId = conversations.LastOrDefault()?.MessageId ?? string.Empty,
ToolCallId = toolCall?.Id,
FunctionName = toolCall?.FunctionName,
FunctionArgs = toolCall?.FunctionArguments?.ToString()

View file

@ -1,5 +1,6 @@
using BotSharp.Abstraction.Files;
using BotSharp.Abstraction.Infrastructures;
using BotSharp.Abstraction.Repositories;
using BotSharp.Core.Infrastructures;
using BotSharp.Plugin.Twilio.Interfaces;
using BotSharp.Plugin.Twilio.Models;
@ -126,9 +127,11 @@ public class TwilioVoiceController : TwilioController
SeqNumber = request.SeqNum,
Content = messageContent,
Digits = request.Digits,
From = request.From,
From = string.Equals(request.Direction, "inbound") ? request.From : request.To,
States = ParseStates(request.States)
};
callerMessage.RequestHeaders = new KeyValuePair<string, Microsoft.Extensions.Primitives.StringValues>[Request.Headers.Count];
Request.Headers.CopyTo(callerMessage.RequestHeaders, 0);
await messageQueue.EnqueueAsync(callerMessage);
response = new VoiceResponse();
@ -387,6 +390,9 @@ public class TwilioVoiceController : TwilioController
$"twilio/voice/speeches/{conversationId}/intial.mp3"
}
};
string tag = $"twilio:{Request.Form["AnsweredBy"]}";
var db = _services.GetRequiredService<IBotSharpRepository>();
db.AppendConversationTags(conversationId, new List<string> { tag });
var twilio = _services.GetRequiredService<TwilioService>();
var response = twilio.ReturnNoninterruptedInstructions(instruction);
return TwiML(response);

View file

@ -1,3 +1,5 @@
using Microsoft.Extensions.Primitives;
namespace BotSharp.Plugin.Twilio.Models
{
public class CallerMessage
@ -8,6 +10,7 @@ namespace BotSharp.Plugin.Twilio.Models
public string Digits { get; set; }
public string From { get; set; }
public Dictionary<string, string> States { get; set; } = new();
public KeyValuePair<string, StringValues>[] RequestHeaders { get; set; }
public override string ToString()
{

View file

@ -64,13 +64,16 @@ namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Functions
Channel = ConversationChannel.Phone
});
var conversationId = newConv.Id;
convStorage.Append(conversationId, new RoleDialogModel(AgentRole.User, "Hi, I'm calling to check my work order quote status, please help me locate my work order number and let me know what to do next.")
convStorage.Append(conversationId, new List<RoleDialogModel>
{
CurrentAgentId = entryAgentId
});
convStorage.Append(conversationId, new RoleDialogModel(AgentRole.Assistant, args.InitialMessage)
{
CurrentAgentId = entryAgentId
new RoleDialogModel(AgentRole.User, "Hi, I'm calling to check my work order quote status, please help me locate my work order number and let me know what to do next.")
{
CurrentAgentId = entryAgentId
},
new RoleDialogModel(AgentRole.Assistant, args.InitialMessage)
{
CurrentAgentId = entryAgentId
}
});
// Generate audio
@ -89,7 +92,9 @@ namespace BotSharp.Plugin.Twilio.OutboundPhoneCallHandler.Functions
var call = await CallResource.CreateAsync(
url: new Uri($"{_twilioSetting.CallbackHost}/twilio/voice/init-call?conversationId={conversationId}"),
to: new PhoneNumber(args.PhoneNumber),
from: new PhoneNumber(_twilioSetting.PhoneNumber));
from: new PhoneNumber(_twilioSetting.PhoneNumber),
asyncAmd: "true",
machineDetection: "DetectMessageEnd");
message.Content = $"The generated phone message: {args.InitialMessage}. \r\n[Conversation ID: {conversationId}]" ?? message.Content;
message.StopCompletion = true;

View file

@ -65,6 +65,10 @@ namespace BotSharp.Plugin.Twilio.Services
var httpContext = sp.GetRequiredService<IHttpContextAccessor>();
httpContext.HttpContext = new DefaultHttpContext();
httpContext.HttpContext.User = new ClaimsPrincipal(new ClaimsIdentity());
foreach (var header in message.RequestHeaders)
{
httpContext.HttpContext.Request.Headers[header.Key] = header.Value;
}
httpContext.HttpContext.Request.Headers["X-Twilio-BotSharp"] = "LOST";
AssistantMessage reply = null;

View file

@ -59,7 +59,10 @@ public class TwilioService
Gather.InputEnum.Speech,
Gather.InputEnum.Dtmf
},
Action = new Uri($"{_settings.CallbackHost}/twilio/voice/{twilioSetting.AgentId}")
Action = new Uri($"{_settings.CallbackHost}/twilio/voice/{twilioSetting.AgentId}"),
Enhanced = true,
SpeechModel = Gather.SpeechModelEnum.PhoneCall,
SpeechTimeout = "auto"
};
gather.Say(message);
@ -78,6 +81,7 @@ public class TwilioService
Gather.InputEnum.Dtmf
},
Action = new Uri($"{_settings.CallbackHost}/{conversationalVoiceResponse.CallbackPath}"),
Enhanced = true,
SpeechModel = Gather.SpeechModelEnum.PhoneCall,
SpeechTimeout = "auto", // timeout > 0 ? timeout.ToString() : "3",
Timeout = conversationalVoiceResponse.Timeout > 0 ? conversationalVoiceResponse.Timeout : 3,
@ -115,6 +119,7 @@ public class TwilioService
Gather.InputEnum.Dtmf
},
Action = new Uri($"{_settings.CallbackHost}/{conversationalVoiceResponse.CallbackPath}"),
Enhanced = true,
SpeechModel = Gather.SpeechModelEnum.PhoneCall,
SpeechTimeout = "auto", // conversationalVoiceResponse.Timeout > 0 ? conversationalVoiceResponse.Timeout.ToString() : "3",
Timeout = conversationalVoiceResponse.Timeout > 0 ? conversationalVoiceResponse.Timeout : 3,