-
Notifications
You must be signed in to change notification settings - Fork 11
feat: inject device area context for room-aware voice responses #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
2a89103
f18b522
cc8c423
0b4321f
2fe9e6d
f3ab293
b1202c8
dc5e3c8
a14f2fb
6e6c1b7
4664b54
8cb0b1a
d63fd5e
2a69f63
8edfc60
fa64a25
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -13,6 +13,7 @@ | |||||||||
| from homeassistant.components import conversation | ||||||||||
| from homeassistant.config_entries import ConfigEntry | ||||||||||
| from homeassistant.core import HomeAssistant | ||||||||||
| from homeassistant.helpers import area_registry as ar, device_registry as dr | ||||||||||
| from homeassistant.helpers.entity_platform import AddEntitiesCallback | ||||||||||
| from homeassistant.helpers import intent | ||||||||||
|
|
||||||||||
|
|
@@ -138,17 +139,36 @@ async def async_process( | |||||||||
| ) | ||||||||||
| exposed_context = apply_context_policy(raw_context, max_chars, strategy) | ||||||||||
| extra_system_prompt = getattr(user_input, "extra_system_prompt", None) | ||||||||||
|
|
||||||||||
| # Resolve the originating device's area for room-aware responses | ||||||||||
| device_area_context = self._resolve_device_area(user_input) | ||||||||||
|
|
||||||||||
| system_prompt = "\n\n".join( | ||||||||||
| part for part in (exposed_context, extra_system_prompt) if part | ||||||||||
| part | ||||||||||
| for part in (device_area_context, exposed_context, extra_system_prompt) | ||||||||||
| if part | ||||||||||
| ) or None | ||||||||||
|
|
||||||||||
| # Add device/area headers when available | ||||||||||
| device_id = getattr(user_input, "device_id", None) | ||||||||||
| if device_id or device_area_context: | ||||||||||
| voice_headers = dict(_VOICE_REQUEST_HEADERS) | ||||||||||
| if device_id: | ||||||||||
| voice_headers["x-openclaw-device-id"] = device_id | ||||||||||
| if device_area_context: | ||||||||||
| area_name = device_area_context.removeprefix("[Voice command from: ").removesuffix("]") | ||||||||||
| voice_headers["x-openclaw-area"] = area_name | ||||||||||
|
Comment on lines
+181
to
+183
|
||||||||||
| else: | ||||||||||
| voice_headers = None | ||||||||||
|
|
||||||||||
| try: | ||||||||||
| full_response = await self._get_response( | ||||||||||
| client, | ||||||||||
| message, | ||||||||||
| conversation_id, | ||||||||||
| voice_agent_id, | ||||||||||
| system_prompt, | ||||||||||
| voice_headers, | ||||||||||
| ) | ||||||||||
| except OpenClawApiError as err: | ||||||||||
| _LOGGER.error("OpenClaw conversation error: %s", err) | ||||||||||
|
|
@@ -165,6 +185,7 @@ async def async_process( | |||||||||
| conversation_id, | ||||||||||
| voice_agent_id, | ||||||||||
| system_prompt, | ||||||||||
| voice_headers, | ||||||||||
| ) | ||||||||||
| except OpenClawApiError as retry_err: | ||||||||||
| return self._error_result( | ||||||||||
|
|
@@ -227,6 +248,34 @@ def _resolve_conversation_id(self, user_input: conversation.ConversationInput) - | |||||||||
|
|
||||||||||
| return "assist_default" | ||||||||||
|
|
||||||||||
| def _resolve_device_area( | ||||||||||
| self, user_input: conversation.ConversationInput | ||||||||||
| ) -> str | None: | ||||||||||
| """Resolve the area name for the device that initiated the conversation. | ||||||||||
|
|
||||||||||
| Returns a short context string like '[Voice command from: Study]' | ||||||||||
| so the agent knows which room the user is in. | ||||||||||
| """ | ||||||||||
| device_id = getattr(user_input, "device_id", None) | ||||||||||
| if not device_id: | ||||||||||
| return None | ||||||||||
|
|
||||||||||
| try: | ||||||||||
| dev_reg = dr.async_get(self.hass) | ||||||||||
| device_entry = dev_reg.async_get(device_id) | ||||||||||
| if not device_entry or not device_entry.area_id: | ||||||||||
| return None | ||||||||||
|
|
||||||||||
| area_reg = ar.async_get(self.hass) | ||||||||||
| area_entry = area_reg.async_get_area(device_entry.area_id) | ||||||||||
| if not area_entry: | ||||||||||
| return None | ||||||||||
|
|
||||||||||
| return f"[Voice command from: {area_entry.name}]" | ||||||||||
| except Exception: | ||||||||||
| _LOGGER.debug("Could not resolve area for device %s", device_id) | ||||||||||
|
||||||||||
| _LOGGER.debug("Could not resolve area for device %s", device_id) | |
| _LOGGER.debug("Could not resolve area for device %s", device_id, exc_info=True) |
Copilot
AI
Mar 29, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
headers = extra_headers or _VOICE_REQUEST_HEADERS treats extra_headers as a full replacement, even though the parameter name suggests these are additional headers. To avoid accidentally dropping the voice routing headers in future call sites, consider always merging with the defaults (e.g., start from _VOICE_REQUEST_HEADERS and update with extra_headers when provided).
| headers = extra_headers or _VOICE_REQUEST_HEADERS | |
| headers: dict[str, str] = dict(_VOICE_REQUEST_HEADERS) | |
| if extra_headers: | |
| headers.update(extra_headers) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extracting
area_nameby parsingdevice_area_context(removeprefix/removesuffix) is brittle because any future change to the prompt wording or formatting will silently break the header value. Prefer having_resolve_device_areareturn the rawarea_name(or a small dataclass/tuple like(area_name, context_line)) and build both the system prompt line and thex-openclaw-areaheader from that.