Init Crontab

This commit is contained in:
Haiping Chen 2024-12-03 16:52:14 -06:00
parent 61c0f91a2e
commit 43b9dd931d
14 changed files with 297 additions and 4 deletions

View file

@ -56,4 +56,9 @@ public class BuiltInAgentId
/// Translates user-defined natural language rules into programmatic code
/// </summary>
public const string RuleEncoder = "6acfb93c-3412-402e-9ba5-c5d3cd8f0161";
/// <summary>
/// Schedule job
/// </summary>
public const string Crontab = "c2o139da-a62a-4355-8605-fdf0ffaca58e";
}

View file

@ -7,4 +7,5 @@ public class ConversationChannel
public const string Phone = "phone";
public const string Messenger = "messenger";
public const string Email = "email";
public const string Cron = "cron";
}

View file

@ -0,0 +1,7 @@
namespace BotSharp.Abstraction.Infrastructures;
public interface IDistributedLocker
{
bool Lock(string resource, Action action, int timeout = 30);
Task<bool> LockAsync(string resource, Func<Task> action, int timeout = 30);
}

View file

@ -0,0 +1,8 @@
using BotSharp.Core.Crontab.Models;
namespace BotSharp.Core.Crontab.Abstraction;
public interface ICrontabHook
{
Task OnCronTriggered(CrontabItem item);
}

View file

@ -0,0 +1,9 @@
using BotSharp.Core.Crontab.Models;
namespace BotSharp.Core.Crontab.Abstraction;
public interface ICrontabService
{
Task<List<CrontabItem>> GetCrontable();
Task ScheduledTimeArrived(CrontabItem item);
}

View file

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<None Remove="data\agents\c2o139da-a62a-4355-8605-fdf0ffaca58e\agent.json" />
</ItemGroup>
<ItemGroup>
<Content Include="data\agents\c2o139da-a62a-4355-8605-fdf0ffaca58e\agent.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\BotSharp.Abstraction\BotSharp.Abstraction.csproj" />
<ProjectReference Include="..\BotSharp.Core\BotSharp.Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="NCrontab" Version="3.3.3" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,40 @@
/*****************************************************************************
Copyright 2024 Written by Haiping Chen. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
******************************************************************************/
namespace BotSharp.Core.Crontab;
/// <summary>
/// Crontab plugin is a time-based job scheduler in agent framework.
/// The cron system is used for automating repetitive tasks, such as trigger AI Agent to do specific task periodically.
/// </summary>
public class CrontabPlugin : IBotSharpPlugin
{
public string Id => "3155c15e-28d3-43f7-8ead-fc43324ec21a";
public string Name => "BotSharp Crontab";
public string Description => "Crontab plugin is a time-based job scheduler in agent framework. The cron system is used to trigger AI Agent to do specific task periodically.";
public string IconUrl => "https://icon-library.com/images/stop-watch-icon/stop-watch-icon-10.jpg";
public string[] AgentIds =
[
BuiltInAgentId.Crontab
];
public void RegisterDI(IServiceCollection services, IConfiguration config)
{
services.AddScoped<ICrontabService, CrontabService>();
services.AddHostedService<CrontabWatcher>();
}
}

View file

@ -0,0 +1,14 @@
namespace BotSharp.Core.Crontab.Models;
public class CrontabItem
{
public string UserId { get; set; } = null!;
public string AgentId { get; set; } = null!;
public string Topic { get; set; } = null!;
public string Cron { get; set; } = null!;
public override string ToString()
{
return $"AgentId: {AgentId}, UserId: {UserId}, Topic: {Topic}";
}
}

View file

@ -0,0 +1,77 @@
/*****************************************************************************
Copyright 2024 Written by Haiping Chen. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
******************************************************************************/
using BotSharp.Abstraction.Agents.Enums;
using BotSharp.Core.Crontab.Models;
using BotSharp.Core.Infrastructures;
/*****************************************************************************
Copyright 2024 Written by Haiping Chen. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
******************************************************************************/
using Microsoft.Extensions.Logging;
namespace BotSharp.Core.Crontab.Services;
/// <summary>
/// The Crontab service schedules distributed events based on the execution times provided by users.
/// In a scalable environment, distributed locks are used to ensure that each event is triggered only once.
/// </summary>
public class CrontabService : ICrontabService
{
private readonly IServiceProvider _services;
private ILogger _logger;
public CrontabService(IServiceProvider services, ILogger<CrontabService> logger)
{
_services = services;
_logger = logger;
}
public async Task<List<CrontabItem>> GetCrontable()
{
return
[
new CrontabItem
{
Cron = "*/30 * * * * *",
AgentId = BuiltInAgentId.AIAssistant,
}
];
}
public async Task ScheduledTimeArrived(CrontabItem item)
{
_logger.LogInformation("ScheduledTimeArrived");
await HookEmitter.Emit<ICrontabHook>(_services, async hook =>
await hook.OnCronTriggered(item)
);
await Task.Delay(1000 * 10);
}
}

