diff --git a/CHANGELOG.md b/CHANGELOG.md index e525dde..27bdf6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to the OpenClaw Home Assistant Integration will be documented in this file. +## [0.1.12] - 2026-02-20 + +### Fixed +- Improved response parsing for nested/modern OpenAI-compatible payloads (including `output` / nested `content` shapes), which could previously result in missing UI replies. +- Applied the same recursive extraction strategy to both service-based chat responses and Assist conversation fallback parsing. + ## [0.1.11] - 2026-02-20 ### Fixed diff --git a/custom_components/openclaw/__init__.py b/custom_components/openclaw/__init__.py index 773b7ec..933a22f 100644 --- a/custom_components/openclaw/__init__.py +++ b/custom_components/openclaw/__init__.py @@ -380,8 +380,11 @@ def _get_first_entry_data(hass: HomeAssistant) -> dict[str, Any] | None: return None -def _coerce_text(value: Any) -> str | None: - """Convert known message content shapes to plain text.""" +def _extract_text_recursive(value: Any, depth: int = 0) -> str | None: + """Recursively extract assistant text from nested response payloads.""" + if depth > 8: + return None + if isinstance(value, str): text = value.strip() return text or None @@ -389,38 +392,41 @@ def _coerce_text(value: Any) -> str | None: if isinstance(value, list): parts: list[str] = [] for item in value: - if isinstance(item, str): - if item.strip(): - parts.append(item.strip()) - continue - if isinstance(item, dict): - text_part = item.get("text") or item.get("content") - if isinstance(text_part, str) and text_part.strip(): - parts.append(text_part.strip()) + extracted = _extract_text_recursive(item, depth + 1) + if extracted: + parts.append(extracted) if parts: return "\n".join(parts) + return None + + if isinstance(value, dict): + priority_keys = ( + "output_text", + "text", + "content", + "message", + "response", + "answer", + "choices", + "output", + "delta", + ) + + for key in priority_keys: + if key not in value: + continue + extracted = _extract_text_recursive(value.get(key), depth + 1) + if extracted: + return extracted + + for nested_value in value.values(): + extracted = _extract_text_recursive(nested_value, depth + 1) + if extracted: + return extracted return None def _extract_assistant_message(response: dict[str, Any]) -> str | None: - """Extract assistant text from multiple OpenAI-compatible response shapes.""" - choices = response.get("choices") - if isinstance(choices, list) and choices: - first = choices[0] - if isinstance(first, dict): - message = first.get("message") - if isinstance(message, dict): - extracted = _coerce_text(message.get("content")) - if extracted: - return extracted - extracted = _coerce_text(first.get("text")) - if extracted: - return extracted - - for key in ("output_text", "response", "message", "content", "answer"): - extracted = _coerce_text(response.get(key)) - if extracted: - return extracted - - return None + """Extract assistant text from modern/legacy OpenAI-compatible responses.""" + return _extract_text_recursive(response) diff --git a/custom_components/openclaw/conversation.py b/custom_components/openclaw/conversation.py index f115dae..0cbdfcc 100644 --- a/custom_components/openclaw/conversation.py +++ b/custom_components/openclaw/conversation.py @@ -174,10 +174,54 @@ class OpenClawConversationAgent(conversation.AbstractConversationAgent): message=message, session_id=conversation_id, ) - choices = response.get("choices", []) - if choices: - return choices[0].get("message", {}).get("content", "") - return "" + extracted = self._extract_text_recursive(response) + return extracted or "" + + def _extract_text_recursive(self, value: Any, depth: int = 0) -> str | None: + """Recursively extract assistant text from nested response payloads.""" + if depth > 8: + return None + + if isinstance(value, str): + text = value.strip() + return text or None + + if isinstance(value, list): + parts: list[str] = [] + for item in value: + extracted = self._extract_text_recursive(item, depth + 1) + if extracted: + parts.append(extracted) + if parts: + return "\n".join(parts) + return None + + if isinstance(value, dict): + priority_keys = ( + "output_text", + "text", + "content", + "message", + "response", + "answer", + "choices", + "output", + "delta", + ) + + for key in priority_keys: + if key not in value: + continue + extracted = self._extract_text_recursive(value.get(key), depth + 1) + if extracted: + return extracted + + for nested_value in value.values(): + extracted = self._extract_text_recursive(nested_value, depth + 1) + if extracted: + return extracted + + return None def _error_result( self, diff --git a/custom_components/openclaw/manifest.json b/custom_components/openclaw/manifest.json index b1dd9e9..73e1d86 100644 --- a/custom_components/openclaw/manifest.json +++ b/custom_components/openclaw/manifest.json @@ -8,7 +8,7 @@ "iot_class": "local_polling", "issue_tracker": "https://github.com/techartdev/OpenClawHomeAssistant/issues", "requirements": [], - "version": "0.1.11", + "version": "0.1.12", "dependencies": ["conversation"], "after_dependencies": ["hassio", "lovelace"] }