feat(telegram): /a2a lists a chat's A2A callers and recent tasks
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using Telegram.Bot.Types;
|
using Telegram.Bot.Types;
|
||||||
|
using Volcanic.AgenticCore.A2A.Server;
|
||||||
|
using Volcanic.AgenticCore.A2A.Server.Callers;
|
||||||
|
using Volcanic.AgenticCore.A2A.Server.Tasks;
|
||||||
using Volcanic.AgenticCore.Abstractions.Providers;
|
using Volcanic.AgenticCore.Abstractions.Providers;
|
||||||
using Volcanic.AgenticCore.Extensions.Scheduler;
|
using Volcanic.AgenticCore.Extensions.Scheduler;
|
||||||
using Volcanic.AgenticCore.Hosting;
|
using Volcanic.AgenticCore.Hosting;
|
||||||
@@ -11,7 +14,8 @@ using Volcanic.AgenticCore.Telegram.Triggers;
|
|||||||
namespace Volcanic.AgenticCore.Telegram.Chats;
|
namespace Volcanic.AgenticCore.Telegram.Chats;
|
||||||
|
|
||||||
/// <summary>The bot's own slash commands. Anything else starting with <c>/</c> goes to the session (extension commands).</summary>
|
/// <summary>The bot's own slash commands. Anything else starting with <c>/</c> goes to the session (extension commands).</summary>
|
||||||
public sealed class ChatCommands(TelegramSender sender, ChatTimeZoneService? zones = null, JobScheduler? scheduler = null, TriggerServices? triggers = null)
|
public sealed class ChatCommands(TelegramSender sender, ChatTimeZoneService? zones = null, JobScheduler? scheduler = null, TriggerServices? triggers = null,
|
||||||
|
A2AHostOptions? a2a = null)
|
||||||
{
|
{
|
||||||
public static readonly IReadOnlyList<BotCommand> Menu =
|
public static readonly IReadOnlyList<BotCommand> Menu =
|
||||||
[
|
[
|
||||||
@@ -25,6 +29,7 @@ public sealed class ChatCommands(TelegramSender sender, ChatTimeZoneService? zon
|
|||||||
new() { Command = "timezone", Description = "Show or set this chat's time zone" },
|
new() { Command = "timezone", Description = "Show or set this chat's time zone" },
|
||||||
.. SchedulerSetup.Menu,
|
.. SchedulerSetup.Menu,
|
||||||
TriggersSetup.MenuCommand,
|
TriggersSetup.MenuCommand,
|
||||||
|
new() { Command = "a2a", Description = "A2A callers and tasks of this chat" },
|
||||||
new() { Command = "help", Description = "What this bot does" },
|
new() { Command = "help", Description = "What this bot does" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -119,6 +124,10 @@ public sealed class ChatCommands(TelegramSender sender, ChatTimeZoneService? zon
|
|||||||
await ReplyAsync(chat, Jobs(chat), cancellationToken).ConfigureAwait(false);
|
await ReplyAsync(chat, Jobs(chat), cancellationToken).ConfigureAwait(false);
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
case "a2a":
|
||||||
|
await ReplyAsync(chat, A2AStatus(chat), cancellationToken).ConfigureAwait(false);
|
||||||
|
return true;
|
||||||
|
|
||||||
case "cancel":
|
case "cancel":
|
||||||
await ReplyAsync(chat, await CancelJobAsync(chat, command.Args, cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false);
|
await ReplyAsync(chat, await CancelJobAsync(chat, command.Args, cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false);
|
||||||
return true;
|
return true;
|
||||||
@@ -146,7 +155,7 @@ public sealed class ChatCommands(TelegramSender sender, ChatTimeZoneService? zon
|
|||||||
/compact – summarise the conversation to free context
|
/compact – summarise the conversation to free context
|
||||||
/thinking [level] – show or set the thinking level
|
/thinking [level] – show or set the thinking level
|
||||||
/timezone [IANA name|reset] – this chat's time zone
|
/timezone [IANA name|reset] – this chat's time zone
|
||||||
""" + SchedulerSetup.HelpLines + TriggersSetup.HelpLines + GoogleCalendarSetup.HelpLines(chat.Session.Commands);
|
""" + SchedulerSetup.HelpLines + "\n/a2a – A2A callers and recent tasks of this chat" + TriggersSetup.HelpLines + GoogleCalendarSetup.HelpLines(chat.Session.Commands);
|
||||||
|
|
||||||
private string Status(ChatContext chat)
|
private string Status(ChatContext chat)
|
||||||
{
|
{
|
||||||
@@ -216,6 +225,19 @@ public sealed class ChatCommands(TelegramSender sender, ChatTimeZoneService? zon
|
|||||||
string.Join('\n', jobs.Select(j => JobFormat.Line(j, zone, now)));
|
string.Join('\n', jobs.Select(j => JobFormat.Line(j, zone, now)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string A2AStatus(ChatContext chat)
|
||||||
|
{
|
||||||
|
if (a2a is not { Enabled: true }) return "A2A is off. Set A2A__Enabled=true (and A2A__PublicUrl) to let other agents talk to this assistant.";
|
||||||
|
var workspace = chat.Session.Cwd;
|
||||||
|
var callers = CallersFile.Read(workspace, Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance);
|
||||||
|
var lines = new List<string> { "**A2A callers** (.volcanic/a2a/callers.json)" };
|
||||||
|
lines.AddRange(callers.Count == 0 ? ["none"] : callers.Select(c => $"• {c.Name} ({c.Mode}) key …{(c.Key.Length > 4 ? c.Key[^4..] : "")}"));
|
||||||
|
var tasks = A2ATaskFiles.Recent(workspace, 10);
|
||||||
|
lines.Add("**Recent A2A tasks**");
|
||||||
|
lines.AddRange(tasks.Count == 0 ? ["none"] : tasks.Select(t => $"• {t.Caller}: {t.State} ({t.TaskId[..Math.Min(8, t.TaskId.Length)]})"));
|
||||||
|
return string.Join('\n', lines);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<string> CancelJobAsync(ChatContext chat, string arg, CancellationToken cancellationToken)
|
private async Task<string> CancelJobAsync(ChatContext chat, string arg, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (scheduler is not { IsStarted: true }) return SchedulerSetup.DisabledReply;
|
if (scheduler is not { IsStarted: true }) return SchedulerSetup.DisabledReply;
|
||||||
|
|||||||
@@ -50,4 +50,26 @@ public class A2AHostTests
|
|||||||
Assert.Contains(services, d => d.ServiceType == typeof(IServer) && d.ImplementationType == typeof(NoHttpServer));
|
Assert.Contains(services, d => d.ServiceType == typeof(IServer) && d.ImplementationType == typeof(NoHttpServer));
|
||||||
Assert.DoesNotContain(services, d => d.ServiceType == typeof(IA2ACallerDirectory));
|
Assert.DoesNotContain(services, d => d.ServiceType == typeof(IA2ACallerDirectory));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task A2aCommand_ListsMaskedCallersAndTasks()
|
||||||
|
{
|
||||||
|
await using var host = new ChatHost(a2a: new A2AHostOptions { Enabled = true });
|
||||||
|
WriteCallers(host.WorkspaceOf(), """[ { "name": "travel-bot", "key": "secret-key-1234" } ]""");
|
||||||
|
await host.SendAsync("/a2a");
|
||||||
|
await host.SettleAsync();
|
||||||
|
var reply = host.LastReply();
|
||||||
|
Assert.Contains("travel-bot", reply);
|
||||||
|
Assert.Contains("…1234", reply);
|
||||||
|
Assert.DoesNotContain("secret-key-1234", reply);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task A2aCommand_DisabledReply()
|
||||||
|
{
|
||||||
|
await using var host = new ChatHost();
|
||||||
|
await host.SendAsync("/a2a");
|
||||||
|
await host.SettleAsync();
|
||||||
|
Assert.Contains("A2A is off", host.LastReply());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,8 @@ public sealed class ChatHost : IAsyncDisposable
|
|||||||
public ChatHost(Action<ChatHost, IServiceCollection>? configure = null, Action<AgenticCoreOptions>? core = null, TimeProvider? time = null,
|
public ChatHost(Action<ChatHost, IServiceCollection>? configure = null, Action<AgenticCoreOptions>? core = null, TimeProvider? time = null,
|
||||||
Volcanic.AgenticCore.Extensions.GoogleCalendar.GoogleCalendarOptions? calendar = null,
|
Volcanic.AgenticCore.Extensions.GoogleCalendar.GoogleCalendarOptions? calendar = null,
|
||||||
Volcanic.AgenticCore.Telegram.Speech.ISpeechToText? speech = null, string? root = null, SchedulerOptions? scheduler = null,
|
Volcanic.AgenticCore.Telegram.Speech.ISpeechToText? speech = null, string? root = null, SchedulerOptions? scheduler = null,
|
||||||
IScriptRunner? runner = null, TriggerOptions? triggers = null, IJevClient? jev = null)
|
IScriptRunner? runner = null, TriggerOptions? triggers = null, IJevClient? jev = null,
|
||||||
|
Volcanic.AgenticCore.A2A.Server.A2AHostOptions? a2a = null)
|
||||||
{
|
{
|
||||||
_ownsRoot = root is null;
|
_ownsRoot = root is null;
|
||||||
_root = root ?? Path.Combine(Path.GetTempPath(), "volcanic-telegram", Guid.NewGuid().ToString("N"));
|
_root = root ?? Path.Combine(Path.GetTempPath(), "volcanic-telegram", Guid.NewGuid().ToString("N"));
|
||||||
@@ -115,7 +116,7 @@ public sealed class ChatHost : IAsyncDisposable
|
|||||||
if (Scheduler is not null) Delivery = new TelegramJobDelivery(Chats, Settings, Options, clock, NullLogger<TelegramJobDelivery>.Instance);
|
if (Scheduler is not null) Delivery = new TelegramJobDelivery(Chats, Settings, Options, clock, NullLogger<TelegramJobDelivery>.Instance);
|
||||||
var media = new InboundMedia(sender, Options, time ?? TimeProvider.System, NullLogger<InboundMedia>.Instance, speech);
|
var media = new InboundMedia(sender, Options, time ?? TimeProvider.System, NullLogger<InboundMedia>.Instance, speech);
|
||||||
Composer = new InboundComposer(sender, media, NullLogger<InboundComposer>.Instance);
|
Composer = new InboundComposer(sender, media, NullLogger<InboundComposer>.Instance);
|
||||||
Router = new UpdateRouter(Options, Chats, new ChatCommands(sender, Zones, Scheduler, Triggers), sender, Composer, NullLogger<UpdateRouter>.Instance, Impersonation, Feed);
|
Router = new UpdateRouter(Options, Chats, new ChatCommands(sender, Zones, Scheduler, Triggers, a2a), sender, Composer, NullLogger<UpdateRouter>.Instance, Impersonation, Feed);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly TriggerDispatcher? _dispatcher;
|
private readonly TriggerDispatcher? _dispatcher;
|
||||||
|
|||||||
Reference in New Issue
Block a user