Skip to content

fix(homeassistant): support return_response for HA response-only services (#27256)#27270

Open
xxxigm wants to merge 3 commits into
NousResearch:mainfrom
xxxigm:fix/27256-ha-response-services
Open

fix(homeassistant): support return_response for HA response-only services (#27256)#27270
xxxigm wants to merge 3 commits into
NousResearch:mainfrom
xxxigm:fix/27256-ha-response-services

Conversation

@xxxigm

@xxxigm xxxigm commented May 17, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes homeassistant_tool.ha_call_service silently returning HTTP 400 on Home Assistant response services such as todo.get_items, calendar.get_events, weather.get_forecasts and conversation.process.

HA registers these with SupportsResponse.ONLY, meaning a REST call without ?return_response=true is rejected with 400 — "Service ... requires response data". The previous implementation never appended that query parameter, so every response service 400'd while non-response services on the same domain (e.g. todo.add_item, todo.remove_item) kept working — exactly the symptom in the bug report.

The fix is defense-in-depth so the model rarely has to think about the rule:

  1. A new return_response parameter on _async_call_service / ha_call_service that toggles ?return_response=true.
  2. A static allowlist _RESPONSE_ONLY_SERVICES for the well-known response services (auto-opt-in).
  3. Auto-retry-once when HA's 400 body literally mentions return_response / requires response — covers user HA versions and custom integrations not on the static list. Unrelated 400s (auth, unknown service, invalid entity_id) are not retried.
  4. _parse_service_response recognises the new shape {changed_states, service_response} and surfaces service_response to the caller; the legacy list shape (all non-response services) is unchanged.

Related Issue

Closes #27256.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • tools/homeassistant_tool.py — new return_response parameter on _async_call_service and the ha_call_service schema; new helpers _service_url, _is_response_required_hint; new constant _RESPONSE_ONLY_SERVICES; auto-retry-once logic in _async_call_service; response-shape-aware _parse_service_response; bool/string coercion for return_response in the handler; updated HA_CALL_SERVICE_SCHEMA description + new return_response field with examples.
  • tests/tools/test_homeassistant_response_services.py — 48 new regression tests:
    • TestServiceURL — 2 cases pinning query-string construction.
    • TestIsResponseRequiredHint — 10 cases (4 positive wordings + 6 negatives for unrelated 400s / non-400s / empty body / success).
    • TestResponseOnlyAllowlist — 5 cases (4 known services + regular-services-not-listed).
    • TestParseServiceResponseNewShape — 6 cases (new shape, legacy list, empty dict, None, malformed entries skipped).
    • TestAsyncCallServiceQueryString — 8 cases over a scripted aiohttp double (no query for regular service, auto-opt-in for known, explicit flag, auto-retry on 400 hint, no retry on unrelated 400s, no double-retry when already opted in, non-JSON / empty bodies).
    • TestHandlerReturnResponseWiring — 12 cases (bool true/false, string coercion across 9 values, default false). Uses AsyncMock to avoid leaking unawaited coroutines.
    • TestSchemaSurface — 3 cases (field present, not required, description mentions response services + todo.get_items).
    • TestIssue27256Repro — 2 end-to-end cases: todo.get_items returns parsed items, todo.add_item keeps original wire format.
  • website/docs/reference/tools-reference.mdha_call_service description aligned with the new schema text.

How to Test

  1. Check out this branch and ensure .venv is set up: python3 -m venv .venv && source .venv/bin/activate && pip install -e ".[all,dev]"
  2. Run the new tests on their own:
    scripts/run_tests.sh tests/tools/test_homeassistant_response_services.py -v
    
    Expected: 48 passed.
  3. Run the full Home Assistant tool suite to confirm no regression:
    scripts/run_tests.sh tests/tools/test_homeassistant_response_services.py tests/tools/test_homeassistant_tool.py
    
    Expected: 116 passed (68 pre-existing + 48 new).
  4. Manual smoke against a real HA instance (optional, requires HASS_TOKEN / HASS_URL):
    • ha_call_service with domain=todo, service=get_items, entity_id=todo.<your_list> should now return the items in result.service_response.
    • ha_call_service with domain=todo, service=add_item, entity_id=todo.<your_list>, data='{"item":"buy milk"}' should keep working (no behavioural change for non-response services).

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(homeassistant): ..., test(homeassistant): ..., docs(homeassistant): ...)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix (no unrelated commits)
  • I've run scripts/run_tests.sh tests/tools/test_homeassistant_response_services.py tests/tools/test_homeassistant_tool.py and all tests pass
  • I've added tests for my changes (48 new regression tests covering helpers, network behaviour, handler wiring, schema, and the exact issue repro)
  • I've tested on my platform: macOS 15.2 (Darwin 24.6.0), Python 3.12

Documentation & Housekeeping

  • I've updated relevant documentation (website/docs/reference/tools-reference.mdha_call_service description now mentions response services and return_response)
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — fix is HTTP-layer only, no platform-specific code; tests use a fake aiohttp session (no network, no filesystem)
  • I've updated tool descriptions/schemas if I changed tool behavior — HA_CALL_SERVICE_SCHEMA description and the new return_response field cover the new behaviour

Screenshots / Logs

$ scripts/run_tests.sh tests/tools/test_homeassistant_response_services.py -v
48 passed in 2.75s

$ scripts/run_tests.sh tests/tools/test_homeassistant_response_services.py tests/tools/test_homeassistant_tool.py -q
116 passed in 2.14s

xxxigm added 3 commits May 17, 2026 11:55
…ices

`todo.get_items`, `calendar.get_events`, `weather.get_forecasts` and
`conversation.process` are registered in Home Assistant with
SupportsResponse.ONLY. Calling them via the REST API without
`?return_response=true` makes HA reject the request with HTTP 400
("Service ... requires response data"), so `ha_call_service` silently
400'd on `todo.get_items` while `add_item` / `remove_item` (non-response
services using the same flat payload) kept working — exactly the
symptom reported in NousResearch#27256.

