mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-22 15:14:01 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
43cbac2d04 | ||
|
|
986c78dd90 | ||
|
|
0859c35aec | ||
|
|
922fefbd43 | ||
|
|
a5ac100b06 | ||
|
|
ef579af7c1 | ||
|
|
3097106e93 | ||
|
|
35073960b8 | ||
|
|
8d0e0b5e44 | ||
|
|
e91c3701c5 | ||
|
|
7f62101b3e | ||
|
|
3729c3736f | ||
|
|
f5ce5e459a | ||
|
|
4064486b1e | ||
|
|
185778dbd0 | ||
|
|
83a255dee0 | ||
|
|
6b66dd6a4d | ||
|
|
bd82f23120 | ||
|
|
eee9754033 | ||
|
|
517b1f11ae | ||
|
|
ed8f19bfa9 | ||
|
|
7e3daf611b | ||
|
|
37919be70f | ||
|
|
e427254584 | ||
|
|
76c5629fa0 | ||
|
|
7958bd010b | ||
|
|
8cd876195a | ||
|
|
376753e001 | ||
|
|
b6e73e847d | ||
|
|
440c734214 | ||
|
|
73788373cd | ||
|
|
4bfc96019b | ||
|
|
2138fc7654 | ||
|
|
95bd2ebb41 | ||
|
|
cad0fd7031 | ||
|
|
c003b258f6 | ||
|
|
65a10c77f4 | ||
|
|
e1463828c9 |
@@ -1,5 +1,6 @@
|
||||
name: Validate with hassfest
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and upload release asset
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
|
||||
- name: Create zip archive
|
||||
run: |
|
||||
cd custom_components
|
||||
zip -r ../ha_text_ai.zip ha_text_ai \
|
||||
-x "ha_text_ai/__pycache__/*" \
|
||||
-x "*.pyc" \
|
||||
-x "*.pyo" \
|
||||
-x "*/__pycache__/*" \
|
||||
-x "*.DS_Store"
|
||||
|
||||
- name: Upload release asset
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: ${{ github.event.release.tag_name }}
|
||||
files: ha_text_ai.zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1,4 +1,6 @@
|
||||
name: Validate
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
on:
|
||||
push:
|
||||
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Home Assistant
|
||||
.storage
|
||||
.cloud
|
||||
.google.token
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
*.psd
|
||||
*.zip
|
||||
*.txt
|
||||
*.pdf
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
<div align="center">
|
||||
|
||||
  [](https://creativecommons.org/licenses/by-nc-sa/4.0/) [](https://github.com/hacs/integration)
|
||||
  [](https://creativecommons.org/licenses/by-nc-sa/4.0/) [](https://github.com/hacs/integration)
|
||||
       
|
||||
|
||||
|
||||
<img src="https://github.com/smkrv/ha-text-ai/blob/15c717fcb0204bf4a0d4b4b4c6f0bb93e9f6c9a9/custom_components/ha_text_ai/icons/logo%402x.png" alt="HA Text AI" style="width: 50%; max-width: 256px; max-height: 128px; aspect-ratio: 2/1; object-fit: contain;"/>
|
||||
<img src="https://github.com/smkrv/ha-text-ai/blob/main/custom_components/ha_text_ai/icons/logo%402x.png" alt="HA Text AI" style="width: 50%; max-width: 256px; max-height: 128px; aspect-ratio: 2/1; object-fit: contain;"/>
|
||||
|
||||
### Advanced AI Integration for [Home Assistant](https://www.home-assistant.io/) with LLM multi-provider support
|
||||
</div>
|
||||
@@ -106,12 +106,13 @@ Transform your smart home experience with powerful AI assistance powered by mult
|
||||
|
||||
## 📋 Prerequisites
|
||||
|
||||
- Home Assistant 2024.11 or later
|
||||
- Home Assistant 2024.12.0 or later (recommended for best compatibility)
|
||||
- Active API key from:
|
||||
- OpenAI ([Get key](https://platform.openai.com/account/api-keys))
|
||||
- Anthropic ([Get key](https://console.anthropic.com/))
|
||||
- DeepSeek 🆕 ([Get key](https://platform.deepseek.com/api_keys))
|
||||
- DeepSeek ([Get key](https://platform.deepseek.com/api_keys))
|
||||
- OpenRouter ([Get key](https://openrouter.ai/keys))
|
||||
- Google Gemini 🆕 ([Get key](https://ai.google.dev/gemini-api/docs/api-key)) thanks to ([@Azzedde](https://github.com/Azzedde))
|
||||
- Any OpenAI-compatible API provider
|
||||
- Python 3.9 or newer
|
||||
- Stable internet connection
|
||||
@@ -119,7 +120,7 @@ Transform your smart home experience with powerful AI assistance powered by mult
|
||||
## Configuration Options
|
||||
|
||||
### 🔧 **Core Configuration Settings**
|
||||
- 🌐 **API Provider**: OpenAI/Anthropic/DeepSeek
|
||||
- 🌐 **API Provider**: OpenAI/Anthropic/DeepSeek/Gemini
|
||||
- 🔑 **API Key**: Provider-specific authentication
|
||||
- 🤖 **Model Selection**: Flexible, provider-specific models
|
||||
- 🌡️ **Temperature**: Creativity control (0.0-2.0)
|
||||
@@ -128,6 +129,25 @@ Transform your smart home experience with powerful AI assistance powered by mult
|
||||
- 💾 **History Size**: Number of messages to retain
|
||||
- 🌍 **Custom API Endpoint**: Optional advanced configuration
|
||||
|
||||
### 🤖 **Recommended Models**
|
||||
|
||||
#### OpenAI Models
|
||||
- **GPT-5** - The latest flagship model, best for complex reasoning
|
||||
- **GPT-5 mini** - A cost-effective and fast model, suitable for most tasks
|
||||
|
||||
#### Anthropic Claude Models
|
||||
- **Claude Opus 4.1** - The most capable model for handling complex tasks
|
||||
- **Claude Sonnet 4** - Offers a balance between performance and cost
|
||||
- **Claude Haiku 4** - The fastest and most economical option in the series
|
||||
|
||||
#### DeepSeek Models
|
||||
- **DeepSeek-V3.1** - A general-purpose model for a wide range of tasks
|
||||
- **DeepSeek-R1** - A specialized model focused on reasoning and coding
|
||||
|
||||
#### Google Gemini Models
|
||||
- **Gemini 2.5 Pro & 2.5 Flash** - The newest and most advanced models available
|
||||
- **Gemini 2.0 Pro & 2.0 Flash** - Previous generation models that are still powerful and efficient
|
||||
|
||||
<details>
|
||||
<summary>🌐 Potentially Compatible Providers</summary>
|
||||
|
||||
@@ -159,17 +179,23 @@ To be compatible, a provider should support:
|
||||
## ⚡ Installation
|
||||
|
||||
### HACS Installation (Recommended)
|
||||
>[!TIP]
|
||||
>HA Text AI is available in the default HACS repository. You can install it directly through HACS or click the button below to open it there.
|
||||
|
||||
<a href="https://my.home-assistant.io/redirect/hacs_repository/?owner=smkrv&repository=ha-text-ai&category=Integration"><img src="https://my.home-assistant.io/badges/hacs_repository.svg" width="170" height="auto"></a>
|
||||
1. Open HACS in Home Assistant
|
||||
2. Click on "Integrations"
|
||||
3. Click "..." in top right corner
|
||||
4. Select "Custom repositories"
|
||||
5. Add repository URL: `https://github.com/smkrv/ha-text-ai`
|
||||
6. Choose "Integration" as category
|
||||
7. Click "Download"
|
||||
8. Restart Home Assistant
|
||||
3. Search for "HA Text AI"
|
||||
4. Click "Download"
|
||||
5. Restart Home Assistant
|
||||
|
||||
Note: Also Integration has been submitted to HACS store and is currently pending review in [pull request #2896](https://github.com/hacs/default/pull/2896).
|
||||
**Alternative Method (Custom Repository):**
|
||||
If the integration is not found in the default repository:
|
||||
1. Click "..." in top right corner of HACS
|
||||
2. Select "Custom repositories"
|
||||
3. Add repository URL: `https://github.com/smkrv/ha-text-ai`
|
||||
4. Choose "Integration" as category
|
||||
5. Click "Download"
|
||||
|
||||
### Manual Installation
|
||||
1. Download the latest release
|
||||
@@ -194,7 +220,7 @@ Note: Also Integration has been submitted to HACS store and is currently pending
|
||||
ha_text_ai:
|
||||
api_provider: openai # Required
|
||||
api_key: !secret ai_api_key # Required
|
||||
model: gpt-4o-mini # Strongly recommended
|
||||
model: gpt-4o # Strongly recommended
|
||||
temperature: 0.7 # Optional
|
||||
max_tokens: 1000 # Optional
|
||||
request_interval: 1.0 # Optional
|
||||
@@ -211,7 +237,7 @@ sensor:
|
||||
- platform: ha_text_ai
|
||||
name: "My AI Assistant" # Required, unique identifier
|
||||
api_provider: openai # Optional (inherits from platform)
|
||||
model: "gpt-4o-mini" # Optional
|
||||
model: "gpt-4o" # Optional
|
||||
temperature: 0.7 # Optional
|
||||
max_tokens: 1000 # Optional
|
||||
```
|
||||
@@ -222,16 +248,16 @@ sensor:
|
||||
|
||||
| Parameter | Type | Required | Default | Description |
|
||||
|-----------|------|----------|---------|-------------|
|
||||
| `api_provider` | String | ✅ | - | AI service provider (openai, anthropic) |
|
||||
| `api_provider` | String | ✅ | - | AI service provider (openai, anthropic, deepseek, gemini) |
|
||||
| `api_key` | String | ✅ | - | Authentication key for AI service |
|
||||
| `model` | String | ⚠️ | Provider default | Strongly recommended: Specific AI model to use. If not specified, the provider's default model will be used |
|
||||
| `temperature` | Float | ❌ | 0.7 | Response creativity level (0.0-2.0) |
|
||||
| `model` | String | ⚠️ | gpt-4o-mini | Strongly recommended: Specific AI model to use. Default varies by provider |
|
||||
| `temperature` | Float | ❌ | 0.1 | Response creativity level (0.0-2.0) |
|
||||
| `max_tokens` | Integer | ❌ | 1000 | Maximum response length |
|
||||
| `request_interval` | Float | ❌ | 1.0 | Delay between API requests |
|
||||
| `api_endpoint` | URL | ⚠️ | Provider default | Custom API endpoint |
|
||||
| `system_prompt` | String | ❌ | - | Default context for AI interactions |
|
||||
| `max_history_size` | Integer | ❌ | 100 | Maximum number of conversation entries to store |
|
||||
| `history_file_size` | Integer | ⚠️ | 1 | Maximum history file size in MB |
|
||||
| `max_history_size` | Integer | ❌ | 50 | Maximum number of conversation entries to store |
|
||||
| `context_messages` | Integer | ❌ | 5 | Number of previous messages to include in context (1-20) |
|
||||
|
||||
#### Sensor Configuration
|
||||
|
||||
@@ -240,25 +266,51 @@ sensor:
|
||||
| `platform` | String | ✅ | - | Must be `ha_text_ai` |
|
||||
| `name` | String | ✅ | - | Unique sensor identifier |
|
||||
| `api_provider` | String | ❌ | Platform setting | Override global provider |
|
||||
| `model` | String | ⚠️ | Platform setting | Recommended: Override global model. If not specified, uses platform or provider default |
|
||||
| `temperature` | Float | ❌ | Platform setting | Override global temperature |
|
||||
| `max_tokens` | Integer | ❌ | Platform setting | Override global max tokens |
|
||||
| `model` | String | ⚠️ | Provider default | Recommended: Override global model (gpt-4o-mini, deepseek-chat, gemini-2.0-flash) |
|
||||
| `temperature` | Float | ❌ | 0.1 | Override global temperature |
|
||||
| `max_tokens` | Integer | ❌ | 1000 | Override global max tokens |
|
||||
|
||||
</details>
|
||||
|
||||
## 🛠️ 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
|
||||
```yaml
|
||||
service: ha_text_ai.ask_question
|
||||
data:
|
||||
question: "What's the optimal temperature for sleeping?"
|
||||
model: "claude-3-sonnet" # optional
|
||||
model: "claude-3.5-sonnet" # optional
|
||||
temperature: 0.5 # optional
|
||||
max_tokens: 500 # optional
|
||||
context_messages: 10 #optional, number of previous messages to include in context, default: 5
|
||||
system_prompt: "You are a sleep optimization expert" # optional
|
||||
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.5-sonnet"
|
||||
instance: "sensor.ha_text_ai_gpt"
|
||||
question: "What's the optimal temperature for sleeping?"
|
||||
timestamp: "2025-02-09T16:57:00.000Z"
|
||||
success: true
|
||||
# error: "Error message" (only present if success: false)
|
||||
```
|
||||
|
||||
### set_system_prompt
|
||||
@@ -285,11 +337,157 @@ data:
|
||||
```yaml
|
||||
service: ha_text_ai.get_history
|
||||
data:
|
||||
limit: 5 # optional
|
||||
filter_model: "gpt-4o" # optional
|
||||
limit: 5 # optional, number of conversations to return (1-100)
|
||||
filter_model: "gpt-4o" # optional, filter by specific AI model
|
||||
start_date: "2025-02-01" # optional, filter conversations from this date
|
||||
include_metadata: false # optional, include tokens, response time, etc.
|
||||
sort_order: "newest" # optional, sort order: "newest" or "oldest"
|
||||
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
|
||||
|
||||
#### Character Restrictions
|
||||
@@ -455,10 +653,10 @@ Conversation history stored in `.storage/ha_text_ai_history/` directory:
|
||||
## 📘 FAQ
|
||||
|
||||
**Q: Which AI providers are supported?**
|
||||
A: Currently OpenAI (GPT models) and Anthropic (Claude models) are supported, with more providers planned.
|
||||
A: OpenAI (GPT models), Anthropic (Claude models), DeepSeek, Google Gemini, and OpenRouter are officially supported, with many other OpenAI-compatible providers working as well.
|
||||
|
||||
**Q: How can I reduce API costs?**
|
||||
A: Use GPT-3.5-Turbo or Claude-3-Sonnet for most queries, implement caching, and optimize token usage.
|
||||
A: Use gpt-4o-mini or claude-3.5-haiku for most queries, implement caching, and optimize token usage.
|
||||
|
||||
**Q: Are there limitations on the number of requests?**
|
||||
A: Depends on your API provider's plan. We recommend monitoring usage and implementing request throttling via `request_interval` configuration.
|
||||
@@ -469,8 +667,11 @@ A: Yes, you can configure custom endpoints and use any compatible model by speci
|
||||
**Q: How do I switch between different AI providers?**
|
||||
A: Simply change the model parameter in your configuration or service calls to use the desired provider's model.
|
||||
|
||||
**Q: How can I reduce API costs?**
|
||||
A: Use GPT-3.5-Turbo for most queries, implement caching, and optimize token usage.
|
||||
**Q: What are the token limits for different models?**
|
||||
A: Token limits vary by provider and model. OpenAI's gpt-4o supports up to 128K tokens, Claude 3.5 Sonnet supports up to 200K tokens, while smaller models typically have 8K-32K limits. Check your provider's documentation for specific limits.
|
||||
|
||||
**Q: How do I monitor token usage?**
|
||||
A: Use the sensor attributes like `Total tokens`, `Prompt tokens`, and `Completion tokens` to track usage. You can also create automations to alert you when usage exceeds certain thresholds.
|
||||
|
||||
**Q: Is my data secure?**
|
||||
A: Yes, your data is secure. The system operates entirely on your local machine, keeping your data under your control. API keys are stored securely and all external communications use encrypted connections.
|
||||
@@ -536,9 +737,9 @@ If you want to say thanks financially, you can send a small token of appreciatio
|
||||
<div align="center"><img src="https://github.com/smkrv/ha-text-ai/blob/2aaf3405759eb2d97624834594e24ace896131df/assets/images/icons/footer_icon.png" alt="HA Text AI" style="width: 128px; height: auto;"/></div>
|
||||
<div align="center">
|
||||
|
||||
Made with ❤️ for the Home Assistant Community,
|
||||
utilizing Claude 3.5 Sonnet, Gemini Pro 1.5, and Qwen 2.5 Coder 32B Instruct.
|
||||
Made with ❤️ for the Home Assistant Community
|
||||
|
||||
[Report Bug](https://github.com/smkrv/ha-text-ai/issues) · [Request Feature](https://github.com/smkrv/ha-text-ai/issues)
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -35,21 +35,25 @@ from .const import (
|
||||
CONF_MAX_TOKENS,
|
||||
CONF_API_ENDPOINT,
|
||||
CONF_REQUEST_INTERVAL,
|
||||
CONF_API_TIMEOUT,
|
||||
CONF_API_PROVIDER,
|
||||
CONF_CONTEXT_MESSAGES,
|
||||
API_PROVIDER_OPENAI,
|
||||
API_PROVIDER_ANTHROPIC,
|
||||
API_PROVIDER_DEEPSEEK,
|
||||
API_PROVIDER_GEMINI,
|
||||
DEFAULT_MODEL,
|
||||
DEFAULT_DEEPSEEK_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_TEMPERATURE,
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_OPENAI_ENDPOINT,
|
||||
DEFAULT_ANTHROPIC_ENDPOINT,
|
||||
DEFAULT_DEEPSEEK_ENDPOINT,
|
||||
DEFAULT_GEMINI_ENDPOINT,
|
||||
DEFAULT_REQUEST_INTERVAL,
|
||||
DEFAULT_API_TIMEOUT,
|
||||
DEFAULT_CONTEXT_MESSAGES,
|
||||
API_TIMEOUT,
|
||||
SERVICE_ASK_QUESTION,
|
||||
SERVICE_CLEAR_HISTORY,
|
||||
SERVICE_GET_HISTORY,
|
||||
@@ -72,6 +76,8 @@ SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
|
||||
vol.Optional("temperature"): cv.positive_float,
|
||||
vol.Optional("max_tokens"): cv.positive_int,
|
||||
vol.Optional("context_messages"): cv.positive_int,
|
||||
vol.Optional("structured_output", default=False): cv.boolean,
|
||||
vol.Optional("json_schema"): cv.string,
|
||||
})
|
||||
|
||||
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
|
||||
@@ -83,6 +89,9 @@ SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
|
||||
vol.Required("instance"): cv.string,
|
||||
vol.Optional("limit"): cv.positive_int,
|
||||
vol.Optional("filter_model"): cv.string,
|
||||
vol.Optional("start_date"): cv.string,
|
||||
vol.Optional("include_metadata"): cv.boolean,
|
||||
vol.Optional("sort_order"): vol.In(["newest", "oldest"]),
|
||||
})
|
||||
|
||||
def get_coordinator_by_instance(hass: HomeAssistant, instance: str) -> HATextAICoordinator:
|
||||
@@ -109,21 +118,48 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
# Initialize domain data storage
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
async def async_ask_question(call: ServiceCall) -> None:
|
||||
"""Handle ask_question service."""
|
||||
async def async_ask_question(call: ServiceCall) -> dict:
|
||||
"""Handle ask_question service with response data."""
|
||||
try:
|
||||
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
||||
await coordinator.async_ask_question(
|
||||
response = await coordinator.async_ask_question(
|
||||
question=call.data["question"],
|
||||
model=call.data.get("model"),
|
||||
temperature=call.data.get("temperature"),
|
||||
max_tokens=call.data.get("max_tokens"),
|
||||
system_prompt=call.data.get("system_prompt"),
|
||||
context_messages=call.data.get("context_messages"),
|
||||
structured_output=call.data.get("structured_output", False),
|
||||
json_schema=call.data.get("json_schema"),
|
||||
)
|
||||
|
||||
# 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:
|
||||
_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:
|
||||
"""Handle clear_history service."""
|
||||
@@ -140,7 +176,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
||||
return await coordinator.async_get_history(
|
||||
limit=call.data.get("limit"),
|
||||
filter_model=call.data.get("filter_model")
|
||||
filter_model=call.data.get("filter_model"),
|
||||
start_date=call.data.get("start_date"),
|
||||
include_metadata=call.data.get("include_metadata", False),
|
||||
sort_order=call.data.get("sort_order", "newest")
|
||||
)
|
||||
except Exception as err:
|
||||
_LOGGER.error("Error getting history: %s", str(err))
|
||||
@@ -160,7 +199,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
DOMAIN,
|
||||
SERVICE_ASK_QUESTION,
|
||||
async_ask_question,
|
||||
schema=SERVICE_SCHEMA_ASK_QUESTION
|
||||
schema=SERVICE_SCHEMA_ASK_QUESTION,
|
||||
supports_response=True
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
@@ -233,22 +273,30 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
|
||||
return True
|
||||
|
||||
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, api_timeout: int = DEFAULT_API_TIMEOUT) -> bool:
|
||||
"""Check API availability for different providers."""
|
||||
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"
|
||||
elif provider == API_PROVIDER_DEEPSEEK:
|
||||
check_url = f"{endpoint}/models" # DeepSeek
|
||||
check_url = f"{endpoint}/models"
|
||||
else: # OpenAI
|
||||
check_url = f"{endpoint}/models"
|
||||
|
||||
async with timeout(API_TIMEOUT):
|
||||
async with timeout(api_timeout):
|
||||
async with session.get(check_url, headers=headers) as response:
|
||||
if response.status in [200, 404]:
|
||||
return True
|
||||
elif response.status == 401:
|
||||
raise ConfigEntryNotReady("Invalid API key")
|
||||
_LOGGER.error("Invalid API key")
|
||||
return False
|
||||
elif response.status == 429:
|
||||
_LOGGER.warning("Rate limit exceeded during API check")
|
||||
return False
|
||||
@@ -264,26 +312,42 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
_LOGGER.debug(f"Setting up HA Text AI entry: {entry.data}")
|
||||
|
||||
try:
|
||||
if CONF_API_PROVIDER not in entry.data:
|
||||
# Get provider from data or options (options takes precedence)
|
||||
config = {**entry.data, **entry.options}
|
||||
api_provider = config.get(CONF_API_PROVIDER)
|
||||
|
||||
if not api_provider:
|
||||
_LOGGER.error("API provider not specified")
|
||||
raise ConfigEntryNotReady("API provider is required")
|
||||
|
||||
# Get configuration
|
||||
session = aiohttp_client.async_get_clientsession(hass)
|
||||
api_provider = entry.data.get(CONF_API_PROVIDER)
|
||||
model = entry.data.get(CONF_MODEL, DEFAULT_MODEL)
|
||||
endpoint = entry.data.get(
|
||||
CONF_API_ENDPOINT,
|
||||
DEFAULT_OPENAI_ENDPOINT if api_provider == API_PROVIDER_OPENAI
|
||||
else DEFAULT_ANTHROPIC_ENDPOINT
|
||||
).rstrip('/')
|
||||
api_key = entry.data[CONF_API_KEY]
|
||||
|
||||
# Get default endpoint based on provider
|
||||
default_endpoint = {
|
||||
API_PROVIDER_OPENAI: DEFAULT_OPENAI_ENDPOINT,
|
||||
API_PROVIDER_ANTHROPIC: DEFAULT_ANTHROPIC_ENDPOINT,
|
||||
API_PROVIDER_DEEPSEEK: DEFAULT_DEEPSEEK_ENDPOINT,
|
||||
API_PROVIDER_GEMINI: DEFAULT_GEMINI_ENDPOINT,
|
||||
}.get(api_provider, DEFAULT_OPENAI_ENDPOINT)
|
||||
|
||||
# Get default model based on provider
|
||||
default_model = (
|
||||
DEFAULT_DEEPSEEK_MODEL if api_provider == API_PROVIDER_DEEPSEEK else
|
||||
DEFAULT_GEMINI_MODEL if api_provider == API_PROVIDER_GEMINI else
|
||||
DEFAULT_MODEL
|
||||
)
|
||||
|
||||
model = config.get(CONF_MODEL, default_model)
|
||||
endpoint = config.get(CONF_API_ENDPOINT, default_endpoint).rstrip('/')
|
||||
# API key can now be updated via options
|
||||
api_key = config.get(CONF_API_KEY, entry.data.get(CONF_API_KEY))
|
||||
instance_name = entry.data.get(CONF_NAME, entry.entry_id)
|
||||
request_interval = entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)
|
||||
max_tokens = entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)
|
||||
temperature = entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)
|
||||
max_history_size = entry.data.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY)
|
||||
context_messages = entry.data.get(CONF_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES)
|
||||
request_interval = config.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)
|
||||
api_timeout = config.get(CONF_API_TIMEOUT, DEFAULT_API_TIMEOUT)
|
||||
max_tokens = config.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)
|
||||
temperature = config.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)
|
||||
max_history_size = config.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY)
|
||||
context_messages = config.get(CONF_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES)
|
||||
is_anthropic = api_provider == API_PROVIDER_ANTHROPIC
|
||||
|
||||
headers = {
|
||||
@@ -297,7 +361,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
else:
|
||||
headers["Authorization"] = f"Bearer {api_key}"
|
||||
|
||||
if not await async_check_api(session, endpoint, headers, api_provider):
|
||||
if not await async_check_api(session, endpoint, headers, api_provider, api_timeout):
|
||||
raise ConfigEntryNotReady("API connection failed")
|
||||
|
||||
_LOGGER.debug("Creating API client for %s with endpoint %s", api_provider, endpoint)
|
||||
@@ -308,6 +372,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
headers=headers,
|
||||
api_provider=api_provider,
|
||||
model=model,
|
||||
api_timeout=api_timeout,
|
||||
)
|
||||
|
||||
coordinator = HATextAICoordinator(
|
||||
@@ -321,6 +386,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
max_history_size=max_history_size,
|
||||
context_messages=context_messages,
|
||||
is_anthropic=is_anthropic,
|
||||
api_timeout=api_timeout,
|
||||
)
|
||||
|
||||
_LOGGER.debug(f"Created coordinator for {instance_name}")
|
||||
@@ -334,6 +400,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
# Set up platforms
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
# Register update listener for options changes
|
||||
entry.async_on_unload(entry.add_update_listener(async_update_options))
|
||||
|
||||
_LOGGER.debug(f"Setup completed for {instance_name}")
|
||||
|
||||
return True
|
||||
@@ -342,6 +411,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
_LOGGER.exception(f"Error setting up HA Text AI: {err}")
|
||||
raise
|
||||
|
||||
async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Handle options update - reload the config entry."""
|
||||
_LOGGER.info("Options updated for %s, reloading integration", entry.title)
|
||||
await hass.config_entries.async_reload(entry.entry_id)
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
try:
|
||||
|
||||
@@ -11,15 +11,17 @@ import asyncio
|
||||
from typing import Any, Dict, List, Optional
|
||||
from aiohttp import ClientSession, ClientTimeout
|
||||
from async_timeout import timeout
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from .const import (
|
||||
API_TIMEOUT,
|
||||
DEFAULT_API_TIMEOUT,
|
||||
API_RETRY_COUNT,
|
||||
API_PROVIDER_ANTHROPIC,
|
||||
API_PROVIDER_DEEPSEEK,
|
||||
API_PROVIDER_OPENAI,
|
||||
API_PROVIDER_GEMINI,
|
||||
MIN_TEMPERATURE,
|
||||
MAX_TEMPERATURE,
|
||||
MIN_MAX_TOKENS,
|
||||
@@ -39,6 +41,7 @@ class APIClient:
|
||||
headers: Dict[str, str],
|
||||
api_provider: str,
|
||||
model: str,
|
||||
api_timeout: int = DEFAULT_API_TIMEOUT,
|
||||
) -> None:
|
||||
"""Initialize API client."""
|
||||
self.session = session
|
||||
@@ -46,21 +49,38 @@ class APIClient:
|
||||
self.headers = headers
|
||||
self.api_provider = api_provider
|
||||
self.model = model
|
||||
self.timeout = ClientTimeout(total=API_TIMEOUT)
|
||||
self.api_timeout = 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(
|
||||
self,
|
||||
temperature: float,
|
||||
max_tokens: int,
|
||||
) -> 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:
|
||||
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:
|
||||
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(
|
||||
@@ -69,10 +89,13 @@ class APIClient:
|
||||
payload: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""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):
|
||||
try:
|
||||
async with timeout(API_TIMEOUT):
|
||||
async with timeout(self.api_timeout):
|
||||
async with self.session.post(
|
||||
url,
|
||||
json=payload,
|
||||
@@ -82,16 +105,18 @@ class APIClient:
|
||||
_LOGGER.debug(f"Response status: {response.status}")
|
||||
if response.status != 200:
|
||||
error_data = await response.json()
|
||||
_LOGGER.error(f"API error: {error_data}")
|
||||
raise HomeAssistantError(f"API error: {error_data}")
|
||||
# Log error without sensitive 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()
|
||||
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:
|
||||
raise HomeAssistantError("API request timed out")
|
||||
await asyncio.sleep(1 * (attempt + 1))
|
||||
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:
|
||||
raise
|
||||
await asyncio.sleep(1 * (attempt + 1))
|
||||
@@ -102,6 +127,8 @@ class APIClient:
|
||||
messages: List[Dict[str, str]],
|
||||
temperature: float,
|
||||
max_tokens: int,
|
||||
structured_output: bool = False,
|
||||
json_schema: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create completion using appropriate API."""
|
||||
try:
|
||||
@@ -109,15 +136,23 @@ class APIClient:
|
||||
|
||||
if self.api_provider == API_PROVIDER_ANTHROPIC:
|
||||
return await self._create_anthropic_completion(
|
||||
model, messages, temperature, max_tokens
|
||||
model, messages, temperature, max_tokens,
|
||||
structured_output, json_schema
|
||||
)
|
||||
elif self.api_provider == API_PROVIDER_DEEPSEEK:
|
||||
return await self._create_deepseek_completion(
|
||||
model, messages, temperature, max_tokens
|
||||
model, messages, temperature, max_tokens,
|
||||
structured_output, json_schema
|
||||
)
|
||||
elif self.api_provider == API_PROVIDER_GEMINI:
|
||||
return await self._create_gemini_completion(
|
||||
model, messages, temperature, max_tokens,
|
||||
structured_output, json_schema
|
||||
)
|
||||
else:
|
||||
return await self._create_openai_completion(
|
||||
model, messages, temperature, max_tokens
|
||||
model, messages, temperature, max_tokens,
|
||||
structured_output, json_schema
|
||||
)
|
||||
except Exception as e:
|
||||
_LOGGER.error("API request failed: %s", str(e))
|
||||
@@ -129,6 +164,8 @@ class APIClient:
|
||||
messages: List[Dict[str, str]],
|
||||
temperature: float,
|
||||
max_tokens: int,
|
||||
structured_output: bool = False,
|
||||
json_schema: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create completion using DeepSeek API."""
|
||||
url = f"{self.endpoint}/chat/completions"
|
||||
@@ -140,6 +177,24 @@ class APIClient:
|
||||
"stream": False
|
||||
}
|
||||
|
||||
# Add structured output format if enabled (DeepSeek is OpenAI-compatible)
|
||||
if structured_output and json_schema:
|
||||
try:
|
||||
import json
|
||||
schema = json.loads(json_schema)
|
||||
payload["response_format"] = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "structured_response",
|
||||
"strict": True,
|
||||
"schema": schema
|
||||
}
|
||||
}
|
||||
_LOGGER.debug("DeepSeek structured output enabled with schema")
|
||||
except json.JSONDecodeError as e:
|
||||
_LOGGER.warning(f"Invalid JSON schema provided: {e}. Falling back to json_object mode.")
|
||||
payload["response_format"] = {"type": "json_object"}
|
||||
|
||||
data = await self._make_request(url, payload)
|
||||
return {
|
||||
"choices": [
|
||||
@@ -160,6 +215,8 @@ class APIClient:
|
||||
messages: List[Dict[str, str]],
|
||||
temperature: float,
|
||||
max_tokens: int,
|
||||
structured_output: bool = False,
|
||||
json_schema: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create completion using OpenAI API."""
|
||||
url = f"{self.endpoint}/chat/completions"
|
||||
@@ -170,6 +227,24 @@ class APIClient:
|
||||
"max_tokens": max_tokens,
|
||||
}
|
||||
|
||||
# Add structured output format if enabled
|
||||
if structured_output and json_schema:
|
||||
try:
|
||||
import json
|
||||
schema = json.loads(json_schema)
|
||||
payload["response_format"] = {
|
||||
"type": "json_schema",
|
||||
"json_schema": {
|
||||
"name": "structured_response",
|
||||
"strict": True,
|
||||
"schema": schema
|
||||
}
|
||||
}
|
||||
_LOGGER.debug("OpenAI structured output enabled with schema")
|
||||
except json.JSONDecodeError as e:
|
||||
_LOGGER.warning(f"Invalid JSON schema provided: {e}. Falling back to json_object mode.")
|
||||
payload["response_format"] = {"type": "json_object"}
|
||||
|
||||
data = await self._make_request(url, payload)
|
||||
return {
|
||||
"choices": [
|
||||
@@ -190,6 +265,8 @@ class APIClient:
|
||||
messages: List[Dict[str, str]],
|
||||
temperature: float,
|
||||
max_tokens: int,
|
||||
structured_output: bool = False,
|
||||
json_schema: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create completion using Anthropic API."""
|
||||
url = f"{self.endpoint}/v1/messages"
|
||||
@@ -205,6 +282,20 @@ class APIClient:
|
||||
else:
|
||||
filtered_messages.append(msg)
|
||||
|
||||
# For Anthropic, add structured output instruction to system prompt
|
||||
if structured_output and json_schema:
|
||||
schema_instruction = (
|
||||
f"\n\nIMPORTANT: You MUST respond ONLY with valid JSON that matches "
|
||||
f"this JSON Schema:\n{json_schema}\n"
|
||||
f"Do not include any text before or after the JSON. "
|
||||
f"Do not wrap the JSON in markdown code blocks."
|
||||
)
|
||||
if system_prompt:
|
||||
system_prompt += schema_instruction
|
||||
else:
|
||||
system_prompt = schema_instruction.strip()
|
||||
_LOGGER.debug("Anthropic structured output enabled via system prompt")
|
||||
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": filtered_messages,
|
||||
@@ -238,7 +329,164 @@ class APIClient:
|
||||
_LOGGER.error(f"Connection check failed: {str(e)}")
|
||||
return False
|
||||
|
||||
async def _create_gemini_completion(
|
||||
self,
|
||||
model: str,
|
||||
messages: List[Dict[str, str]],
|
||||
temperature: float,
|
||||
max_tokens: int,
|
||||
structured_output: bool = False,
|
||||
json_schema: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Create completion using Gemini API with google-genai library.
|
||||
|
||||
Args:
|
||||
model: The model name to use
|
||||
messages: List of message dictionaries with role and content
|
||||
temperature: Sampling temperature between 0.0 and 2.0
|
||||
max_tokens: Maximum number of tokens to generate
|
||||
structured_output: Enable JSON structured output mode
|
||||
json_schema: JSON Schema for structured output validation
|
||||
|
||||
Returns:
|
||||
Dictionary with response content and token usage
|
||||
"""
|
||||
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']}]
|
||||
})
|
||||
|
||||
# Parse JSON schema if structured output is enabled
|
||||
parsed_schema = None
|
||||
if structured_output and json_schema:
|
||||
try:
|
||||
import json
|
||||
parsed_schema = json.loads(json_schema)
|
||||
_LOGGER.debug("Gemini structured output enabled with schema")
|
||||
except json.JSONDecodeError as e:
|
||||
_LOGGER.warning(f"Invalid JSON schema provided: {e}. Structured output disabled.")
|
||||
|
||||
# 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()
|
||||
|
||||
# Add structured output configuration for Gemini
|
||||
if structured_output and parsed_schema:
|
||||
config.response_mime_type = "application/json"
|
||||
config.response_schema = parsed_schema
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
except ImportError as e:
|
||||
_LOGGER.error(f"Google Gemini library not installed: {str(e)}")
|
||||
raise HomeAssistantError(f"Missing dependency: {str(e)}. Please install google-genai.")
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Gemini API error: {str(e)}")
|
||||
raise HomeAssistantError(f"Gemini API error: {str(e)}")
|
||||
|
||||
async def shutdown(self) -> None:
|
||||
"""Shutdown API client."""
|
||||
_LOGGER.debug("Shutting down API client")
|
||||
await self.session.close()
|
||||
self._closed = True
|
||||
# Do NOT close the shared Home Assistant session
|
||||
|
||||
@@ -8,6 +8,7 @@ Config flow for HA text AI integration.
|
||||
"""
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
from homeassistant import config_entries
|
||||
@@ -24,26 +25,33 @@ from .const import (
|
||||
CONF_MAX_TOKENS,
|
||||
CONF_API_ENDPOINT,
|
||||
CONF_REQUEST_INTERVAL,
|
||||
CONF_API_TIMEOUT,
|
||||
CONF_API_PROVIDER,
|
||||
CONF_CONTEXT_MESSAGES,
|
||||
API_PROVIDER_OPENAI,
|
||||
API_PROVIDER_ANTHROPIC,
|
||||
API_PROVIDER_DEEPSEEK,
|
||||
API_PROVIDER_GEMINI,
|
||||
API_PROVIDERS,
|
||||
DEFAULT_MODEL,
|
||||
DEFAULT_DEEPSEEK_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_TEMPERATURE,
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_REQUEST_INTERVAL,
|
||||
DEFAULT_API_TIMEOUT,
|
||||
DEFAULT_OPENAI_ENDPOINT,
|
||||
DEFAULT_ANTHROPIC_ENDPOINT,
|
||||
DEFAULT_DEEPSEEK_ENDPOINT,
|
||||
DEFAULT_GEMINI_ENDPOINT,
|
||||
DEFAULT_CONTEXT_MESSAGES,
|
||||
MIN_TEMPERATURE,
|
||||
MAX_TEMPERATURE,
|
||||
MIN_MAX_TOKENS,
|
||||
MAX_MAX_TOKENS,
|
||||
MIN_REQUEST_INTERVAL,
|
||||
MIN_API_TIMEOUT,
|
||||
MAX_API_TIMEOUT,
|
||||
DEFAULT_NAME_PREFIX,
|
||||
DEFAULT_MAX_HISTORY,
|
||||
CONF_MAX_HISTORY_SIZE,
|
||||
@@ -93,15 +101,20 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
self._errors = {}
|
||||
|
||||
if user_input is None:
|
||||
# Выбор endpoint по провайдеру
|
||||
# Selecting an endpoint by provider
|
||||
default_endpoint = {
|
||||
API_PROVIDER_OPENAI: DEFAULT_OPENAI_ENDPOINT,
|
||||
API_PROVIDER_ANTHROPIC: DEFAULT_ANTHROPIC_ENDPOINT,
|
||||
API_PROVIDER_DEEPSEEK: DEFAULT_DEEPSEEK_ENDPOINT,
|
||||
API_PROVIDER_GEMINI: DEFAULT_GEMINI_ENDPOINT,
|
||||
}.get(self._provider, DEFAULT_OPENAI_ENDPOINT)
|
||||
|
||||
# Выбор модели по умолчанию по провайдеру
|
||||
default_model = DEFAULT_DEEPSEEK_MODEL if self._provider == API_PROVIDER_DEEPSEEK else DEFAULT_MODEL
|
||||
# Selecting the default model by provider
|
||||
default_model = (
|
||||
DEFAULT_DEEPSEEK_MODEL if self._provider == API_PROVIDER_DEEPSEEK else
|
||||
DEFAULT_GEMINI_MODEL if self._provider == API_PROVIDER_GEMINI else
|
||||
DEFAULT_MODEL
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="provider",
|
||||
@@ -122,6 +135,10 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=MIN_REQUEST_INTERVAL)
|
||||
),
|
||||
vol.Optional(CONF_API_TIMEOUT, default=DEFAULT_API_TIMEOUT): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=MIN_API_TIMEOUT, max=MAX_API_TIMEOUT)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_CONTEXT_MESSAGES,
|
||||
default=DEFAULT_CONTEXT_MESSAGES
|
||||
@@ -139,41 +156,185 @@ 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()
|
||||
|
||||
# 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_API_TIMEOUT, default=input_copy.get(CONF_API_TIMEOUT, DEFAULT_API_TIMEOUT)): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=MIN_API_TIMEOUT, max=MAX_API_TIMEOUT)
|
||||
),
|
||||
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:
|
||||
# Validate and normalize the name
|
||||
normalized_name = self._validate_and_normalize_name(input_copy[CONF_NAME])
|
||||
input_copy[CONF_NAME] = normalized_name
|
||||
except ValueError as e:
|
||||
return self.async_show_form(
|
||||
step_id="provider",
|
||||
data_schema=vol.Schema({
|
||||
vol.Required(CONF_NAME, default=input_copy[CONF_NAME]): str,
|
||||
vol.Required(CONF_API_KEY, default=input_copy[CONF_API_KEY]): str,
|
||||
vol.Required(CONF_MODEL, default=input_copy[CONF_MODEL]): str,
|
||||
vol.Required(CONF_API_ENDPOINT, default=input_copy[CONF_API_ENDPOINT]): str,
|
||||
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,
|
||||
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_API_TIMEOUT, default=input_copy.get(CONF_API_TIMEOUT, DEFAULT_API_TIMEOUT)): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=MIN_API_TIMEOUT, max=MAX_API_TIMEOUT)
|
||||
),
|
||||
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)}
|
||||
)
|
||||
|
||||
try:
|
||||
if not await self._async_validate_api(input_copy):
|
||||
return self.async_show_form(
|
||||
step_id="provider",
|
||||
data_schema=vol.Schema({}),
|
||||
errors=self._errors
|
||||
)
|
||||
# Special handling for Gemini API validation
|
||||
if self._provider == API_PROVIDER_GEMINI:
|
||||
# For Gemini, we just check if API key is present as there's no simple endpoint to validate
|
||||
if not input_copy.get(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)): 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_API_TIMEOUT, default=input_copy.get(CONF_API_TIMEOUT, DEFAULT_API_TIMEOUT)): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=MIN_API_TIMEOUT, max=MAX_API_TIMEOUT)
|
||||
),
|
||||
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:
|
||||
# Handle any unexpected exceptions during validation
|
||||
_LOGGER.exception("Unexpected error during API validation")
|
||||
return self.async_show_form(
|
||||
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)}
|
||||
)
|
||||
|
||||
# All validation passed, create the entry
|
||||
return await self._create_entry(input_copy)
|
||||
|
||||
def _validate_and_normalize_name(self, name: str) -> str:
|
||||
@@ -211,23 +372,34 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
async def _async_validate_api(self, user_input: Dict[str, Any]) -> bool:
|
||||
"""Validate API connection."""
|
||||
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)
|
||||
headers = self._get_api_headers(user_input)
|
||||
endpoint = user_input[CONF_API_ENDPOINT].rstrip('/')
|
||||
|
||||
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:
|
||||
if self._provider == API_PROVIDER_GEMINI:
|
||||
if not user_input[CONF_API_KEY]:
|
||||
self._errors["base"] = "invalid_auth"
|
||||
return False
|
||||
elif response.status not in [200, 404]:
|
||||
self._errors["base"] = "cannot_connect"
|
||||
return False
|
||||
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:
|
||||
_LOGGER.error("API validation error: %s", str(err))
|
||||
@@ -236,6 +408,9 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
def _get_api_headers(self, user_input: Dict[str, Any]) -> Dict[str, str]:
|
||||
"""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]
|
||||
|
||||
if self._provider == API_PROVIDER_ANTHROPIC:
|
||||
@@ -244,6 +419,11 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"anthropic-version": "2023-06-01",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
elif self._provider == API_PROVIDER_GEMINI:
|
||||
return {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
return {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
@@ -256,7 +436,11 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
unique_id = f"{DOMAIN}_{normalized_name}_{self._provider}".lower()
|
||||
|
||||
default_model = DEFAULT_DEEPSEEK_MODEL if self._provider == API_PROVIDER_DEEPSEEK else DEFAULT_MODEL
|
||||
default_model = (
|
||||
DEFAULT_DEEPSEEK_MODEL if self._provider == API_PROVIDER_DEEPSEEK else
|
||||
DEFAULT_GEMINI_MODEL if self._provider == API_PROVIDER_GEMINI else
|
||||
DEFAULT_MODEL
|
||||
)
|
||||
|
||||
entry_data = {
|
||||
CONF_API_PROVIDER: self._provider,
|
||||
@@ -269,6 +453,7 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
CONF_TEMPERATURE: user_input.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
|
||||
CONF_MAX_TOKENS: user_input.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
|
||||
CONF_REQUEST_INTERVAL: user_input.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL),
|
||||
CONF_API_TIMEOUT: user_input.get(CONF_API_TIMEOUT, DEFAULT_API_TIMEOUT),
|
||||
CONF_CONTEXT_MESSAGES: user_input.get(CONF_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES),
|
||||
CONF_MAX_HISTORY_SIZE: user_input.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY),
|
||||
}
|
||||
@@ -288,70 +473,229 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: config_entries.ConfigEntry) -> config_entries.OptionsFlow:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
return OptionsFlowHandler()
|
||||
|
||||
|
||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle options flow."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
def __init__(self) -> None:
|
||||
"""Initialize options flow."""
|
||||
self.config_entry = config_entry
|
||||
self._errors = {}
|
||||
self._selected_provider = None
|
||||
|
||||
def _get_default_endpoint(self, provider: str) -> str:
|
||||
"""Get default endpoint for provider."""
|
||||
return {
|
||||
API_PROVIDER_OPENAI: DEFAULT_OPENAI_ENDPOINT,
|
||||
API_PROVIDER_ANTHROPIC: DEFAULT_ANTHROPIC_ENDPOINT,
|
||||
API_PROVIDER_DEEPSEEK: DEFAULT_DEEPSEEK_ENDPOINT,
|
||||
API_PROVIDER_GEMINI: DEFAULT_GEMINI_ENDPOINT,
|
||||
}.get(provider, DEFAULT_OPENAI_ENDPOINT)
|
||||
|
||||
def _get_default_model(self, provider: str) -> str:
|
||||
"""Get default model for provider."""
|
||||
return (
|
||||
DEFAULT_DEEPSEEK_MODEL if provider == API_PROVIDER_DEEPSEEK else
|
||||
DEFAULT_GEMINI_MODEL if provider == API_PROVIDER_GEMINI else
|
||||
DEFAULT_MODEL
|
||||
)
|
||||
|
||||
def _get_api_headers(self, api_key: str, provider: str) -> Dict[str, str]:
|
||||
"""Get API headers based on provider."""
|
||||
if provider == API_PROVIDER_ANTHROPIC:
|
||||
return {
|
||||
"x-api-key": api_key,
|
||||
"anthropic-version": "2023-06-01",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
return {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
async def _async_validate_api(self, provider: str, api_key: str, endpoint: str) -> bool:
|
||||
"""Validate API connection."""
|
||||
try:
|
||||
if not api_key:
|
||||
self._errors["base"] = "invalid_auth"
|
||||
return False
|
||||
|
||||
# For Gemini, just check if API key is present
|
||||
if provider == API_PROVIDER_GEMINI:
|
||||
return True
|
||||
|
||||
session = async_get_clientsession(self.hass)
|
||||
headers = self._get_api_headers(api_key, provider)
|
||||
endpoint = endpoint.rstrip('/')
|
||||
|
||||
check_url = (
|
||||
f"{endpoint}/v1/models" if 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:
|
||||
_LOGGER.error("API validation error: %s", str(err))
|
||||
self._errors["base"] = "cannot_connect"
|
||||
return False
|
||||
|
||||
async def async_step_init(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
|
||||
"""Manage the options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
"""Handle provider selection step."""
|
||||
current_data = {**self.config_entry.data, **self.config_entry.options}
|
||||
current_provider = current_data.get(CONF_API_PROVIDER, API_PROVIDER_OPENAI)
|
||||
|
||||
if user_input is not None:
|
||||
self._selected_provider = user_input.get(CONF_API_PROVIDER, current_provider)
|
||||
return await self.async_step_settings()
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema({
|
||||
vol.Optional(
|
||||
CONF_MODEL,
|
||||
default=current_data.get(CONF_MODEL, default_model)
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_TEMPERATURE,
|
||||
default=current_data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)
|
||||
): vol.All(
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_MAX_TOKENS,
|
||||
default=current_data.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=current_data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)
|
||||
): vol.All(
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=MIN_REQUEST_INTERVAL)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_CONTEXT_MESSAGES,
|
||||
default=current_data.get(
|
||||
CONF_CONTEXT_MESSAGES,
|
||||
DEFAULT_CONTEXT_MESSAGES
|
||||
vol.Required(
|
||||
CONF_API_PROVIDER,
|
||||
default=current_provider
|
||||
): selector.SelectSelector(
|
||||
selector.SelectSelectorConfig(
|
||||
options=API_PROVIDERS,
|
||||
translation_key="api_provider"
|
||||
)
|
||||
): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=1, max=20)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_MAX_HISTORY_SIZE,
|
||||
default=current_data.get(
|
||||
CONF_MAX_HISTORY_SIZE,
|
||||
DEFAULT_MAX_HISTORY
|
||||
)
|
||||
): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=1, max=100)
|
||||
),
|
||||
})
|
||||
}),
|
||||
description_placeholders={
|
||||
"current_provider": current_provider
|
||||
}
|
||||
)
|
||||
|
||||
async def async_step_settings(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
|
||||
"""Handle settings configuration step."""
|
||||
self._errors = {}
|
||||
current_data = {**self.config_entry.data, **self.config_entry.options}
|
||||
provider = self._selected_provider or current_data.get(CONF_API_PROVIDER, API_PROVIDER_OPENAI)
|
||||
|
||||
# Determine if provider changed to show appropriate defaults
|
||||
provider_changed = provider != current_data.get(CONF_API_PROVIDER)
|
||||
|
||||
# Use new defaults if provider changed, otherwise use current values
|
||||
if provider_changed:
|
||||
default_endpoint = self._get_default_endpoint(provider)
|
||||
default_model = self._get_default_model(provider)
|
||||
else:
|
||||
default_endpoint = current_data.get(CONF_API_ENDPOINT, self._get_default_endpoint(provider))
|
||||
default_model = current_data.get(CONF_MODEL, self._get_default_model(provider))
|
||||
|
||||
if user_input is not None:
|
||||
# Validate API connection
|
||||
api_key = user_input.get(CONF_API_KEY, current_data.get(CONF_API_KEY, ""))
|
||||
endpoint = user_input.get(CONF_API_ENDPOINT, default_endpoint)
|
||||
|
||||
if await self._async_validate_api(provider, api_key, endpoint):
|
||||
# Merge with provider selection
|
||||
final_data = {
|
||||
CONF_API_PROVIDER: provider,
|
||||
**user_input
|
||||
}
|
||||
return self.async_create_entry(title="", data=final_data)
|
||||
|
||||
# Show form again with errors
|
||||
return self.async_show_form(
|
||||
step_id="settings",
|
||||
data_schema=self._get_settings_schema(
|
||||
provider=provider,
|
||||
current_data=current_data,
|
||||
user_input=user_input,
|
||||
default_endpoint=default_endpoint,
|
||||
default_model=default_model,
|
||||
),
|
||||
errors=self._errors
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="settings",
|
||||
data_schema=self._get_settings_schema(
|
||||
provider=provider,
|
||||
current_data=current_data,
|
||||
user_input=None,
|
||||
default_endpoint=default_endpoint,
|
||||
default_model=default_model,
|
||||
),
|
||||
description_placeholders={
|
||||
"provider": provider
|
||||
}
|
||||
)
|
||||
|
||||
def _get_settings_schema(
|
||||
self,
|
||||
provider: str,
|
||||
current_data: Dict[str, Any],
|
||||
user_input: Optional[Dict[str, Any]],
|
||||
default_endpoint: str,
|
||||
default_model: str,
|
||||
) -> vol.Schema:
|
||||
"""Build settings schema."""
|
||||
data = user_input or current_data
|
||||
|
||||
return vol.Schema({
|
||||
vol.Required(
|
||||
CONF_API_KEY,
|
||||
default=data.get(CONF_API_KEY, "")
|
||||
): str,
|
||||
vol.Required(
|
||||
CONF_API_ENDPOINT,
|
||||
default=data.get(CONF_API_ENDPOINT, default_endpoint)
|
||||
): str,
|
||||
vol.Required(
|
||||
CONF_MODEL,
|
||||
default=data.get(CONF_MODEL, default_model)
|
||||
): str,
|
||||
vol.Optional(
|
||||
CONF_TEMPERATURE,
|
||||
default=data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)
|
||||
): vol.All(
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_MAX_TOKENS,
|
||||
default=data.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=data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)
|
||||
): vol.All(
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=MIN_REQUEST_INTERVAL)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_API_TIMEOUT,
|
||||
default=data.get(CONF_API_TIMEOUT, DEFAULT_API_TIMEOUT)
|
||||
): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=MIN_API_TIMEOUT, max=MAX_API_TIMEOUT)
|
||||
),
|
||||
vol.Optional(
|
||||
CONF_CONTEXT_MESSAGES,
|
||||
default=data.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=data.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY)
|
||||
): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=1, max=100)
|
||||
),
|
||||
})
|
||||
|
||||
@@ -12,6 +12,8 @@ from typing import Final
|
||||
import voluptuous as vol
|
||||
from homeassistant.const import Platform, CONF_API_KEY, CONF_NAME
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
import logging
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Domain and platforms
|
||||
DOMAIN: Final = "ha_text_ai"
|
||||
@@ -22,11 +24,13 @@ CONF_API_PROVIDER: Final = "api_provider"
|
||||
API_PROVIDER_OPENAI: Final = "openai"
|
||||
API_PROVIDER_ANTHROPIC: Final = "anthropic"
|
||||
API_PROVIDER_DEEPSEEK: Final = "deepseek"
|
||||
API_PROVIDER_GEMINI: Final = "gemini"
|
||||
|
||||
API_PROVIDERS: Final = [
|
||||
API_PROVIDER_OPENAI,
|
||||
API_PROVIDER_ANTHROPIC,
|
||||
API_PROVIDER_DEEPSEEK
|
||||
API_PROVIDER_DEEPSEEK,
|
||||
API_PROVIDER_GEMINI
|
||||
]
|
||||
|
||||
# Read version from manifest.json
|
||||
@@ -49,6 +53,7 @@ except Exception as err:
|
||||
DEFAULT_OPENAI_ENDPOINT: Final = "https://api.openai.com/v1"
|
||||
DEFAULT_ANTHROPIC_ENDPOINT: Final = "https://api.anthropic.com"
|
||||
DEFAULT_DEEPSEEK_ENDPOINT: Final = "https://api.deepseek.com"
|
||||
DEFAULT_GEMINI_ENDPOINT: Final = "https://generativelanguage.googleapis.com/v1beta"
|
||||
|
||||
# Configuration constants
|
||||
CONF_MODEL: Final = "model"
|
||||
@@ -56,10 +61,13 @@ CONF_TEMPERATURE: Final = "temperature"
|
||||
CONF_MAX_TOKENS: Final = "max_tokens"
|
||||
CONF_API_ENDPOINT: Final = "api_endpoint"
|
||||
CONF_REQUEST_INTERVAL: Final = "request_interval"
|
||||
CONF_API_TIMEOUT: Final = "api_timeout"
|
||||
CONF_INSTANCE: Final = "instance"
|
||||
CONF_MAX_HISTORY_SIZE: Final = "max_history_size" # Correct constant name
|
||||
CONF_IS_ANTHROPIC: Final = "is_anthropic"
|
||||
CONF_CONTEXT_MESSAGES: Final = "context_messages"
|
||||
CONF_STRUCTURED_OUTPUT: Final = "structured_output"
|
||||
CONF_JSON_SCHEMA: Final = "json_schema"
|
||||
|
||||
ABSOLUTE_MAX_HISTORY_SIZE = 500
|
||||
MAX_ATTRIBUTE_SIZE = 4 * 1024
|
||||
@@ -69,10 +77,12 @@ ICONS_SUBDOMAIN = "icons"
|
||||
# Default values
|
||||
DEFAULT_MODEL: Final = "gpt-4o-mini"
|
||||
DEFAULT_DEEPSEEK_MODEL: Final = "deepseek-chat"
|
||||
DEFAULT_GEMINI_MODEL: Final = "gemini-2.0-flash"
|
||||
DEFAULT_TEMPERATURE: Final = 0.1
|
||||
DEFAULT_MAX_TOKENS: Final = 1000
|
||||
DEFAULT_REQUEST_INTERVAL: Final = 1.0
|
||||
DEFAULT_TIMEOUT: Final = 30
|
||||
DEFAULT_API_TIMEOUT: Final = 30
|
||||
DEFAULT_MAX_HISTORY: Final = 50
|
||||
DEFAULT_NAME: Final = "HA Text AI"
|
||||
DEFAULT_NAME_PREFIX = "ha_text_ai"
|
||||
@@ -87,9 +97,11 @@ MIN_MAX_TOKENS: Final = 1
|
||||
MAX_MAX_TOKENS: Final = 100000
|
||||
MIN_REQUEST_INTERVAL: Final = 0.1
|
||||
MAX_REQUEST_INTERVAL: Final = 60.0
|
||||
MIN_API_TIMEOUT: Final = 5
|
||||
MAX_API_TIMEOUT: Final = 600
|
||||
|
||||
# API constants
|
||||
API_TIMEOUT: Final = 30
|
||||
API_TIMEOUT: Final = 30 # Legacy constant, use CONF_API_TIMEOUT from config
|
||||
API_RETRY_COUNT: Final = 3
|
||||
|
||||
# Service names
|
||||
@@ -189,7 +201,9 @@ SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
|
||||
vol.Optional("context_messages"): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=1, max=20)
|
||||
)
|
||||
),
|
||||
vol.Optional(CONF_STRUCTURED_OUTPUT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_JSON_SCHEMA): cv.string,
|
||||
})
|
||||
|
||||
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
|
||||
@@ -226,6 +240,10 @@ CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Coerce(float),
|
||||
vol.Range(min=MIN_REQUEST_INTERVAL, max=MAX_REQUEST_INTERVAL)
|
||||
),
|
||||
vol.Optional(CONF_API_TIMEOUT, default=DEFAULT_API_TIMEOUT): vol.All(
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=MIN_API_TIMEOUT, max=MAX_API_TIMEOUT)
|
||||
),
|
||||
vol.Optional(CONF_MAX_HISTORY_SIZE, default=DEFAULT_MAX_HISTORY): vol.All( # Correct usage
|
||||
vol.Coerce(int),
|
||||
vol.Range(min=1, max=100),
|
||||
|
||||
@@ -37,6 +37,7 @@ from .const import (
|
||||
DEFAULT_MAX_TOKENS,
|
||||
DEFAULT_TEMPERATURE,
|
||||
DEFAULT_MAX_HISTORY,
|
||||
DEFAULT_API_TIMEOUT,
|
||||
DEFAULT_CONTEXT_MESSAGES,
|
||||
ABSOLUTE_MAX_HISTORY_SIZE,
|
||||
MAX_ATTRIBUTE_SIZE,
|
||||
@@ -46,19 +47,6 @@ from .const import (
|
||||
|
||||
_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:
|
||||
"""Async context manager for file operations."""
|
||||
@@ -89,6 +77,7 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
||||
max_history_size: int = DEFAULT_MAX_HISTORY,
|
||||
context_messages: int = DEFAULT_CONTEXT_MESSAGES,
|
||||
is_anthropic: bool = False,
|
||||
api_timeout: int = DEFAULT_API_TIMEOUT,
|
||||
) -> None:
|
||||
"""Initialize coordinator."""
|
||||
self.instance_name = instance_name
|
||||
@@ -128,6 +117,7 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
||||
ABSOLUTE_MAX_HISTORY_SIZE
|
||||
)
|
||||
self.is_anthropic = is_anthropic
|
||||
self.api_timeout = api_timeout
|
||||
|
||||
# Initialize essential attributes
|
||||
self._is_processing = False
|
||||
@@ -199,16 +189,10 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
||||
# Maximum history file size (1 MB) from const.py
|
||||
self._max_history_file_size = MAX_HISTORY_FILE_SIZE
|
||||
|
||||
# Asynchronous file initialization
|
||||
hass.async_create_task(self.async_initialize_history_file())
|
||||
self.context_messages = context_messages
|
||||
|
||||
_LOGGER.info(f"Initialized HA Text AI coordinator with instance: {instance_name}")
|
||||
|
||||
# Register instance
|
||||
self.hass.data.setdefault(DOMAIN, {})
|
||||
self.hass.data[DOMAIN][instance_name] = self
|
||||
self.context_messages = context_messages
|
||||
|
||||
@property
|
||||
def last_response(self) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -549,25 +533,57 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
||||
return 0
|
||||
|
||||
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:
|
||||
history = []
|
||||
if os.path.exists(self._history_file):
|
||||
with open(self._history_file, 'r') as f:
|
||||
content = f.read()
|
||||
if content:
|
||||
history = json.loads(content)
|
||||
try:
|
||||
with open(self._history_file, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
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)
|
||||
|
||||
if len(history) > self.max_history_size:
|
||||
history = history[-self.max_history_size:]
|
||||
|
||||
with open(self._history_file, 'w') as f:
|
||||
json.dump(history, f, indent=2)
|
||||
# Write with atomic operation
|
||||
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:
|
||||
_LOGGER.error(f"Synchronous history entry writing failed: {e}")
|
||||
raise
|
||||
|
||||
async def _rotate_history(self) -> None:
|
||||
"""Rotate conversation history with file management."""
|
||||
@@ -804,6 +820,8 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
context_messages: Optional[int] = None,
|
||||
structured_output: bool = False,
|
||||
json_schema: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""
|
||||
Process a question with optional parameters.
|
||||
@@ -819,12 +837,15 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
||||
max_tokens: Optional maximum response length
|
||||
system_prompt: Optional system-level instruction
|
||||
context_messages: Optional number of context messages to include
|
||||
structured_output: Enable JSON structured output mode
|
||||
json_schema: JSON Schema for structured output validation
|
||||
|
||||
Returns:
|
||||
Full response dictionary from the AI
|
||||
"""
|
||||
return await self.async_process_question(
|
||||
question, model, temperature, max_tokens, system_prompt, context_messages
|
||||
question, model, temperature, max_tokens, system_prompt, context_messages,
|
||||
structured_output, json_schema
|
||||
)
|
||||
|
||||
async def async_process_question(
|
||||
@@ -835,6 +856,8 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
||||
max_tokens: Optional[int] = None,
|
||||
system_prompt: Optional[str] = None,
|
||||
context_messages: Optional[int] = None,
|
||||
structured_output: bool = False,
|
||||
json_schema: Optional[str] = None,
|
||||
) -> dict:
|
||||
"""Process question with context management."""
|
||||
if self.client is None:
|
||||
@@ -873,6 +896,8 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
||||
"temperature": temp_temperature,
|
||||
"max_tokens": temp_max_tokens,
|
||||
"messages": messages,
|
||||
"structured_output": structured_output,
|
||||
"json_schema": json_schema,
|
||||
}
|
||||
|
||||
response = await self.async_process_message(question, **kwargs)
|
||||
@@ -887,7 +912,7 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
||||
return response
|
||||
|
||||
except Exception as err:
|
||||
self._handle_error(err)
|
||||
await self._handle_error(err)
|
||||
raise HomeAssistantError(f"Failed to process question: {err}")
|
||||
|
||||
finally:
|
||||
@@ -897,55 +922,52 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
||||
async def async_process_message(self, question: str, **kwargs) -> dict:
|
||||
"""Process message using the AI client."""
|
||||
try:
|
||||
async with asyncio.timeout(60): # 60 second timeout
|
||||
if self.is_anthropic:
|
||||
response = await self._process_anthropic_message(question, **kwargs)
|
||||
else:
|
||||
response = await self._process_openai_message(question, **kwargs)
|
||||
structured_output = kwargs.pop("structured_output", False)
|
||||
json_schema = kwargs.pop("json_schema", None)
|
||||
|
||||
async with asyncio.timeout(self.api_timeout):
|
||||
# APIClient.create() handles provider routing internally
|
||||
response = await self._process_openai_message(
|
||||
question, structured_output=structured_output,
|
||||
json_schema=json_schema, **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 = {
|
||||
"timestamp": dt_util.utcnow().isoformat(),
|
||||
"timestamp": timestamp,
|
||||
"question": question,
|
||||
"response": response["content"],
|
||||
"model": kwargs.get("model", self.model),
|
||||
"model": model_used,
|
||||
"instance": self.instance_name,
|
||||
"normalized_name": self.normalized_name,
|
||||
"error": None,
|
||||
}
|
||||
|
||||
return response
|
||||
return enhanced_response
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
raise HomeAssistantError("Request timed out")
|
||||
|
||||
except Exception as err:
|
||||
self._handle_error(err)
|
||||
await self._handle_error(err)
|
||||
raise
|
||||
|
||||
async def _process_anthropic_message(self, question: str, **kwargs) -> dict:
|
||||
"""Process message using Anthropic API."""
|
||||
try:
|
||||
_LOGGER.debug(f"Anthropic API call: model={kwargs['model']}, max_tokens={kwargs['max_tokens']}")
|
||||
response = await self.client.messages.create(
|
||||
model=kwargs["model"],
|
||||
max_tokens=kwargs["max_tokens"],
|
||||
messages=kwargs["messages"],
|
||||
temperature=kwargs["temperature"],
|
||||
)
|
||||
_LOGGER.debug(f"Anthropic response: tokens={response.usage}")
|
||||
return {
|
||||
"content": response.content[0].text,
|
||||
"tokens": {
|
||||
"prompt": response.usage.input_tokens,
|
||||
"completion": response.usage.output_tokens,
|
||||
"total": response.usage.input_tokens + response.usage.output_tokens,
|
||||
},
|
||||
}
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Anthropic API error: {str(e)}")
|
||||
raise
|
||||
|
||||
async def _process_openai_message(self, question: str, **kwargs) -> dict:
|
||||
async def _process_openai_message(self, question: str, structured_output: bool = False,
|
||||
json_schema: Optional[str] = None, **kwargs) -> dict:
|
||||
"""Process message using OpenAI API."""
|
||||
try:
|
||||
response = await self.client.create(
|
||||
@@ -953,6 +975,8 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
||||
messages=kwargs["messages"],
|
||||
temperature=kwargs["temperature"],
|
||||
max_tokens=kwargs["max_tokens"],
|
||||
structured_output=structured_output,
|
||||
json_schema=json_schema,
|
||||
)
|
||||
|
||||
return {
|
||||
@@ -1051,15 +1075,84 @@ class HATextAICoordinator(DataUpdateCoordinator):
|
||||
_LOGGER.error(f"Error clearing history: {e}")
|
||||
_LOGGER.debug(traceback.format_exc())
|
||||
|
||||
async def async_get_history(self) -> List[Dict[str, str]]:
|
||||
"""Get conversation history."""
|
||||
return self._conversation_history
|
||||
async def async_get_history(
|
||||
self,
|
||||
limit: Optional[int] = None,
|
||||
filter_model: Optional[str] = None,
|
||||
start_date: Optional[str] = None,
|
||||
include_metadata: bool = False,
|
||||
sort_order: str = "newest"
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Get conversation history with optional filtering and sorting."""
|
||||
try:
|
||||
history = self._conversation_history.copy()
|
||||
|
||||
# Filter by model if specified
|
||||
if filter_model:
|
||||
history = [entry for entry in history if entry.get("model") == filter_model]
|
||||
|
||||
# Filter by start date if specified
|
||||
if start_date:
|
||||
try:
|
||||
from datetime import datetime
|
||||
start_dt = datetime.fromisoformat(start_date.replace('Z', '+00:00'))
|
||||
history = [
|
||||
entry for entry in history
|
||||
if datetime.fromisoformat(entry["timestamp"].replace('Z', '+00:00')) >= start_dt
|
||||
]
|
||||
except (ValueError, KeyError) as e:
|
||||
_LOGGER.warning(f"Invalid start_date format: {start_date}. Error: {e}")
|
||||
|
||||
# Sort history
|
||||
if sort_order == "oldest":
|
||||
history.sort(key=lambda x: x.get("timestamp", ""))
|
||||
else: # newest (default)
|
||||
history.sort(key=lambda x: x.get("timestamp", ""), reverse=True)
|
||||
|
||||
# Apply limit
|
||||
if limit and limit > 0:
|
||||
history = history[:limit]
|
||||
|
||||
# Add metadata if requested
|
||||
if include_metadata:
|
||||
for entry in history:
|
||||
entry["metadata"] = {
|
||||
"entry_size": len(str(entry)),
|
||||
"question_length": len(entry.get("question", "")),
|
||||
"response_length": len(entry.get("response", "")),
|
||||
"model_used": entry.get("model", self.model),
|
||||
"instance": self.instance_name
|
||||
}
|
||||
|
||||
return history
|
||||
|
||||
except Exception as e:
|
||||
_LOGGER.error(f"Error getting history: {e}")
|
||||
return []
|
||||
|
||||
async def async_set_system_prompt(self, prompt: str) -> None:
|
||||
"""Set system prompt."""
|
||||
self._system_prompt = prompt
|
||||
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:
|
||||
"""Shutdown coordinator."""
|
||||
_LOGGER.debug(f"Shutting down coordinator for {self.instance_name}")
|
||||
|
||||
@@ -13,16 +13,18 @@
|
||||
"loggers": ["custom_components.ha_text_ai"],
|
||||
"mqtt": [],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"openai>=1.12.0",
|
||||
"anthropic>=0.8.0",
|
||||
"aiohttp>=3.8.0",
|
||||
"async-timeout>=4.0.0",
|
||||
"certifi>=2024.2.2"
|
||||
],
|
||||
"requirements": [
|
||||
"aiofiles>=23.0.0",
|
||||
"aiohttp>=3.8.0",
|
||||
"anthropic>=0.8.0",
|
||||
"async-timeout>=4.0.0",
|
||||
"certifi>=2024.2.2",
|
||||
"google-genai>=1.16.0",
|
||||
"openai>=1.12.0"
|
||||
],
|
||||
"single_config_entry": false,
|
||||
"ssdp": [],
|
||||
"usb": [],
|
||||
"version": "2.1.1",
|
||||
"version": "2.3.0",
|
||||
"zeroconf": []
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ Sensor platform for HA Text AI.
|
||||
import logging
|
||||
import math
|
||||
from typing import Any, Dict
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
|
||||
@@ -3,6 +3,7 @@ ask_question:
|
||||
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.
|
||||
This service now returns response data directly, eliminating the need to read from sensors.
|
||||
fields:
|
||||
instance:
|
||||
name: Instance
|
||||
@@ -73,6 +74,24 @@ ask_question:
|
||||
step: 1
|
||||
mode: box
|
||||
|
||||
structured_output:
|
||||
name: Structured Output
|
||||
description: Enable JSON structured output mode. When enabled, the AI will respond with valid JSON matching the provided schema.
|
||||
required: false
|
||||
default: false
|
||||
selector:
|
||||
boolean: {}
|
||||
|
||||
json_schema:
|
||||
name: JSON Schema
|
||||
description: >-
|
||||
JSON Schema defining the structure of the expected response.
|
||||
Required when structured_output is enabled.
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
multiline: true
|
||||
|
||||
clear_history:
|
||||
name: Clear History
|
||||
description: >-
|
||||
@@ -134,7 +153,7 @@ get_history:
|
||||
required: false
|
||||
default: false
|
||||
selector:
|
||||
boolean:
|
||||
boolean: {}
|
||||
|
||||
sort_order:
|
||||
name: Sort Order
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"temperature": "Kreativität der Antwort (0-2, niedriger = fokussierter)",
|
||||
"max_tokens": "Maximale Länge der Antwort (1-100000 Token)",
|
||||
"request_interval": "Minimale Zeit zwischen Anfragen (0,1-60 Sekunden)",
|
||||
"api_timeout": "API-Anfrage Timeout in Sekunden (5-600)",
|
||||
"context_messages": "Anzahl der zu behaltenden Kontextnachrichten (1-20)",
|
||||
"max_history_size": "Maximale Größe des Gesprächsverlaufs (1-100)"
|
||||
}
|
||||
@@ -37,6 +38,7 @@
|
||||
"api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)",
|
||||
"api_provider": "API-Anbieter",
|
||||
"request_interval": "Minimale Zeit zwischen Anfragen (0,1-60 Sekunden)",
|
||||
"api_timeout": "API-Anfrage Timeout in Sekunden (5-600)",
|
||||
"context_messages": "Anzahl der zu behaltenden Kontextnachrichten (1-20)",
|
||||
"max_history_size": "Maximale Größe des Gesprächsverlaufs (1-100)"
|
||||
}
|
||||
@@ -72,13 +74,23 @@
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Instanzeinstellungen aktualisieren",
|
||||
"description": "Ändern Sie die Einstellungen für diese AI-Assistenteninstanz.",
|
||||
"title": "Anbieter auswählen",
|
||||
"description": "Wählen Sie den AI-Anbieter für diese Instanz. Die Integration wird nach dem Speichern der Änderungen neu geladen.",
|
||||
"data": {
|
||||
"api_provider": "API-Anbieter"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Verbindungs- und Modelleinstellungen",
|
||||
"description": "Konfigurieren Sie API-Anmeldeinformationen und Modellparameter. Änderungen werden nach dem Neuladen der Integration wirksam.",
|
||||
"data": {
|
||||
"api_key": "API-Schlüssel",
|
||||
"api_endpoint": "API-Endpunkt-URL",
|
||||
"model": "AI-Modell",
|
||||
"temperature": "Kreativität der Antwort (0-2)",
|
||||
"max_tokens": "Maximale Länge der Antwort (1-100000)",
|
||||
"request_interval": "Minimale Anfrageintervall (0,1-60 Sekunden)",
|
||||
"request_interval": "Minimales Anfrageintervall (0,1-60 Sekunden)",
|
||||
"api_timeout": "API-Anfrage Timeout in Sekunden (5-600)",
|
||||
"context_messages": "Anzahl der vorherigen Nachrichten, die im Kontext enthalten sein sollen (1-20)",
|
||||
"max_history_size": "Maximale Größe des Gesprächsverlaufs (1-100)"
|
||||
}
|
||||
@@ -88,16 +100,17 @@
|
||||
"selector": {
|
||||
"api_provider": {
|
||||
"options": {
|
||||
"openai": "OpenAI (kompatibel)",
|
||||
"anthropic": "Anthropic (kompatibel)",
|
||||
"deepseek": "DeepSeek"
|
||||
"openai": "OpenAI (compatible)",
|
||||
"anthropic": "Anthropic (compatible)",
|
||||
"deepseek": "DeepSeek",
|
||||
"gemini": "Google Gemini"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"ask_question": {
|
||||
"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": {
|
||||
"instance": {
|
||||
"name": "Instanz",
|
||||
@@ -126,6 +139,14 @@
|
||||
"max_tokens": {
|
||||
"name": "Max Tokens",
|
||||
"description": "Maximale Länge der Antwort (1-100000 Token)"
|
||||
},
|
||||
"structured_output": {
|
||||
"name": "Strukturierte Ausgabe",
|
||||
"description": "JSON-Strukturausgabemodus aktivieren. Bei Aktivierung antwortet die KI mit gültigem JSON, das dem angegebenen Schema entspricht."
|
||||
},
|
||||
"json_schema": {
|
||||
"name": "JSON-Schema",
|
||||
"description": "JSON-Schema, das die Struktur der erwarteten Antwort definiert. Erforderlich wenn structured_output aktiviert ist."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"temperature": "Response creativity (0-2, lower = more focused)",
|
||||
"max_tokens": "Maximum response length (1-100000 tokens)",
|
||||
"request_interval": "Minimum time between requests (0.1-60 seconds)",
|
||||
"api_timeout": "API request timeout in seconds (5-600)",
|
||||
"context_messages": "Number of context messages to retain (1-20)",
|
||||
"max_history_size": "Maximum conversation history size (1-100)"
|
||||
}
|
||||
@@ -37,6 +38,7 @@
|
||||
"api_endpoint": "Custom API endpoint URL (optional)",
|
||||
"api_provider": "API Provider",
|
||||
"request_interval": "Minimum time between requests (0.1-60 seconds)",
|
||||
"api_timeout": "API request timeout in seconds (5-600)",
|
||||
"context_messages": "Number of context messages to retain (1-20)",
|
||||
"max_history_size": "Maximum conversation history size (1-100)"
|
||||
}
|
||||
@@ -72,13 +74,23 @@
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Update Instance Settings",
|
||||
"description": "Modify settings for this AI assistant instance.",
|
||||
"title": "Select Provider",
|
||||
"description": "Choose the AI provider for this instance. The integration will reload after saving changes.",
|
||||
"data": {
|
||||
"api_provider": "API Provider"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Connection & Model Settings",
|
||||
"description": "Configure API credentials and model parameters. Changes will take effect after the integration reloads.",
|
||||
"data": {
|
||||
"api_key": "API Key",
|
||||
"api_endpoint": "API Endpoint URL",
|
||||
"model": "AI model",
|
||||
"temperature": "Response creativity (0-2)",
|
||||
"max_tokens": "Maximum response length (1-100000)",
|
||||
"request_interval": "Minimum request interval (0.1-60 seconds)",
|
||||
"api_timeout": "API request timeout in seconds (5-600)",
|
||||
"context_messages": "Number of previous messages to include in context (1-20)",
|
||||
"max_history_size": "Maximum conversation history size (1-100)"
|
||||
}
|
||||
@@ -86,18 +98,19 @@
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"api_provider": {
|
||||
"options": {
|
||||
"openai": "OpenAI (compatible)",
|
||||
"anthropic": "Anthropic (compatible)",
|
||||
"deepseek": "DeepSeek"
|
||||
"api_provider": {
|
||||
"options": {
|
||||
"openai": "OpenAI (compatible)",
|
||||
"anthropic": "Anthropic (compatible)",
|
||||
"deepseek": "DeepSeek",
|
||||
"gemini": "Google Gemini"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"services": {
|
||||
"ask_question": {
|
||||
"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": {
|
||||
"instance": {
|
||||
"name": "Instance",
|
||||
@@ -126,6 +139,14 @@
|
||||
"max_tokens": {
|
||||
"name": "Max Tokens",
|
||||
"description": "Maximum length of the response (1-100000 tokens)"
|
||||
},
|
||||
"structured_output": {
|
||||
"name": "Structured Output",
|
||||
"description": "Enable JSON structured output mode. When enabled, the AI will respond with valid JSON matching the provided schema."
|
||||
},
|
||||
"json_schema": {
|
||||
"name": "JSON Schema",
|
||||
"description": "JSON Schema defining the structure of the expected response. Required when structured_output is enabled."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"temperature": "Creatividad de la respuesta (0-2, menor = más enfocado)",
|
||||
"max_tokens": "Longitud máxima de la respuesta (1-100000 tokens)",
|
||||
"request_interval": "Tiempo mínimo entre solicitudes (0.1-60 segundos)",
|
||||
"api_timeout": "Tiempo de espera de solicitud API en segundos (5-600)",
|
||||
"context_messages": "Número de mensajes de contexto a retener (1-20)",
|
||||
"max_history_size": "Tamaño máximo del historial de conversación (1-100)"
|
||||
}
|
||||
@@ -37,6 +38,7 @@
|
||||
"api_endpoint": "URL del endpoint de API personalizado (opcional)",
|
||||
"api_provider": "Proveedor de API",
|
||||
"request_interval": "Tiempo mínimo entre solicitudes (0.1-60 segundos)",
|
||||
"api_timeout": "Tiempo de espera de solicitud API en segundos (5-600)",
|
||||
"context_messages": "Número de mensajes de contexto a retener (1-20)",
|
||||
"max_history_size": "Tamaño máximo del historial de conversación (1-100)"
|
||||
}
|
||||
@@ -72,13 +74,23 @@
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Actualizar configuración de la instancia",
|
||||
"description": "Modifica la configuración para esta instancia de asistente de IA.",
|
||||
"title": "Seleccionar proveedor",
|
||||
"description": "Elige el proveedor de IA para esta instancia. La integración se recargará después de guardar los cambios.",
|
||||
"data": {
|
||||
"api_provider": "Proveedor de API"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Configuración de conexión y modelo",
|
||||
"description": "Configura las credenciales de API y los parámetros del modelo. Los cambios tendrán efecto después de recargar la integración.",
|
||||
"data": {
|
||||
"api_key": "Clave API",
|
||||
"api_endpoint": "URL del endpoint de API",
|
||||
"model": "Modelo de IA",
|
||||
"temperature": "Creatividad de la respuesta (0-2)",
|
||||
"max_tokens": "Longitud máxima de la respuesta (1-100000)",
|
||||
"request_interval": "Intervalo mínimo de solicitud (0.1-60 segundos)",
|
||||
"api_timeout": "Tiempo de espera de solicitud API en segundos (5-600)",
|
||||
"context_messages": "Número de mensajes anteriores a incluir en el contexto (1-20)",
|
||||
"max_history_size": "Tamaño máximo del historial de conversación (1-100)"
|
||||
}
|
||||
@@ -90,14 +102,15 @@
|
||||
"options": {
|
||||
"openai": "OpenAI (compatible)",
|
||||
"anthropic": "Anthropic (compatible)",
|
||||
"deepseek": "DeepSeek"
|
||||
"deepseek": "DeepSeek",
|
||||
"gemini": "Google Gemini"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"ask_question": {
|
||||
"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": {
|
||||
"instance": {
|
||||
"name": "Instancia",
|
||||
@@ -126,6 +139,14 @@
|
||||
"max_tokens": {
|
||||
"name": "Máx. Tokens",
|
||||
"description": "Longitud máxima de la respuesta (1-100000 tokens)"
|
||||
},
|
||||
"structured_output": {
|
||||
"name": "Salida Estructurada",
|
||||
"description": "Habilitar modo de salida JSON estructurada. Cuando está habilitado, la IA responderá con JSON válido que coincida con el esquema proporcionado."
|
||||
},
|
||||
"json_schema": {
|
||||
"name": "Esquema JSON",
|
||||
"description": "Esquema JSON que define la estructura de la respuesta esperada. Requerido cuando structured_output está habilitado."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"provider": {
|
||||
"title": "एआई प्रदाता चुनें",
|
||||
"description": "इस उदाहरण के लिए किस एआई सेवा प्रदाता का उपयोग करना है, चुनें।",
|
||||
"data": {
|
||||
"api_provider": "एपीआई प्रदाता",
|
||||
"context_messages": "रखने के लिए संदर्भ संदेशों की संख्या (1-20)",
|
||||
"max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)"
|
||||
}
|
||||
},
|
||||
"provider": {
|
||||
"title": "प्रदाता सेटिंग्स",
|
||||
"description": "आपके द्वारा चुने गए एआई प्रदाता के लिए कनेक्शन विवरण प्रदान करें।",
|
||||
@@ -21,6 +12,7 @@
|
||||
"temperature": "प्रतिक्रिया की रचनात्मकता (0-2, कम = अधिक केंद्रित)",
|
||||
"max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-100000 टोकन)",
|
||||
"request_interval": "अनुरोधों के बीच न्यूनतम समय (0.1-60 सेकंड)",
|
||||
"api_timeout": "एपीआई अनुरोध टाइमआउट सेकंड में (5-600)",
|
||||
"context_messages": "रखने के लिए संदर्भ संदेशों की संख्या (1-20)",
|
||||
"max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)"
|
||||
}
|
||||
@@ -37,6 +29,7 @@
|
||||
"api_endpoint": "कस्टम एपीआई एंडपॉइंट यूआरएल (वैकल्पिक)",
|
||||
"api_provider": "एपीआई प्रदाता",
|
||||
"request_interval": "अनुरोधों के बीच न्यूनतम समय (0.1-60 सेकंड)",
|
||||
"api_timeout": "एपीआई अनुरोध टाइमआउट सेकंड में (5-600)",
|
||||
"context_messages": "रखने के लिए संदर्भ संदेशों की संख्या (1-20)",
|
||||
"max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)"
|
||||
}
|
||||
@@ -72,13 +65,23 @@
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "उदाहरण सेटिंग्स अपडेट करें",
|
||||
"description": "इस एआई सहायक उदाहरण के लिए सेटिंग्स संशोधित करें।",
|
||||
"title": "प्रदाता चुनें",
|
||||
"description": "इस उदाहरण के लिए एआई प्रदाता चुनें। परिवर्तन सहेजने के बाद एकीकरण पुनः लोड होगा।",
|
||||
"data": {
|
||||
"api_provider": "एपीआई प्रदाता"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "कनेक्शन और मॉडल सेटिंग्स",
|
||||
"description": "एपीआई क्रेडेंशियल और मॉडल पैरामीटर कॉन्फ़िगर करें। एकीकरण पुनः लोड होने के बाद परिवर्तन प्रभावी होंगे।",
|
||||
"data": {
|
||||
"api_key": "एपीआई कुंजी",
|
||||
"api_endpoint": "एपीआई एंडपॉइंट यूआरएल",
|
||||
"model": "एआई मॉडल",
|
||||
"temperature": "प्रतिक्रिया की रचनात्मकता (0-2)",
|
||||
"max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-100000)",
|
||||
"request_interval": "न्यूनतम अनुरोध अंतराल (0.1-60 सेकंड)",
|
||||
"api_timeout": "एपीआई अनुरोध टाइमआउट सेकंड में (5-600)",
|
||||
"context_messages": "संदर्भ में शामिल करने के लिए पिछले संदेशों की संख्या (1-20)",
|
||||
"max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)"
|
||||
}
|
||||
@@ -90,14 +93,15 @@
|
||||
"options": {
|
||||
"openai": "OpenAI (अनुकूलित)",
|
||||
"anthropic": "Anthropic (अनुकूलित)",
|
||||
"deepseek": "DeepSeek"
|
||||
"deepseek": "DeepSeek",
|
||||
"gemini": "Google Gemini"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"ask_question": {
|
||||
"name": "प्रश्न पूछें (HA Text AI)",
|
||||
"description": "एआई मॉडल को एक प्रश्न भेजें और विस्तृत प्रतिक्रिया प्राप्त करें। प्रतिक्रिया बातचीत के इतिहास में संग्रहीत की जाएगी और बाद में पुनर्प्राप्त की जा सकती है।",
|
||||
"description": "AI मॉडल को प्रश्न भेजें और विस्तृत उत्तर प्राप्त करें। यह सेवा अब प्रत्यक्ष रूप से प्रतिक्रिया डेटा वापस करती है, अलग टेक्स्ट सेंसर की आवश्यकता और 255 वर्ण की सीमा को समाप्त करती है। प्रतिक्रिया को बातचीत के इतिहास में भी संग्रहीत किया जाएगा।",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "उदाहरण",
|
||||
@@ -126,6 +130,14 @@
|
||||
"max_tokens": {
|
||||
"name": "अधिकतम टोकन",
|
||||
"description": "प्रतिक्रिया की अधिकतम लंबाई (1-100000 टोकन)"
|
||||
},
|
||||
"structured_output": {
|
||||
"name": "संरचित आउटपुट",
|
||||
"description": "JSON संरचित आउटपुट मोड सक्षम करें। सक्षम होने पर, AI प्रदान किए गए स्कीमा से मेल खाने वाले वैध JSON के साथ प्रतिक्रिया देगा।"
|
||||
},
|
||||
"json_schema": {
|
||||
"name": "JSON स्कीमा",
|
||||
"description": "अपेक्षित प्रतिक्रिया की संरचना को परिभाषित करने वाला JSON स्कीमा। structured_output सक्षम होने पर आवश्यक।"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -294,4 +306,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@
|
||||
"temperature": "Creatività della risposta (0-2, più basso = più focalizzato)",
|
||||
"max_tokens": "Lunghezza massima della risposta (1-100000 token)",
|
||||
"request_interval": "Tempo minimo tra le richieste (0.1-60 secondi)",
|
||||
"api_timeout": "Timeout della richiesta API in secondi (5-600)",
|
||||
"context_messages": "Numero di messaggi di contesto da mantenere (1-20)",
|
||||
"max_history_size": "Dimensione massima della cronologia delle conversazioni (1-100)"
|
||||
}
|
||||
@@ -37,6 +38,7 @@
|
||||
"api_endpoint": "URL dell'endpoint API personalizzato (opzionale)",
|
||||
"api_provider": "Fornitore API",
|
||||
"request_interval": "Tempo minimo tra le richieste (0.1-60 secondi)",
|
||||
"api_timeout": "Timeout della richiesta API in secondi (5-600)",
|
||||
"context_messages": "Numero di messaggi di contesto da mantenere (1-20)",
|
||||
"max_history_size": "Dimensione massima della cronologia delle conversazioni (1-100)"
|
||||
}
|
||||
@@ -72,13 +74,23 @@
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Aggiorna impostazioni dell'istanza",
|
||||
"description": "Modifica le impostazioni per questa istanza di assistente AI.",
|
||||
"title": "Seleziona fornitore",
|
||||
"description": "Scegli il fornitore AI per questa istanza. L'integrazione verrà ricaricata dopo aver salvato le modifiche.",
|
||||
"data": {
|
||||
"api_provider": "Fornitore API"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Impostazioni di connessione e modello",
|
||||
"description": "Configura le credenziali API e i parametri del modello. Le modifiche avranno effetto dopo il ricaricamento dell'integrazione.",
|
||||
"data": {
|
||||
"api_key": "Chiave API",
|
||||
"api_endpoint": "URL dell'endpoint API",
|
||||
"model": "Modello AI",
|
||||
"temperature": "Creatività della risposta (0-2)",
|
||||
"max_tokens": "Lunghezza massima della risposta (1-100000)",
|
||||
"request_interval": "Intervallo minimo di richiesta (0.1-60 secondi)",
|
||||
"api_timeout": "Timeout della richiesta API in secondi (5-600)",
|
||||
"context_messages": "Numero di messaggi precedenti da includere nel contesto (1-20)",
|
||||
"max_history_size": "Dimensione massima della cronologia delle conversazioni (1-100)"
|
||||
}
|
||||
@@ -90,14 +102,15 @@
|
||||
"options": {
|
||||
"openai": "OpenAI (compatibile)",
|
||||
"anthropic": "Anthropic (compatibile)",
|
||||
"deepseek": "DeepSeek"
|
||||
"deepseek": "DeepSeek",
|
||||
"gemini": "Google Gemini"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"ask_question": {
|
||||
"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": {
|
||||
"instance": {
|
||||
"name": "Istanze",
|
||||
@@ -126,6 +139,14 @@
|
||||
"max_tokens": {
|
||||
"name": "Token massimi",
|
||||
"description": "Lunghezza massima della risposta (1-100000 token)"
|
||||
},
|
||||
"structured_output": {
|
||||
"name": "Output Strutturato",
|
||||
"description": "Abilita la modalità di output JSON strutturato. Quando abilitato, l'IA risponderà con JSON valido corrispondente allo schema fornito."
|
||||
},
|
||||
"json_schema": {
|
||||
"name": "Schema JSON",
|
||||
"description": "Schema JSON che definisce la struttura della risposta attesa. Richiesto quando structured_output è abilitato."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
"temperature": "Креативность ответа (0-2, меньше = более сфокусированно)",
|
||||
"max_tokens": "Максимальная длина ответа (1-100000 токенов)",
|
||||
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
|
||||
"api_timeout": "Таймаут API-запроса в секундах (5-600)",
|
||||
"context_messages": "Количество сохраняемых контекстных сообщений (1-20)",
|
||||
"max_history_size": "Максимальный размер истории разговора (1-100)"
|
||||
}
|
||||
@@ -37,6 +38,7 @@
|
||||
"api_endpoint": "Пользовательский URL-адрес конечной точки API (необязательно)",
|
||||
"api_provider": "Провайдер API",
|
||||
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
|
||||
"api_timeout": "Таймаут API-запроса в секундах (5-600)",
|
||||
"context_messages": "Количество сохраняемых контекстных сообщений (1-20)",
|
||||
"max_history_size": "Максимальный размер истории разговора (1-100)"
|
||||
}
|
||||
@@ -72,13 +74,23 @@
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Обновление настроек экземпляра",
|
||||
"description": "Измените настройки для этого экземпляра ИИ-помощника.",
|
||||
"title": "Выбор провайдера",
|
||||
"description": "Выберите провайдера ИИ для этого экземпляра. Интеграция перезагрузится после сохранения изменений.",
|
||||
"data": {
|
||||
"api_provider": "Провайдер API"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Настройки подключения и модели",
|
||||
"description": "Настройте учётные данные API и параметры модели. Изменения вступят в силу после перезагрузки интеграции.",
|
||||
"data": {
|
||||
"api_key": "API-ключ",
|
||||
"api_endpoint": "URL конечной точки API",
|
||||
"model": "Модель ИИ",
|
||||
"temperature": "Креативность ответа (0-2)",
|
||||
"max_tokens": "Максимальная длина ответа (1-100000)",
|
||||
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
|
||||
"api_timeout": "Таймаут API-запроса в секундах (5-600)",
|
||||
"context_messages": "Количество предыдущих сообщений для включения в контекст (1-20)",
|
||||
"max_history_size": "Максимальный размер истории разговора (1-100)"
|
||||
}
|
||||
@@ -90,14 +102,15 @@
|
||||
"options": {
|
||||
"openai": "OpenAI (совместимый)",
|
||||
"anthropic": "Anthropic (совместимый)",
|
||||
"deepseek": "DeepSeek"
|
||||
"deepseek": "DeepSeek",
|
||||
"gemini": "Google Gemini"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"ask_question": {
|
||||
"name": "Задать вопрос (HA Text AI)",
|
||||
"description": "Отправить вопрос модели ИИ и получить подробный ответ. Ответ будет сохранен в истории разговора и может быть получен позже.",
|
||||
"description": "Отправить вопрос модели ИИ и получить подробный ответ. Сервис теперь возвращает данные ответа напрямую, устраняя необходимость в отдельных текстовых сенсорах и ограничение в 255 символов. Ответ также будет сохранен в истории разговора.",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "Экземпляр",
|
||||
@@ -126,6 +139,14 @@
|
||||
"max_tokens": {
|
||||
"name": "Максимум токенов",
|
||||
"description": "Максимальная длина ответа (1-100000 токенов)"
|
||||
},
|
||||
"structured_output": {
|
||||
"name": "Структурированный вывод",
|
||||
"description": "Включить режим структурированного JSON-вывода. При включении ИИ будет отвечать валидным JSON, соответствующим указанной схеме."
|
||||
},
|
||||
"json_schema": {
|
||||
"name": "JSON Schema",
|
||||
"description": "JSON-схема, определяющая структуру ожидаемого ответа. Обязательна при включении structured_output."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,15 +1,6 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"provider": {
|
||||
"title": "Изаберите AI провајдера",
|
||||
"description": "Изаберите који AI сервис провајдер да користите за ову инстанцу.",
|
||||
"data": {
|
||||
"api_provider": "API провајдер",
|
||||
"context_messages": "Број контекстуалних порука које треба задржати (1-20)",
|
||||
"max_history_size": "Максимална величина историје разговора (1-100)"
|
||||
}
|
||||
},
|
||||
"provider": {
|
||||
"title": "Подешавања провајдера",
|
||||
"description": "Обезбедите детаље о вези за изабраног AI провајдера.",
|
||||
@@ -21,6 +12,7 @@
|
||||
"temperature": "Креативност одговора (0-2, нижа = фокусираније)",
|
||||
"max_tokens": "Максимална дужина одговора (1-100000 токена)",
|
||||
"request_interval": "Минимално време између захтева (0.1-60 секунди)",
|
||||
"api_timeout": "Временско ограничење API захтева у секундама (5-600)",
|
||||
"context_messages": "Број контекстуалних порука које треба задржати (1-20)",
|
||||
"max_history_size": "Максимална величина историје разговора (1-100)"
|
||||
}
|
||||
@@ -37,6 +29,7 @@
|
||||
"api_endpoint": "Прилагођени URL API крајње тачке (опционо)",
|
||||
"api_provider": "API провајдер",
|
||||
"request_interval": "Минимално време између захтева (0.1-60 секунди)",
|
||||
"api_timeout": "Временско ограничење API захтева у секундама (5-600)",
|
||||
"context_messages": "Број контекстуалних порука које треба задржати (1-20)",
|
||||
"max_history_size": "Максимална величина историје разговора (1-100)"
|
||||
}
|
||||
@@ -72,13 +65,23 @@
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Ажурирајте подешавања инстанце",
|
||||
"description": "Измените подешавања за ову AI асистент инстанцу.",
|
||||
"title": "Изаберите провајдера",
|
||||
"description": "Изаберите AI провајдера за ову инстанцу. Интеграција ће се поново учитати након чувања измена.",
|
||||
"data": {
|
||||
"api_provider": "API провајдер"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "Подешавања везе и модела",
|
||||
"description": "Конфигуришите API акредитиве и параметре модела. Промене ће ступити на снагу након поновног учитавања интеграције.",
|
||||
"data": {
|
||||
"api_key": "API кључ",
|
||||
"api_endpoint": "URL API крајње тачке",
|
||||
"model": "AI модел",
|
||||
"temperature": "Креативност одговора (0-2)",
|
||||
"max_tokens": "Максимална дужина одговора (1-100000)",
|
||||
"request_interval": "Минимално време између захтева (0.1-60 секунди)",
|
||||
"api_timeout": "Временско ограничење API захтева у секундама (5-600)",
|
||||
"context_messages": "Број претходних порука које треба укључити у контекст (1-20)",
|
||||
"max_history_size": "Максимална величина историје разговора (1-100)"
|
||||
}
|
||||
@@ -90,14 +93,15 @@
|
||||
"options": {
|
||||
"openai": "OpenAI (компатибилан)",
|
||||
"anthropic": "Anthropic (компатибилан)",
|
||||
"deepseek": "DeepSeek"
|
||||
"deepseek": "DeepSeek",
|
||||
"gemini": "Google Gemini"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"ask_question": {
|
||||
"name": "Поставите питање (HA Text AI)",
|
||||
"description": "Пошаљите питање AI моделу и примите детаљан одговор. Одговор ће бити сачуван у историји разговора и може се касније повратити.",
|
||||
"description": "Пошаљите питање AI моделу и добијте детаљан одговор. Овај сервис сада враћа податке одговора директно, елиминишући потребу за засебним текстуалним сензорима и ограничење од 255 карактера. Одговор ће такође бити сачуван у историји разговора.",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "Инстанца",
|
||||
@@ -126,6 +130,14 @@
|
||||
"max_tokens": {
|
||||
"name": "Максимални токени",
|
||||
"description": "Максимална дужина одговора (1-100000 токена)"
|
||||
},
|
||||
"structured_output": {
|
||||
"name": "Структурисани излаз",
|
||||
"description": "Омогући JSON структурисани излаз. Када је омогућено, AI ће одговарати валидним JSON-ом који одговара датој шеми."
|
||||
},
|
||||
"json_schema": {
|
||||
"name": "JSON шема",
|
||||
"description": "JSON шема која дефинише структуру очекиваног одговора. Обавезна када је structured_output омогућен."
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -294,4 +306,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,6 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"provider": {
|
||||
"title": "选择AI提供者",
|
||||
"description": "选择要用于此实例的AI服务提供者。",
|
||||
"data": {
|
||||
"api_provider": "API提供者",
|
||||
"context_messages": "保留的上下文消息数量(1-20)",
|
||||
"max_history_size": "最大对话历史大小(1-100)"
|
||||
}
|
||||
},
|
||||
"provider": {
|
||||
"title": "提供者设置",
|
||||
"description": "提供所选AI提供者的连接详细信息。",
|
||||
@@ -21,6 +12,7 @@
|
||||
"temperature": "响应创造力(0-2,越低越专注)",
|
||||
"max_tokens": "最大响应长度(1-100000个标记)",
|
||||
"request_interval": "请求之间的最小时间(0.1-60秒)",
|
||||
"api_timeout": "API请求超时时间(5-600秒)",
|
||||
"context_messages": "保留的上下文消息数量(1-20)",
|
||||
"max_history_size": "最大对话历史大小(1-100)"
|
||||
}
|
||||
@@ -37,6 +29,7 @@
|
||||
"api_endpoint": "自定义API端点URL(可选)",
|
||||
"api_provider": "API提供者",
|
||||
"request_interval": "请求之间的最小时间(0.1-60秒)",
|
||||
"api_timeout": "API请求超时时间(5-600秒)",
|
||||
"context_messages": "保留的上下文消息数量(1-20)",
|
||||
"max_history_size": "最大对话历史大小(1-100)"
|
||||
}
|
||||
@@ -72,13 +65,23 @@
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "更新实例设置",
|
||||
"description": "修改此AI助手实例的设置。",
|
||||
"title": "选择提供者",
|
||||
"description": "选择此实例的AI提供者。保存更改后集成将重新加载。",
|
||||
"data": {
|
||||
"api_provider": "API提供者"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"title": "连接和模型设置",
|
||||
"description": "配置API凭据和模型参数。更改将在集成重新加载后生效。",
|
||||
"data": {
|
||||
"api_key": "API密钥",
|
||||
"api_endpoint": "API端点URL",
|
||||
"model": "AI模型",
|
||||
"temperature": "响应创造力(0-2)",
|
||||
"max_tokens": "最大响应长度(1-100000)",
|
||||
"request_interval": "最小请求间隔(0.1-60秒)",
|
||||
"api_timeout": "API请求超时时间(5-600秒)",
|
||||
"context_messages": "要包含在上下文中的先前消息数量(1-20)",
|
||||
"max_history_size": "最大对话历史大小(1-100)"
|
||||
}
|
||||
@@ -90,14 +93,15 @@
|
||||
"options": {
|
||||
"openai": "OpenAI(兼容)",
|
||||
"anthropic": "Anthropic(兼容)",
|
||||
"deepseek": "DeepSeek "
|
||||
"deepseek": "DeepSeek",
|
||||
"gemini": "Google Gemini"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"ask_question": {
|
||||
"name": "提问 (HA Text AI)",
|
||||
"description": "向AI模型发送问题并接收详细响应。响应将存储在对话历史中,可以稍后检索。",
|
||||
"description": "向AI模型发送问题并获得详细回答。此服务现在直接返回响应数据,消除了对单独文本传感器的需要和255字符限制。响应也将存储在对话历史中。",
|
||||
"fields": {
|
||||
"instance": {
|
||||
"name": "实例",
|
||||
@@ -126,6 +130,14 @@
|
||||
"max_tokens": {
|
||||
"name": "最大标记数",
|
||||
"description": "响应的最大长度(1-100000个标记)"
|
||||
},
|
||||
"structured_output": {
|
||||
"name": "结构化输出",
|
||||
"description": "启用JSON结构化输出模式。启用后,AI将以符合提供的模式的有效JSON进行响应。"
|
||||
},
|
||||
"json_schema": {
|
||||
"name": "JSON模式",
|
||||
"description": "定义预期响应结构的JSON模式。启用structured_output时必需。"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -294,4 +306,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user