diff --git a/backend.tests/GetCurrentTimeToolTests.cs b/backend.tests/GetCurrentTimeToolTests.cs new file mode 100644 index 0000000..beb0134 --- /dev/null +++ b/backend.tests/GetCurrentTimeToolTests.cs @@ -0,0 +1,29 @@ +using System.Text.Json; +using backend.Tools; +using FluentAssertions; +using Xunit; + +namespace backend.tests; + +public class GetCurrentTimeToolTests +{ + [Fact] + public async Task Returns_now_field_with_iso_utc_timestamp() + { + var tool = new GetCurrentTimeTool(); + var args = JsonDocument.Parse("{}").RootElement; + var ctx = new DeviceContext(Guid.NewGuid(), Guid.NewGuid(), new NoopChannel()); + + var res = await tool.ExecuteAsync(new ToolInvocation("call_1", args), ctx, default); + + res.Output.TryGetProperty("now", out var now).Should().BeTrue(); + now.GetString().Should().MatchRegex(@"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}"); + now.GetString()!.Should().EndWith("Z"); + } + + private class NoopChannel : IDeviceChannel + { + public Task CallPiToolAsync(string name, JsonElement args, CancellationToken ct) + => throw new NotSupportedException(); + } +} diff --git a/backend.tests/ToolRegistryTests.cs b/backend.tests/ToolRegistryTests.cs new file mode 100644 index 0000000..aea942f --- /dev/null +++ b/backend.tests/ToolRegistryTests.cs @@ -0,0 +1,29 @@ +using backend.Tools; +using FluentAssertions; +using Xunit; + +namespace backend.tests; + +public class ToolRegistryTests +{ + private readonly ITool[] _all = new ITool[] + { + new GetCurrentTimeTool(), + }; + + [Fact] + public void Get_returns_tool_by_exact_name() + { + var reg = new ToolRegistry(_all); + reg.Get("get_current_time").Should().NotBeNull(); + reg.Get("does_not_exist").Should().BeNull(); + } + + [Fact] + public void Filter_returns_only_enabled_tools_preserving_order() + { + var reg = new ToolRegistry(_all); + var enabled = new HashSet { "get_current_time", "ghost" }; + reg.EnabledFor(enabled).Select(t => t.Name).Should().Equal("get_current_time"); + } +} diff --git a/backend/Tools/GetCurrentTimeTool.cs b/backend/Tools/GetCurrentTimeTool.cs new file mode 100644 index 0000000..e603082 --- /dev/null +++ b/backend/Tools/GetCurrentTimeTool.cs @@ -0,0 +1,20 @@ +using System.Text.Json; + +namespace backend.Tools; + +public class GetCurrentTimeTool : ITool +{ + public string Name => "get_current_time"; + public string Description => "Returns the current UTC time as an ISO-8601 string."; + public bool RunsDuringResponse => false; + + public JsonElement ParameterSchema { get; } = + JsonDocument.Parse("""{"type":"object","properties":{},"required":[]}""").RootElement; + + public Task ExecuteAsync(ToolInvocation invocation, DeviceContext device, CancellationToken ct) + { + var now = DateTimeOffset.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"); + var output = JsonDocument.Parse($$"""{"now":"{{now}}"}""").RootElement; + return Task.FromResult(new ToolResult(output)); + } +} diff --git a/backend/Tools/IDeviceChannel.cs b/backend/Tools/IDeviceChannel.cs new file mode 100644 index 0000000..1658bc4 --- /dev/null +++ b/backend/Tools/IDeviceChannel.cs @@ -0,0 +1,8 @@ +using System.Text.Json; + +namespace backend.Tools; + +public interface IDeviceChannel +{ + Task CallPiToolAsync(string name, JsonElement args, CancellationToken ct); +} diff --git a/backend/Tools/ITool.cs b/backend/Tools/ITool.cs new file mode 100644 index 0000000..a0e1dbc --- /dev/null +++ b/backend/Tools/ITool.cs @@ -0,0 +1,19 @@ +using System.Text.Json; + +namespace backend.Tools; + +public record DeviceContext(Guid DeviceId, Guid ConversationId, IDeviceChannel Channel); +public record ToolInvocation(string CallId, JsonElement Arguments); +public record ToolResult(JsonElement Output); + +public interface ITool +{ + string Name { get; } + string Description { get; } + JsonElement ParameterSchema { get; } + bool RunsDuringResponse { get; } + Task ExecuteAsync( + ToolInvocation invocation, + DeviceContext device, + CancellationToken ct); +} diff --git a/backend/Tools/ToolRegistry.cs b/backend/Tools/ToolRegistry.cs new file mode 100644 index 0000000..dfc21bc --- /dev/null +++ b/backend/Tools/ToolRegistry.cs @@ -0,0 +1,20 @@ +namespace backend.Tools; + +public class ToolRegistry +{ + private readonly Dictionary _byName; + private readonly List _all; + + public ToolRegistry(IEnumerable tools) + { + _all = tools.ToList(); + _byName = _all.ToDictionary(t => t.Name); + } + + public IReadOnlyList All => _all; + + public ITool? Get(string name) => _byName.GetValueOrDefault(name); + + public IEnumerable EnabledFor(IReadOnlySet enabled) + => _all.Where(t => enabled.Contains(t.Name)); +}