diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d75e14..33862ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.8.6] - 2025-12-17 + +### Fixed + +- **Housekeeper API paths** - Updated all client endpoints to use `/housekeeping/` prefix to match core-api routes +- **Housekeeper entity hallucination** - Improved system prompt with critical rule requiring `list_devices()` before any control action to prevent guessing entity IDs + +### Added + +- **Housekeeping API spec** - Added `docs/housekeeping-api-spec.md` documenting the core-api home automation interface + ## [1.8.5] - 2025-12-16 ### Fixed diff --git a/docs/housekeeping-api-spec.md b/docs/housekeeping-api-spec.md new file mode 100644 index 0000000..79dfe34 --- /dev/null +++ b/docs/housekeeping-api-spec.md @@ -0,0 +1,317 @@ +# Home Automation API Interface Specification + +## Purpose + +This document specifies the expected endpoints for a home automation abstraction layer in core-api. These endpoints will be consumed by the Tatlock Housekeeper agent and potentially other projects (scheduler, dashboards). + +The goal is to provide a simplified, domain-specific interface for home automation that abstracts away the underlying platform (initially Home Assistant, but swappable). + +--- + +## Endpoints + +### Device Discovery + +#### `GET /housekeeping/devices` + +List available devices. + +**Query Parameters:** +- `domain` (optional): Filter by device type (e.g., `light`, `switch`, `climate`, `media_player`) +- `area` (optional): Filter by area/room name + +**Response:** +```json +{ + "devices": [ + { + "entity_id": "light.living_room", + "name": "Living Room Light", + "domain": "light", + "area": "Living Room", + "state": "on", + "attributes": { + "brightness": 255, + "color_temp": 370 + } + } + ] +} +``` + +--- + +#### `GET /housekeeping/devices/{entity_id}` + +Get detailed state of a specific device. + +**Response:** +```json +{ + "entity_id": "light.living_room", + "name": "Living Room Light", + "domain": "light", + "area": "Living Room", + "state": "on", + "attributes": { + "brightness": 255, + "color_temp": 370, + "supported_features": ["brightness", "color_temp"] + }, + "last_changed": "2025-12-16T10:30:00Z" +} +``` + +--- + +#### `GET /housekeeping/areas` + +List all areas/rooms. + +**Response:** +```json +{ + "areas": [ + {"id": "living_room", "name": "Living Room"}, + {"id": "bedroom", "name": "Bedroom"}, + {"id": "kitchen", "name": "Kitchen"} + ] +} +``` + +--- + +### Device Control + +#### `POST /housekeeping/devices/{entity_id}/control` + +Control a device (turn on, turn off, toggle, or set attributes). + +**Request Body:** +```json +{ + "action": "turn_on", + "brightness": 128, + "color_temp": 400 +} +``` + +- `action` (required): One of `turn_on`, `turn_off`, `toggle` +- Additional attributes vary by device type (brightness, color_temp, rgb_color, etc.) + +**Response:** +```json +{ + "success": true, + "entity_id": "light.living_room", + "new_state": "on", + "message": "Light turned on" +} +``` + +--- + +### Scenes + +#### `GET /housekeeping/scenes` + +List available scenes. + +**Response:** +```json +{ + "scenes": [ + {"id": "scene.movie_night", "name": "Movie Night"}, + {"id": "scene.good_morning", "name": "Good Morning"}, + {"id": "scene.all_off", "name": "All Off"} + ] +} +``` + +--- + +#### `POST /housekeeping/scenes/{scene_id}/activate` + +Activate a scene. + +**Response:** +```json +{ + "success": true, + "scene_id": "scene.movie_night", + "message": "Scene activated" +} +``` + +--- + +### Scripts + +#### `GET /housekeeping/scripts` + +List available scripts/sequences. + +**Response:** +```json +{ + "scripts": [ + {"id": "script.bedtime_routine", "name": "Bedtime Routine"}, + {"id": "script.welcome_home", "name": "Welcome Home"} + ] +} +``` + +--- + +#### `POST /housekeeping/scripts/{script_id}/run` + +Execute a script with optional variables. + +**Request Body (optional):** +```json +{ + "variables": { + "brightness_level": 50, + "target_room": "bedroom" + } +} +``` + +**Response:** +```json +{ + "success": true, + "script_id": "script.bedtime_routine", + "message": "Script executed" +} +``` + +--- + +### Automations + +#### `GET /housekeeping/automations` + +List automations and their enabled/disabled status. + +**Response:** +```json +{ + "automations": [ + { + "id": "automation.motion_lights", + "name": "Motion Lights", + "enabled": true + }, + { + "id": "automation.night_mode", + "name": "Night Mode", + "enabled": false + } + ] +} +``` + +--- + +#### `POST /housekeeping/automations/{automation_id}/toggle` + +Enable or disable an automation. + +**Request Body:** +```json +{ + "enabled": true +} +``` + +**Response:** +```json +{ + "success": true, + "automation_id": "automation.motion_lights", + "enabled": true, + "message": "Automation enabled" +} +``` + +--- + +### Utility + +#### `GET /housekeeping/history` + +Get state history for a device. + +**Query Parameters:** +- `entity_id` (required): Device to get history for +- `hours` (optional, default 24): Hours of history to retrieve + +**Response:** +```json +{ + "entity_id": "light.living_room", + "history": [ + { + "state": "on", + "timestamp": "2025-12-16T10:30:00Z", + "attributes": {"brightness": 255} + }, + { + "state": "off", + "timestamp": "2025-12-16T08:00:00Z", + "attributes": {} + } + ] +} +``` + +--- + +#### `GET /housekeeping/health` + +Health check for home automation connection. + +**Response:** +```json +{ + "status": "healthy", + "connected": true, + "platform": "home_assistant", + "version": "2024.12.0" +} +``` + +--- + +## Error Responses + +All endpoints should return consistent error responses: + +```json +{ + "error": true, + "code": "DEVICE_NOT_FOUND", + "message": "Device light.nonexistent not found" +} +``` + +Common error codes: +- `DEVICE_NOT_FOUND` - Entity ID doesn't exist +- `INVALID_ACTION` - Unsupported action for device type +- `CONNECTION_ERROR` - Cannot reach home automation platform +- `UNAUTHORIZED` - Invalid or missing credentials + +--- + +## Authentication + +All endpoints require authentication via Bearer token in the `Authorization` header. + +--- + +## Consuming Client + +The Tatlock project has an existing client (`src/agents/housekeeper/client.py`) that expects these endpoints. No changes to Tatlock are needed once these endpoints are available. + +Reference: `CoreAPIClient` class in Tatlock expects these exact endpoint patterns. diff --git a/pyproject.toml b/pyproject.toml index 5d6c014..6663199 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "tatlock" -version = "1.8.5" +version = "1.8.6" description = "OpenAI-compatible API with Ollama backend" requires-python = ">=3.12" dependencies = [] diff --git a/src/agents/housekeeper/agent.py b/src/agents/housekeeper/agent.py index 7d3bc5f..cef931a 100644 --- a/src/agents/housekeeper/agent.py +++ b/src/agents/housekeeper/agent.py @@ -74,21 +74,35 @@ Your role is to help users control and monitor their smart home through Home Ass ### History Tools - **get_history**: Check a device's state history +## Critical Rules + +**NEVER GUESS ENTITY IDs.** You do not know what devices exist. Entity IDs vary between +installations. You MUST call list_devices() FIRST to discover actual entity_ids before +ANY control action (turn_on, turn_off, toggle). + +Wrong approach: + User: "Turn off the study lights" + You: turn_off("light.study") ← WRONG! You guessed the entity_id + +Correct approach: + User: "Turn off the study lights" + You: list_devices(domain="light") ← First discover what exists + You: [See results like light.study_main, light.study] + You: turn_off("light.study_main"), turn_off("light.study") ← Use actual IDs + ## Best Practices -1. **Device Discovery First**: If the user asks about devices without being specific, - use list_devices to find what's available before acting. +1. **ALWAYS list_devices first** before any control action. No exceptions. + Filter by domain and/or area to narrow results. -2. **Confirm State After Actions**: After turning something on/off, you can verify - with get_device_state if needed. +2. **Use exact entity_ids** from list_devices results. Never construct or guess them. -3. **Use Entity IDs**: Devices are identified by entity_id (e.g., light.living_room). - Always use the exact entity_id from list_devices. +3. **Area-aware filtering**: Use area parameter when users mention a room. + Note: Some devices may have area=None but contain the room name in entity_id. -4. **Area-Aware**: When users say "living room lights", filter by area="living_room". +4. **Verify after actions**: Use get_device_state to confirm state changes if needed. -5. **Safety**: For actions affecting multiple devices or automations, summarize - what you're about to do. +5. **Safety for bulk actions**: When affecting multiple devices, summarize first. ## Common Patterns diff --git a/src/agents/housekeeper/client.py b/src/agents/housekeeper/client.py index c1211a4..db50f97 100644 --- a/src/agents/housekeeper/client.py +++ b/src/agents/housekeeper/client.py @@ -182,7 +182,7 @@ class CoreAPIClient: logger.debug("core_api_list_devices", domain=domain, area=area) - response = await client.get("/devices", params=params or None) + response = await client.get("/housekeeping/devices", params=params or None) response.raise_for_status() data = response.json() @@ -199,7 +199,7 @@ class CoreAPIClient: logger.debug("core_api_list_areas") - response = await client.get("/areas") + response = await client.get("/housekeeping/areas") response.raise_for_status() data = response.json() @@ -219,7 +219,7 @@ class CoreAPIClient: logger.debug("core_api_get_state", entity_id=entity_id) - response = await client.get(f"/entities/{entity_id}") + response = await client.get(f"/housekeeping/devices/{entity_id}") response.raise_for_status() return DeviceState(**response.json()) @@ -260,7 +260,7 @@ class CoreAPIClient: logger.info("core_api_turn_on", entity_id=entity_id, payload=payload) response = await client.post( - f"/devices/{entity_id}/control", + f"/housekeeping/devices/{entity_id}/control", json=payload, ) response.raise_for_status() @@ -288,7 +288,7 @@ class CoreAPIClient: logger.info("core_api_turn_off", entity_id=entity_id) response = await client.post( - f"/devices/{entity_id}/control", + f"/housekeeping/devices/{entity_id}/control", json={"action": "turn_off"}, ) response.raise_for_status() @@ -316,7 +316,7 @@ class CoreAPIClient: logger.info("core_api_toggle", entity_id=entity_id) response = await client.post( - f"/devices/{entity_id}/control", + f"/housekeeping/devices/{entity_id}/control", json={"action": "toggle"}, ) response.raise_for_status() @@ -344,7 +344,7 @@ class CoreAPIClient: logger.debug("core_api_list_scenes") - response = await client.get("/scenes") + response = await client.get("/housekeeping/scenes") response.raise_for_status() data = response.json() @@ -364,7 +364,7 @@ class CoreAPIClient: logger.info("core_api_activate_scene", scene_id=scene_id) - response = await client.post(f"/scenes/{scene_id}/activate") + response = await client.post(f"/housekeeping/scenes/{scene_id}/activate") response.raise_for_status() data = response.json() @@ -390,7 +390,7 @@ class CoreAPIClient: logger.debug("core_api_list_scripts") - response = await client.get("/scripts") + response = await client.get("/housekeeping/scripts") response.raise_for_status() data = response.json() @@ -420,7 +420,7 @@ class CoreAPIClient: logger.info("core_api_run_script", script_id=script_id) response = await client.post( - f"/scripts/{script_id}/run", + f"/housekeeping/scripts/{script_id}/run", json=payload or None, ) response.raise_for_status() @@ -448,7 +448,7 @@ class CoreAPIClient: logger.debug("core_api_list_automations") - response = await client.get("/automations") + response = await client.get("/housekeeping/automations") response.raise_for_status() data = response.json() @@ -478,7 +478,7 @@ class CoreAPIClient: ) response = await client.post( - f"/automations/{automation_id}/toggle", + f"/housekeeping/automations/{automation_id}/toggle", json={"enable": enable}, ) response.raise_for_status() @@ -515,7 +515,7 @@ class CoreAPIClient: logger.debug("core_api_get_history", entity_id=entity_id, hours=hours) response = await client.get( - "/history", + "/housekeeping/history", params={"entity_id": entity_id, "hours": hours}, ) response.raise_for_status() @@ -536,7 +536,7 @@ class CoreAPIClient: """ try: client = self._ensure_client() - response = await client.get("/health") + response = await client.get("/housekeeping/health") return response.status_code == 200 except Exception as e: logger.warning("core_api_health_check_failed", error=str(e))