From 212ce06f0756b16ef5ccb6ef3a28b54fbd09ca64 Mon Sep 17 00:00:00 2001 From: Assistant builder Date: Thu, 11 Jun 2026 21:22:45 +0000 Subject: [PATCH] Add SetVolumeTool (validates 0..100, delegates to Pi via IDeviceChannel) --- backend.tests/SetVolumeToolTests.cs | 52 +++++++++++++++++++++++++++++ backend/Tools/SetVolumeTool.cs | 34 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 backend.tests/SetVolumeToolTests.cs create mode 100644 backend/Tools/SetVolumeTool.cs diff --git a/backend.tests/SetVolumeToolTests.cs b/backend.tests/SetVolumeToolTests.cs new file mode 100644 index 0000000..893f307 --- /dev/null +++ b/backend.tests/SetVolumeToolTests.cs @@ -0,0 +1,52 @@ +using System.Text.Json; +using backend.Tools; +using FluentAssertions; +using Xunit; + +namespace backend.tests; + +public class SetVolumeToolTests +{ + [Fact] + public async Task Forwards_call_to_pi_and_returns_its_result() + { + var fake = new RecordingChannel( + JsonDocument.Parse("""{"ok":true}""").RootElement); + var tool = new SetVolumeTool(); + var ctx = new DeviceContext(Guid.NewGuid(), Guid.NewGuid(), fake); + + var args = JsonDocument.Parse("""{"level":42}""").RootElement; + var res = await tool.ExecuteAsync(new ToolInvocation("c1", args), ctx, default); + + fake.LastName.Should().Be("set_volume"); + fake.LastArgs.GetProperty("level").GetInt32().Should().Be(42); + res.Output.GetProperty("ok").GetBoolean().Should().BeTrue(); + } + + [Fact] + public async Task Rejects_out_of_range_level_without_calling_pi() + { + var fake = new RecordingChannel(JsonDocument.Parse("""{"ok":true}""").RootElement); + var tool = new SetVolumeTool(); + var ctx = new DeviceContext(Guid.NewGuid(), Guid.NewGuid(), fake); + + var args = JsonDocument.Parse("""{"level":150}""").RootElement; + var res = await tool.ExecuteAsync(new ToolInvocation("c1", args), ctx, default); + + fake.LastName.Should().BeNull(); + res.Output.GetProperty("ok").GetBoolean().Should().BeFalse(); + res.Output.GetProperty("error").GetString().Should().Contain("0..100"); + } + + private class RecordingChannel(JsonElement reply) : IDeviceChannel + { + public string? LastName { get; private set; } + public JsonElement LastArgs { get; private set; } + public Task CallPiToolAsync(string name, JsonElement args, CancellationToken ct) + { + LastName = name; + LastArgs = args; + return Task.FromResult(reply); + } + } +} diff --git a/backend/Tools/SetVolumeTool.cs b/backend/Tools/SetVolumeTool.cs new file mode 100644 index 0000000..b40d7e2 --- /dev/null +++ b/backend/Tools/SetVolumeTool.cs @@ -0,0 +1,34 @@ +using System.Text.Json; + +namespace backend.Tools; + +public class SetVolumeTool : ITool +{ + public string Name => "set_volume"; + public string Description => + "Sets the assistant's audio output volume on the device. Level is 0..100."; + public bool RunsDuringResponse => true; + + public JsonElement ParameterSchema { get; } = JsonDocument.Parse( + """ + { + "type":"object", + "properties":{"level":{"type":"integer","minimum":0,"maximum":100}}, + "required":["level"] + } + """).RootElement; + + public async Task ExecuteAsync(ToolInvocation invocation, DeviceContext device, CancellationToken ct) + { + if (!invocation.Arguments.TryGetProperty("level", out var lvlEl) + || !lvlEl.TryGetInt32(out var level) + || level < 0 || level > 100) + { + var err = JsonDocument.Parse("""{"ok":false,"error":"level must be an integer in 0..100"}""").RootElement; + return new ToolResult(err); + } + + var piReply = await device.Channel.CallPiToolAsync(Name, invocation.Arguments, ct); + return new ToolResult(piReply); + } +}