Skip to content

Commit d4d7455

Browse files
authored
[voice] Add REST endpoint to get LLM tools (#5654)
Signed-off-by: Florian Hotze <dev@florianhotze.com>
1 parent 053fcdc commit d4d7455

4 files changed

Lines changed: 125 additions & 2 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Copyright (c) 2010-2026 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.core.io.rest.voice.internal;
14+
15+
import org.eclipse.jdt.annotation.NonNullByDefault;
16+
17+
import io.swagger.v3.oas.annotations.media.Schema;
18+
19+
/**
20+
* A DTO that is used on the REST API to provide details about LLM tools.
21+
*
22+
* @author Florian Hotze - Initial contribution
23+
*/
24+
@Schema(name = "LLMTool")
25+
@NonNullByDefault
26+
public record LLMToolDTO(@Schema(requiredMode = Schema.RequiredMode.REQUIRED) String id,
27+
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) String label, String description) {
28+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright (c) 2010-2026 Contributors to the openHAB project
3+
*
4+
* See the NOTICE file(s) distributed with this work for additional
5+
* information.
6+
*
7+
* This program and the accompanying materials are made available under the
8+
* terms of the Eclipse Public License 2.0 which is available at
9+
* http://www.eclipse.org/legal/epl-2.0
10+
*
11+
* SPDX-License-Identifier: EPL-2.0
12+
*/
13+
package org.openhab.core.io.rest.voice.internal;
14+
15+
import java.util.Locale;
16+
17+
import org.eclipse.jdt.annotation.NonNullByDefault;
18+
import org.eclipse.jdt.annotation.Nullable;
19+
import org.openhab.core.voice.text.interpreter.llm.LLMTool;
20+
21+
/**
22+
* Mapper class that maps {@link LLMTool} instances to their respective DTOs.
23+
*
24+
* @author Florian Hotze - Initial contribution
25+
*/
26+
@NonNullByDefault
27+
public class LLMToolMapper {
28+
29+
/**
30+
* Maps a {@link LLMTool} to a {@link LLMToolDTO}.
31+
*
32+
* @param tool the LLM tool
33+
* @param locale the locale to use for the DTO
34+
*
35+
* @return the corresponding DTO
36+
*/
37+
public static LLMToolDTO map(LLMTool tool, @Nullable Locale locale) {
38+
return new LLMToolDTO(tool.getUID(), tool.getLabel(locale), tool.getDescription(locale));
39+
}
40+
}

bundles/org.openhab.core.io.rest.voice/src/main/java/org/openhab/core/io/rest/voice/internal/VoiceResource.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@
5252
import org.openhab.core.voice.text.InterpretationException;
5353
import org.openhab.core.voice.text.conversation.Conversation;
5454
import org.openhab.core.voice.text.conversation.ConversationManager;
55+
import org.openhab.core.voice.text.interpreter.llm.LLMTool;
56+
import org.openhab.core.voice.text.interpreter.llm.LLMToolRegistry;
5557
import org.osgi.service.component.annotations.Activate;
5658
import org.osgi.service.component.annotations.Component;
5759
import org.osgi.service.component.annotations.Reference;
@@ -95,17 +97,20 @@ public class VoiceResource implements RESTResource {
9597
private final AudioManager audioManager;
9698
private final VoiceManager voiceManager;
9799
private final ConversationManager conversationManager;
100+
private final LLMToolRegistry llmToolRegistry;
98101

99102
@Activate
100103
public VoiceResource( //
101104
final @Reference LocaleService localeService, //
102105
final @Reference AudioManager audioManager, //
103106
final @Reference VoiceManager voiceManager, //
104-
final @Reference ConversationManager conversationManager) {
107+
final @Reference ConversationManager conversationManager, //
108+
final @Reference LLMToolRegistry llmToolRegistry) {
105109
this.localeService = localeService;
106110
this.audioManager = audioManager;
107111
this.voiceManager = voiceManager;
108112
this.conversationManager = conversationManager;
113+
this.llmToolRegistry = llmToolRegistry;
109114
}
110115

111116
@GET
@@ -247,6 +252,20 @@ public Response interpret(
247252
}
248253
}
249254

255+
@GET
256+
@Path("/llmtools")
257+
@Produces(MediaType.APPLICATION_JSON)
258+
@Operation(operationId = "getLLMTools", summary = "Get the list of all LLM tools.", responses = {
259+
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = LLMToolDTO.class)))) })
260+
public Response getLLMTools(
261+
@HeaderParam(HttpHeaders.ACCEPT_LANGUAGE) @Parameter(description = "language") @Nullable String language) {
262+
final Locale locale = localeService.getLocale(language);
263+
List<LLMToolDTO> dtos = llmToolRegistry.getAll().stream()
264+
.sorted(java.util.Comparator.comparing(LLMTool::getUID)).map(tool -> LLMToolMapper.map(tool, locale))
265+
.toList();
266+
return Response.ok(dtos).build();
267+
}
268+
250269
@GET
251270
@Path("/voices")
252271
@Produces(MediaType.APPLICATION_JSON)

