mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-22 23:24:03 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eee9754033 | ||
|
|
517b1f11ae | ||
|
|
ed8f19bfa9 | ||
|
|
7e3daf611b | ||
|
|
37919be70f | ||
|
|
e427254584 |
@@ -250,6 +250,16 @@ sensor:
|
|||||||
|
|
||||||
## 🛠️ Available Services
|
## 🛠️ Available Services
|
||||||
|
|
||||||
|
### 🔄 Response Variables (New!)
|
||||||
|
|
||||||
|
**HA Text AI now supports response variables** - a powerful feature that returns AI responses directly from service calls, eliminating the need for separate text sensors and the 255-character limitation!
|
||||||
|
|
||||||
|
#### ✨ Key Benefits:
|
||||||
|
- **Unlimited response length** - No more 255-character truncation
|
||||||
|
- **Direct data access** - Get responses immediately in automations
|
||||||
|
- **Race condition prevention** - Eliminates conflicts in parallel automations
|
||||||
|
- **Simplified workflows** - No need to read from sensors
|
||||||
|
|
||||||
### ask_question
|
### ask_question
|
||||||
```yaml
|
```yaml
|
||||||
service: ha_text_ai.ask_question
|
service: ha_text_ai.ask_question
|
||||||
@@ -261,6 +271,22 @@ data:
|
|||||||
context_messages: 10 #optional, number of previous messages to include in context, default: 5
|
context_messages: 10 #optional, number of previous messages to include in context, default: 5
|
||||||
system_prompt: "You are a sleep optimization expert" # optional
|
system_prompt: "You are a sleep optimization expert" # optional
|
||||||
instance: sensor.ha_text_ai_gpt
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
response_variable: ai_response # NEW! Store response data directly
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 📊 Response Data Structure:
|
||||||
|
```yaml
|
||||||
|
# The service returns structured data:
|
||||||
|
response_text: "The optimal sleeping temperature is 65-68°F (18-20°C)..."
|
||||||
|
tokens_used: 150
|
||||||
|
prompt_tokens: 50
|
||||||
|
completion_tokens: 100
|
||||||
|
model_used: "claude-3-sonnet"
|
||||||
|
instance: "sensor.ha_text_ai_gpt"
|
||||||
|
question: "What's the optimal temperature for sleeping?"
|
||||||
|
timestamp: "2025-01-09T16:57:00.000Z"
|
||||||
|
success: true
|
||||||
|
# error: "Error message" (only present if success: false)
|
||||||
```
|
```
|
||||||
|
|
||||||
### set_system_prompt
|
### set_system_prompt
|
||||||
@@ -292,6 +318,149 @@ data:
|
|||||||
instance: sensor.ha_text_ai_gpt
|
instance: sensor.ha_text_ai_gpt
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🚀 Advanced Automation Examples with Response Variables
|
||||||
|
|
||||||
|
### Example 1: Smart Home Advice with Direct Response
|
||||||
|
```yaml
|
||||||
|
automation:
|
||||||
|
- alias: "Get AI Home Advice"
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: input_button.ask_ai_advice
|
||||||
|
action:
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: "What's the best way to optimize energy usage in my home?"
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
response_variable: ai_advice
|
||||||
|
- service: notify.mobile_app
|
||||||
|
data:
|
||||||
|
title: "🏠 Smart Home Tip"
|
||||||
|
message: |
|
||||||
|
{{ ai_advice.response_text }}
|
||||||
|
|
||||||
|
📊 Tokens used: {{ ai_advice.tokens_used }}
|
||||||
|
🤖 Model: {{ ai_advice.model_used }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Weather-Based AI Recommendations
|
||||||
|
```yaml
|
||||||
|
automation:
|
||||||
|
- alias: "Weather-Based AI Suggestions"
|
||||||
|
trigger:
|
||||||
|
- platform: numeric_state
|
||||||
|
entity_id: sensor.outdoor_temperature
|
||||||
|
below: 0
|
||||||
|
action:
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: |
|
||||||
|
The outdoor temperature is {{ states('sensor.outdoor_temperature') }}°C.
|
||||||
|
What should I do to prepare my home for freezing weather?
|
||||||
|
system_prompt: "You are a home maintenance expert. Provide practical, actionable advice."
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
response_variable: winter_advice
|
||||||
|
- if:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ winter_advice.success }}"
|
||||||
|
then:
|
||||||
|
- service: persistent_notification.create
|
||||||
|
data:
|
||||||
|
title: "❄️ Winter Preparation Advice"
|
||||||
|
message: |
|
||||||
|
{{ winter_advice.response_text }}
|
||||||
|
|
||||||
|
Generated at: {{ winter_advice.timestamp }}
|
||||||
|
else:
|
||||||
|
- service: persistent_notification.create
|
||||||
|
data:
|
||||||
|
title: "⚠️ AI Service Error"
|
||||||
|
message: "Failed to get winter advice: {{ winter_advice.error }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Multi-Step AI Workflow
|
||||||
|
```yaml
|
||||||
|
automation:
|
||||||
|
- alias: "Multi-Step AI Analysis"
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: input_button.analyze_home_status
|
||||||
|
action:
|
||||||
|
# Step 1: Get current status analysis
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: |
|
||||||
|
Current home status:
|
||||||
|
- Temperature: {{ states('sensor.indoor_temperature') }}°C
|
||||||
|
- Humidity: {{ states('sensor.indoor_humidity') }}%
|
||||||
|
- Energy usage: {{ states('sensor.power_consumption') }}W
|
||||||
|
|
||||||
|
Analyze this data and provide insights.
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
response_variable: status_analysis
|
||||||
|
|
||||||
|
# Step 2: Get recommendations based on analysis
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: |
|
||||||
|
Based on this analysis: "{{ status_analysis.response_text[:500] }}"
|
||||||
|
|
||||||
|
Provide 3 specific actionable recommendations for improvement.
|
||||||
|
context_messages: 2 # Include previous conversation
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
response_variable: recommendations
|
||||||
|
|
||||||
|
# Step 3: Send comprehensive report
|
||||||
|
- service: notify.telegram
|
||||||
|
data:
|
||||||
|
title: "🏠 Home Analysis Report"
|
||||||
|
message: |
|
||||||
|
**Analysis:**
|
||||||
|
{{ status_analysis.response_text }}
|
||||||
|
|
||||||
|
**Recommendations:**
|
||||||
|
{{ recommendations.response_text }}
|
||||||
|
|
||||||
|
**Report Details:**
|
||||||
|
- Total tokens used: {{ status_analysis.tokens_used + recommendations.tokens_used }}
|
||||||
|
- Analysis model: {{ status_analysis.model_used }}
|
||||||
|
- Generated: {{ recommendations.timestamp }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 💡 Migration from Sensors to Response Variables
|
||||||
|
|
||||||
|
#### Old Method (Limited):
|
||||||
|
```yaml
|
||||||
|
# ❌ Old way - limited to 255 characters, race conditions
|
||||||
|
automation:
|
||||||
|
- alias: "Old AI Response Method"
|
||||||
|
action:
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: "Long question here..."
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
- delay: "00:00:05" # Wait for sensor update
|
||||||
|
- service: notify.mobile
|
||||||
|
data:
|
||||||
|
message: "{{ state_attr('sensor.ha_text_ai_gpt', 'response')[:255] }}..." # Truncated!
|
||||||
|
```
|
||||||
|
|
||||||
|
#### New Method (Unlimited):
|
||||||
|
```yaml
|
||||||
|
# ✅ New way - unlimited length, immediate access, no race conditions
|
||||||
|
automation:
|
||||||
|
- alias: "New AI Response Method"
|
||||||
|
action:
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: "Long question here..."
|
||||||
|
instance: sensor.ha_text_ai_gpt
|
||||||
|
response_variable: ai_response # Direct access!
|
||||||
|
- service: notify.mobile
|
||||||
|
data:
|
||||||
|
message: "{{ ai_response.response_text }}" # Full response, no truncation!
|
||||||
|
```
|
||||||
|
|
||||||
### 🏷️ HA Text AI Sensor Naming Convention
|
### 🏷️ HA Text AI Sensor Naming Convention
|
||||||
|
|
||||||
#### Character Restrictions
|
#### Character Restrictions
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
# Using response_variable with HA Text AI
|
||||||
|
|
||||||
|
After updating the HA Text AI integration, it now supports using the `response_variable` parameter in Home Assistant scripts and automations.
|
||||||
|
|
||||||
|
## What Changed
|
||||||
|
|
||||||
|
- Added response schema support in the `ha_text_ai.ask_question` service
|
||||||
|
- Service is now correctly registered with `supports_response=True` flag
|
||||||
|
- You can now use `response_variable` to capture AI response in a variable
|
||||||
|
|
||||||
|
## Example Usage in Script
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
action: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
context_messages: 0
|
||||||
|
temperature: 0.7
|
||||||
|
max_tokens: 1000
|
||||||
|
instance: sensor.ha_text_ai_gemini
|
||||||
|
question: "What time is it?"
|
||||||
|
response_variable: ai_response
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example Usage in Automation
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
alias: "Get AI Response"
|
||||||
|
trigger:
|
||||||
|
- platform: state
|
||||||
|
entity_id: input_boolean.ask_ai
|
||||||
|
to: "on"
|
||||||
|
action:
|
||||||
|
- action: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
instance: sensor.ha_text_ai_gemini
|
||||||
|
question: "What's the current weather?"
|
||||||
|
temperature: 0.7
|
||||||
|
max_tokens: 500
|
||||||
|
response_variable: weather_response
|
||||||
|
|
||||||
|
- action: notify.persistent_notification
|
||||||
|
data:
|
||||||
|
title: "AI Response"
|
||||||
|
message: "{{ weather_response.response_text }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Available Fields in response_variable
|
||||||
|
|
||||||
|
When you use `response_variable`, you will receive an object with the following fields:
|
||||||
|
|
||||||
|
- `response_text` (string) - The AI response text
|
||||||
|
- `tokens_used` (integer) - Total number of tokens used
|
||||||
|
- `prompt_tokens` (integer) - Number of tokens in the prompt
|
||||||
|
- `completion_tokens` (integer) - Number of tokens in the completion
|
||||||
|
- `model_used` (string) - The AI model that was used for the response
|
||||||
|
- `instance` (string) - The instance name that was used
|
||||||
|
- `question` (string) - The original question that was asked
|
||||||
|
- `timestamp` (string) - ISO timestamp when the response was generated
|
||||||
|
- `success` (boolean) - Whether the request was successful
|
||||||
|
- `error` (string) - Error message if the request failed
|
||||||
|
|
||||||
|
## Example Using Response Fields
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
action:
|
||||||
|
- action: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
instance: sensor.ha_text_ai_gemini
|
||||||
|
question: "Tell me a joke"
|
||||||
|
response_variable: joke_response
|
||||||
|
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ joke_response.success }}"
|
||||||
|
|
||||||
|
- action: input_text.set_value
|
||||||
|
target:
|
||||||
|
entity_id: input_text.last_ai_response
|
||||||
|
data:
|
||||||
|
value: "{{ joke_response.response_text }}"
|
||||||
|
|
||||||
|
- action: input_number.set_value
|
||||||
|
target:
|
||||||
|
entity_id: input_number.tokens_used
|
||||||
|
data:
|
||||||
|
value: "{{ joke_response.tokens_used }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
action:
|
||||||
|
- action: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
instance: sensor.ha_text_ai_gemini
|
||||||
|
question: "Test question"
|
||||||
|
response_variable: ai_result
|
||||||
|
|
||||||
|
- choose:
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ ai_result.success }}"
|
||||||
|
sequence:
|
||||||
|
- action: notify.mobile_app_phone
|
||||||
|
data:
|
||||||
|
title: "AI Response"
|
||||||
|
message: "{{ ai_result.response_text }}"
|
||||||
|
- conditions:
|
||||||
|
- condition: template
|
||||||
|
value_template: "{{ not ai_result.success }}"
|
||||||
|
sequence:
|
||||||
|
- action: notify.mobile_app_phone
|
||||||
|
data:
|
||||||
|
title: "AI Error"
|
||||||
|
message: "Error: {{ ai_result.error }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration from Old Approach
|
||||||
|
|
||||||
|
**Old method (without response_variable):**
|
||||||
|
```yaml
|
||||||
|
# Ask question
|
||||||
|
- action: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
instance: sensor.ha_text_ai_gemini
|
||||||
|
question: "Hello!"
|
||||||
|
|
||||||
|
# Wait and read response from sensor
|
||||||
|
- delay: 00:00:05
|
||||||
|
- action: notify.mobile_app_phone
|
||||||
|
data:
|
||||||
|
message: "{{ states('sensor.ha_text_ai_gemini') }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
**New method (with response_variable):**
|
||||||
|
```yaml
|
||||||
|
# Ask question and get response immediately
|
||||||
|
- action: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
instance: sensor.ha_text_ai_gemini
|
||||||
|
question: "Hello!"
|
||||||
|
response_variable: greeting_response
|
||||||
|
|
||||||
|
- action: notify.mobile_app_phone
|
||||||
|
data:
|
||||||
|
message: "{{ greeting_response.response_text }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
The new approach is more reliable as it doesn't require waiting and reading from the sensor.
|
||||||
@@ -112,11 +112,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
# Initialize domain data storage
|
# Initialize domain data storage
|
||||||
hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
async def async_ask_question(call: ServiceCall) -> None:
|
async def async_ask_question(call: ServiceCall) -> dict:
|
||||||
"""Handle ask_question service."""
|
"""Handle ask_question service with response data."""
|
||||||
try:
|
try:
|
||||||
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
||||||
await coordinator.async_ask_question(
|
response = await coordinator.async_ask_question(
|
||||||
question=call.data["question"],
|
question=call.data["question"],
|
||||||
model=call.data.get("model"),
|
model=call.data.get("model"),
|
||||||
temperature=call.data.get("temperature"),
|
temperature=call.data.get("temperature"),
|
||||||
@@ -124,9 +124,34 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
system_prompt=call.data.get("system_prompt"),
|
system_prompt=call.data.get("system_prompt"),
|
||||||
context_messages=call.data.get("context_messages"),
|
context_messages=call.data.get("context_messages"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Return structured response data
|
||||||
|
return {
|
||||||
|
"response_text": response.get("content", ""),
|
||||||
|
"tokens_used": response.get("tokens", {}).get("total", 0),
|
||||||
|
"prompt_tokens": response.get("tokens", {}).get("prompt", 0),
|
||||||
|
"completion_tokens": response.get("tokens", {}).get("completion", 0),
|
||||||
|
"model_used": response.get("model", call.data.get("model", coordinator.model)),
|
||||||
|
"instance": call.data["instance"],
|
||||||
|
"question": call.data["question"],
|
||||||
|
"timestamp": response.get("timestamp"),
|
||||||
|
"success": True
|
||||||
|
}
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
_LOGGER.error("Error asking question: %s", str(err))
|
_LOGGER.error("Error asking question: %s", str(err))
|
||||||
raise HomeAssistantError(f"Failed to process question: {str(err)}")
|
# Return error response
|
||||||
|
return {
|
||||||
|
"response_text": "",
|
||||||
|
"tokens_used": 0,
|
||||||
|
"prompt_tokens": 0,
|
||||||
|
"completion_tokens": 0,
|
||||||
|
"model_used": call.data.get("model", ""),
|
||||||
|
"instance": call.data["instance"],
|
||||||
|
"question": call.data["question"],
|
||||||
|
"timestamp": datetime.now().isoformat(),
|
||||||
|
"success": False,
|
||||||
|
"error": str(err)
|
||||||
|
}
|
||||||
|
|
||||||
async def async_clear_history(call: ServiceCall) -> None:
|
async def async_clear_history(call: ServiceCall) -> None:
|
||||||
"""Handle clear_history service."""
|
"""Handle clear_history service."""
|
||||||
@@ -163,7 +188,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
|||||||
DOMAIN,
|
DOMAIN,
|
||||||
SERVICE_ASK_QUESTION,
|
SERVICE_ASK_QUESTION,
|
||||||
async_ask_question,
|
async_ask_question,
|
||||||
schema=SERVICE_SCHEMA_ASK_QUESTION
|
schema=SERVICE_SCHEMA_ASK_QUESTION,
|
||||||
|
supports_response=True
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.async_register(
|
hass.services.async_register(
|
||||||
|
|||||||
@@ -49,20 +49,36 @@ class APIClient:
|
|||||||
self.api_provider = api_provider
|
self.api_provider = api_provider
|
||||||
self.model = model
|
self.model = model
|
||||||
self.timeout = ClientTimeout(total=API_TIMEOUT)
|
self.timeout = ClientTimeout(total=API_TIMEOUT)
|
||||||
|
self._closed = False
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
"""Async context manager entry."""
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
"""Async context manager exit."""
|
||||||
|
await self.shutdown()
|
||||||
|
|
||||||
def _validate_parameters(
|
def _validate_parameters(
|
||||||
self,
|
self,
|
||||||
temperature: float,
|
temperature: float,
|
||||||
max_tokens: int,
|
max_tokens: int,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Validate API parameters."""
|
"""Validate API parameters with enhanced type checking."""
|
||||||
|
# Type validation
|
||||||
|
if not isinstance(temperature, (int, float)):
|
||||||
|
raise TypeError(f"Temperature must be a number, got {type(temperature)}")
|
||||||
|
if not isinstance(max_tokens, int):
|
||||||
|
raise TypeError(f"Max tokens must be an integer, got {type(max_tokens)}")
|
||||||
|
|
||||||
|
# Range validation
|
||||||
if not MIN_TEMPERATURE <= temperature <= MAX_TEMPERATURE:
|
if not MIN_TEMPERATURE <= temperature <= MAX_TEMPERATURE:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Temperature must be between {MIN_TEMPERATURE} and {MAX_TEMPERATURE}"
|
f"Temperature must be between {MIN_TEMPERATURE} and {MAX_TEMPERATURE}, got {temperature}"
|
||||||
)
|
)
|
||||||
if not MIN_MAX_TOKENS <= max_tokens <= MAX_MAX_TOKENS:
|
if not MIN_MAX_TOKENS <= max_tokens <= MAX_MAX_TOKENS:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Max tokens must be between {MIN_MAX_TOKENS} and {MAX_MAX_TOKENS}"
|
f"Max tokens must be between {MIN_MAX_TOKENS} and {MAX_MAX_TOKENS}, got {max_tokens}"
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _make_request(
|
async def _make_request(
|
||||||
@@ -71,7 +87,10 @@ class APIClient:
|
|||||||
payload: Dict[str, Any],
|
payload: Dict[str, Any],
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""Make API request with retry logic."""
|
"""Make API request with retry logic."""
|
||||||
_LOGGER.debug(f"API Request: URL={url}, Payload={payload}")
|
# Log request without sensitive data
|
||||||
|
safe_payload = {k: v for k, v in payload.items() if k not in ['messages', 'system']}
|
||||||
|
_LOGGER.debug(f"API Request: URL={url}, Safe payload: {safe_payload}")
|
||||||
|
|
||||||
for attempt in range(API_RETRY_COUNT):
|
for attempt in range(API_RETRY_COUNT):
|
||||||
try:
|
try:
|
||||||
async with timeout(API_TIMEOUT):
|
async with timeout(API_TIMEOUT):
|
||||||
@@ -84,16 +103,18 @@ class APIClient:
|
|||||||
_LOGGER.debug(f"Response status: {response.status}")
|
_LOGGER.debug(f"Response status: {response.status}")
|
||||||
if response.status != 200:
|
if response.status != 200:
|
||||||
error_data = await response.json()
|
error_data = await response.json()
|
||||||
_LOGGER.error(f"API error: {error_data}")
|
# Log error without sensitive data
|
||||||
raise HomeAssistantError(f"API error: {error_data}")
|
safe_error = {k: v for k, v in error_data.items() if k not in ['message', 'details']}
|
||||||
|
_LOGGER.error(f"API error (status {response.status}): {safe_error}")
|
||||||
|
raise HomeAssistantError(f"API error: status {response.status}")
|
||||||
return await response.json()
|
return await response.json()
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
_LOGGER.warning(f"Timeout on attempt {attempt + 1}")
|
_LOGGER.warning(f"Timeout on attempt {attempt + 1}/{API_RETRY_COUNT}")
|
||||||
if attempt == API_RETRY_COUNT - 1:
|
if attempt == API_RETRY_COUNT - 1:
|
||||||
raise HomeAssistantError("API request timed out")
|
raise HomeAssistantError("API request timed out")
|
||||||
await asyncio.sleep(1 * (attempt + 1))
|
await asyncio.sleep(1 * (attempt + 1))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_LOGGER.warning(f"API request failed on attempt {attempt + 1}: {str(e)}")
|
_LOGGER.warning(f"API request failed on attempt {attempt + 1}/{API_RETRY_COUNT}: {type(e).__name__}")
|
||||||
if attempt == API_RETRY_COUNT - 1:
|
if attempt == API_RETRY_COUNT - 1:
|
||||||
raise
|
raise
|
||||||
await asyncio.sleep(1 * (attempt + 1))
|
await asyncio.sleep(1 * (attempt + 1))
|
||||||
|
|||||||
@@ -46,19 +46,6 @@ from .const import (
|
|||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
def _check_memory_available(self):
|
|
||||||
"""Check if enough memory is available."""
|
|
||||||
memory = psutil.virtual_memory()
|
|
||||||
|
|
||||||
# Log the total and available memory
|
|
||||||
_LOGGER.debug("Total memory: %s, Available memory: %s", memory.total, memory.available)
|
|
||||||
|
|
||||||
if memory.available > 1024 * 1024 * 100: # 100MB
|
|
||||||
_LOGGER.debug("Sufficient memory available: %s bytes", memory.available)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
_LOGGER.warning("Insufficient memory available: %s bytes", memory.available)
|
|
||||||
return False
|
|
||||||
|
|
||||||
class AsyncFileHandler:
|
class AsyncFileHandler:
|
||||||
"""Async context manager for file operations."""
|
"""Async context manager for file operations."""
|
||||||
@@ -549,25 +536,57 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
def _sync_write_history_entry(self, entry: dict) -> None:
|
def _sync_write_history_entry(self, entry: dict) -> None:
|
||||||
"""Synchronous method to write history entry."""
|
"""Synchronous method to write history entry with enhanced error handling."""
|
||||||
try:
|
try:
|
||||||
history = []
|
history = []
|
||||||
if os.path.exists(self._history_file):
|
if os.path.exists(self._history_file):
|
||||||
with open(self._history_file, 'r') as f:
|
try:
|
||||||
content = f.read()
|
with open(self._history_file, 'r', encoding='utf-8') as f:
|
||||||
if content:
|
content = f.read()
|
||||||
history = json.loads(content)
|
if content.strip():
|
||||||
|
history = json.loads(content)
|
||||||
|
except (json.JSONDecodeError, UnicodeDecodeError) as e:
|
||||||
|
_LOGGER.warning(f"Corrupted history file, creating new: {e}")
|
||||||
|
# Backup corrupted file
|
||||||
|
backup_path = f"{self._history_file}.corrupted.{dt_util.utcnow().strftime('%Y%m%d_%H%M%S')}"
|
||||||
|
try:
|
||||||
|
os.rename(self._history_file, backup_path)
|
||||||
|
_LOGGER.info(f"Corrupted history backed up to: {backup_path}")
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
except PermissionError as e:
|
||||||
|
_LOGGER.error(f"Permission denied reading history file: {e}")
|
||||||
|
raise
|
||||||
|
except OSError as e:
|
||||||
|
_LOGGER.error(f"OS error reading history file: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
history.append(entry)
|
history.append(entry)
|
||||||
|
|
||||||
if len(history) > self.max_history_size:
|
if len(history) > self.max_history_size:
|
||||||
history = history[-self.max_history_size:]
|
history = history[-self.max_history_size:]
|
||||||
|
|
||||||
with open(self._history_file, 'w') as f:
|
# Write with atomic operation
|
||||||
json.dump(history, f, indent=2)
|
temp_file = f"{self._history_file}.tmp"
|
||||||
|
try:
|
||||||
|
with open(temp_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(history, f, indent=2, ensure_ascii=False)
|
||||||
|
os.replace(temp_file, self._history_file)
|
||||||
|
except PermissionError as e:
|
||||||
|
_LOGGER.error(f"Permission denied writing history file: {e}")
|
||||||
|
raise
|
||||||
|
except OSError as e:
|
||||||
|
_LOGGER.error(f"OS error writing history file: {e}")
|
||||||
|
# Clean up temp file if it exists
|
||||||
|
try:
|
||||||
|
os.remove(temp_file)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
raise
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
_LOGGER.error(f"Synchronous history entry writing failed: {e}")
|
_LOGGER.error(f"Synchronous history entry writing failed: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
async def _rotate_history(self) -> None:
|
async def _rotate_history(self) -> None:
|
||||||
"""Rotate conversation history with file management."""
|
"""Rotate conversation history with file management."""
|
||||||
@@ -903,23 +922,38 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
else:
|
else:
|
||||||
response = await self._process_openai_message(question, **kwargs)
|
response = await self._process_openai_message(question, **kwargs)
|
||||||
|
|
||||||
|
# Add timestamp and model information to response
|
||||||
|
timestamp = dt_util.utcnow().isoformat()
|
||||||
|
model_used = kwargs.get("model", self.model)
|
||||||
|
|
||||||
|
# Enhance response with metadata
|
||||||
|
enhanced_response = {
|
||||||
|
"content": response["content"],
|
||||||
|
"tokens": response.get("tokens", {}),
|
||||||
|
"model": model_used,
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"instance": self.instance_name,
|
||||||
|
"question": question,
|
||||||
|
"success": True
|
||||||
|
}
|
||||||
|
|
||||||
self.last_response = {
|
self.last_response = {
|
||||||
"timestamp": dt_util.utcnow().isoformat(),
|
"timestamp": timestamp,
|
||||||
"question": question,
|
"question": question,
|
||||||
"response": response["content"],
|
"response": response["content"],
|
||||||
"model": kwargs.get("model", self.model),
|
"model": model_used,
|
||||||
"instance": self.instance_name,
|
"instance": self.instance_name,
|
||||||
"normalized_name": self.normalized_name,
|
"normalized_name": self.normalized_name,
|
||||||
"error": None,
|
"error": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
return enhanced_response
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
raise HomeAssistantError("Request timed out")
|
raise HomeAssistantError("Request timed out")
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
self._handle_error(err)
|
await self._handle_error(err)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
async def _process_anthropic_message(self, question: str, **kwargs) -> dict:
|
async def _process_anthropic_message(self, question: str, **kwargs) -> dict:
|
||||||
@@ -1060,6 +1094,24 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
|||||||
self._system_prompt = prompt
|
self._system_prompt = prompt
|
||||||
await self.async_update_ha_state()
|
await self.async_update_ha_state()
|
||||||
|
|
||||||
|
def _check_memory_available(self) -> bool:
|
||||||
|
"""Check if enough memory is available."""
|
||||||
|
try:
|
||||||
|
memory = psutil.virtual_memory()
|
||||||
|
|
||||||
|
# Log the total and available memory
|
||||||
|
_LOGGER.debug("Total memory: %s, Available memory: %s", memory.total, memory.available)
|
||||||
|
|
||||||
|
if memory.available > 1024 * 1024 * 100: # 100MB
|
||||||
|
_LOGGER.debug("Sufficient memory available: %s bytes", memory.available)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
_LOGGER.warning("Insufficient memory available: %s bytes", memory.available)
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error("Error checking memory availability: %s", e)
|
||||||
|
return True # Assume memory is available if check fails
|
||||||
|
|
||||||
async def async_shutdown(self) -> None:
|
async def async_shutdown(self) -> None:
|
||||||
"""Shutdown coordinator."""
|
"""Shutdown coordinator."""
|
||||||
_LOGGER.debug(f"Shutting down coordinator for {self.instance_name}")
|
_LOGGER.debug(f"Shutting down coordinator for {self.instance_name}")
|
||||||
|
|||||||
@@ -24,6 +24,6 @@
|
|||||||
"single_config_entry": false,
|
"single_config_entry": false,
|
||||||
"ssdp": [],
|
"ssdp": [],
|
||||||
"usb": [],
|
"usb": [],
|
||||||
"version": "2.1.7",
|
"version": "2.1.8",
|
||||||
"zeroconf": []
|
"zeroconf": []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ ask_question:
|
|||||||
description: >-
|
description: >-
|
||||||
Send a question to the AI model and receive a detailed response.
|
Send a question to the AI model and receive a detailed response.
|
||||||
The response will be stored in the conversation history and can be retrieved later.
|
The response will be stored in the conversation history and can be retrieved later.
|
||||||
|
This service now returns response data directly, eliminating the need to read from sensors.
|
||||||
fields:
|
fields:
|
||||||
instance:
|
instance:
|
||||||
name: Instance
|
name: Instance
|
||||||
|
|||||||
@@ -92,13 +92,13 @@
|
|||||||
"anthropic": "Anthropic (compatible)",
|
"anthropic": "Anthropic (compatible)",
|
||||||
"deepseek": "DeepSeek",
|
"deepseek": "DeepSeek",
|
||||||
"gemini": "Google Gemini"
|
"gemini": "Google Gemini"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Frage stellen (HA Text AI)",
|
"name": "Frage stellen (HA Text AI)",
|
||||||
"description": "Stellen Sie eine Frage an das AI-Modell und erhalten Sie eine detaillierte Antwort. Die Antwort wird im Gesprächsverlauf gespeichert und kann später abgerufen werden.",
|
"description": "Stellen Sie eine Frage an das AI-Modell und erhalten Sie eine detaillierte Antwort. Dieser Service gibt jetzt Antwortdaten direkt zurück, wodurch separate Textsensoren und die 255-Zeichen-Begrenzung überflüssig werden. Die Antwort wird auch im Gesprächsverlauf gespeichert.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Instanz",
|
"name": "Instanz",
|
||||||
|
|||||||
@@ -92,13 +92,13 @@
|
|||||||
"anthropic": "Anthropic (compatible)",
|
"anthropic": "Anthropic (compatible)",
|
||||||
"deepseek": "DeepSeek",
|
"deepseek": "DeepSeek",
|
||||||
"gemini": "Google Gemini"
|
"gemini": "Google Gemini"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Ask Question (HA Text AI)",
|
"name": "Ask Question (HA Text AI)",
|
||||||
"description": "Send a question to the AI model and receive a detailed response. The response will be stored in the conversation history and can be retrieved later.",
|
"description": "Send a question to the AI model and receive a detailed response. This service now returns response data directly, eliminating the need for separate text sensors and the 255-character limitation. The response will also be stored in the conversation history.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Instance",
|
"name": "Instance",
|
||||||
|
|||||||
@@ -92,13 +92,13 @@
|
|||||||
"anthropic": "Anthropic (compatible)",
|
"anthropic": "Anthropic (compatible)",
|
||||||
"deepseek": "DeepSeek",
|
"deepseek": "DeepSeek",
|
||||||
"gemini": "Google Gemini"
|
"gemini": "Google Gemini"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
|
||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Hacer Pregunta (HA Text AI)",
|
"name": "Hacer Pregunta (HA Text AI)",
|
||||||
"description": "Envía una pregunta al modelo de IA y recibe una respuesta detallada. La respuesta se almacenará en el historial de conversación y se podrá recuperar más tarde.",
|
"description": "Envía una pregunta al modelo de IA y recibe una respuesta detallada. Este servicio ahora devuelve datos de respuesta directamente, eliminando la necesidad de sensores de texto separados y la limitación de 255 caracteres. La respuesta también se almacenará en el historial de conversación.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Instancia",
|
"name": "Instancia",
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"provider": {
|
|
||||||
"title": "एआई प्रदाता चुनें",
|
|
||||||
"description": "इस उदाहरण के लिए किस एआई सेवा प्रदाता का उपयोग करना है, चुनें।",
|
|
||||||
"data": {
|
|
||||||
"api_provider": "एपीआई प्रदाता",
|
|
||||||
"context_messages": "रखने के लिए संदर्भ संदेशों की संख्या (1-20)",
|
|
||||||
"max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"provider": {
|
"provider": {
|
||||||
"title": "प्रदाता सेटिंग्स",
|
"title": "प्रदाता सेटिंग्स",
|
||||||
"description": "आपके द्वारा चुने गए एआई प्रदाता के लिए कनेक्शन विवरण प्रदान करें।",
|
"description": "आपके द्वारा चुने गए एआई प्रदाता के लिए कनेक्शन विवरण प्रदान करें।",
|
||||||
@@ -98,7 +89,7 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "प्रश्न पूछें (HA Text AI)",
|
"name": "प्रश्न पूछें (HA Text AI)",
|
||||||
"description": "एआई मॉडल को एक प्रश्न भेजें और विस्तृत प्रतिक्रिया प्राप्त करें। प्रतिक्रिया बातचीत के इतिहास में संग्रहीत की जाएगी और बाद में पुनर्प्राप्त की जा सकती है।",
|
"description": "AI मॉडल को प्रश्न भेजें और विस्तृत उत्तर प्राप्त करें। यह सेवा अब प्रत्यक्ष रूप से प्रतिक्रिया डेटा वापस करती है, अलग टेक्स्ट सेंसर की आवश्यकता और 255 वर्ण की सीमा को समाप्त करती है। प्रतिक्रिया को बातचीत के इतिहास में भी संग्रहीत किया जाएगा।",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "उदाहरण",
|
"name": "उदाहरण",
|
||||||
@@ -295,4 +286,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Fai una domanda (HA Text AI)",
|
"name": "Fai una domanda (HA Text AI)",
|
||||||
"description": "Invia una domanda al modello AI e ricevi una risposta dettagliata. La risposta sarà memorizzata nella cronologia delle conversazioni e potrà essere recuperata in seguito.",
|
"description": "Invia una domanda al modello AI e ricevi una risposta dettagliata. Questo servizio ora restituisce i dati di risposta direttamente, eliminando la necessità di sensori di testo separati e la limitazione di 255 caratteri. La risposta sarà anche memorizzata nella cronologia delle conversazioni.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Istanze",
|
"name": "Istanze",
|
||||||
|
|||||||
@@ -98,7 +98,7 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Задать вопрос (HA Text AI)",
|
"name": "Задать вопрос (HA Text AI)",
|
||||||
"description": "Отправить вопрос модели ИИ и получить подробный ответ. Ответ будет сохранен в истории разговора и может быть получен позже.",
|
"description": "Отправить вопрос модели ИИ и получить подробный ответ. Сервис теперь возвращает данные ответа напрямую, устраняя необходимость в отдельных текстовых сенсорах и ограничение в 255 символов. Ответ также будет сохранен в истории разговора.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Экземпляр",
|
"name": "Экземпляр",
|
||||||
|
|||||||
@@ -1,15 +1,6 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"provider": {
|
|
||||||
"title": "Изаберите AI провајдера",
|
|
||||||
"description": "Изаберите који AI сервис провајдер да користите за ову инстанцу.",
|
|
||||||
"data": {
|
|
||||||
"api_provider": "API провајдер",
|
|
||||||
"context_messages": "Број контекстуалних порука које треба задржати (1-20)",
|
|
||||||
"max_history_size": "Максимална величина историје разговора (1-100)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"provider": {
|
"provider": {
|
||||||
"title": "Подешавања провајдера",
|
"title": "Подешавања провајдера",
|
||||||
"description": "Обезбедите детаље о вези за изабраног AI провајдера.",
|
"description": "Обезбедите детаље о вези за изабраног AI провајдера.",
|
||||||
@@ -98,7 +89,7 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "Поставите питање (HA Text AI)",
|
"name": "Поставите питање (HA Text AI)",
|
||||||
"description": "Пошаљите питање AI моделу и примите детаљан одговор. Одговор ће бити сачуван у историји разговора и може се касније повратити.",
|
"description": "Пошаљите питање AI моделу и добијте детаљан одговор. Овај сервис сада враћа податке одговора директно, елиминишући потребу за засебним текстуалним сензорима и ограничење од 255 карактера. Одговор ће такође бити сачуван у историји разговора.",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "Инстанца",
|
"name": "Инстанца",
|
||||||
@@ -295,4 +286,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,6 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"provider": {
|
|
||||||
"title": "选择AI提供者",
|
|
||||||
"description": "选择要用于此实例的AI服务提供者。",
|
|
||||||
"data": {
|
|
||||||
"api_provider": "API提供者",
|
|
||||||
"context_messages": "保留的上下文消息数量(1-20)",
|
|
||||||
"max_history_size": "最大对话历史大小(1-100)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"provider": {
|
"provider": {
|
||||||
"title": "提供者设置",
|
"title": "提供者设置",
|
||||||
"description": "提供所选AI提供者的连接详细信息。",
|
"description": "提供所选AI提供者的连接详细信息。",
|
||||||
@@ -98,7 +89,7 @@
|
|||||||
"services": {
|
"services": {
|
||||||
"ask_question": {
|
"ask_question": {
|
||||||
"name": "提问 (HA Text AI)",
|
"name": "提问 (HA Text AI)",
|
||||||
"description": "向AI模型发送问题并接收详细响应。响应将存储在对话历史中,可以稍后检索。",
|
"description": "向AI模型发送问题并获得详细回答。此服务现在直接返回响应数据,消除了对单独文本传感器的需要和255字符限制。响应也将存储在对话历史中。",
|
||||||
"fields": {
|
"fields": {
|
||||||
"instance": {
|
"instance": {
|
||||||
"name": "实例",
|
"name": "实例",
|
||||||
@@ -295,4 +286,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user