Compare commits

...
12 Commits
Author SHA1 Message Date
SMKRV eee9754033 fix: Fix JSON syntax errors in translation files
- Fixed missing closing brace in es.json selector.api_provider.options
- Fixed missing closing brace in de.json selector.api_provider.options
- All other translation files (hi.json, it.json, sr.json, zh.json) have correct syntax
- Ensures proper JSON validation and prevents parsing errors
2025-09-02 01:26:49 +03:00
SMKRV 517b1f11ae fix: Remove invalid response schema from services.yaml
Home Assistant's hassfest validation does not support 'response' section in services.yaml.
The response_variable functionality still works through supports_response=True flag in service registration.

Fixes hassfest validation error: extra keys not allowed @ data['ask_question']['response']
2025-09-02 01:22:20 +03:00
SMKRV ed8f19bfa9 fix: Add support for response_variable in ask_question service
- Added response schema definition in services.yaml for ask_question service
- Set supports_response=True flag when registering the service
- Fixed JSON syntax error in English translation file
- Added comprehensive documentation with examples for response_variable usage
- Users can now capture AI responses directly in variables without sensor delays

Resolves issue where scripts failed with 'Script does not support response_variable' error
2025-09-02 01:19:44 +03:00
SMKRV 7e3daf611b fix: Remove target requirements from services to fix mandatory device/area/entity selection issue
- Removed target blocks from all services in services.yaml
- Services now work as global services without requiring device/area/entity selection
- Users can call services directly with only required parameters
- Fixes issue #2 where services incorrectly required target selection after v2.1.8 update
2025-09-01 23:40:26 +03:00
SMKRV 37919be70f fix: Resolve hassfest validation errors in services.yaml
- Remove invalid response schema from ask_question service
- Add required target configuration for all services
- Ensure compliance with Home Assistant service schema requirements
2025-09-01 17:20:40 +03:00
SMKRV e427254584 feat: Implement response variables support and comprehensive production audit (v2.1.8)
🚀 Major Features:
- Add response variables support to ask_question service
- Eliminate 255-character limitation for AI responses
- Enable direct data access in automations without sensors
- Prevent race conditions in parallel automations

🔧 Production Code Audit & Fixes:
- Enhanced resource management with context managers in api_client.py
- Fixed critical race conditions with asyncio.Semaphore implementation
- Improved file operations with atomic writes and corruption handling
- Enhanced error handling and logging security (removed sensitive data)
- Fixed _check_memory_available method placement in coordinator.py

🌐 Translation Updates (8 languages):
- Updated all translation files with response variables information
- Enhanced service descriptions in: en, ru, de, es, it, hi, sr, zh
- Added information about direct response capability
- Maintained consistency across all language files

📚 Documentation Enhancements:
- Added comprehensive Response Variables section to README
- Created advanced automation examples with response_variable usage
- Added migration guide from sensors to response variables
- Enhanced service documentation with response data structure
- Added practical examples for multi-step AI workflows

🔄 Service Improvements:
- Enhanced ask_question service to return structured response data
- Added comprehensive response schema in services.yaml
- Improved error handling with success/failure indicators
- Added metadata support (tokens, model, timestamp)

�� Version & Manifest:
- Bumped version to 2.1.8
- Maintained compatibility with existing integrations
- Updated service documentation

This release addresses GitHub issue #2 and significantly improves the integration's
production readiness while adding powerful new response variable functionality.
2025-09-01 17:14:23 +03:00
SMKRV 76c5629fa0 refactor(google-gemini): rewrite integration using google-genai 1.16.0
Completely rewrote the Google Gemini integration logic based on google-genai 1.16.0 to fix issue #6.
Key changes:
- Updated to the latest google-genai library
- Made API endpoint abstract while retaining option for custom endpoint configuration
- Refactored logic and classes exclusively within Google Gemini implementation
- All changes are limited to Google Gemini integration refactoring with no impact on other functionality.
2025-05-21 01:27:47 +03:00
SMKRV 7958bd010b refactor(google-gemini): rewrite integration using google-genai 1.16.0
Completely rewrote the Google Gemini integration logic based on google-genai 1.16.0 to fix issue #6.
Key changes:
- Updated to the latest google-genai library
- Made API endpoint abstract while retaining option for custom endpoint configuration
- Refactored logic and classes exclusively within Google Gemini implementation
- All changes are limited to Google Gemini integration refactoring with no impact on other functionality.
2025-05-21 01:26:42 +03:00
SMKRV 8cd876195a Bump to version 2.1.6 2025-05-20 01:50:06 +03:00
SMKRV 376753e001 fix: correct field naming in Gemini API requests from camelCase to snake_case and improve message handling 2025-05-20 01:42:38 +03:00
SMKRV b6e73e847d fix(api_client): correct Google Gemini API integration
- Change JSON field names from camelCase to snake_case as required by Gemini API
  (generation_config, max_output_tokens, system_instruction)
