[gemini] GeminiHLIService: Provide user-friendly, localized error messages (#21048)

Signed-off-by: Florian Hotze <dev@florianhotze.com>
This commit is contained in:
Florian Hotze
2026-06-26 14:03:47 +02:00
committed by GitHub
parent 331f60c71f
commit 2c5f67d303
2 changed files with 79 additions and 17 deletions
@@ -33,6 +33,7 @@ import org.openhab.binding.gemini.internal.api.dto.GeminiFunctionCall;
import org.openhab.binding.gemini.internal.api.dto.GeminiPart;
import org.openhab.binding.gemini.internal.api.dto.response.GeminiCandidate;
import org.openhab.binding.gemini.internal.api.dto.response.GeminiResponse;
import org.openhab.core.i18n.TranslationProvider;
import org.openhab.core.thing.binding.ThingHandler;
import org.openhab.core.thing.binding.ThingHandlerService;
import org.openhab.core.voice.text.HumanLanguageInterpreter;
@@ -44,7 +45,13 @@ import org.openhab.core.voice.text.conversation.ConversationRole;
import org.openhab.core.voice.text.interpreter.llm.LLMTool;
import org.openhab.core.voice.text.interpreter.llm.LLMToolCall;
import org.openhab.core.voice.text.interpreter.llm.LLMToolException;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The {@link GeminiHLIService} is a {@link HumanLanguageInterpreter} implementation based on Google Gemini.
@@ -55,9 +62,23 @@ import org.osgi.service.component.annotations.Component;
@NonNullByDefault
public class GeminiHLIService implements ThingHandlerService, HumanLanguageInterpreter {
private static final String LABEL = "Gemini Human Language Interpreter";
private static final String ERROR_KEY_MISSING_CONFIG = "hli.error.missing-configuration";
private static final String ERROR_KEY_TECHNICAL_PROBLEM = "hli.error.technical-problem";
private static final String DEFAULT_ERROR_MISSING_CONFIG = "Cannot interpret due to missing configuration.";
private static final String DEFAULT_ERROR_TECHNICAL_PROBLEM = "Cannot interpret due to a technical problem.";
private final Logger logger = LoggerFactory.getLogger(GeminiHLIService.class);
private final TranslationProvider i18nProvider;
private final Bundle bundle;
private @Nullable GeminiHandler handler;
@Activate
public GeminiHLIService(final @Reference TranslationProvider i18nProvider, BundleContext context) {
this.i18nProvider = i18nProvider;
this.bundle = context.getBundle();
}
@Override
public void setThingHandler(@Nullable ThingHandler handler) {
if (handler instanceof GeminiHandler geminiHandler) {
@@ -97,19 +118,30 @@ public class GeminiHLIService implements ThingHandlerService, HumanLanguageInter
return null;
}
private String getLocalizedMessage(String key, String defaultText, Locale locale) {
String message = i18nProvider.getText(bundle, key, defaultText, locale);
return message != null ? message : defaultText;
}
@Override
public String interpret(Locale locale, String text) throws InterpretationException {
GeminiHandler geminiHandler = handler;
if (geminiHandler == null) {
throw new InterpretationException("GeminiHandler is not initialized");
logger.warn("Cannot interpret: GeminiHandler is not initialized");
throw new InterpretationException(
getLocalizedMessage(ERROR_KEY_MISSING_CONFIG, DEFAULT_ERROR_MISSING_CONFIG, locale));
}
GeminiApiClient apiClient = geminiHandler.getApiClient();
if (apiClient == null) {
throw new InterpretationException("Gemini API client is not available");
logger.warn("Cannot interpret: Gemini API client is not available");
throw new InterpretationException(
getLocalizedMessage(ERROR_KEY_MISSING_CONFIG, DEFAULT_ERROR_MISSING_CONFIG, locale));
}
GeminiConfiguration config = geminiHandler.getGeminiConfiguration();
if (config == null) {
throw new InterpretationException("Gemini HLI configuration is not available");
logger.warn("Cannot interpret: Gemini HLI configuration is not available");
throw new InterpretationException(
getLocalizedMessage(ERROR_KEY_MISSING_CONFIG, DEFAULT_ERROR_MISSING_CONFIG, locale));
}
String model = config.model.isBlank() ? DEFAULT_MODEL : config.model;
try {
@@ -120,27 +152,37 @@ public class GeminiHLIService implements ThingHandlerService, HumanLanguageInter
return response;
}
} catch (GeminiApiException e) {
var ex = new InterpretationException("Failed to get response from Gemini: " + e.getMessage());
logger.warn("Request to Gemini failed: {}", e.getMessage(), e);
var ex = new InterpretationException(
getLocalizedMessage(ERROR_KEY_TECHNICAL_PROBLEM, DEFAULT_ERROR_TECHNICAL_PROBLEM, locale));
ex.initCause(e);
throw ex;
}
throw new InterpretationException("No valid response received");
logger.warn("Cannot interpret: No valid response received");
throw new InterpretationException(
getLocalizedMessage(ERROR_KEY_TECHNICAL_PROBLEM, DEFAULT_ERROR_TECHNICAL_PROBLEM, locale));
}
@Override
public String interpret(Locale locale, InterpreterContext interpreterContext) throws InterpretationException {
GeminiHandler geminiHandler = handler;
if (geminiHandler == null) {
throw new InterpretationException("GeminiHandler is not initialized");
logger.warn("Cannot interpret: GeminiHandler is not initialized");
throw new InterpretationException(
getLocalizedMessage(ERROR_KEY_MISSING_CONFIG, DEFAULT_ERROR_MISSING_CONFIG, locale));
}
GeminiApiClient apiClient = geminiHandler.getApiClient();
if (apiClient == null) {
throw new InterpretationException("Gemini API client is not available");
logger.warn("Cannot interpret: Gemini API client is not available");
throw new InterpretationException(
getLocalizedMessage(ERROR_KEY_MISSING_CONFIG, DEFAULT_ERROR_MISSING_CONFIG, locale));
}
GeminiConfiguration config = geminiHandler.getGeminiConfiguration();
if (config == null) {
throw new InterpretationException("Gemini HLI configuration is not available");
logger.warn("Cannot interpret: Gemini HLI configuration is not available");
throw new InterpretationException(
getLocalizedMessage(ERROR_KEY_MISSING_CONFIG, DEFAULT_ERROR_MISSING_CONFIG, locale));
}
Conversation conversation = interpreterContext.conversation();
@@ -148,7 +190,9 @@ public class GeminiHLIService implements ThingHandlerService, HumanLanguageInter
String systemMessage = interpreterContext.systemPrompt();
if (systemMessage == null || systemMessage.isBlank()) {
throw new InterpretationException("System prompt is missing or empty");
logger.warn("Cannot interpret: System prompt is missing or empty");
throw new InterpretationException(
getLocalizedMessage(ERROR_KEY_MISSING_CONFIG, DEFAULT_ERROR_MISSING_CONFIG, locale));
}
String locationItem = interpreterContext.locationItem();
if (locationItem != null && !locationItem.isBlank()) {
@@ -169,8 +213,9 @@ public class GeminiHLIService implements ThingHandlerService, HumanLanguageInter
final int maxLoops = 10;
while (true) {
if (loopCount >= maxLoops) {
logger.warn("Cannot interpret: Tool execution loop limit exceeded (max {} iterations)", maxLoops);
throw new InterpretationException(
"Tool execution loop limit exceeded (max " + maxLoops + " iterations)");
getLocalizedMessage(ERROR_KEY_TECHNICAL_PROBLEM, DEFAULT_ERROR_TECHNICAL_PROBLEM, locale));
}
loopCount++;
String model = config.model.isBlank() ? DEFAULT_MODEL : config.model;
@@ -180,15 +225,21 @@ public class GeminiHLIService implements ThingHandlerService, HumanLanguageInter
List<GeminiCandidate> candidates = geminiResponse.candidates();
if (candidates == null || candidates.isEmpty()) {
throw new InterpretationException("No valid response received");
logger.warn("Cannot interpret: No valid response received (no candidates)");
throw new InterpretationException(
getLocalizedMessage(ERROR_KEY_TECHNICAL_PROBLEM, DEFAULT_ERROR_TECHNICAL_PROBLEM, locale));
}
GeminiContent responseContent = candidates.getFirst().content();
if (responseContent == null) {
throw new InterpretationException("No valid response received");
logger.warn("Cannot interpret: No valid response received (content is null)");
throw new InterpretationException(
getLocalizedMessage(ERROR_KEY_TECHNICAL_PROBLEM, DEFAULT_ERROR_TECHNICAL_PROBLEM, locale));
}
List<GeminiPart> parts = responseContent.parts();
if (parts == null) {
throw new InterpretationException("No valid response received");
logger.warn("Cannot interpret: No valid response received (parts is null)");
throw new InterpretationException(
getLocalizedMessage(ERROR_KEY_TECHNICAL_PROBLEM, DEFAULT_ERROR_TECHNICAL_PROBLEM, locale));
}
boolean hasToolCall = false;
@@ -206,7 +257,9 @@ public class GeminiHLIService implements ThingHandlerService, HumanLanguageInter
try {
conversation.addMessage(ConversationRole.TOOL_CALL, llmToolCall.toJson());
} catch (ConversationException e) {
var ex = new InterpretationException("Failed to add TOOL_CALL to conversation");
logger.warn("Cannot interpret: Failed to add TOOL_CALL to conversation", e);
var ex = new InterpretationException(getLocalizedMessage(ERROR_KEY_TECHNICAL_PROBLEM,
DEFAULT_ERROR_TECHNICAL_PROBLEM, locale));
ex.initCause(e);
throw ex;
}
@@ -216,7 +269,9 @@ public class GeminiHLIService implements ThingHandlerService, HumanLanguageInter
try {
conversation.addMessage(ConversationRole.TOOL_RETURN, result);
} catch (ConversationException e) {
var ex = new InterpretationException("Failed to add TOOL_RETURN to conversation");
logger.warn("Cannot interpret: Failed to add TOOL_RETURN to conversation", e);
var ex = new InterpretationException(getLocalizedMessage(ERROR_KEY_TECHNICAL_PROBLEM,
DEFAULT_ERROR_TECHNICAL_PROBLEM, locale));
ex.initCause(e);
throw ex;
}
@@ -230,15 +285,18 @@ public class GeminiHLIService implements ThingHandlerService, HumanLanguageInter
try {
conversation.addMessage(ConversationRole.OPENHAB, finalResponse);
} catch (ConversationException e) {
var ex = new InterpretationException("Failed to add OPENHAB message to conversation");
logger.warn("Cannot interpret: Failed to add OPENHAB message to conversation", e);
var ex = new InterpretationException(getLocalizedMessage(ERROR_KEY_TECHNICAL_PROBLEM,
DEFAULT_ERROR_TECHNICAL_PROBLEM, locale));
ex.initCause(e);
throw ex;
}
return finalResponse;
}
} catch (GeminiApiException e) {
logger.warn("Communication with Gemini failed: {}", e.getMessage(), e);
InterpretationException ex = new InterpretationException(
"Failed to communicate with Gemini: " + e.getMessage());
getLocalizedMessage(ERROR_KEY_TECHNICAL_PROBLEM, DEFAULT_ERROR_TECHNICAL_PROBLEM, locale));
ex.initCause(e);
throw ex;
}
@@ -71,3 +71,7 @@ action.sendMessage.result.description = The text response returned by Gemini.
offline.configuration-error = No API key configured
requestTimeout.configuration-error = Timeout must be greater than zero
offline.communication-error = Could not connect to Gemini API
# HLI error messages
hli.error.missing-configuration = Cannot interpret due to missing configuration.
hli.error.technical-problem = Cannot interpret due to a technical problem.