mirror of
https://github.com/danieldemus/openhab-addons
synced 2026-07-29 12:34:21 +02:00
[gemini] GeminiHLIService: Provide user-friendly, localized error messages (#21048)
Signed-off-by: Florian Hotze <dev@florianhotze.com>
This commit is contained in:
+75
-17
@@ -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.GeminiPart;
|
||||||
import org.openhab.binding.gemini.internal.api.dto.response.GeminiCandidate;
|
import org.openhab.binding.gemini.internal.api.dto.response.GeminiCandidate;
|
||||||
import org.openhab.binding.gemini.internal.api.dto.response.GeminiResponse;
|
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.ThingHandler;
|
||||||
import org.openhab.core.thing.binding.ThingHandlerService;
|
import org.openhab.core.thing.binding.ThingHandlerService;
|
||||||
import org.openhab.core.voice.text.HumanLanguageInterpreter;
|
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.LLMTool;
|
||||||
import org.openhab.core.voice.text.interpreter.llm.LLMToolCall;
|
import org.openhab.core.voice.text.interpreter.llm.LLMToolCall;
|
||||||
import org.openhab.core.voice.text.interpreter.llm.LLMToolException;
|
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.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.
|
* The {@link GeminiHLIService} is a {@link HumanLanguageInterpreter} implementation based on Google Gemini.
|
||||||
@@ -55,9 +62,23 @@ import org.osgi.service.component.annotations.Component;
|
|||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
public class GeminiHLIService implements ThingHandlerService, HumanLanguageInterpreter {
|
public class GeminiHLIService implements ThingHandlerService, HumanLanguageInterpreter {
|
||||||
private static final String LABEL = "Gemini Human Language Interpreter";
|
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;
|
private @Nullable GeminiHandler handler;
|
||||||
|
|
||||||
|
@Activate
|
||||||
|
public GeminiHLIService(final @Reference TranslationProvider i18nProvider, BundleContext context) {
|
||||||
|
this.i18nProvider = i18nProvider;
|
||||||
|
this.bundle = context.getBundle();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setThingHandler(@Nullable ThingHandler handler) {
|
public void setThingHandler(@Nullable ThingHandler handler) {
|
||||||
if (handler instanceof GeminiHandler geminiHandler) {
|
if (handler instanceof GeminiHandler geminiHandler) {
|
||||||
@@ -97,19 +118,30 @@ public class GeminiHLIService implements ThingHandlerService, HumanLanguageInter
|
|||||||
return null;
|
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
|
@Override
|
||||||
public String interpret(Locale locale, String text) throws InterpretationException {
|
public String interpret(Locale locale, String text) throws InterpretationException {
|
||||||
GeminiHandler geminiHandler = handler;
|
GeminiHandler geminiHandler = handler;
|
||||||
if (geminiHandler == null) {
|
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();
|
GeminiApiClient apiClient = geminiHandler.getApiClient();
|
||||||
if (apiClient == null) {
|
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();
|
GeminiConfiguration config = geminiHandler.getGeminiConfiguration();
|
||||||
if (config == null) {
|
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;
|
String model = config.model.isBlank() ? DEFAULT_MODEL : config.model;
|
||||||
try {
|
try {
|
||||||
@@ -120,27 +152,37 @@ public class GeminiHLIService implements ThingHandlerService, HumanLanguageInter
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
} catch (GeminiApiException e) {
|
} 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);
|
ex.initCause(e);
|
||||||
throw ex;
|
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
|
@Override
|
||||||
public String interpret(Locale locale, InterpreterContext interpreterContext) throws InterpretationException {
|
public String interpret(Locale locale, InterpreterContext interpreterContext) throws InterpretationException {
|
||||||
GeminiHandler geminiHandler = handler;
|
GeminiHandler geminiHandler = handler;
|
||||||
if (geminiHandler == null) {
|
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();
|
GeminiApiClient apiClient = geminiHandler.getApiClient();
|
||||||
if (apiClient == null) {
|
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();
|
GeminiConfiguration config = geminiHandler.getGeminiConfiguration();
|
||||||
if (config == null) {
|
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();
|
Conversation conversation = interpreterContext.conversation();
|
||||||
@@ -148,7 +190,9 @@ public class GeminiHLIService implements ThingHandlerService, HumanLanguageInter
|
|||||||
|
|
||||||
String systemMessage = interpreterContext.systemPrompt();
|
String systemMessage = interpreterContext.systemPrompt();
|
||||||
if (systemMessage == null || systemMessage.isBlank()) {
|
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();
|
String locationItem = interpreterContext.locationItem();
|
||||||
if (locationItem != null && !locationItem.isBlank()) {
|
if (locationItem != null && !locationItem.isBlank()) {
|
||||||
@@ -169,8 +213,9 @@ public class GeminiHLIService implements ThingHandlerService, HumanLanguageInter
|
|||||||
final int maxLoops = 10;
|
final int maxLoops = 10;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (loopCount >= maxLoops) {
|
if (loopCount >= maxLoops) {
|
||||||
|
logger.warn("Cannot interpret: Tool execution loop limit exceeded (max {} iterations)", maxLoops);
|
||||||
throw new InterpretationException(
|
throw new InterpretationException(
|
||||||
"Tool execution loop limit exceeded (max " + maxLoops + " iterations)");
|
getLocalizedMessage(ERROR_KEY_TECHNICAL_PROBLEM, DEFAULT_ERROR_TECHNICAL_PROBLEM, locale));
|
||||||
}
|
}
|
||||||
loopCount++;
|
loopCount++;
|
||||||
String model = config.model.isBlank() ? DEFAULT_MODEL : config.model;
|
String model = config.model.isBlank() ? DEFAULT_MODEL : config.model;
|
||||||
@@ -180,15 +225,21 @@ public class GeminiHLIService implements ThingHandlerService, HumanLanguageInter
|
|||||||
|
|
||||||
List<GeminiCandidate> candidates = geminiResponse.candidates();
|
List<GeminiCandidate> candidates = geminiResponse.candidates();
|
||||||
if (candidates == null || candidates.isEmpty()) {
|
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();
|
GeminiContent responseContent = candidates.getFirst().content();
|
||||||
if (responseContent == null) {
|
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();
|
List<GeminiPart> parts = responseContent.parts();
|
||||||
if (parts == null) {
|
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;
|
boolean hasToolCall = false;
|
||||||
@@ -206,7 +257,9 @@ public class GeminiHLIService implements ThingHandlerService, HumanLanguageInter
|
|||||||
try {
|
try {
|
||||||
conversation.addMessage(ConversationRole.TOOL_CALL, llmToolCall.toJson());
|
conversation.addMessage(ConversationRole.TOOL_CALL, llmToolCall.toJson());
|
||||||
} catch (ConversationException e) {
|
} 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);
|
ex.initCause(e);
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
@@ -216,7 +269,9 @@ public class GeminiHLIService implements ThingHandlerService, HumanLanguageInter
|
|||||||
try {
|
try {
|
||||||
conversation.addMessage(ConversationRole.TOOL_RETURN, result);
|
conversation.addMessage(ConversationRole.TOOL_RETURN, result);
|
||||||
} catch (ConversationException e) {
|
} 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);
|
ex.initCause(e);
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
@@ -230,15 +285,18 @@ public class GeminiHLIService implements ThingHandlerService, HumanLanguageInter
|
|||||||
try {
|
try {
|
||||||
conversation.addMessage(ConversationRole.OPENHAB, finalResponse);
|
conversation.addMessage(ConversationRole.OPENHAB, finalResponse);
|
||||||
} catch (ConversationException e) {
|
} 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);
|
ex.initCause(e);
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
return finalResponse;
|
return finalResponse;
|
||||||
}
|
}
|
||||||
} catch (GeminiApiException e) {
|
} catch (GeminiApiException e) {
|
||||||
|
logger.warn("Communication with Gemini failed: {}", e.getMessage(), e);
|
||||||
InterpretationException ex = new InterpretationException(
|
InterpretationException ex = new InterpretationException(
|
||||||
"Failed to communicate with Gemini: " + e.getMessage());
|
getLocalizedMessage(ERROR_KEY_TECHNICAL_PROBLEM, DEFAULT_ERROR_TECHNICAL_PROBLEM, locale));
|
||||||
ex.initCause(e);
|
ex.initCause(e);
|
||||||
throw ex;
|
throw ex;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,3 +71,7 @@ action.sendMessage.result.description = The text response returned by Gemini.
|
|||||||
offline.configuration-error = No API key configured
|
offline.configuration-error = No API key configured
|
||||||
requestTimeout.configuration-error = Timeout must be greater than zero
|
requestTimeout.configuration-error = Timeout must be greater than zero
|
||||||
offline.communication-error = Could not connect to Gemini API
|
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user