bundles/org.openhab.core.io.rest.voice/src/test/java/org/openhab/core/io/rest/voice/internal/VoiceResourceTest.java

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import org.openhab.core.voice.VoiceManager;
2828
import org.openhab.core.voice.text.conversation.Conversation;
2929
import org.openhab.core.voice.text.conversation.ConversationManager;
30+
import org.openhab.core.voice.text.interpreter.llm.LLMTool;
31+
import org.openhab.core.voice.text.interpreter.llm.LLMToolRegistry;
3032

3133
/**
3234
* Test class for {@link VoiceResource}.
@@ -40,9 +42,10 @@ public class VoiceResourceTest {
4042
private AudioManager audioManager = mock(AudioManager.class);
4143
private VoiceManager voiceManager = mock(VoiceManager.class);
4244
private ConversationManager conversationManager = mock(ConversationManager.class);
45+
private LLMToolRegistry llmToolRegistry = mock(LLMToolRegistry.class);
4346

4447
private VoiceResource voiceResource = new VoiceResource(localeService, audioManager, voiceManager,
45-
conversationManager);
48+
conversationManager, llmToolRegistry);
4649

4750
@SuppressWarnings("unchecked")
4851
@Test
@@ -76,4 +79,37 @@ public void testListConversations() {
7679
assertEquals(Instant.ofEpochMilli(3000), dto2.created());
7780
assertEquals(Instant.ofEpochMilli(4000), dto2.lastUpdated());
7881
}
82+
83+
@SuppressWarnings("unchecked")
84+
@Test
85+
public void testGetLLMTools() {
86+
LLMTool tool1 = mock(LLMTool.class);
87+
when(tool1.getUID()).thenReturn("tool-1");
88+
when(tool1.getLabel(any())).thenReturn("Tool 1");
89+
when(tool1.getDescription(any())).thenReturn("Description of tool 1");
90+
91+
LLMTool tool2 = mock(LLMTool.class);
92+
when(tool2.getUID()).thenReturn("tool-2");
93+
when(tool2.getLabel(any())).thenReturn("Tool 2");
94+
when(tool2.getDescription(any())).thenReturn("Description of tool 2");
95+
96+
when(llmToolRegistry.getAll()).thenReturn(List.of(tool1, tool2));
97+
98+
Response response = voiceResource.getLLMTools(null);
99+
assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
100+
101+
List<LLMToolDTO> entity = (List<LLMToolDTO>) response.getEntity();
102+
assertNotNull(entity);
103+
assertEquals(2, entity.size());
104+
105+
LLMToolDTO dto1 = entity.get(0);
106+
assertEquals("tool-1", dto1.id());
107+
assertEquals("Tool 1", dto1.label());
108+
assertEquals("Description of tool 1", dto1.description());
109+
110+
LLMToolDTO dto2 = entity.get(1);
111+
assertEquals("tool-2", dto2.id());
112+
assertEquals("Tool 2", dto2.label());
113+
assertEquals("Description of tool 2", dto2.description());
114+
}
79115
}

0 commit comments

Comments
 (0)