- Improve message handling to ensure proper role alternation (user/model)
- Add safety checks for empty contents and ensure first message is always from user
- Implement robust error handling and response parsing
- Handle edge cases where candidatesTokenCount might be returned as a list

Fixes #6
2025-05-20 01:16:41 +03:00
SMKRV 440c734214 Bump release version to v2.1.4 2025-05-19 23:20:27 +03:00
18 changed files with 793 additions and 155 deletions
+169
View File
@@ -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
+148
View File
@@ -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.
+42 -8
View File
@@ -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(
@@ -239,10 +265,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
async def async_check_api(session, endpoint: str, headers: dict, provider: str) -> bool: async def async_check_api(session, endpoint: str, headers: dict, provider: str) -> bool:
"""Check API availability for different providers.""" """Check API availability for different providers."""
try: try:
if provider == API_PROVIDER_ANTHROPIC: if provider == API_PROVIDER_GEMINI:
# Gemini API does not support GET /models for validation, just check key presence
if headers.get("Authorization", "").replace("Bearer ", ""):
return True
else:
_LOGGER.error("Gemini API key is missing or empty")
return False
elif provider == API_PROVIDER_ANTHROPIC:
check_url = f"{endpoint}/v1/models" check_url = f"{endpoint}/v1/models"
elif provider == API_PROVIDER_DEEPSEEK: elif provider == API_PROVIDER_DEEPSEEK:
check_url = f"{endpoint}/models" # DeepSeek check_url = f"{endpoint}/models"
else: # OpenAI else: # OpenAI
check_url = f"{endpoint}/models" check_url = f"{endpoint}/models"
@@ -251,7 +284,8 @@ async def async_check_api(session, endpoint: str, headers: dict, provider: str)
if response.status in [200, 404]: if response.status in [200, 404]:
return True return True
elif response.status == 401: elif response.status == 401:
raise ConfigEntryNotReady("Invalid API key") _LOGGER.error("Invalid API key")
return False
elif response.status == 429: elif response.status == 429:
_LOGGER.warning("Rate limit exceeded during API check") _LOGGER.warning("Rate limit exceeded during API check")
return False return False
+156 -48
View File
@@ -11,6 +11,7 @@ import asyncio
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from aiohttp import ClientSession, ClientTimeout from aiohttp import ClientSession, ClientTimeout
from async_timeout import timeout from async_timeout import timeout
from datetime import datetime, timedelta
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
@@ -48,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(
@@ -70,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):
@@ -83,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))
@@ -250,49 +272,135 @@ class APIClient:
temperature: float, temperature: float,
max_tokens: int, max_tokens: int,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Create completion using Gemini API.""" """Create completion using Gemini API with google-genai library.
# Extract API key from headers (Bearer token)
api_key = self.headers.get("Authorization", "").replace("Bearer ", "")
url = f"{self.endpoint}/models/{model}:generateContent?key={api_key}"
# Convert messages to Gemini format
contents = []
system_instruction = ""
for msg in messages:
if msg['role'] == 'system':
system_instruction += msg['content'] + "\n"
else:
contents.append({
"role": "user" if msg['role'] == 'user' else "model",
"parts": [{"text": msg['content']}]
})
payload = { Args:
"contents": contents, model: The model name to use
"generationConfig": { messages: List of message dictionaries with role and content
"temperature": temperature, temperature: Sampling temperature between 0.0 and 2.0
"maxOutputTokens": max_tokens max_tokens: Maximum number of tokens to generate
}
} Returns:
if system_instruction: Dictionary with response content and token usage
payload["systemInstruction"] = { """
"parts": [{"text": system_instruction}] try:
def import_genai():
from google import genai
return genai
genai = await asyncio.to_thread(import_genai)
# Extract API key from headers (Bearer token)
api_key = self.headers.get("Authorization", "").replace("Bearer ", "")
def create_client():
if self.endpoint and self.endpoint != "https://generativelanguage.googleapis.com/v1beta":
return genai.Client(api_key=api_key, transport="rest",
client_options={"api_endpoint": self.endpoint})
else:
return genai.Client(api_key=api_key)
client = await asyncio.to_thread(create_client)
# Process messages to extract system instruction and chat history
system_instruction = ""
contents = []
for msg in messages:
if msg['role'] == 'system':
system_instruction += msg['content'] + "\n"
else:
# For chat history, we need to convert to the format Gemini expects
role = "user" if msg['role'] == 'user' else "model"
contents.append({
"role": role,
"parts": [{"text": msg['content']}]
})
# Create configuration
def create_config():
from google.genai import types
config = types.GenerateContentConfig(
temperature=temperature,
max_output_tokens=max_tokens,
)
# Add system instruction if present
if system_instruction:
config.system_instruction = system_instruction.strip()
return config
config = await asyncio.to_thread(create_config)
def generate_content():
# For single message without history, use generate_content
if len(contents) <= 1:
# If we have no content yet, create a simple prompt
if not contents:
prompt = "I need your assistance."
else:
prompt = contents[0]["parts"][0]["text"]
return client.models.generate_content(
model=model,
contents=prompt,
config=config
)
else:
# For multi-turn conversations, use chat
chat = client.chats.create(model=model, config=config)
# Send all messages in sequence
for content in contents:
if content["role"] == "user":
response = chat.send_message(content["parts"][0]["text"])
# We don't send assistant messages as they're already part of the history
return response
response = await asyncio.to_thread(generate_content)
# Extract response text
def extract_response():
response_text = response.text if hasattr(response, 'text') else ""
# Try to get token usage if available
usage = {}
if hasattr(response, 'usage_metadata'):
usage = {
"prompt_tokens": getattr(response.usage_metadata, 'prompt_token_count', 0),
"completion_tokens": getattr(response.usage_metadata, 'candidates_token_count', 0),
"total_tokens": getattr(response.usage_metadata, 'total_token_count', 0)
}
else:
# Estimate token count as fallback
usage = {
"prompt_tokens": len(" ".join([m["content"] for m in messages]).split()) // 3,
"completion_tokens": len(response_text.split()) // 3,
"total_tokens": 0 # Will be calculated below
}
usage["total_tokens"] = usage["prompt_tokens"] + usage["completion_tokens"]
return response_text, usage
response_text, usage = await asyncio.to_thread(extract_response)
return {
"choices": [{
"message": {
"content": response_text
}
}],
"usage": usage
} }
data = await self._make_request(url, payload) except ImportError as e:
return { _LOGGER.error(f"Google Gemini library not installed: {str(e)}")
"choices": [{ raise HomeAssistantError(f"Missing dependency: {str(e)}. Please install google-genai.")
"message": { except Exception as e:
"content": data["candidates"][0]["content"]["parts"][0]["text"] _LOGGER.error(f"Gemini API error: {str(e)}")
} raise HomeAssistantError(f"Gemini API error: {str(e)}")
}],
"usage": {
"prompt_tokens": data["usageMetadata"]["promptTokenCount"],
"completion_tokens": data["usageMetadata"]["candidatesTokenCount"],
"total_tokens": data["usageMetadata"]["totalTokenCount"]
}
}
async def shutdown(self) -> None: async def shutdown(self) -> None:
"""Shutdown API client.""" """Shutdown API client."""
+173 -21
View File
@@ -8,6 +8,7 @@ Config flow for HA text AI integration.
""" """
import logging import logging
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from datetime import datetime, timedelta
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
@@ -147,41 +148,173 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
}) })
) )
# Debug log to identify what's in the input
_LOGGER.debug(f"Provider step input data: {user_input}")
input_copy = user_input.copy() input_copy = user_input.copy()
# Check if CONF_NAME exists in input_copy and ensure it's not empty
if CONF_NAME not in input_copy or not input_copy[CONF_NAME]:
_LOGGER.warning(f"Missing name in configuration input: {input_copy}")
input_copy[CONF_NAME] = f"gemini_assistant_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
_LOGGER.info(f"Auto-generated name: {input_copy[CONF_NAME]}")
# Ensure API key is present
if CONF_API_KEY not in input_copy or not input_copy[CONF_API_KEY]:
self._errors["base"] = "invalid_auth"
_LOGGER.error("API validation error: 'api_key'")
return self.async_show_form(
step_id="provider",
data_schema=vol.Schema({
vol.Required(CONF_NAME, default=input_copy.get(CONF_NAME, "my_assistant")): str,
vol.Required(CONF_API_KEY): str,
vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, DEFAULT_GEMINI_MODEL if self._provider == API_PROVIDER_GEMINI else DEFAULT_MODEL)): str,
vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, DEFAULT_GEMINI_ENDPOINT if self._provider == API_PROVIDER_GEMINI else DEFAULT_OPENAI_ENDPOINT)): str,
vol.Optional(CONF_TEMPERATURE, default=input_copy.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
),
vol.Optional(CONF_MAX_TOKENS, default=input_copy.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)): vol.All(
vol.Coerce(int),
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
),
vol.Optional(CONF_REQUEST_INTERVAL, default=input_copy.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL)
),
vol.Optional(
CONF_CONTEXT_MESSAGES,
default=input_copy.get(CONF_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES)
): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=20)
),
vol.Optional(
CONF_MAX_HISTORY_SIZE,
default=input_copy.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY)
): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=100)
),
}),
errors=self._errors
)
try: try:
# Validate and normalize the name
normalized_name = self._validate_and_normalize_name(input_copy[CONF_NAME]) normalized_name = self._validate_and_normalize_name(input_copy[CONF_NAME])
input_copy[CONF_NAME] = normalized_name input_copy[CONF_NAME] = normalized_name
except ValueError as e: except ValueError as e:
return self.async_show_form( return self.async_show_form(
step_id="provider", step_id="provider",
data_schema=vol.Schema({ data_schema=vol.Schema({
vol.Required(CONF_NAME, default=input_copy[CONF_NAME]): str, vol.Required(CONF_NAME, default=input_copy.get(CONF_NAME, "my_assistant")): str,
vol.Required(CONF_API_KEY, default=input_copy[CONF_API_KEY]): str, vol.Required(CONF_API_KEY, default=input_copy.get(CONF_API_KEY, "")): str,
vol.Required(CONF_MODEL, default=input_copy[CONF_MODEL]): str, vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, DEFAULT_GEMINI_MODEL if self._provider == API_PROVIDER_GEMINI else DEFAULT_MODEL)): str,
vol.Required(CONF_API_ENDPOINT, default=input_copy[CONF_API_ENDPOINT]): str, vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, DEFAULT_GEMINI_ENDPOINT if self._provider == API_PROVIDER_GEMINI else DEFAULT_OPENAI_ENDPOINT)): str,
vol.Optional(CONF_TEMPERATURE, default=input_copy.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.All( vol.Optional(CONF_TEMPERATURE, default=input_copy.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.All(
vol.Coerce(float), vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE) vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
), ),
vol.Optional(CONF_MAX_TOKENS, default=input_copy.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)): vol.All(
vol.Coerce(int),
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
),
vol.Optional(CONF_REQUEST_INTERVAL, default=input_copy.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL)
),
vol.Optional(
CONF_CONTEXT_MESSAGES,
default=input_copy.get(CONF_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES)
): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=20)
),
vol.Optional(
CONF_MAX_HISTORY_SIZE,
default=input_copy.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY)
): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=100)
),
}), }),
errors={"name": str(e)} errors={"name": str(e)}
) )
try: try:
if not await self._async_validate_api(input_copy): # Special handling for Gemini API validation
return self.async_show_form( if self._provider == API_PROVIDER_GEMINI:
step_id="provider", # For Gemini, we just check if API key is present as there's no simple endpoint to validate
data_schema=vol.Schema({}), if not input_copy.get(CONF_API_KEY):
errors=self._errors self._errors["base"] = "invalid_auth"
) _LOGGER.error("API validation error: 'api_key'")
return self.async_show_form(
step_id="provider",
data_schema=vol.Schema({
vol.Required(CONF_NAME, default=input_copy.get(CONF_NAME, "my_assistant")): str,
vol.Required(CONF_API_KEY): str,
vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, DEFAULT_GEMINI_MODEL)): str,
vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, DEFAULT_GEMINI_ENDPOINT)): str,
# Other fields remain the same
}),
errors=self._errors
)
else:
# For other providers, validate API connection
if not await self._async_validate_api(input_copy):
return self.async_show_form(
step_id="provider",
data_schema=vol.Schema({
vol.Required(CONF_NAME, default=input_copy.get(CONF_NAME, "my_assistant")): str,
vol.Required(CONF_API_KEY, default=input_copy.get(CONF_API_KEY, "")): str,
vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, DEFAULT_MODEL)): str,
vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, DEFAULT_OPENAI_ENDPOINT)): str,
vol.Optional(CONF_TEMPERATURE, default=input_copy.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
),
vol.Optional(CONF_MAX_TOKENS, default=input_copy.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)): vol.All(
vol.Coerce(int),
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
),
vol.Optional(CONF_REQUEST_INTERVAL, default=input_copy.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL)
),
vol.Optional(
CONF_CONTEXT_MESSAGES,
default=input_copy.get(CONF_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES)
): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=20)
),
vol.Optional(
CONF_MAX_HISTORY_SIZE,
default=input_copy.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY)
): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=100)
),
}),
errors=self._errors
)
except Exception as e: except Exception as e:
# Handle any unexpected exceptions during validation
_LOGGER.exception("Unexpected error during API validation")
return self.async_show_form( return self.async_show_form(
step_id="provider", step_id="provider",
data_schema=vol.Schema({}), data_schema=vol.Schema({
vol.Required(CONF_NAME, default=input_copy.get(CONF_NAME, "my_assistant")): str,
vol.Required(CONF_API_KEY, default=input_copy.get(CONF_API_KEY, "")): str,
vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, DEFAULT_GEMINI_MODEL if self._provider == API_PROVIDER_GEMINI else DEFAULT_MODEL)): str,
vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, DEFAULT_GEMINI_ENDPOINT if self._provider == API_PROVIDER_GEMINI else DEFAULT_OPENAI_ENDPOINT)): str,
# Other fields remain the same
}),
errors={"base": str(e)} errors={"base": str(e)}
) )
# All validation passed, create the entry
return await self._create_entry(input_copy) return await self._create_entry(input_copy)
def _validate_and_normalize_name(self, name: str) -> str: def _validate_and_normalize_name(self, name: str) -> str:
@@ -219,23 +352,34 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def _async_validate_api(self, user_input: Dict[str, Any]) -> bool: async def _async_validate_api(self, user_input: Dict[str, Any]) -> bool:
"""Validate API connection.""" """Validate API connection."""
try: try:
if CONF_API_KEY not in user_input:
_LOGGER.error("API validation error: 'api_key'")
self._errors["base"] = "invalid_auth"
return False
session = async_get_clientsession(self.hass) session = async_get_clientsession(self.hass)
headers = self._get_api_headers(user_input) headers = self._get_api_headers(user_input)
endpoint = user_input[CONF_API_ENDPOINT].rstrip('/') endpoint = user_input[CONF_API_ENDPOINT].rstrip('/')
check_url = ( if self._provider == API_PROVIDER_GEMINI:
f"{endpoint}/v1/models" if self._provider == API_PROVIDER_ANTHROPIC if not user_input[CONF_API_KEY]:
else f"{endpoint}/models"
)
async with session.get(check_url, headers=headers) as response:
if response.status == 401:
self._errors["base"] = "invalid_auth" self._errors["base"] = "invalid_auth"
return False return False
elif response.status not in [200, 404]:
self._errors["base"] = "cannot_connect"
return False
return True return True
else:
check_url = (
f"{endpoint}/v1/models" if self._provider == API_PROVIDER_ANTHROPIC
else f"{endpoint}/models"
)
async with session.get(check_url, headers=headers) as response:
if response.status == 401:
self._errors["base"] = "invalid_auth"
return False
elif response.status not in [200, 404]:
self._errors["base"] = "cannot_connect"
return False
return True
except Exception as err: except Exception as err:
_LOGGER.error("API validation error: %s", str(err)) _LOGGER.error("API validation error: %s", str(err))
@@ -244,6 +388,9 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def _get_api_headers(self, user_input: Dict[str, Any]) -> Dict[str, str]: def _get_api_headers(self, user_input: Dict[str, Any]) -> Dict[str, str]:
"""Get API headers based on provider.""" """Get API headers based on provider."""
if CONF_API_KEY not in user_input:
return {"Content-Type": "application/json"}
api_key = user_input[CONF_API_KEY] api_key = user_input[CONF_API_KEY]
if self._provider == API_PROVIDER_ANTHROPIC: if self._provider == API_PROVIDER_ANTHROPIC:
@@ -252,6 +399,11 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"anthropic-version": "2023-06-01", "anthropic-version": "2023-06-01",
"Content-Type": "application/json" "Content-Type": "application/json"
} }
elif self._provider == API_PROVIDER_GEMINI:
return {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
return { return {
"Authorization": f"Bearer {api_key}", "Authorization": f"Bearer {api_key}",
"Content-Type": "application/json" "Content-Type": "application/json"
+1 -1
View File
@@ -74,7 +74,7 @@ ICONS_SUBDOMAIN = "icons"
# Default values # Default values
DEFAULT_MODEL: Final = "gpt-4o-mini" DEFAULT_MODEL: Final = "gpt-4o-mini"
DEFAULT_DEEPSEEK_MODEL: Final = "deepseek-chat" DEFAULT_DEEPSEEK_MODEL: Final = "deepseek-chat"
DEFAULT_GEMINI_MODEL: Final = "gemini-pro" DEFAULT_GEMINI_MODEL: Final = "gemini-2.0-flash"
DEFAULT_TEMPERATURE: Final = 0.1 DEFAULT_TEMPERATURE: Final = 0.1
DEFAULT_MAX_TOKENS: Final = 1000 DEFAULT_MAX_TOKENS: Final = 1000
DEFAULT_REQUEST_INTERVAL: Final = 1.0 DEFAULT_REQUEST_INTERVAL: Final = 1.0
+76 -24
View File
@@ -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}")
+9 -9
View File
@@ -13,17 +13,17 @@
"loggers": ["custom_components.ha_text_ai"], "loggers": ["custom_components.ha_text_ai"],
"mqtt": [], "mqtt": [],
"quality_scale": "silver", "quality_scale": "silver",
"requirements": [ "requirements": [
"openai>=1.12.0", "openai>=1.12.0",
"anthropic>=0.8.0", "anthropic>=0.8.0",
"google-generativeai>=0.3.0", "google-genai>=1.16.0",
"aiohttp>=3.8.0", "aiohttp>=3.8.0",
"async-timeout>=4.0.0", "async-timeout>=4.0.0",
"certifi>=2024.2.2" "certifi>=2024.2.2"
], ],
"single_config_entry": false, "single_config_entry": false,
"ssdp": [], "ssdp": [],
"usb": [], "usb": [],
"version": "2.1.3", "version": "2.1.8",
"zeroconf": [] "zeroconf": []
} }
+1
View File
@@ -9,6 +9,7 @@ Sensor platform for HA Text AI.
import logging import logging
import math import math
from typing import Any, Dict from typing import Any, Dict
from datetime import datetime, timedelta
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorEntity, SensorEntity,
@@ -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 @@
} }
} }
} }
} }