diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index 1ffb8747d91..7c24fe649b1 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -364,7 +364,7 @@ class NevermindIntentHandler(intent.IntentHandler): """Takes no action.""" intent_type = intent.INTENT_NEVERMIND - description = "Cancels the current request and does nothing" + description = "Cancel the current conversation if it was started by mistake or the user wants it to stop." async def async_handle(self, intent_obj: intent.Intent) -> intent.IntentResponse: """Do nothing and produces an empty response.""" diff --git a/homeassistant/components/openai_conversation/conversation.py b/homeassistant/components/openai_conversation/conversation.py index 9c73766c8d4..95d356b26e2 100644 --- a/homeassistant/components/openai_conversation/conversation.py +++ b/homeassistant/components/openai_conversation/conversation.py @@ -30,6 +30,7 @@ from homeassistant.exceptions import HomeAssistantError, TemplateError from homeassistant.helpers import device_registry as dr, intent, llm, template from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import ulid +from homeassistant.util.json import JsonObjectType from . import OpenAIConfigEntry from .const import ( @@ -292,6 +293,8 @@ class OpenAIConversationEntity( if not tool_calls or not llm_api: break + aborting = False + for tool_call in tool_calls: tool_input = llm.ToolInput( tool_name=tool_call.function.name, @@ -301,12 +304,25 @@ class OpenAIConversationEntity( "Tool call: %s(%s)", tool_input.tool_name, tool_input.tool_args ) - try: - tool_response = await llm_api.async_call_tool(tool_input) - except (HomeAssistantError, vol.Invalid) as e: - tool_response = {"error": type(e).__name__} - if str(e): - tool_response["error_text"] = str(e) + # OpenAI requires a tool response for every tool call in history + if aborting: + tool_response: JsonObjectType = { + "error": "Aborted", + "error_text": "Abort conversation requested", + } + if not aborting: + try: + tool_response = await llm_api.async_call_tool(tool_input) + except llm.AbortConversation as e: + aborting = True + tool_response = { + "error": "Aborted", + "error_text": str(e) or "Abort conversation requested", + } + except (HomeAssistantError, vol.Invalid) as e: + tool_response = {"error": type(e).__name__} + if str(e): + tool_response["error_text"] = str(e) LOGGER.debug("Tool response: %s", tool_response) messages.append( @@ -317,6 +333,9 @@ class OpenAIConversationEntity( ) ) + if aborting: + break + self.history[conversation_id] = messages intent_response = intent.IntentResponse(language=user_input.language) diff --git a/homeassistant/helpers/llm.py b/homeassistant/helpers/llm.py index 38d80d5649d..0dba73374ee 100644 --- a/homeassistant/helpers/llm.py +++ b/homeassistant/helpers/llm.py @@ -113,6 +113,10 @@ def async_get_apis(hass: HomeAssistant) -> list[API]: return list(_async_get_apis(hass).values()) +class AbortConversation(HomeAssistantError): + """Abort the conversation.""" + + @dataclass(slots=True) class LLMContext: """Tool input to be processed.""" @@ -169,6 +173,9 @@ class APIInstance: {"tool_name": tool_input.tool_name, "tool_args": tool_input.tool_args}, ) + if tool_input.tool_name == intent.INTENT_NEVERMIND: + raise AbortConversation("Nevermind intent called") + for tool in self.tools: if tool.name == tool_input.tool_name: break @@ -273,7 +280,6 @@ class AssistAPI(API): INTENT_OPEN_COVER, # deprecated INTENT_CLOSE_COVER, # deprecated intent.INTENT_GET_STATE, - intent.INTENT_NEVERMIND, intent.INTENT_TOGGLE, intent.INTENT_GET_CURRENT_DATE, intent.INTENT_GET_CURRENT_TIME,