diff --git a/bundles/org.openhab.core.io.rest.voice/src/main/java/org/openhab/core/io/rest/voice/internal/LLMToolDTO.java b/bundles/org.openhab.core.io.rest.voice/src/main/java/org/openhab/core/io/rest/voice/internal/LLMToolDTO.java new file mode 100644 index 000000000..5e9cdaa23 --- /dev/null +++ b/bundles/org.openhab.core.io.rest.voice/src/main/java/org/openhab/core/io/rest/voice/internal/LLMToolDTO.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2010-2026 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.rest.voice.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * A DTO that is used on the REST API to provide details about LLM tools. + * + * @author Florian Hotze - Initial contribution + */ +@Schema(name = "LLMTool") +@NonNullByDefault +public record LLMToolDTO(@Schema(requiredMode = Schema.RequiredMode.REQUIRED) String id, + @Schema(requiredMode = Schema.RequiredMode.REQUIRED) String label, String description) { +} diff --git a/bundles/org.openhab.core.io.rest.voice/src/main/java/org/openhab/core/io/rest/voice/internal/LLMToolMapper.java b/bundles/org.openhab.core.io.rest.voice/src/main/java/org/openhab/core/io/rest/voice/internal/LLMToolMapper.java new file mode 100644 index 000000000..328adbd1a --- /dev/null +++ b/bundles/org.openhab.core.io.rest.voice/src/main/java/org/openhab/core/io/rest/voice/internal/LLMToolMapper.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2010-2026 Contributors to the openHAB project + * + * See the NOTICE file(s) distributed with this work for additional + * information. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0 + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.openhab.core.io.rest.voice.internal; + +import java.util.Locale; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.voice.text.interpreter.llm.LLMTool; + +/** + * Mapper class that maps {@link LLMTool} instances to their respective DTOs. + * + * @author Florian Hotze - Initial contribution + */ +@NonNullByDefault +public class LLMToolMapper { + + /** + * Maps a {@link LLMTool} to a {@link LLMToolDTO}. + * + * @param tool the LLM tool + * @param locale the locale to use for the DTO + * + * @return the corresponding DTO + */ + public static LLMToolDTO map(LLMTool tool, @Nullable Locale locale) { + return new LLMToolDTO(tool.getUID(), tool.getLabel(locale), tool.getDescription(locale)); + } +} diff --git a/bundles/org.openhab.core.io.rest.voice/src/main/java/org/openhab/core/io/rest/voice/internal/VoiceResource.java b/bundles/org.openhab.core.io.rest.voice/src/main/java/org/openhab/core/io/rest/voice/internal/VoiceResource.java index 22ca5c7db..4f513f565 100644 --- a/bundles/org.openhab.core.io.rest.voice/src/main/java/org/openhab/core/io/rest/voice/internal/VoiceResource.java +++ b/bundles/org.openhab.core.io.rest.voice/src/main/java/org/openhab/core/io/rest/voice/internal/VoiceResource.java @@ -52,6 +52,8 @@ import org.openhab.core.voice.text.InterpretationArguments; import org.openhab.core.voice.text.InterpretationException; import org.openhab.core.voice.text.conversation.Conversation; import org.openhab.core.voice.text.conversation.ConversationManager; +import org.openhab.core.voice.text.interpreter.llm.LLMTool; +import org.openhab.core.voice.text.interpreter.llm.LLMToolRegistry; import org.osgi.service.component.annotations.Activate; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; @@ -95,17 +97,20 @@ public class VoiceResource implements RESTResource { private final AudioManager audioManager; private final VoiceManager voiceManager; private final ConversationManager conversationManager; + private final LLMToolRegistry llmToolRegistry; @Activate public VoiceResource( // final @Reference LocaleService localeService, // final @Reference AudioManager audioManager, // final @Reference VoiceManager voiceManager, // - final @Reference ConversationManager conversationManager) { + final @Reference ConversationManager conversationManager, // + final @Reference LLMToolRegistry llmToolRegistry) { this.localeService = localeService; this.audioManager = audioManager; this.voiceManager = voiceManager; this.conversationManager = conversationManager; + this.llmToolRegistry = llmToolRegistry; } @GET @@ -247,6 +252,20 @@ public class VoiceResource implements RESTResource { } } + @GET + @Path("/llmtools") + @Produces(MediaType.APPLICATION_JSON) + @Operation(operationId = "getLLMTools", summary = "Get the list of all LLM tools.", responses = { + @ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = LLMToolDTO.class)))) }) + public Response getLLMTools( + @HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language) { + final Locale locale = localeService.getLocale(language); + List dtos = llmToolRegistry.getAll().stream() + .sorted(java.util.Comparator.comparing(LLMTool::getUID)).map(tool -> LLMToolMapper.map(tool, locale)) + .toList(); + return Response.ok(dtos).build(); + } + @GET @Path("/voices") @Produces(MediaType.APPLICATION_JSON) diff --git a/bundles/org.openhab.core.io.rest.voice/src/test/java/org/openhab/core/io/rest/voice/internal/VoiceResourceTest.java b/bundles/org.openhab.core.io.rest.voice/src/test/java/org/openhab/core/io/rest/voice/internal/VoiceResourceTest.java index ae578bd87..b020bfcbc 100644 --- a/bundles/org.openhab.core.io.rest.voice/src/test/java/org/openhab/core/io/rest/voice/internal/VoiceResourceTest.java +++ b/bundles/org.openhab.core.io.rest.voice/src/test/java/org/openhab/core/io/rest/voice/internal/VoiceResourceTest.java @@ -27,6 +27,8 @@ import org.openhab.core.io.rest.LocaleService; import org.openhab.core.voice.VoiceManager; import org.openhab.core.voice.text.conversation.Conversation; import org.openhab.core.voice.text.conversation.ConversationManager; +import org.openhab.core.voice.text.interpreter.llm.LLMTool; +import org.openhab.core.voice.text.interpreter.llm.LLMToolRegistry; /** * Test class for {@link VoiceResource}. @@ -40,9 +42,10 @@ public class VoiceResourceTest { private AudioManager audioManager = mock(AudioManager.class); private VoiceManager voiceManager = mock(VoiceManager.class); private ConversationManager conversationManager = mock(ConversationManager.class); + private LLMToolRegistry llmToolRegistry = mock(LLMToolRegistry.class); private VoiceResource voiceResource = new VoiceResource(localeService, audioManager, voiceManager, - conversationManager); + conversationManager, llmToolRegistry); @SuppressWarnings("unchecked") @Test @@ -76,4 +79,37 @@ public class VoiceResourceTest { assertEquals(Instant.ofEpochMilli(3000), dto2.created()); assertEquals(Instant.ofEpochMilli(4000), dto2.lastUpdated()); } + + @SuppressWarnings("unchecked") + @Test + public void testGetLLMTools() { + LLMTool tool1 = mock(LLMTool.class); + when(tool1.getUID()).thenReturn("tool-1"); + when(tool1.getLabel(any())).thenReturn("Tool 1"); + when(tool1.getDescription(any())).thenReturn("Description of tool 1"); + + LLMTool tool2 = mock(LLMTool.class); + when(tool2.getUID()).thenReturn("tool-2"); + when(tool2.getLabel(any())).thenReturn("Tool 2"); + when(tool2.getDescription(any())).thenReturn("Description of tool 2"); + + when(llmToolRegistry.getAll()).thenReturn(List.of(tool1, tool2)); + + Response response = voiceResource.getLLMTools(null); + assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); + + List entity = (List) response.getEntity(); + assertNotNull(entity); + assertEquals(2, entity.size()); + + LLMToolDTO dto1 = entity.get(0); + assertEquals("tool-1", dto1.id()); + assertEquals("Tool 1", dto1.label()); + assertEquals("Description of tool 1", dto1.description()); + + LLMToolDTO dto2 = entity.get(1); + assertEquals("tool-2", dto2.id()); + assertEquals("Tool 2", dto2.label()); + assertEquals("Description of tool 2", dto2.description()); + } }