[voice] Add REST endpoint to get LLM tools (#5654)

Signed-off-by: Florian Hotze <dev@florianhotze.com>
This commit is contained in:
Florian Hotze
2026-06-15 12:56:03 +02:00
committed by GitHub
parent 053fcdcfc8
commit d4d7455c57
4 changed files with 125 additions and 2 deletions
@@ -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) {
}
@@ -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));
}
}
@@ -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<LLMToolDTO> 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)
@@ -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<LLMToolDTO> entity = (List<LLMToolDTO>) 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());
}
}