View file

@ -0,0 +1,70 @@
using BotSharp.Abstraction.Infrastructures;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NCrontab;
namespace BotSharp.Core.Crontab.Services;
public class CrontabWatcher : BackgroundService
{
private readonly ILogger _logger;
private readonly IServiceProvider _services;
public CrontabWatcher(IServiceProvider services, ILogger<CrontabWatcher> logger)
{
_logger = logger;
_services = services;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Crontab Watcher background service is running.");
using (var scope = _services.CreateScope())
{
var locker = scope.ServiceProvider.GetRequiredService<IDistributedLocker>();
while (!stoppingToken.IsCancellationRequested)
{
var delay = Task.Delay(1000, stoppingToken);
await locker.LockAsync("CrontabWatcher", async () =>
{
await RunCronChecker(scope.ServiceProvider);
});
await delay;
}
_logger.LogWarning("Crontab Watcher background service is stopped.");
}
}
private async Task RunCronChecker(IServiceProvider services)
{
var cron = services.GetRequiredService<ICrontabService>();
var crons = await cron.GetCrontable();
foreach (var item in crons)
{
var schedule = CrontabSchedule.Parse(item.Cron, new CrontabSchedule.ParseOptions
{
IncludingSeconds = true // Ensure you account for seconds
});
// Get the current time
var currentTime = DateTime.UtcNow;
// Get the next occurrence from the schedule
var nextOccurrence = schedule.GetNextOccurrence(currentTime.AddSeconds(-1));
// Check if the current time matches the schedule
bool matches = currentTime >= nextOccurrence && currentTime < nextOccurrence.AddSeconds(1);
if (matches)
{
_logger.LogInformation($"The current time matches the cron expression {item}");
cron.ScheduledTimeArrived(item);
}
}
}
}

View file

@ -0,0 +1,7 @@
global using Microsoft.Extensions.Configuration;
global using Microsoft.Extensions.DependencyInjection;
global using BotSharp.Abstraction.Agents.Enums;
global using BotSharp.Abstraction.Plugins;
global using BotSharp.Core.Crontab.Services;
global using BotSharp.Core.Crontab.Abstraction;

View file

@ -0,0 +1,24 @@
{
"id": "c2o139da-a62a-4355-8605-fdf0ffaca58e",
"name": "Crontab",
"description": "Convert the user-specified schedule into a Cron expression, accurate to the second level.",
"iconUrl": "https://icon-library.com/images/stop-watch-icon/stop-watch-icon-10.jpg",
"type": "task",
"createdDateTime": "2024-12-03T00:00:00Z",
"updatedDateTime": "2024-12-03T00:00:00Z",
"disabled": false,
"isPublic": true,
"profiles": [ "cron" ],
"llmConfig": {
"provider": "openai",
"model": "gpt-4o-mini"
},
"routingRules": [
{
"field": "cron_expression",
"required": true,
"field_type": "string",
"description": "A Cron expression, accurate to the second level."
}
]
}

View file

@ -25,7 +25,7 @@ public static class BotSharpCoreExtensions
config.Bind("Interpreter", interpreterSettings);
services.AddSingleton(x => interpreterSettings);
services.AddSingleton<DistributedLocker>();
services.AddSingleton<IDistributedLocker, DistributedLocker>();
// Register template render
services.AddSingleton<ITemplateRender, TemplateRender>();

View file

@ -1,9 +1,10 @@
using BotSharp.Abstraction.Infrastructures;
using Medallion.Threading.Redis;
using StackExchange.Redis;
namespace BotSharp.Core.Infrastructures;
public class DistributedLocker
public class DistributedLocker : IDistributedLocker
{
private readonly IConnectionMultiplexer _redis;
private readonly ILogger _logger;
@ -14,7 +15,7 @@ public class DistributedLocker
_logger = logger;
}
public async Task<T> Lock<T>(string resource, Func<Task<T>> action, int timeoutInSeconds = 30)
public async Task<bool> LockAsync(string resource, Func<Task> action, int timeoutInSeconds = 30)
{
var timeout = TimeSpan.FromSeconds(timeoutInSeconds);
@ -24,9 +25,11 @@ public class DistributedLocker
if (handle == null)
{
_logger.LogWarning($"Acquire lock for {resource} failed due to after {timeout}s timeout.");
return false;
}
return await action();
await action();
return true;
}
}