The fix adds three small layers:

1. A `return_response` parameter on `_async_call_service` /
   `ha_call_service` that toggles `?return_response=true`.
2. A static allowlist (`_RESPONSE_ONLY_SERVICES`) of well-known
   response-only services that opt in automatically so the model
   doesn't have to know the rule.
3. A defensive auto-retry-once when HA's 400 body literally mentions
   "return_response" / "requires response" — covers custom
   integrations and future HA versions not in the static list.

`_parse_service_response` now also recognises the new response shape
(`{changed_states, service_response}`) and surfaces the `service_response`
payload back to the caller, while keeping the legacy list shape
unchanged for non-response services.
…ch#27256

48 regression tests for the new return_response plumbing:

* `_service_url` query-string construction.
* `_is_response_required_hint` accept/reject matrix (only 400s with
  return_response / requires response wording are retried).
* `_RESPONSE_ONLY_SERVICES` membership for todo / calendar / weather /
  conversation, and absence for ordinary services.
* `_parse_service_response` handles both legacy list and new
  `{changed_states, service_response}` shapes, including empty,
  None and malformed entries.
* `_async_call_service` over a fake aiohttp session: no query for
  regular services, auto-opt-in for known response services,
  explicit-flag path, auto-retry-once on 400 hint, no retry on
  unrelated 400s, no double-retry when we already opted in,
  non-JSON / empty bodies.
* Handler-level wiring: bool / string coercion for return_response,
  default false, AsyncMock-based to avoid leaking unawaited coroutines.
* Schema surface: field present, not required, description mentions
  response services.
* End-to-end repro: `todo.get_items` returns parsed items, while
  `todo.add_item` keeps its original wire compat.
Update the tools reference page so the description matches the new
schema text and documents that `todo.get_items`, `calendar.get_events`
and `weather.get_forecasts` need `return_response=true` (well-known
response services opt in automatically). Issue NousResearch#27256.
@cardtest15-coder

This comment was marked as spam.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/tools Tool registry, model_tools, toolsets labels May 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/tools Tool registry, model_tools, toolsets P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: homeassistant_tool.py fails to get_items on todo entities

3 participants