Compare commits

...
4 Commits
Author SHA1 Message Date
jpmschweitzerandClaude Opus 4.5 aa16fe4ffd chore: release v1.9.0
Build and Push / build (release) Successful in 1m37s
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 21:00:01 +01:00
jpmschweitzerandClaude Opus 4.5 363ab378af feat: optimize Housekeeper for Mistral-Nemo tool calling
- Rewrite system prompt with negative constraints and step-by-step process
- Set temperature to 0.1 for deterministic tool calling
- Sort room groups to top of device list (address positional bias)
- Add [ROOM GROUP] marker in list_devices output
- Update tool docstrings with explicit entity_id= parameter examples
- Add optimization findings doc (experiment log: 0% → 100% success)
- Add test script for room group detection regression testing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-18 20:58:39 +01:00
jpmschweitzer 43c09f9922 localhost in wakeup script 2025-12-18 20:08:52 +01:00
jpmschweitzer 74cf27980a cleanup 2025-12-17 20:44:09 +01:00
8 changed files with 492 additions and 405 deletions
+14
View File
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.9.0] - 2025-12-18
### Changed
- **Housekeeper prompt optimization** - Rewrote system prompt for Mistral-Nemo function calling with negative constraints, step-by-step process, and explicit entity ID format guidance
- **Housekeeper temperature setting** - Set temperature to 0.1 for deterministic tool calling behavior
- **Device list room group priority** - Room groups now appear first in `list_devices` output with `[ROOM GROUP]` marker to address positional bias
- **Tool docstring improvements** - Updated turn_on/turn_off/toggle with explicit `entity_id=` parameter examples
### Added
- **Housekeeper optimization findings** - Added `docs/housekeeper-optimization-findings.md` documenting the experiment journey from 0% to 100% success rate
- **Housekeeper test script** - Added `scripts/test_housekeeper.sh` for room group detection regression testing
## [1.8.6] - 2025-12-17
### Fixed
+246
View File
@@ -0,0 +1,246 @@
# Housekeeper Agent Optimization Findings
## Background
Research with Gemini identified key issues with mistral-nemo and tool calling:
- "Pre-computation Hallucination" - model answers before using tools
- High default temperature (0.7-0.8) causes wandering
- Model is "chatty and confident" - needs explicit constraints
## Key Recommendations from Gemini Research
1. **Temperature 0.0** for tool-calling agents (deterministic, follows schema)
2. **Chain of Thought (CoT)** - force step-by-step reasoning
3. **Negative constraints** - tell model what NOT to do (Nemo responds better)
4. **Explicit tool descriptions** - verbose docstrings with "never estimate yourself"
5. **"Strictly tool-based assistant"** pattern - NO internal knowledge claim
---
## Experiment Log
### Baseline (v1.8.6)
- **Date**: 2025-12-17
- **Configuration**: Default temperature, improved prompt requiring list_devices first
- **Results**:
- Called list_devices first ✓
- Still hallucinated `light.study_desk` despite seeing list with only `light.study` and `light.study_main`
- Partial success: turned off `light.study_main`, failed on hallucinated entity
- **Success rate**: ~50% (1 of 2 study lights controlled correctly)
---
### Experiment 1: Temperature 0.0
- **Date**: 2025-12-18
- **Change**: Set `model_settings=ModelSettings(temperature=0.0)` for Housekeeper
- **Hypothesis**: Deterministic output will force model to use exact entity IDs from tool results
- **Results**:
**Study lights test:**
- Called `list_devices()` first ✓ (but no domain filter)
- Used wrong parameter `device_id` instead of `entity_id` (recovered after validation error)
- Only identified `light.studeerlamp` as "study" related (Dutch name)
- **Missed `light.study` and `light.study_main`** - didn't match English "study"
- Turned off 1 wrong light, missed 2 actual study lights
**Kitchen lights test:**
- Called `list_devices()` first ✓ (no domain filter)
- Saw full device list including `light.kitchen`
- Used wrong parameter `device_id` instead of `entity_id` (recovered after validation)
- After correction, dropped domain prefix: used `kitchen` instead of `light.kitchen`
- 404 error - device not found
- **Success rate**: 0% (no target lights successfully controlled)
- **Observations**:
- Temperature 0.0 alone is insufficient
- Model consistently confuses `device_id` vs `entity_id` parameter name
- After validation error correction, model truncates entity_id (drops domain prefix)
- Semantic matching of room names to devices is weak
- Model doesn't understand entity_id format: `domain.name`
---
### Experiment 2: Negative Constraints + CoT
- **Date**: 2025-12-18
- **Change**: Complete prompt rewrite with:
- "You have NO Internal Knowledge" - negative framing
- Explicit entity_id format with WRONG/RIGHT examples
- Step-by-step process (ALWAYS FOLLOW)
- Explicit parameter names section
- "What NOT To Do" negative constraints
- **Hypothesis**: Negative constraints work better with Mistral-Nemo
- **Results**:
**Study lights test:**
- Called `list_devices(domain="light")` ✓ with domain filter (improvement!)
- Still used `device_id` first, recovered to `entity_id` after validation error
- After recovery, used correct full format: `light.studeerlamp`
- **Still only matched `studeerlamp` not `light.study` or `light.study_main`**
**Kitchen lights test:**
- Called `list_devices(domain="light")`
- Called `turn_off(entity_id="light.kitchen")` ✓ correct format!
- All 4 kitchen lights turned off (light.kitchen is a group)
- **100% success for kitchen!**
- **Success rate**:
- Study: 0% (wrong semantic match)
- Kitchen: 100% (4/4 lights off)
- Combined: ~50% (1 of 2 tests successful)
- **Observations**:
- Domain filter now consistently used ✓
- Entity_id format correct after recovery ✓
- Semantic matching still fails for "study" → prefers Dutch "studeerlamp" over English "study"
- Parameter name confusion persists (`device_id` vs `entity_id`)
- Simple room names (kitchen) work; mixed language fails (study/studeerlamp)
---
### Experiment 3: Temperature 0.1 + Explicit Tool Docstrings
- **Date**: 2025-12-18
- **Change**:
- Temperature 0.1
- Updated turn_on/turn_off docstrings with explicit `entity_id=` in examples
- **Results**:
- Still uses `device_id` first, recovers to `entity_id` after validation
- Still picks wrong entity (studeerlamp over study)
- **Success rate**: 0%
---
### Experiment 4: Room Group Priority (with explicit examples)
- **Date**: 2025-12-18
- **Change**: Updated prompt with:
- Explicit instruction: "Look for EXACT match `light.<room_name>` first!"
- Concrete examples: "For 'study lights' → look for `light.study`"
- Working example showing `turn_off(entity_id="light.study")`
- **Hypothesis**: Explicit examples will guide model to use room groups
- **Results**:
**Test 1 & 2 (consecutive):**
- Called `list_devices(domain="light")`
- Device list clearly shows `light.study` at the bottom
- First call: `turn_off({"devices":["studeerlamp"]})` - wrong param AND wrong device
- After validation error: `turn_off(entity_id="light.studeerlamp")` - correct param, still wrong device
- **Completely ignored `light.study` despite prompt explicitly saying to use it**
- **Success rate**: 0% (wrong device controlled)
- **Observations**:
- Model ignores explicit step-by-step instructions in favor of substring matching
- Dutch "studeerlamp" contains "studer" which the model prefers over exact "study" match
- Even when prompt has a literal example `turn_off(entity_id="light.study")`, model uses `light.studeerlamp`
- Positional bias possible - `light.study` appears at end of 21-item list
- **Fundamental limitation**: Mistral-Nemo cannot follow explicit matching rules
---
### Experiment 5: Room Groups First (Tool Output Ordering)
- **Date**: 2025-12-18
- **Change**: Modified `list_devices` to sort room groups to top of list using HA attributes (`is_hue_group`, `hue_type="room"`)
- **Hypothesis**: Positional bias - model focuses on items earlier in list
- **Results**:
- Room groups (`light.study`, `light.kitchen`, etc.) now appear first in device list
- Combined with improved prompt, model now consistently uses room groups
- **70% success rate** (7/10 tests) with default q4 quantization
---
### Experiment 6: Model Quantization (q5_1)
- **Date**: 2025-12-18
- **Change**: Upgraded from default Mistral-Nemo quantization (q4) to `mistral-nemo:12b-instruct-2407-q5_1`
- **Hypothesis**: Higher precision weights improve tool calling accuracy
- **Results**:
| Test | Action | Result |
|------|--------|--------|
| 1 | Turn off study | PASS |
| 2 | Turn on study | PASS |
| 3 | Toggle study | PASS |
| 4 | Turn off kitchen | PASS |
| 5 | Turn on kitchen | PASS |
| 6 | Toggle kitchen | PASS |
| 7 | Turn off bedroom | PASS |
| 8 | Turn on bedroom | PASS |
| 9 | Turn off living room | PASS |
| 10 | Turn on living room | PASS |
- **Success rate**: **100%** (10/10 tests)
- **Observations**:
- q5_1 quantization dramatically improves tool calling accuracy
- All room groups correctly identified and used
- No parameter confusion (`entity_id` used correctly)
- No entity_id truncation issues
- Toggle operations now work reliably
- Model fits within 10GB VRAM (q6 did not)
---
### Experiment 7: Device List in System Prompt (Context Injection)
- **Date**: [PENDING]
- **Change**: Store device list in database (per user/household) and inject into system prompt
- **Approach**:
1. Periodically sync device list from Home Assistant to PostgreSQL
2. On each Housekeeper invocation, fetch device list and include in prompt
3. Remove need for model to call list_devices() - just match from context
- **Hypothesis**:
- Eliminates tool call step where errors occur
- Reduces context size by not returning full device list as tool output
- Makes entity matching a language task (in prompt) rather than tool result parsing
- **Trade-offs**:
- Stale data if sync is infrequent
- Prompt size increase (but less than tool call response)
- Need sync mechanism and storage
- **Results**: [TO BE RECORDED]
- **Success rate**: [TO BE RECORDED]
---
## Key Problem Identified (Solved)
The model struggled with:
1. **Parameter schema adherence** - uses `device_id` when schema requires `entity_id`
2. **Value preservation** - truncates values after validation errors (drops `light.` prefix)
3. **Semantic matching** - prefers substring matches ("studeerlamp" contains "studer") over exact matches (`light.study`)
4. **Following explicit instructions** - ignores step-by-step processes even when examples are provided
5. **Positional bias** - may not "see" items at the end of long lists
**Solution**: These issues were resolved by:
1. Using q5_1 quantization instead of default q4 (higher precision weights)
2. Sorting room groups to top of device list (address positional bias)
3. Explicit prompt guidance with negative constraints and examples
---
## Potential Next Experiments
### Experiment 5: Room Groups First (List Ordering)
- **Hypothesis**: Positional bias - model focuses on items earlier in list
- **Change**: Sort device list to put room groups (entities matching `light.<single_word>`) at the TOP
- **Effort**: Low - modify list_devices output formatting
- **Risk**: May affect other use cases where individual devices are needed
### Experiment 6: Simplified Device List Format
- **Hypothesis**: Markdown formatting adds noise that confuses the model
- **Change**: Return simple list: `light.study (Study - GROUP), light.study_main (Ceiling light), ...`
- **Effort**: Low - modify list_devices output
- **Risk**: Less human-readable responses
---
## Learnings to Apply Elsewhere
1. **Quantization matters** - q5_1 dramatically outperforms q4 for tool calling (100% vs 70%)
2. **Positional bias is real** - sort important items to top of lists
3. **Smaller models need simpler workflows** - fewer tool calls, more context injection
4. **Validation errors don't teach** - model often makes worse mistakes on retry
5. **Entity IDs are hard** - domain.name format confuses the model
6. **Consider pre-computation** - move matching logic to code, not LLM
7. **Use explicit negative constraints** - "NEVER do X" works better than "always do Y"
---
## Notes
- Librarian may need higher temperature for creative synthesis
- All "action" agents (Housekeeper, future agents) should use low temperature
- Consider testing with Gemma 2 9B for better function calling (Google, open weights)
-317
View File
@@ -1,317 +0,0 @@
# 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.
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "tatlock"
version = "1.8.6"
version = "1.9.0"
description = "OpenAI-compatible API with Ollama backend"
requires-python = ">=3.12"
dependencies = []
+141
View File
@@ -0,0 +1,141 @@
#!/bin/bash
# Housekeeper Room Group Detection Test Suite
# Verifies room groups are controlled by checking actual state changes
API_URL="http://localhost:8777/v1/chat/completions"
CORE_API="http://192.168.86.149:8083"
RESULTS_FILE="/tmp/housekeeper_test_results.txt"
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
get_state() {
curl -s "$CORE_API/housekeeping/devices/$1" 2>/dev/null | jq -r '.state' 2>/dev/null
}
echo "=========================================="
echo "Housekeeper Room Group Test Suite"
echo "=========================================="
echo ""
> "$RESULTS_FILE"
run_toggle_test() {
local test_num=$1
local room=$2
local entity="light.$room"
local prompt_room="${room//_/ }"
printf "Test %2d: Toggle %-12s lights ... " "$test_num" "$prompt_room"
local before=$(get_state "$entity")
if [ -z "$before" ] || [ "$before" = "null" ]; then
echo -e "${YELLOW}SKIP${NC} (cannot get state)"
echo "SKIP|$test_num|Toggle $room|error" >> "$RESULTS_FILE"
return
fi
curl -s -X POST "$API_URL" \
-H "Content-Type: application/json" \
-d "{\"model\": \"tatlock\", \"messages\": [{\"role\": \"user\", \"content\": \"Toggle the $prompt_room lights\"}]}" > /dev/null
sleep 4
local after=$(get_state "$entity")
if [ "$before" != "$after" ]; then
echo -e "${GREEN}PASS${NC} ($before -> $after)"
echo "PASS|$test_num|Toggle $room|$before->$after" >> "$RESULTS_FILE"
else
echo -e "${RED}FAIL${NC} (state unchanged: $before)"
echo "FAIL|$test_num|Toggle $room|unchanged:$before" >> "$RESULTS_FILE"
fi
}
run_onoff_test() {
local test_num=$1
local room=$2
local action=$3
local expected_state=$4
# Entity uses underscore, prompt uses space
local entity="light.${room//_/ }"
entity="light.$room"
local prompt_room="${room//_/ }"
printf "Test %2d: %-8s %-12s lights ... " "$test_num" "$action" "$prompt_room"
curl -s -X POST "$API_URL" \
-H "Content-Type: application/json" \
-d "{\"model\": \"tatlock\", \"messages\": [{\"role\": \"user\", \"content\": \"$action the $prompt_room lights\"}]}" > /dev/null
sleep 4
local after=$(get_state "$entity")
if [ "$after" = "$expected_state" ]; then
echo -e "${GREEN}PASS${NC} ($after)"
echo "PASS|$test_num|$action $room|$after" >> "$RESULTS_FILE"
else
echo -e "${RED}FAIL${NC} (got $after, expected $expected_state)"
echo "FAIL|$test_num|$action $room|got:$after,expected:$expected_state" >> "$RESULTS_FILE"
fi
}
echo "Running tests (~4s each)..."
echo ""
# Study tests
run_onoff_test 1 "study" "Turn off" "off"
run_onoff_test 2 "study" "Turn on" "on"
run_toggle_test 3 "study"
# Kitchen tests
run_onoff_test 4 "kitchen" "Turn off" "off"
run_onoff_test 5 "kitchen" "Turn on" "on"
run_toggle_test 6 "kitchen"
# Bedroom tests
run_onoff_test 7 "bedroom" "Turn off" "off"
run_onoff_test 8 "bedroom" "Turn on" "on"
# Living room tests (entity is light.living_room)
run_onoff_test 9 "living_room" "Turn off" "off"
run_onoff_test 10 "living_room" "Turn on" "on"
# Ensure all lights end up ON
echo ""
echo "Restoring all lights to ON..."
for room in "study" "kitchen" "bedroom" "living room"; do
curl -s -X POST "$API_URL" \
-H "Content-Type: application/json" \
-d "{\"model\": \"tatlock\", \"messages\": [{\"role\": \"user\", \"content\": \"Turn on the $room lights\"}]}" > /dev/null
sleep 3
done
echo "Done."
echo ""
echo "=========================================="
echo "Results"
echo "=========================================="
PASS=$(grep -c "^PASS" "$RESULTS_FILE" 2>/dev/null || echo 0)
FAIL=$(grep -c "^FAIL" "$RESULTS_FILE" 2>/dev/null || echo 0)
SKIP=$(grep -c "^SKIP" "$RESULTS_FILE" 2>/dev/null || echo 0)
TOTAL=$((PASS + FAIL))
echo "Passed: $PASS"
echo "Failed: $FAIL"
echo "Skipped: $SKIP"
if [ "$TOTAL" -gt 0 ]; then
echo ""
echo "Success Rate: $((PASS * 100 / TOTAL))% ($PASS/$TOTAL)"
fi
if [ "$FAIL" -gt 0 ]; then
echo ""
echo "Failures:"
grep "^FAIL" "$RESULTS_FILE"
fi
+55 -71
View File
@@ -32,93 +32,69 @@ from src.core.logging_config import get_logger
logger = get_logger(__name__)
# Housekeeper system prompt
HOUSEKEEPER_SYSTEM_PROMPT = """You are The Housekeeper, an expert home automation assistant in the Tatlock household.
# Housekeeper system prompt - Optimized for Mistral-Nemo function calling
HOUSEKEEPER_SYSTEM_PROMPT = """You are a strictly tool-based home automation assistant.
Your role is to help users control and monitor their smart home through Home Assistant:
- Lights, switches, and other devices
- Scenes (pre-configured device states)
- Scripts (automation sequences)
- Automations (event-triggered rules)
## CRITICAL: You Have NO Internal Knowledge
## Your Personality
- Efficient and practical
- Safety-conscious (confirm destructive actions)
- Proactive in suggesting optimizations
- Clear about what actions you're taking
You do NOT know what devices exist. You do NOT know any entity IDs.
Entity IDs are different in every installation. You MUST discover them using tools.
## Your Tools
## Entity ID Format
### Discovery Tools
- **list_areas**: See all rooms/areas configured in Home Assistant
- **list_devices**: Find devices by type (domain) or location (area)
- **get_device_state**: Check a device's current state and attributes
Entity IDs follow the format: `domain.name`
Examples: `light.kitchen`, `light.study_main`, `switch.coffee_maker`
### Control Tools
- **turn_on**: Turn on lights, switches, etc. (supports brightness/color for lights)
- **turn_off**: Turn off devices
- **toggle**: Flip a device's state
The `entity_id` parameter MUST be the COMPLETE value including the domain prefix.
WRONG: `entity_id="kitchen"`
RIGHT: `entity_id="light.kitchen"`
### Scene Tools
- **list_scenes**: See available scene presets
- **activate_scene**: Activate a scene (e.g., "movie night", "good morning")
## Step-by-Step Process (ALWAYS FOLLOW)
### Script Tools
- **list_scripts**: See available automation scripts
- **run_script**: Execute a script
When asked to control devices in a room:
### Automation Tools
- **list_automations**: See all automations and their status
- **toggle_automation**: Enable or disable an automation
1. THINK: What domain? (light, switch, climate, etc.)
2. CALL: list_devices(domain="light") to discover available devices
3. CHECK: Look for EXACT match `light.<room_name>` first!
- For "study lights" → look for `light.study` (not light.study_main, not light.studeerlamp)
- For "kitchen lights" → look for `light.kitchen` (not light.kitchen_spot_1)
- These room groups control ALL lights in that room at once
- If found, use ONLY the group (stop looking for individual lights)
4. FALLBACK: Only if no exact room group exists, find entity_ids containing the room name
5. CALL: turn_on/turn_off using the EXACT entity_id from step 3 or 4
### History Tools
- **get_history**: Check a device's state history
Example for "Turn off study lights":
1. Domain is "light"
2. Call list_devices(domain="light")
3. Look for room group: `light.study` - FOUND!
4. Call turn_off(entity_id="light.study") # This controls all study lights
## Critical Rules
Example for "Turn off hallway lights" (no room group):
1. Domain is "light"
2. Call list_devices(domain="light")
3. Look for room group: `light.hallway` - NOT FOUND
4. Find all with "hallway": light.hallway_spot_1, light.hallway_spot_2
5. Call turn_off for each
**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).
## Tool Parameter Names
Wrong approach:
User: "Turn off the study lights"
You: turn_off("light.study") ← WRONG! You guessed the entity_id
- turn_on, turn_off, toggle: Use `entity_id` (NOT device_id, NOT id)
- activate_scene: Use `scene_id`
- run_script: Use `script_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
## What NOT To Do
## Best Practices
1. **ALWAYS list_devices first** before any control action. No exceptions.
Filter by domain and/or area to narrow results.
2. **Use exact entity_ids** from list_devices results. Never construct or guess them.
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. **Verify after actions**: Use get_device_state to confirm state changes if needed.
5. **Safety for bulk actions**: When affecting multiple devices, summarize first.
## Common Patterns
- "Turn on the lights" → list_devices(domain="light"), then turn_on each
- "What's on?" → list_devices() and filter for state="on"
- "Movie time" → Either activate_scene("scene.movie_night") or run_script if available
- "Dim the bedroom" → turn_on("light.bedroom", brightness=64)
- NEVER guess an entity_id
- NEVER construct an entity_id from the room name
- NEVER drop the domain prefix (light., switch., etc.)
- NEVER use "device_id" - the parameter is called "entity_id"
- NEVER provide an answer without calling list_devices first
## Response Format
Your responses are returned to Tatlock (the butler) who will synthesize them into
a final answer for the user. Keep this in mind:
- Lead with confirmation of what you did or found
- Be specific about which devices were affected
- Include relevant state information
- Note any issues or failures
- Be concise - Tatlock will format the final response
After completing actions, briefly confirm:
- Which devices were affected (list the entity_ids)
- Whether each action succeeded or failed
"""
# Lazy initialization to avoid connection issues during imports
@@ -231,9 +207,13 @@ async def run_housekeeper(
)
try:
# Use temperature 0.1 for slight exploration
from pydantic_ai.settings import ModelSettings
result = await agent.run(
prompt,
message_history=message_history,
model_settings=ModelSettings(temperature=0.1),
)
logger.info(
@@ -289,9 +269,13 @@ async def run_housekeeper_stream(
)
try:
# Use temperature 0.1 for slight exploration
from pydantic_ai.settings import ModelSettings
async with agent.run_stream(
prompt,
message_history=message_history,
model_settings=ModelSettings(temperature=0.1),
) as response:
async for delta in response.stream_text(delta=True):
yield delta
+34 -15
View File
@@ -59,10 +59,27 @@ async def list_devices(
for dom, dom_devices in sorted(by_domain.items()):
output_parts.append(f"### {dom.title()}s")
for device in dom_devices:
# Sort devices: room groups first (using Home Assistant's is_hue_group attribute)
def is_room_group(d: object) -> bool:
"""Check if device is a room group based on HA attributes."""
attrs = getattr(d, "attributes", {})
# Check for Hue room groups
if attrs.get("is_hue_group") and attrs.get("hue_type") == "room":
return True
# Check for other group indicators (icon or entity_id list)
if "entity_id" in attrs and isinstance(attrs["entity_id"], list):
return True
return False
sorted_devices = sorted(dom_devices, key=lambda d: (not is_room_group(d), d.entity_id))
for device in sorted_devices:
state_icon = "on" if device.state == "on" else "off" if device.state == "off" else device.state
area_str = f" ({device.area})" if device.area else ""
output_parts.append(f"- **{device.name}**{area_str}: {state_icon}")
# Mark room groups clearly using actual HA data
group_marker = " [ROOM GROUP]" if is_room_group(device) else ""
output_parts.append(f"- **{device.name}**{area_str}{group_marker}: {state_icon}")
output_parts.append(f" ID: `{device.entity_id}`")
output_parts.append("")
@@ -164,12 +181,12 @@ async def turn_on(
color_temp: int | None = None,
) -> str:
"""
Turn on a device.
Turn on a device. Use the entity_id parameter with the EXACT value from list_devices.
For lights, can optionally set brightness and color temperature.
Args:
entity_id: Device to turn on (e.g., light.living_room, switch.coffee_maker)
entity_id: The EXACT entity ID from list_devices including domain prefix.
brightness: Optional brightness for lights (0-255, where 255 is full brightness)
color_temp: Optional color temperature in Kelvin (2700=warm, 6500=cool)
@@ -177,10 +194,9 @@ async def turn_on(
Confirmation of the action
Examples:
turn_on("light.living_room") # Turn on at current brightness
turn_on("light.bedroom", brightness=128) # Turn on at 50% brightness
turn_on("light.office", brightness=255, color_temp=4000) # Full, neutral white
turn_on("switch.coffee_maker") # Turn on a switch
turn_on(entity_id="light.living_room")
turn_on(entity_id="light.bedroom", brightness=128)
turn_on(entity_id="switch.coffee_maker")
"""
try:
async with CoreAPIClient() as client:
@@ -209,17 +225,18 @@ async def turn_on(
async def turn_off(entity_id: str) -> str:
"""
Turn off a device.
Turn off a device. Use the entity_id parameter with the EXACT value from list_devices.
Args:
entity_id: Device to turn off (e.g., light.living_room, switch.coffee_maker)
entity_id: The EXACT entity ID from list_devices including domain prefix.
Returns:
Confirmation of the action
Examples:
turn_off("light.living_room")
turn_off("switch.coffee_maker")
turn_off(entity_id="light.living_room")
turn_off(entity_id="switch.coffee_maker")
turn_off(entity_id="light.kitchen")
"""
try:
async with CoreAPIClient() as client:
@@ -239,15 +256,17 @@ async def toggle(entity_id: str) -> str:
"""
Toggle a device's state (on becomes off, off becomes on).
Use the entity_id parameter with the EXACT value from list_devices.
Args:
entity_id: Device to toggle
entity_id: The EXACT entity ID from list_devices including domain prefix.
Returns:
Confirmation with the new state
Examples:
toggle("light.living_room")
toggle("switch.fan")
toggle(entity_id="light.living_room")
toggle(entity_id="switch.fan")
"""
try:
async with CoreAPIClient() as client:
+1 -1
View File
@@ -47,4 +47,4 @@ echo -e "${GREEN}Starting uvicorn server on http://localhost:8777${NC}"
echo -e "${YELLOW}Press Ctrl+C to stop the server${NC}"
echo ""
uvicorn src.main:app --reload --host 0.0.0.0 --port 8777 2>&1 | tee "$LOG_FILE"
uvicorn src.main:app --reload --host localhost --port 8777 2>&1 | tee "$LOG_FILE"