Compare commits

...
50 Commits
Author SHA1 Message Date
SMKRV 43cbac2d04 feat: Add ability to edit provider, API key, and endpoint in existing integrations
- Extended OptionsFlowHandler with two-step configuration flow
- Step 1: Select provider (OpenAI, Anthropic, DeepSeek, Gemini)
- Step 2: Configure API key, endpoint, model, and other settings
- Auto-reload integration on options change
- When switching providers, show appropriate default endpoint and model
- Updated translations for all 8 languages
2025-12-30 17:22:48 +03:00
SMKRV 986c78dd90 fix: Fix OptionsFlowHandler for HA 2024.1+ compatibility
- Remove __init__ method that was passing config_entry as argument
- OptionsFlow now automatically receives config_entry from base class
- Fixes '500 Internal Server Error' when editing existing integrations
2025-12-30 17:10:06 +03:00
SMKRV 0859c35aec fix: Change release workflow to trigger on release created event 2025-12-30 16:56:27 +03:00
SMKRV 922fefbd43 chore: Bump version to 2.3.0 2025-12-30 16:54:23 +03:00
SMKRV a5ac100b06 feat: Add structured output support with JSON schema validation
- Introduced `structured_output` and `json_schema` parameters to enhance API responses.
- Updated service schemas and API client methods to handle structured output.
- Added translations for new parameters in multiple languages.
- Updated documentation to reflect changes in service capabilities.

Closes #9
2025-12-30 16:44:01 +03:00
smkrvandGitHub ef579af7c1 Create release.yml 2025-12-30 16:27:46 +03:00
SMKRV 3097106e93 feat: Add configurable API timeout setting
- Add CONF_API_TIMEOUT configuration option (5-600 seconds, default 30)
- Update config_flow.py with api_timeout field in provider form and options flow
- Update api_client.py to use configurable timeout instead of hardcoded value
- Update coordinator.py to use api_timeout for async_process_message
- Update __init__.py to read and pass api_timeout from config
- Merge entry.data with entry.options for proper options flow support
- Add translations for api_timeout in all 8 language files (en, ru, de, es, it, hi, sr, zh)
- Bump version to 2.2.0

Closes #8
2025-12-22 00:07:17 +03:00
smkrvandGitHub 35073960b8 Delete ha-text-ai.code-workspace 2025-09-03 00:56:56 +03:00
SMKRV 8d0e0b5e44 docs: update README with context_messages parameter
- Added context_messages parameter to Platform Configuration table
- Fixed duplicate parameter entry in configuration docs
- Parameter allows 1-20 previous messages in context (default: 5)
2025-09-02 23:50:20 +03:00
SMKRV e91c3701c5 Fix: Resolve get_history service parameter handling issue
- Fixed async_get_history method to accept limit parameter and other filtering options
- Updated service schema to support all parameters from services.yaml
- Added support for start_date, include_metadata, and sort_order parameters
- Version bump to 2.1.9
2025-09-02 23:27:34 +03:00
smkrv 7f62101b3e Update HACS minimum HA version to align with README requirement (2024.12.0) 2025-09-02 23:15:01 +03:00
smkrvandGitHub 3729c3736f Update hassfest.yaml 2025-09-02 09:36:46 +03:00
smkrvandGitHub f5ce5e459a Update hassfest.yaml
fix: https://github.com/smkrv/ha-text-ai/security/code-scanning/2
2025-09-02 09:24:35 +03:00
smkrvGitHubCopilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
4064486b1e Potential fix for code scanning alert no. 1: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-09-02 09:18:15 +03:00
SMKRV 185778dbd0 docs: Update AI models to latest versions
- Update OpenAI models to GPT-5 and GPT-5 mini
- Update Anthropic Claude models to 4.1, 4.0 series
- Update DeepSeek models to V3.1 and R1
- Update Google Gemini models to 2.5 and 2.0 series
- Modernize model descriptions and capabilities
2025-09-02 02:19:37 +03:00
SMKRV 83a255dee0 docs: Update README.md - actualize documentation
- Simplify HACS installation instructions
- Update recommended models section (remove year from title)
- Update Claude model names to current format (claude-3.5-sonnet, claude-3.5-haiku)
- Add missing parameters to get_history service documentation
- Remove non-configurable history_file_size parameter from table
- Add missing context_messages parameter to configuration table
- Update all model references in examples to use current naming
2025-09-02 02:11:34 +03:00
SMKRV 6b66dd6a4d docs: Update README with latest configuration defaults and Gemini models
- Update default model from gpt-4o to gpt-4o-mini
- Update default temperature from 0.7 to 0.1
- Update default max_history_size from 100 to 50
- Add gemini-2.0-flash as latest recommended Gemini model
- Fix logo image link to use main branch instead of specific commit
- Update configuration parameters table with current defaults
2025-09-02 02:06:58 +03:00
SMKRV bd82f23120 docs: Update HACS badge from Custom to Default 2025-09-02 01:56:24 +03:00
SMKRV eee9754033 fix: Fix JSON syntax errors in translation files
- Fixed missing closing brace in es.json selector.api_provider.options
- Fixed missing closing brace in de.json selector.api_provider.options
- All other translation files (hi.json, it.json, sr.json, zh.json) have correct syntax
- Ensures proper JSON validation and prevents parsing errors
2025-09-02 01:26:49 +03:00
SMKRV 517b1f11ae fix: Remove invalid response schema from services.yaml
Home Assistant's hassfest validation does not support 'response' section in services.yaml.
The response_variable functionality still works through supports_response=True flag in service registration.

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

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

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

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

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

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

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

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

Fixes #6
2025-05-20 01:16:41 +03:00
SMKRV 440c734214 Bump release version to v2.1.4 2025-05-19 23:20:27 +03:00
SMKRV 73788373cd Release v2.1.3 2025-05-19 23:12:55 +03:00
SMKRV 4bfc96019b fix: DEFAULT_GEMINI_ENDPOINT 2025-05-19 15:53:43 +03:00
SMKRV 2138fc7654 fix: DEFAULT_GEMINI_ENDPOINT 2025-05-19 15:36:58 +03:00
SMKRV 95bd2ebb41 Add support for Google Gemini (thanks to @Azzedde) #5 2025-05-19 15:10:19 +03:00
smkrvandGitHub cad0fd7031 Merge pull request #5 from Azzedde/main
Add Gemini API provider support to HA Text AI integration by @Azzedde
2025-05-19 14:44:06 +03:00
Azzedde c003b258f6 Add Gemini API provider support to HA Text AI integration 2025-05-18 13:23:55 +02:00
SMKRV 65a10c77f4 ~ 2025-01-30 01:15:13 +03:00
SMKRV e1463828c9 ~ 2025-01-30 01:14:24 +03:00
SMKRV 5ebb9c9c66 fix: max_tokens value 2025-01-29 18:04:13 +03:00
SMKRV f17c631a79 fix: max_tokens value 2025-01-29 18:02:42 +03:00
SMKRV 0e06794384 refactor(docs): shields & community links updated 2025-01-29 03:05:47 +03:00
SMKRV d8a924909b refactor(docs): shields & community links updated 2025-01-29 03:05:11 +03:00
SMKRV 29f1659a02 refactor(docs): shields & community links updated 2025-01-29 03:04:39 +03:00
SMKRV 5b7905de80 refactor(docs): shields & community links updated 2025-01-29 03:03:48 +03:00
SMKRV cf9ac6dcea refactor(docs): shields & community links updated 2025-01-29 01:08:45 +03:00
SMKRV 568eb3e16c refactor(docs): shields updated 2025-01-29 00:58:27 +03:00
SMKRV 53fb150389 refactor(docs): shields updated 2025-01-29 00:58:09 +03:00
SMKRV acbb53d2af refactor(docs): shields updated 2025-01-29 00:57:27 +03:00
SMKRV e19db29441 refactor(docs): DeepSeek Integration 2025-01-28 16:25:59 +03:00
SMKRV bfd64d1122 Release v2.1.1: Token Handling Improvement and DeepSeek Support
- Completely reworked token handling mechanism
- Removed custom token calculation logic
- Direct max_tokens passing to LLM APIs
- Added support for DeepSeek provider
- Integrated deepseek-chat and deepseek-reasoner models

Thanks to @estiens for reporting token handling issues and providing valuable feedback (https://github.com/smkrv/ha-text-ai/issues/1).
2025-01-28 15:54:48 +03:00
23 changed files with 1740 additions and 472 deletions
+2 -1
View File
@@ -1,5 +1,6 @@
name: Validate with hassfest name: Validate with hassfest
permissions:
contents: read
on: on:
push: push:
branches: branches:
+37
View File
@@ -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 }}
+2
View File
@@ -1,4 +1,6 @@
name: Validate name: Validate
permissions:
contents: read
on: on:
push: push:
-41
View File
@@ -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
+240 -36
View File
@@ -2,32 +2,33 @@
<div align="center"> <div align="center">
![GitHub release](https://img.shields.io/github/release/smkrv/ha-text-ai.svg?style=flat-square) ![GitHub last commit](https://img.shields.io/github/last-commit/smkrv/ha-text-ai.svg?style=flat-square) [![License: CC BY-NC-SA 4.0](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg?style=flat-square)](https://creativecommons.org/licenses/by-nc-sa/4.0/) [![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg?style=flat-square)](https://github.com/hacs/integration) ![GitHub release](https://img.shields.io/github/v/release/smkrv/ha-text-ai?style=flat-square) ![GitHub last commit](https://img.shields.io/github/last-commit/smkrv/ha-text-ai?style=flat-square) [![License: CC BY-NC-SA 4.0](https://img.shields.io/badge/License-CC%20BY--NC--SA%204.0-lightgrey.svg?style=flat-square)](https://creativecommons.org/licenses/by-nc-sa/4.0/) [![hacs_badge](https://img.shields.io/badge/HACS-Default-41BDF5.svg?style=flat-square)](https://github.com/hacs/integration)
![Deutsch](https://img.shields.io/badge/de-Deutsch-blue?style=flat-square) ![English](https://img.shields.io/badge/en-English-blue?style=flat-square) ![Español](https://img.shields.io/badge/es-Español-blue?style=flat-square) ![हिन्दी](https://img.shields.io/badge/hi-हिन्दी-blue?style=flat-square) ![Italiano](https://img.shields.io/badge/it-Italiano-blue?style=flat-square) ![Русский](https://img.shields.io/badge/ru-Русский-blue?style=flat-square) ![Српски](https://img.shields.io/badge/sr-Српски-blue?style=flat-square) ![中文](https://img.shields.io/badge/zh-中文-blue?style=flat-square) ![Deutsch](https://img.shields.io/badge/lang-DE-blue?style=flat-square) ![English](https://img.shields.io/badge/lang-EN-blue?style=flat-square) ![Español](https://img.shields.io/badge/lang-ES-blue?style=flat-square) ![हिन्दी](https://img.shields.io/badge/lang-HI-blue?style=flat-square) ![Italiano](https://img.shields.io/badge/lang-IT-blue?style=flat-square) ![Русский](https://img.shields.io/badge/lang-RU-blue?style=flat-square) ![Српски](https://img.shields.io/badge/lang-SR-blue?style=flat-square) ![中文](https://img.shields.io/badge/lang-ZH-blue?style=flat-square)
<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 ### Advanced AI Integration for [Home Assistant](https://www.home-assistant.io/) with LLM multi-provider support
</div> </div>
<p align="center"> <p align="center">
Transform your smart home experience with powerful AI assistance powered by multiple AI providers including OpenAI GPT and Anthropic Claude models. Get intelligent responses, automate complex scenarios, and enhance your home automation with advanced natural language processing. Transform your smart home experience with powerful AI assistance powered by multiple AI providers including OpenAI GPT, DeepSeek and Anthropic Claude models. Get intelligent responses, automate complex scenarios, and enhance your home automation with advanced natural language processing.
</p> </p>
--- ---
> [!IMPORTANT] > [!IMPORTANT]
> 🤝 Community Driven > 🤝 Community Driven: for more details on the integration,
> check out the discussion on the **[Home Assistant Community forum](https://community.home-assistant.io/t/ha-text-ai-transforming-home-automation-through-multi-llm-integration/799741)**
> >
> <a href="https://community.home-assistant.io/t/ha-text-ai-transforming-home-automation-with-multi-provider-language-models/799741"><img src="https://img.shields.io/badge/Community-blue?style=for-the-badge&logo=homeassistant&logoColor=white&color=03a9f4"/></a> > <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="210" height="auto"></a>
> >
> [Screenshots](assets/images/screenshots/screenshot.jpg) > [Screenshots](assets/images/screenshots/screenshot.jpg)
## 🌟 Features ## 🌟 Features
- 🧠 **Multi-Provider AI Integration**: Support for OpenAI GPT and Anthropic Claude models - 🧠 **Multi-Provider AI Integration**: Support for OpenAI GPT, DeepSeek and Anthropic Claude models
- 💬 **Advanced Language Processing**: Context-aware, multi-turn conversations - 💬 **Advanced Language Processing**: Context-aware, multi-turn conversations
- 📝 **Enhanced Memory Management**: Secure file-based history storage - 📝 **Enhanced Memory Management**: Secure file-based history storage
-**Performance Optimization**: Efficient token usage and smart rate limiting -**Performance Optimization**: Efficient token usage and smart rate limiting
@@ -42,6 +43,7 @@ Transform your smart home experience with powerful AI assistance powered by mult
### 🧠 **Multi-Provider AI Integration** ### 🧠 **Multi-Provider AI Integration**
- Support for OpenAI GPT models - Support for OpenAI GPT models
- Anthropic Claude integration - Anthropic Claude integration
- DeepSeek integration
- Custom API endpoints - Custom API endpoints
- Flexible model selection - Flexible model selection
@@ -104,11 +106,13 @@ Transform your smart home experience with powerful AI assistance powered by mult
## 📋 Prerequisites ## 📋 Prerequisites
- Home Assistant 2024.11 or later - Home Assistant 2024.12.0 or later (recommended for best compatibility)
- Active API key from: - Active API key from:
- OpenAI ([Get key](https://platform.openai.com/account/api-keys)) - OpenAI ([Get key](https://platform.openai.com/account/api-keys))
- Anthropic ([Get key](https://console.anthropic.com/)) - Anthropic ([Get key](https://console.anthropic.com/))
- DeepSeek ([Get key](https://platform.deepseek.com/api_keys))
- OpenRouter ([Get key](https://openrouter.ai/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 - Any OpenAI-compatible API provider
- Python 3.9 or newer - Python 3.9 or newer
- Stable internet connection - Stable internet connection
@@ -116,15 +120,34 @@ Transform your smart home experience with powerful AI assistance powered by mult
## Configuration Options ## Configuration Options
### 🔧 **Core Configuration Settings** ### 🔧 **Core Configuration Settings**
- 🌐 **API Provider**: OpenAI/Anthropic - 🌐 **API Provider**: OpenAI/Anthropic/DeepSeek/Gemini
- 🔑 **API Key**: Provider-specific authentication - 🔑 **API Key**: Provider-specific authentication
- 🤖 **Model Selection**: Flexible, provider-specific models - 🤖 **Model Selection**: Flexible, provider-specific models
- 🌡️ **Temperature**: Creativity control (0.0-2.0) - 🌡️ **Temperature**: Creativity control (0.0-2.0)
- 📏 **Max Tokens**: Response length limit (token usage is estimated using a heuristic method based on word count and specific word characteristics, which may differ from actual token usage) - 📏 **Max Tokens**: Response length limit (passed directly to the LLM API to control the maximum length of the response)
- ⏱️ **Request Interval**: API call throttling - ⏱️ **Request Interval**: API call throttling
- 💾 **History Size**: Number of messages to retain - 💾 **History Size**: Number of messages to retain
- 🌍 **Custom API Endpoint**: Optional advanced configuration - 🌍 **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> <details>
<summary>🌐 Potentially Compatible Providers</summary> <summary>🌐 Potentially Compatible Providers</summary>
@@ -156,17 +179,23 @@ To be compatible, a provider should support:
## ⚡ Installation ## ⚡ Installation
### HACS Installation (Recommended) ### 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> <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 1. Open HACS in Home Assistant
2. Click on "Integrations" 2. Click on "Integrations"
3. Click "..." in top right corner 3. Search for "HA Text AI"
4. Select "Custom repositories" 4. Click "Download"
5. Add repository URL: `https://github.com/smkrv/ha-text-ai` 5. Restart Home Assistant
6. Choose "Integration" as category
7. Click "Download"
8. 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 ### Manual Installation
1. Download the latest release 1. Download the latest release
@@ -191,7 +220,7 @@ Note: Also Integration has been submitted to HACS store and is currently pending
ha_text_ai: ha_text_ai:
api_provider: openai # Required api_provider: openai # Required
api_key: !secret ai_api_key # Required api_key: !secret ai_api_key # Required
model: gpt-4o-mini # Strongly recommended model: gpt-4o # Strongly recommended
temperature: 0.7 # Optional temperature: 0.7 # Optional
max_tokens: 1000 # Optional max_tokens: 1000 # Optional
request_interval: 1.0 # Optional request_interval: 1.0 # Optional
@@ -208,7 +237,7 @@ sensor:
- platform: ha_text_ai - platform: ha_text_ai
name: "My AI Assistant" # Required, unique identifier name: "My AI Assistant" # Required, unique identifier
api_provider: openai # Optional (inherits from platform) api_provider: openai # Optional (inherits from platform)
model: "gpt-4o-mini" # Optional model: "gpt-4o" # Optional
temperature: 0.7 # Optional temperature: 0.7 # Optional
max_tokens: 1000 # Optional max_tokens: 1000 # Optional
``` ```
@@ -219,16 +248,16 @@ sensor:
| Parameter | Type | Required | Default | Description | | 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 | | `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 | | `model` | String | ⚠️ | gpt-4o-mini | Strongly recommended: Specific AI model to use. Default varies by provider |
| `temperature` | Float | ❌ | 0.7 | Response creativity level (0.0-2.0) | | `temperature` | Float | ❌ | 0.1 | Response creativity level (0.0-2.0) |
| `max_tokens` | Integer | ❌ | 1000 | Maximum response length | | `max_tokens` | Integer | ❌ | 1000 | Maximum response length |
| `request_interval` | Float | ❌ | 1.0 | Delay between API requests | | `request_interval` | Float | ❌ | 1.0 | Delay between API requests |
| `api_endpoint` | URL | ⚠️ | Provider default | Custom API endpoint | | `api_endpoint` | URL | ⚠️ | Provider default | Custom API endpoint |
| `system_prompt` | String | ❌ | - | Default context for AI interactions | | `system_prompt` | String | ❌ | - | Default context for AI interactions |
| `max_history_size` | Integer | ❌ | 100 | Maximum number of conversation entries to store | | `max_history_size` | Integer | ❌ | 50 | Maximum number of conversation entries to store |
| `history_file_size` | Integer | ⚠️ | 1 | Maximum history file size in MB | | `context_messages` | Integer | | 5 | Number of previous messages to include in context (1-20) |
#### Sensor Configuration #### Sensor Configuration
@@ -237,25 +266,51 @@ sensor:
| `platform` | String | ✅ | - | Must be `ha_text_ai` | | `platform` | String | ✅ | - | Must be `ha_text_ai` |
| `name` | String | ✅ | - | Unique sensor identifier | | `name` | String | ✅ | - | Unique sensor identifier |
| `api_provider` | String | ❌ | Platform setting | Override global provider | | `api_provider` | String | ❌ | Platform setting | Override global provider |
| `model` | String | ⚠️ | Platform setting | Recommended: Override global model. If not specified, uses platform or provider default | | `model` | String | ⚠️ | Provider default | Recommended: Override global model (gpt-4o-mini, deepseek-chat, gemini-2.0-flash) |
| `temperature` | Float | ❌ | Platform setting | Override global temperature | | `temperature` | Float | ❌ | 0.1 | Override global temperature |
| `max_tokens` | Integer | ❌ | Platform setting | Override global max tokens | | `max_tokens` | Integer | ❌ | 1000 | Override global max tokens |
</details> </details>
## 🛠️ Available Services ## 🛠️ Available Services
### 🔄 Response Variables (New!)
**HA Text AI now supports response variables** - a powerful feature that returns AI responses directly from service calls, eliminating the need for separate text sensors and the 255-character limitation!
#### ✨ Key Benefits:
- **Unlimited response length** - No more 255-character truncation
- **Direct data access** - Get responses immediately in automations
- **Race condition prevention** - Eliminates conflicts in parallel automations
- **Simplified workflows** - No need to read from sensors
### ask_question ### ask_question
```yaml ```yaml
service: ha_text_ai.ask_question service: ha_text_ai.ask_question
data: data:
question: "What's the optimal temperature for sleeping?" question: "What's the optimal temperature for sleeping?"
model: "claude-3-sonnet" # optional model: "claude-3.5-sonnet" # optional
temperature: 0.5 # optional temperature: 0.5 # optional
max_tokens: 500 # optional max_tokens: 500 # optional
context_messages: 10 #optional, number of previous messages to include in context, default: 5 context_messages: 10 #optional, number of previous messages to include in context, default: 5
system_prompt: "You are a sleep optimization expert" # optional system_prompt: "You are a sleep optimization expert" # optional
instance: sensor.ha_text_ai_gpt instance: sensor.ha_text_ai_gpt
response_variable: ai_response # NEW! Store response data directly
```
#### 📊 Response Data Structure:
```yaml
# The service returns structured data:
response_text: "The optimal sleeping temperature is 65-68°F (18-20°C)..."
tokens_used: 150
prompt_tokens: 50
completion_tokens: 100
model_used: "claude-3.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 ### set_system_prompt
@@ -282,11 +337,157 @@ data:
```yaml ```yaml
service: ha_text_ai.get_history service: ha_text_ai.get_history
data: data:
limit: 5 # optional limit: 5 # optional, number of conversations to return (1-100)
filter_model: "gpt-4o" # optional 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 instance: sensor.ha_text_ai_gpt
``` ```
## 🚀 Advanced Automation Examples with Response Variables
### Example 1: Smart Home Advice with Direct Response
```yaml
automation:
- alias: "Get AI Home Advice"
trigger:
- platform: state
entity_id: input_button.ask_ai_advice
action:
- service: ha_text_ai.ask_question
data:
question: "What's the best way to optimize energy usage in my home?"
instance: sensor.ha_text_ai_gpt
response_variable: ai_advice
- service: notify.mobile_app
data:
title: "🏠 Smart Home Tip"
message: |
{{ ai_advice.response_text }}
📊 Tokens used: {{ ai_advice.tokens_used }}
🤖 Model: {{ ai_advice.model_used }}
```
### Example 2: Weather-Based AI Recommendations
```yaml
automation:
- alias: "Weather-Based AI Suggestions"
trigger:
- platform: numeric_state
entity_id: sensor.outdoor_temperature
below: 0
action:
- service: ha_text_ai.ask_question
data:
question: |
The outdoor temperature is {{ states('sensor.outdoor_temperature') }}°C.
What should I do to prepare my home for freezing weather?
system_prompt: "You are a home maintenance expert. Provide practical, actionable advice."
instance: sensor.ha_text_ai_gpt
response_variable: winter_advice
- if:
- condition: template
value_template: "{{ winter_advice.success }}"
then:
- service: persistent_notification.create
data:
title: "❄️ Winter Preparation Advice"
message: |
{{ winter_advice.response_text }}
Generated at: {{ winter_advice.timestamp }}
else:
- service: persistent_notification.create
data:
title: "⚠️ AI Service Error"
message: "Failed to get winter advice: {{ winter_advice.error }}"
```
### Example 3: Multi-Step AI Workflow
```yaml
automation:
- alias: "Multi-Step AI Analysis"
trigger:
- platform: state
entity_id: input_button.analyze_home_status
action:
# Step 1: Get current status analysis
- service: ha_text_ai.ask_question
data:
question: |
Current home status:
- Temperature: {{ states('sensor.indoor_temperature') }}°C
- Humidity: {{ states('sensor.indoor_humidity') }}%
- Energy usage: {{ states('sensor.power_consumption') }}W
Analyze this data and provide insights.
instance: sensor.ha_text_ai_gpt
response_variable: status_analysis
# Step 2: Get recommendations based on analysis
- service: ha_text_ai.ask_question
data:
question: |
Based on this analysis: "{{ status_analysis.response_text[:500] }}"
Provide 3 specific actionable recommendations for improvement.
context_messages: 2 # Include previous conversation
instance: sensor.ha_text_ai_gpt
response_variable: recommendations
# Step 3: Send comprehensive report
- service: notify.telegram
data:
title: "🏠 Home Analysis Report"
message: |
**Analysis:**
{{ status_analysis.response_text }}
**Recommendations:**
{{ recommendations.response_text }}
**Report Details:**
- Total tokens used: {{ status_analysis.tokens_used + recommendations.tokens_used }}
- Analysis model: {{ status_analysis.model_used }}
- Generated: {{ recommendations.timestamp }}
```
### 💡 Migration from Sensors to Response Variables
#### Old Method (Limited):
```yaml
# ❌ Old way - limited to 255 characters, race conditions
automation:
- alias: "Old AI Response Method"
action:
- service: ha_text_ai.ask_question
data:
question: "Long question here..."
instance: sensor.ha_text_ai_gpt
- delay: "00:00:05" # Wait for sensor update
- service: notify.mobile
data:
message: "{{ state_attr('sensor.ha_text_ai_gpt', 'response')[:255] }}..." # Truncated!
```
#### New Method (Unlimited):
```yaml
# ✅ New way - unlimited length, immediate access, no race conditions
automation:
- alias: "New AI Response Method"
action:
- service: ha_text_ai.ask_question
data:
question: "Long question here..."
instance: sensor.ha_text_ai_gpt
response_variable: ai_response # Direct access!
- service: notify.mobile
data:
message: "{{ ai_response.response_text }}" # Full response, no truncation!
```
### 🏷️ HA Text AI Sensor Naming Convention ### 🏷️ HA Text AI Sensor Naming Convention
#### Character Restrictions #### Character Restrictions
@@ -452,10 +653,10 @@ Conversation history stored in `.storage/ha_text_ai_history/` directory:
## 📘 FAQ ## 📘 FAQ
**Q: Which AI providers are supported?** **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?** **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?** **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. A: Depends on your API provider's plan. We recommend monitoring usage and implementing request throttling via `request_interval` configuration.
@@ -466,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?** **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. 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?** **Q: What are the token limits for different models?**
A: Use GPT-3.5-Turbo for most queries, implement caching, and optimize token usage. 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?** **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. 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.
@@ -533,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"><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"> <div align="center">
Made with ❤️ for the Home Assistant Community, Made with ❤️ for the Home Assistant Community
utilizing Claude 3.5 Sonnet, Gemini Pro 1.5, and Qwen 2.5 Coder 32B Instruct.
[Report Bug](https://github.com/smkrv/ha-text-ai/issues) · [Request Feature](https://github.com/smkrv/ha-text-ai/issues) [Report Bug](https://github.com/smkrv/ha-text-ai/issues) · [Request Feature](https://github.com/smkrv/ha-text-ai/issues)
</div> </div>
+148
View File
@@ -0,0 +1,148 @@
# Using response_variable with HA Text AI
After updating the HA Text AI integration, it now supports using the `response_variable` parameter in Home Assistant scripts and automations.
## What Changed
- Added response schema support in the `ha_text_ai.ask_question` service
- Service is now correctly registered with `supports_response=True` flag
- You can now use `response_variable` to capture AI response in a variable
## Example Usage in Script
```yaml
action: ha_text_ai.ask_question
data:
context_messages: 0
temperature: 0.7
max_tokens: 1000
instance: sensor.ha_text_ai_gemini
question: "What time is it?"
response_variable: ai_response
```
## Example Usage in Automation
```yaml
alias: "Get AI Response"
trigger:
- platform: state
entity_id: input_boolean.ask_ai
to: "on"
action:
- action: ha_text_ai.ask_question
data:
instance: sensor.ha_text_ai_gemini
question: "What's the current weather?"
temperature: 0.7
max_tokens: 500
response_variable: weather_response
- action: notify.persistent_notification
data:
title: "AI Response"
message: "{{ weather_response.response_text }}"
```
## Available Fields in response_variable
When you use `response_variable`, you will receive an object with the following fields:
- `response_text` (string) - The AI response text
- `tokens_used` (integer) - Total number of tokens used
- `prompt_tokens` (integer) - Number of tokens in the prompt
- `completion_tokens` (integer) - Number of tokens in the completion
- `model_used` (string) - The AI model that was used for the response
- `instance` (string) - The instance name that was used
- `question` (string) - The original question that was asked
- `timestamp` (string) - ISO timestamp when the response was generated
- `success` (boolean) - Whether the request was successful
- `error` (string) - Error message if the request failed
## Example Using Response Fields
```yaml
action:
- action: ha_text_ai.ask_question
data:
instance: sensor.ha_text_ai_gemini
question: "Tell me a joke"
response_variable: joke_response
- condition: template
value_template: "{{ joke_response.success }}"
- action: input_text.set_value
target:
entity_id: input_text.last_ai_response
data:
value: "{{ joke_response.response_text }}"
- action: input_number.set_value
target:
entity_id: input_number.tokens_used
data:
value: "{{ joke_response.tokens_used }}"
```
## Error Handling
```yaml
action:
- action: ha_text_ai.ask_question
data:
instance: sensor.ha_text_ai_gemini
question: "Test question"
response_variable: ai_result
- choose:
- conditions:
- condition: template
value_template: "{{ ai_result.success }}"
sequence:
- action: notify.mobile_app_phone
data:
title: "AI Response"
message: "{{ ai_result.response_text }}"
- conditions:
- condition: template
value_template: "{{ not ai_result.success }}"
sequence:
- action: notify.mobile_app_phone
data:
title: "AI Error"
message: "Error: {{ ai_result.error }}"
```
## Migration from Old Approach
**Old method (without response_variable):**
```yaml
# Ask question
- action: ha_text_ai.ask_question
data:
instance: sensor.ha_text_ai_gemini
question: "Hello!"
# Wait and read response from sensor
- delay: 00:00:05
- action: notify.mobile_app_phone
data:
message: "{{ states('sensor.ha_text_ai_gemini') }}"
```
**New method (with response_variable):**
```yaml
# Ask question and get response immediately
- action: ha_text_ai.ask_question
data:
instance: sensor.ha_text_ai_gemini
question: "Hello!"
response_variable: greeting_response
- action: notify.mobile_app_phone
data:
message: "{{ greeting_response.response_text }}"
```
The new approach is more reliable as it doesn't require waiting and reading from the sensor.
+107 -27
View File
@@ -35,18 +35,25 @@ from .const import (
CONF_MAX_TOKENS, CONF_MAX_TOKENS,
CONF_API_ENDPOINT, CONF_API_ENDPOINT,
CONF_REQUEST_INTERVAL, CONF_REQUEST_INTERVAL,
CONF_API_TIMEOUT,
CONF_API_PROVIDER, CONF_API_PROVIDER,
CONF_CONTEXT_MESSAGES, CONF_CONTEXT_MESSAGES,
API_PROVIDER_OPENAI, API_PROVIDER_OPENAI,
API_PROVIDER_ANTHROPIC, API_PROVIDER_ANTHROPIC,
API_PROVIDER_DEEPSEEK,
API_PROVIDER_GEMINI,
DEFAULT_MODEL, DEFAULT_MODEL,
DEFAULT_DEEPSEEK_MODEL,
DEFAULT_GEMINI_MODEL,
DEFAULT_TEMPERATURE, DEFAULT_TEMPERATURE,
DEFAULT_MAX_TOKENS, DEFAULT_MAX_TOKENS,
DEFAULT_OPENAI_ENDPOINT, DEFAULT_OPENAI_ENDPOINT,
DEFAULT_ANTHROPIC_ENDPOINT, DEFAULT_ANTHROPIC_ENDPOINT,
DEFAULT_DEEPSEEK_ENDPOINT,
DEFAULT_GEMINI_ENDPOINT,
DEFAULT_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL,
DEFAULT_API_TIMEOUT,
DEFAULT_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES,
API_TIMEOUT,
SERVICE_ASK_QUESTION, SERVICE_ASK_QUESTION,
SERVICE_CLEAR_HISTORY, SERVICE_CLEAR_HISTORY,
SERVICE_GET_HISTORY, SERVICE_GET_HISTORY,
@@ -69,6 +76,8 @@ SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
vol.Optional("temperature"): cv.positive_float, vol.Optional("temperature"): cv.positive_float,
vol.Optional("max_tokens"): cv.positive_int, vol.Optional("max_tokens"): cv.positive_int,
vol.Optional("context_messages"): 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({ SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
@@ -80,6 +89,9 @@ SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
vol.Required("instance"): cv.string, vol.Required("instance"): cv.string,
vol.Optional("limit"): cv.positive_int, vol.Optional("limit"): cv.positive_int,
vol.Optional("filter_model"): cv.string, 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: def get_coordinator_by_instance(hass: HomeAssistant, instance: str) -> HATextAICoordinator:
@@ -106,21 +118,48 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
# Initialize domain data storage # Initialize domain data storage
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
async def async_ask_question(call: ServiceCall) -> None: async def async_ask_question(call: ServiceCall) -> dict:
"""Handle ask_question service.""" """Handle ask_question service with response data."""
try: try:
coordinator = get_coordinator_by_instance(hass, call.data["instance"]) coordinator = get_coordinator_by_instance(hass, call.data["instance"])
await coordinator.async_ask_question( response = await coordinator.async_ask_question(
question=call.data["question"], question=call.data["question"],
model=call.data.get("model"), model=call.data.get("model"),
temperature=call.data.get("temperature"), temperature=call.data.get("temperature"),
max_tokens=call.data.get("max_tokens"), max_tokens=call.data.get("max_tokens"),
system_prompt=call.data.get("system_prompt"), system_prompt=call.data.get("system_prompt"),
context_messages=call.data.get("context_messages"), context_messages=call.data.get("context_messages"),
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: except Exception as err:
_LOGGER.error("Error asking question: %s", str(err)) _LOGGER.error("Error asking question: %s", str(err))
raise HomeAssistantError(f"Failed to process question: {str(err)}") # Return error response
return {
"response_text": "",
"tokens_used": 0,
"prompt_tokens": 0,
"completion_tokens": 0,
"model_used": call.data.get("model", ""),
"instance": call.data["instance"],
"question": call.data["question"],
"timestamp": datetime.now().isoformat(),
"success": False,
"error": str(err)
}
async def async_clear_history(call: ServiceCall) -> None: async def async_clear_history(call: ServiceCall) -> None:
"""Handle clear_history service.""" """Handle clear_history service."""
@@ -137,7 +176,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
coordinator = get_coordinator_by_instance(hass, call.data["instance"]) coordinator = get_coordinator_by_instance(hass, call.data["instance"])
return await coordinator.async_get_history( return await coordinator.async_get_history(
limit=call.data.get("limit"), 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: except Exception as err:
_LOGGER.error("Error getting history: %s", str(err)) _LOGGER.error("Error getting history: %s", str(err))
@@ -157,7 +199,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
DOMAIN, DOMAIN,
SERVICE_ASK_QUESTION, SERVICE_ASK_QUESTION,
async_ask_question, async_ask_question,
schema=SERVICE_SCHEMA_ASK_QUESTION schema=SERVICE_SCHEMA_ASK_QUESTION,
supports_response=True
) )
hass.services.async_register( hass.services.async_register(
@@ -230,20 +273,30 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True 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.""" """Check API availability for different providers."""
try: try:
if provider == API_PROVIDER_ANTHROPIC: if provider == API_PROVIDER_GEMINI:
# Gemini API does not support GET /models for validation, just check key presence
if headers.get("Authorization", "").replace("Bearer ", ""):
return True
else:
_LOGGER.error("Gemini API key is missing or empty")
return False
elif provider == API_PROVIDER_ANTHROPIC:
check_url = f"{endpoint}/v1/models" check_url = f"{endpoint}/v1/models"
elif provider == API_PROVIDER_DEEPSEEK:
check_url = f"{endpoint}/models"
else: # OpenAI else: # OpenAI
check_url = f"{endpoint}/models" 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: async with session.get(check_url, headers=headers) as response:
if response.status in [200, 404]: if response.status in [200, 404]:
return True return True
elif response.status == 401: elif response.status == 401:
raise ConfigEntryNotReady("Invalid API key") _LOGGER.error("Invalid API key")
return False
elif response.status == 429: elif response.status == 429:
_LOGGER.warning("Rate limit exceeded during API check") _LOGGER.warning("Rate limit exceeded during API check")
return False return False
@@ -259,26 +312,42 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.debug(f"Setting up HA Text AI entry: {entry.data}") _LOGGER.debug(f"Setting up HA Text AI entry: {entry.data}")
try: 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") _LOGGER.error("API provider not specified")
raise ConfigEntryNotReady("API provider is required") raise ConfigEntryNotReady("API provider is required")
# Get configuration
session = aiohttp_client.async_get_clientsession(hass) session = aiohttp_client.async_get_clientsession(hass)
api_provider = entry.data.get(CONF_API_PROVIDER)
model = entry.data.get(CONF_MODEL, DEFAULT_MODEL) # Get default endpoint based on provider
endpoint = entry.data.get( default_endpoint = {
CONF_API_ENDPOINT, API_PROVIDER_OPENAI: DEFAULT_OPENAI_ENDPOINT,
DEFAULT_OPENAI_ENDPOINT if api_provider == API_PROVIDER_OPENAI API_PROVIDER_ANTHROPIC: DEFAULT_ANTHROPIC_ENDPOINT,
else DEFAULT_ANTHROPIC_ENDPOINT API_PROVIDER_DEEPSEEK: DEFAULT_DEEPSEEK_ENDPOINT,
).rstrip('/') API_PROVIDER_GEMINI: DEFAULT_GEMINI_ENDPOINT,
api_key = entry.data[CONF_API_KEY] }.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) instance_name = entry.data.get(CONF_NAME, entry.entry_id)
request_interval = entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL) request_interval = config.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)
max_tokens = entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS) api_timeout = config.get(CONF_API_TIMEOUT, DEFAULT_API_TIMEOUT)
temperature = entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE) max_tokens = config.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)
max_history_size = entry.data.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY) temperature = config.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)
context_messages = entry.data.get(CONF_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES) 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 is_anthropic = api_provider == API_PROVIDER_ANTHROPIC
headers = { headers = {
@@ -292,7 +361,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
else: else:
headers["Authorization"] = f"Bearer {api_key}" 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") raise ConfigEntryNotReady("API connection failed")
_LOGGER.debug("Creating API client for %s with endpoint %s", api_provider, endpoint) _LOGGER.debug("Creating API client for %s with endpoint %s", api_provider, endpoint)
@@ -303,6 +372,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
headers=headers, headers=headers,
api_provider=api_provider, api_provider=api_provider,
model=model, model=model,
api_timeout=api_timeout,
) )
coordinator = HATextAICoordinator( coordinator = HATextAICoordinator(
@@ -316,6 +386,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
max_history_size=max_history_size, max_history_size=max_history_size,
context_messages=context_messages, context_messages=context_messages,
is_anthropic=is_anthropic, is_anthropic=is_anthropic,
api_timeout=api_timeout,
) )
_LOGGER.debug(f"Created coordinator for {instance_name}") _LOGGER.debug(f"Created coordinator for {instance_name}")
@@ -329,6 +400,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# Set up platforms # Set up platforms
await hass.config_entries.async_forward_entry_setups(entry, 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}") _LOGGER.debug(f"Setup completed for {instance_name}")
return True return True
@@ -337,6 +411,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.exception(f"Error setting up HA Text AI: {err}") _LOGGER.exception(f"Error setting up HA Text AI: {err}")
raise 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: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
try: try:
+299 -19
View File
@@ -11,13 +11,17 @@ import asyncio
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional
from aiohttp import ClientSession, ClientTimeout from aiohttp import ClientSession, ClientTimeout
from async_timeout import timeout from async_timeout import timeout
from datetime import datetime, timedelta
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from .const import ( from .const import (
API_TIMEOUT, DEFAULT_API_TIMEOUT,
API_RETRY_COUNT, API_RETRY_COUNT,
API_PROVIDER_ANTHROPIC, API_PROVIDER_ANTHROPIC,
API_PROVIDER_DEEPSEEK,
API_PROVIDER_OPENAI,
API_PROVIDER_GEMINI,
MIN_TEMPERATURE, MIN_TEMPERATURE,
MAX_TEMPERATURE, MAX_TEMPERATURE,
MIN_MAX_TOKENS, MIN_MAX_TOKENS,
@@ -37,6 +41,7 @@ class APIClient:
headers: Dict[str, str], headers: Dict[str, str],
api_provider: str, api_provider: str,
model: str, model: str,
api_timeout: int = DEFAULT_API_TIMEOUT,
) -> None: ) -> None:
"""Initialize API client.""" """Initialize API client."""
self.session = session self.session = session
@@ -44,21 +49,38 @@ class APIClient:
self.headers = headers self.headers = headers
self.api_provider = api_provider self.api_provider = api_provider
self.model = model 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( def _validate_parameters(
self, self,
temperature: float, temperature: float,
max_tokens: int, max_tokens: int,
) -> None: ) -> None:
"""Validate API parameters.""" """Validate API parameters with enhanced type checking."""
# Type validation
if not isinstance(temperature, (int, float)):
raise TypeError(f"Temperature must be a number, got {type(temperature)}")
if not isinstance(max_tokens, int):
raise TypeError(f"Max tokens must be an integer, got {type(max_tokens)}")
# Range validation
if not MIN_TEMPERATURE <= temperature <= MAX_TEMPERATURE: if not MIN_TEMPERATURE <= temperature <= MAX_TEMPERATURE:
raise ValueError( raise ValueError(
f"Temperature must be between {MIN_TEMPERATURE} and {MAX_TEMPERATURE}" f"Temperature must be between {MIN_TEMPERATURE} and {MAX_TEMPERATURE}, got {temperature}"
) )
if not MIN_MAX_TOKENS <= max_tokens <= MAX_MAX_TOKENS: if not MIN_MAX_TOKENS <= max_tokens <= MAX_MAX_TOKENS:
raise ValueError( raise ValueError(
f"Max tokens must be between {MIN_MAX_TOKENS} and {MAX_MAX_TOKENS}" f"Max tokens must be between {MIN_MAX_TOKENS} and {MAX_MAX_TOKENS}, got {max_tokens}"
) )
async def _make_request( async def _make_request(
@@ -67,10 +89,13 @@ class APIClient:
payload: Dict[str, Any], payload: Dict[str, Any],
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Make API request with retry logic.""" """Make API request with retry logic."""
_LOGGER.debug(f"API Request: URL={url}, Payload={payload}") # Log request without sensitive data
safe_payload = {k: v for k, v in payload.items() if k not in ['messages', 'system']}
_LOGGER.debug(f"API Request: URL={url}, Safe payload: {safe_payload}")
for attempt in range(API_RETRY_COUNT): for attempt in range(API_RETRY_COUNT):
try: try:
async with timeout(API_TIMEOUT): async with timeout(self.api_timeout):
async with self.session.post( async with self.session.post(
url, url,
json=payload, json=payload,
@@ -80,16 +105,18 @@ class APIClient:
_LOGGER.debug(f"Response status: {response.status}") _LOGGER.debug(f"Response status: {response.status}")
if response.status != 200: if response.status != 200:
error_data = await response.json() error_data = await response.json()
_LOGGER.error(f"API error: {error_data}") # Log error without sensitive data
raise HomeAssistantError(f"API error: {error_data}") safe_error = {k: v for k, v in error_data.items() if k not in ['message', 'details']}
_LOGGER.error(f"API error (status {response.status}): {safe_error}")
raise HomeAssistantError(f"API error: status {response.status}")
return await response.json() return await response.json()
except asyncio.TimeoutError: except asyncio.TimeoutError:
_LOGGER.warning(f"Timeout on attempt {attempt + 1}") _LOGGER.warning(f"Timeout on attempt {attempt + 1}/{API_RETRY_COUNT}")
if attempt == API_RETRY_COUNT - 1: if attempt == API_RETRY_COUNT - 1:
raise HomeAssistantError("API request timed out") raise HomeAssistantError("API request timed out")
await asyncio.sleep(1 * (attempt + 1)) await asyncio.sleep(1 * (attempt + 1))
except Exception as e: except Exception as e:
_LOGGER.warning(f"API request failed on attempt {attempt + 1}: {str(e)}") _LOGGER.warning(f"API request failed on attempt {attempt + 1}/{API_RETRY_COUNT}: {type(e).__name__}")
if attempt == API_RETRY_COUNT - 1: if attempt == API_RETRY_COUNT - 1:
raise raise
await asyncio.sleep(1 * (attempt + 1)) await asyncio.sleep(1 * (attempt + 1))
@@ -100,6 +127,8 @@ class APIClient:
messages: List[Dict[str, str]], messages: List[Dict[str, str]],
temperature: float, temperature: float,
max_tokens: int, max_tokens: int,
structured_output: bool = False,
json_schema: Optional[str] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Create completion using appropriate API.""" """Create completion using appropriate API."""
try: try:
@@ -107,27 +136,87 @@ class APIClient:
if self.api_provider == API_PROVIDER_ANTHROPIC: if self.api_provider == API_PROVIDER_ANTHROPIC:
return await self._create_anthropic_completion( 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,
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: else:
return await self._create_openai_completion( return await self._create_openai_completion(
model, messages, temperature, max_tokens model, messages, temperature, max_tokens,
structured_output, json_schema
) )
except (KeyError, IndexError) as e:
if "'choices'" in str(e) or "'message'" in str(e):
raise HomeAssistantError("Failed to get a response from the AI model. Please check your internet connection and try again later.")
else:
raise
except Exception as e: except Exception as e:
_LOGGER.error("API request failed: %s", str(e)) _LOGGER.error("API request failed: %s", str(e))
raise HomeAssistantError(f"API request failed: {str(e)}") raise HomeAssistantError(f"API request failed: {str(e)}")
async def _create_deepseek_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 DeepSeek API."""
url = f"{self.endpoint}/chat/completions"
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
"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": [
{
"message": {"content": data["choices"][0]["message"]["content"]},
}
],
"usage": {
"prompt_tokens": data["usage"]["prompt_tokens"],
"completion_tokens": data["usage"]["completion_tokens"],
"total_tokens": data["usage"]["total_tokens"],
},
}
async def _create_openai_completion( async def _create_openai_completion(
self, self,
model: str, model: str,
messages: List[Dict[str, str]], messages: List[Dict[str, str]],
temperature: float, temperature: float,
max_tokens: int, max_tokens: int,
structured_output: bool = False,
json_schema: Optional[str] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Create completion using OpenAI API.""" """Create completion using OpenAI API."""
url = f"{self.endpoint}/chat/completions" url = f"{self.endpoint}/chat/completions"
@@ -138,6 +227,24 @@ class APIClient:
"max_tokens": max_tokens, "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) data = await self._make_request(url, payload)
return { return {
"choices": [ "choices": [
@@ -158,6 +265,8 @@ class APIClient:
messages: List[Dict[str, str]], messages: List[Dict[str, str]],
temperature: float, temperature: float,
max_tokens: int, max_tokens: int,
structured_output: bool = False,
json_schema: Optional[str] = None,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""Create completion using Anthropic API.""" """Create completion using Anthropic API."""
url = f"{self.endpoint}/v1/messages" url = f"{self.endpoint}/v1/messages"
@@ -173,6 +282,20 @@ class APIClient:
else: else:
filtered_messages.append(msg) 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 = { payload = {
"model": model, "model": model,
"messages": filtered_messages, "messages": filtered_messages,
@@ -206,7 +329,164 @@ class APIClient:
_LOGGER.error(f"Connection check failed: {str(e)}") _LOGGER.error(f"Connection check failed: {str(e)}")
return False 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: async def shutdown(self) -> None:
"""Shutdown API client.""" """Shutdown API client."""
_LOGGER.debug("Shutting down API client") _LOGGER.debug("Shutting down API client")
await self.session.close() self._closed = True
# Do NOT close the shared Home Assistant session
+429 -77
View File
@@ -8,6 +8,7 @@ Config flow for HA text AI integration.
""" """
import logging import logging
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from datetime import datetime, timedelta
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
@@ -24,23 +25,33 @@ from .const import (
CONF_MAX_TOKENS, CONF_MAX_TOKENS,
CONF_API_ENDPOINT, CONF_API_ENDPOINT,
CONF_REQUEST_INTERVAL, CONF_REQUEST_INTERVAL,
CONF_API_TIMEOUT,
CONF_API_PROVIDER, CONF_API_PROVIDER,
CONF_CONTEXT_MESSAGES, CONF_CONTEXT_MESSAGES,
API_PROVIDER_OPENAI, API_PROVIDER_OPENAI,
API_PROVIDER_ANTHROPIC, API_PROVIDER_ANTHROPIC,
API_PROVIDER_DEEPSEEK,
API_PROVIDER_GEMINI,
API_PROVIDERS, API_PROVIDERS,
DEFAULT_MODEL, DEFAULT_MODEL,
DEFAULT_DEEPSEEK_MODEL,
DEFAULT_GEMINI_MODEL,
DEFAULT_TEMPERATURE, DEFAULT_TEMPERATURE,
DEFAULT_MAX_TOKENS, DEFAULT_MAX_TOKENS,
DEFAULT_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL,
DEFAULT_API_TIMEOUT,
DEFAULT_OPENAI_ENDPOINT, DEFAULT_OPENAI_ENDPOINT,
DEFAULT_ANTHROPIC_ENDPOINT, DEFAULT_ANTHROPIC_ENDPOINT,
DEFAULT_DEEPSEEK_ENDPOINT,
DEFAULT_GEMINI_ENDPOINT,
DEFAULT_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES,
MIN_TEMPERATURE, MIN_TEMPERATURE,
MAX_TEMPERATURE, MAX_TEMPERATURE,
MIN_MAX_TOKENS, MIN_MAX_TOKENS,
MAX_MAX_TOKENS, MAX_MAX_TOKENS,
MIN_REQUEST_INTERVAL, MIN_REQUEST_INTERVAL,
MIN_API_TIMEOUT,
MAX_API_TIMEOUT,
DEFAULT_NAME_PREFIX, DEFAULT_NAME_PREFIX,
DEFAULT_MAX_HISTORY, DEFAULT_MAX_HISTORY,
CONF_MAX_HISTORY_SIZE, CONF_MAX_HISTORY_SIZE,
@@ -90,9 +101,19 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
self._errors = {} self._errors = {}
if user_input is None: if user_input is None:
default_endpoint = ( # Selecting an endpoint by provider
DEFAULT_OPENAI_ENDPOINT if self._provider == API_PROVIDER_OPENAI default_endpoint = {
else DEFAULT_ANTHROPIC_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)
# 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( return self.async_show_form(
@@ -100,7 +121,7 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
data_schema=vol.Schema({ data_schema=vol.Schema({
vol.Required(CONF_NAME, default="my_assistant"): str, vol.Required(CONF_NAME, default="my_assistant"): str,
vol.Required(CONF_API_KEY): str, vol.Required(CONF_API_KEY): str,
vol.Required(CONF_MODEL, default=DEFAULT_MODEL): str, vol.Required(CONF_MODEL, default=default_model): str,
vol.Required(CONF_API_ENDPOINT, default=default_endpoint): str, vol.Required(CONF_API_ENDPOINT, default=default_endpoint): str,
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All( vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
vol.Coerce(float), vol.Coerce(float),
@@ -114,6 +135,10 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
vol.Coerce(float), vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL) 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( vol.Optional(
CONF_CONTEXT_MESSAGES, CONF_CONTEXT_MESSAGES,
default=DEFAULT_CONTEXT_MESSAGES default=DEFAULT_CONTEXT_MESSAGES
@@ -131,43 +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() 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: try:
# Validate and normalize the name
normalized_name = self._validate_and_normalize_name(input_copy[CONF_NAME]) normalized_name = self._validate_and_normalize_name(input_copy[CONF_NAME])
input_copy[CONF_NAME] = normalized_name input_copy[CONF_NAME] = normalized_name
except ValueError as e: except ValueError as e:
return self.async_show_form( return self.async_show_form(
step_id="provider", step_id="provider",
data_schema=vol.Schema({ data_schema=vol.Schema({
vol.Required(CONF_NAME, default=input_copy[CONF_NAME]): str, vol.Required(CONF_NAME, default=input_copy.get(CONF_NAME, "my_assistant")): str,
vol.Required(CONF_API_KEY, default=input_copy[CONF_API_KEY]): str, vol.Required(CONF_API_KEY, default=input_copy.get(CONF_API_KEY, "")): str,
vol.Required(CONF_MODEL, default=input_copy[CONF_MODEL]): str, vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, DEFAULT_GEMINI_MODEL if self._provider == API_PROVIDER_GEMINI else DEFAULT_MODEL)): str,
vol.Required(CONF_API_ENDPOINT, default=input_copy[CONF_API_ENDPOINT]): str, vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, DEFAULT_GEMINI_ENDPOINT if self._provider == API_PROVIDER_GEMINI else DEFAULT_OPENAI_ENDPOINT)): str,
vol.Optional(CONF_TEMPERATURE, default=input_copy.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.All( vol.Optional(CONF_TEMPERATURE, default=input_copy.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.All(
vol.Coerce(float), vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE) vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
), ),
vol.Optional(CONF_MAX_TOKENS, default=input_copy.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)): vol.All(
vol.Coerce(int),
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
),
vol.Optional(CONF_REQUEST_INTERVAL, default=input_copy.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL)
),
vol.Optional(CONF_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)} errors={"name": str(e)}
) )
try: try:
if not await self._async_validate_api(input_copy): # Special handling for Gemini API validation
return self.async_show_form( if self._provider == API_PROVIDER_GEMINI:
step_id="provider", # For Gemini, we just check if API key is present as there's no simple endpoint to validate
data_schema=vol.Schema({ if not input_copy.get(CONF_API_KEY):
}), self._errors["base"] = "invalid_auth"
errors=self._errors _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: except Exception as e:
# Handle any unexpected exceptions during validation
_LOGGER.exception("Unexpected error during API validation")
return self.async_show_form( return self.async_show_form(
step_id="provider", step_id="provider",
data_schema=vol.Schema({ data_schema=vol.Schema({
vol.Required(CONF_NAME, default=input_copy.get(CONF_NAME, "my_assistant")): str,
vol.Required(CONF_API_KEY, default=input_copy.get(CONF_API_KEY, "")): str,
vol.Required(CONF_MODEL, default=input_copy.get(CONF_MODEL, DEFAULT_GEMINI_MODEL if self._provider == API_PROVIDER_GEMINI else DEFAULT_MODEL)): str,
vol.Required(CONF_API_ENDPOINT, default=input_copy.get(CONF_API_ENDPOINT, DEFAULT_GEMINI_ENDPOINT if self._provider == API_PROVIDER_GEMINI else DEFAULT_OPENAI_ENDPOINT)): str,
# Other fields remain the same
}), }),
errors={"base": str(e)} errors={"base": str(e)}
) )
# All validation passed, create the entry
return await self._create_entry(input_copy) return await self._create_entry(input_copy)
def _validate_and_normalize_name(self, name: str) -> str: def _validate_and_normalize_name(self, name: str) -> str:
@@ -205,23 +372,34 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def _async_validate_api(self, user_input: Dict[str, Any]) -> bool: async def _async_validate_api(self, user_input: Dict[str, Any]) -> bool:
"""Validate API connection.""" """Validate API connection."""
try: try:
if CONF_API_KEY not in user_input:
_LOGGER.error("API validation error: 'api_key'")
self._errors["base"] = "invalid_auth"
return False
session = async_get_clientsession(self.hass) session = async_get_clientsession(self.hass)
headers = self._get_api_headers(user_input) headers = self._get_api_headers(user_input)
endpoint = user_input[CONF_API_ENDPOINT].rstrip('/') endpoint = user_input[CONF_API_ENDPOINT].rstrip('/')
check_url = ( if self._provider == API_PROVIDER_GEMINI:
f"{endpoint}/v1/models" if self._provider == API_PROVIDER_ANTHROPIC if not user_input[CONF_API_KEY]:
else f"{endpoint}/models"
)
async with session.get(check_url, headers=headers) as response:
if response.status == 401:
self._errors["base"] = "invalid_auth" self._errors["base"] = "invalid_auth"
return False return False
elif response.status not in [200, 404]:
self._errors["base"] = "cannot_connect"
return False
return True return True
else:
check_url = (
f"{endpoint}/v1/models" if self._provider == API_PROVIDER_ANTHROPIC
else f"{endpoint}/models"
)
async with session.get(check_url, headers=headers) as response:
if response.status == 401:
self._errors["base"] = "invalid_auth"
return False
elif response.status not in [200, 404]:
self._errors["base"] = "cannot_connect"
return False
return True
except Exception as err: except Exception as err:
_LOGGER.error("API validation error: %s", str(err)) _LOGGER.error("API validation error: %s", str(err))
@@ -230,6 +408,9 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
def _get_api_headers(self, user_input: Dict[str, Any]) -> Dict[str, str]: def _get_api_headers(self, user_input: Dict[str, Any]) -> Dict[str, str]:
"""Get API headers based on provider.""" """Get API headers based on provider."""
if CONF_API_KEY not in user_input:
return {"Content-Type": "application/json"}
api_key = user_input[CONF_API_KEY] api_key = user_input[CONF_API_KEY]
if self._provider == API_PROVIDER_ANTHROPIC: if self._provider == API_PROVIDER_ANTHROPIC:
@@ -238,6 +419,11 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"anthropic-version": "2023-06-01", "anthropic-version": "2023-06-01",
"Content-Type": "application/json" "Content-Type": "application/json"
} }
elif self._provider == API_PROVIDER_GEMINI:
return {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
return { return {
"Authorization": f"Bearer {api_key}", "Authorization": f"Bearer {api_key}",
"Content-Type": "application/json" "Content-Type": "application/json"
@@ -250,6 +436,12 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
unique_id = f"{DOMAIN}_{normalized_name}_{self._provider}".lower() unique_id = f"{DOMAIN}_{normalized_name}_{self._provider}".lower()
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 = { entry_data = {
CONF_API_PROVIDER: self._provider, CONF_API_PROVIDER: self._provider,
CONF_NAME: instance_name, CONF_NAME: instance_name,
@@ -257,10 +449,11 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
CONF_API_KEY: user_input.get(CONF_API_KEY), CONF_API_KEY: user_input.get(CONF_API_KEY),
CONF_API_ENDPOINT: user_input.get(CONF_API_ENDPOINT), CONF_API_ENDPOINT: user_input.get(CONF_API_ENDPOINT),
"unique_id": unique_id, "unique_id": unique_id,
CONF_MODEL: user_input.get(CONF_MODEL, DEFAULT_MODEL), CONF_MODEL: user_input.get(CONF_MODEL, default_model),
CONF_TEMPERATURE: user_input.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE), CONF_TEMPERATURE: user_input.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
CONF_MAX_TOKENS: user_input.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS), 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_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_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), CONF_MAX_HISTORY_SIZE: user_input.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY),
} }
@@ -280,70 +473,229 @@ class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
@callback @callback
def async_get_options_flow(config_entry: config_entries.ConfigEntry) -> config_entries.OptionsFlow: def async_get_options_flow(config_entry: config_entries.ConfigEntry) -> config_entries.OptionsFlow:
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return OptionsFlowHandler(config_entry) return OptionsFlowHandler()
class OptionsFlowHandler(config_entries.OptionsFlow): class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options flow.""" """Handle options flow."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None: def __init__(self) -> None:
"""Initialize options flow.""" """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: async def async_step_init(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
"""Manage the options.""" """Handle provider selection step."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)
current_data = {**self.config_entry.data, **self.config_entry.options} 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( return self.async_show_form(
step_id="init", step_id="init",
data_schema=vol.Schema({ data_schema=vol.Schema({
vol.Optional( vol.Required(
CONF_MODEL, CONF_API_PROVIDER,
default=current_data.get(CONF_MODEL, DEFAULT_MODEL) default=current_provider
): str, ): selector.SelectSelector(
vol.Optional( selector.SelectSelectorConfig(
CONF_TEMPERATURE, options=API_PROVIDERS,
default=current_data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE) translation_key="api_provider"
): 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.All(
vol.Coerce(int),
vol.Range(min=1, max=20)
), ),
vol.Optional( }),
CONF_MAX_HISTORY_SIZE, description_placeholders={
default=current_data.get( "current_provider": current_provider
CONF_MAX_HISTORY_SIZE, }
DEFAULT_MAX_HISTORY
)
): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=100)
),
})
) )
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)
),
})
+26 -4
View File
@@ -12,6 +12,8 @@ from typing import Final
import voluptuous as vol import voluptuous as vol
from homeassistant.const import Platform, CONF_API_KEY, CONF_NAME from homeassistant.const import Platform, CONF_API_KEY, CONF_NAME
from homeassistant.helpers import config_validation as cv from homeassistant.helpers import config_validation as cv
import logging
_LOGGER = logging.getLogger(__name__)
# Domain and platforms # Domain and platforms
DOMAIN: Final = "ha_text_ai" DOMAIN: Final = "ha_text_ai"
@@ -21,10 +23,14 @@ PLATFORMS: list[str] = ["sensor"]
CONF_API_PROVIDER: Final = "api_provider" CONF_API_PROVIDER: Final = "api_provider"
API_PROVIDER_OPENAI: Final = "openai" API_PROVIDER_OPENAI: Final = "openai"
API_PROVIDER_ANTHROPIC: Final = "anthropic" API_PROVIDER_ANTHROPIC: Final = "anthropic"
API_PROVIDER_DEEPSEEK: Final = "deepseek"
API_PROVIDER_GEMINI: Final = "gemini"
API_PROVIDERS: Final = [ API_PROVIDERS: Final = [
API_PROVIDER_OPENAI, API_PROVIDER_OPENAI,
API_PROVIDER_ANTHROPIC API_PROVIDER_ANTHROPIC,
API_PROVIDER_DEEPSEEK,
API_PROVIDER_GEMINI
] ]
# Read version from manifest.json # Read version from manifest.json
@@ -46,6 +52,8 @@ except Exception as err:
# Default endpoints # Default endpoints
DEFAULT_OPENAI_ENDPOINT: Final = "https://api.openai.com/v1" DEFAULT_OPENAI_ENDPOINT: Final = "https://api.openai.com/v1"
DEFAULT_ANTHROPIC_ENDPOINT: Final = "https://api.anthropic.com" 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 # Configuration constants
CONF_MODEL: Final = "model" CONF_MODEL: Final = "model"
@@ -53,10 +61,13 @@ CONF_TEMPERATURE: Final = "temperature"
CONF_MAX_TOKENS: Final = "max_tokens" CONF_MAX_TOKENS: Final = "max_tokens"
CONF_API_ENDPOINT: Final = "api_endpoint" CONF_API_ENDPOINT: Final = "api_endpoint"
CONF_REQUEST_INTERVAL: Final = "request_interval" CONF_REQUEST_INTERVAL: Final = "request_interval"
CONF_API_TIMEOUT: Final = "api_timeout"
CONF_INSTANCE: Final = "instance" CONF_INSTANCE: Final = "instance"
CONF_MAX_HISTORY_SIZE: Final = "max_history_size" # Correct constant name CONF_MAX_HISTORY_SIZE: Final = "max_history_size" # Correct constant name
CONF_IS_ANTHROPIC: Final = "is_anthropic" CONF_IS_ANTHROPIC: Final = "is_anthropic"
CONF_CONTEXT_MESSAGES: Final = "context_messages" CONF_CONTEXT_MESSAGES: Final = "context_messages"
CONF_STRUCTURED_OUTPUT: Final = "structured_output"
CONF_JSON_SCHEMA: Final = "json_schema"
ABSOLUTE_MAX_HISTORY_SIZE = 500 ABSOLUTE_MAX_HISTORY_SIZE = 500
MAX_ATTRIBUTE_SIZE = 4 * 1024 MAX_ATTRIBUTE_SIZE = 4 * 1024
@@ -65,10 +76,13 @@ ICONS_SUBDOMAIN = "icons"
# Default values # Default values
DEFAULT_MODEL: Final = "gpt-4o-mini" DEFAULT_MODEL: Final = "gpt-4o-mini"
DEFAULT_DEEPSEEK_MODEL: Final = "deepseek-chat"
DEFAULT_GEMINI_MODEL: Final = "gemini-2.0-flash"
DEFAULT_TEMPERATURE: Final = 0.1 DEFAULT_TEMPERATURE: Final = 0.1
DEFAULT_MAX_TOKENS: Final = 1000 DEFAULT_MAX_TOKENS: Final = 1000
DEFAULT_REQUEST_INTERVAL: Final = 1.0 DEFAULT_REQUEST_INTERVAL: Final = 1.0
DEFAULT_TIMEOUT: Final = 30 DEFAULT_TIMEOUT: Final = 30
DEFAULT_API_TIMEOUT: Final = 30
DEFAULT_MAX_HISTORY: Final = 50 DEFAULT_MAX_HISTORY: Final = 50
DEFAULT_NAME: Final = "HA Text AI" DEFAULT_NAME: Final = "HA Text AI"
DEFAULT_NAME_PREFIX = "ha_text_ai" DEFAULT_NAME_PREFIX = "ha_text_ai"
@@ -80,12 +94,14 @@ TRUNCATION_INDICATOR = " ... "
MIN_TEMPERATURE: Final = 0.0 MIN_TEMPERATURE: Final = 0.0
MAX_TEMPERATURE: Final = 2.0 MAX_TEMPERATURE: Final = 2.0
MIN_MAX_TOKENS: Final = 1 MIN_MAX_TOKENS: Final = 1
MAX_MAX_TOKENS: Final = 4096 MAX_MAX_TOKENS: Final = 100000
MIN_REQUEST_INTERVAL: Final = 0.1 MIN_REQUEST_INTERVAL: Final = 0.1
MAX_REQUEST_INTERVAL: Final = 60.0 MAX_REQUEST_INTERVAL: Final = 60.0
MIN_API_TIMEOUT: Final = 5
MAX_API_TIMEOUT: Final = 600
# API constants # API constants
API_TIMEOUT: Final = 30 API_TIMEOUT: Final = 30 # Legacy constant, use CONF_API_TIMEOUT from config
API_RETRY_COUNT: Final = 3 API_RETRY_COUNT: Final = 3
# Service names # Service names
@@ -185,7 +201,9 @@ SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
vol.Optional("context_messages"): vol.All( vol.Optional("context_messages"): vol.All(
vol.Coerce(int), vol.Coerce(int),
vol.Range(min=1, max=20) 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({ SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
@@ -222,6 +240,10 @@ CONFIG_SCHEMA = vol.Schema({
vol.Coerce(float), vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL, max=MAX_REQUEST_INTERVAL) 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.Optional(CONF_MAX_HISTORY_SIZE, default=DEFAULT_MAX_HISTORY): vol.All( # Correct usage
vol.Coerce(int), vol.Coerce(int),
vol.Range(min=1, max=100), vol.Range(min=1, max=100),
+163 -151
View File
@@ -37,6 +37,7 @@ from .const import (
DEFAULT_MAX_TOKENS, DEFAULT_MAX_TOKENS,
DEFAULT_TEMPERATURE, DEFAULT_TEMPERATURE,
DEFAULT_MAX_HISTORY, DEFAULT_MAX_HISTORY,
DEFAULT_API_TIMEOUT,
DEFAULT_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES,
ABSOLUTE_MAX_HISTORY_SIZE, ABSOLUTE_MAX_HISTORY_SIZE,
MAX_ATTRIBUTE_SIZE, MAX_ATTRIBUTE_SIZE,
@@ -46,19 +47,6 @@ from .const import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def _check_memory_available(self):
"""Check if enough memory is available."""
memory = psutil.virtual_memory()
# Log the total and available memory
_LOGGER.debug("Total memory: %s, Available memory: %s", memory.total, memory.available)
if memory.available > 1024 * 1024 * 100: # 100MB
_LOGGER.debug("Sufficient memory available: %s bytes", memory.available)
return True
else:
_LOGGER.warning("Insufficient memory available: %s bytes", memory.available)
return False
class AsyncFileHandler: class AsyncFileHandler:
"""Async context manager for file operations.""" """Async context manager for file operations."""
@@ -89,6 +77,7 @@ class HATextAICoordinator(DataUpdateCoordinator):
max_history_size: int = DEFAULT_MAX_HISTORY, max_history_size: int = DEFAULT_MAX_HISTORY,
context_messages: int = DEFAULT_CONTEXT_MESSAGES, context_messages: int = DEFAULT_CONTEXT_MESSAGES,
is_anthropic: bool = False, is_anthropic: bool = False,
api_timeout: int = DEFAULT_API_TIMEOUT,
) -> None: ) -> None:
"""Initialize coordinator.""" """Initialize coordinator."""
self.instance_name = instance_name self.instance_name = instance_name
@@ -128,6 +117,7 @@ class HATextAICoordinator(DataUpdateCoordinator):
ABSOLUTE_MAX_HISTORY_SIZE ABSOLUTE_MAX_HISTORY_SIZE
) )
self.is_anthropic = is_anthropic self.is_anthropic = is_anthropic
self.api_timeout = api_timeout
# Initialize essential attributes # Initialize essential attributes
self._is_processing = False self._is_processing = False
@@ -199,16 +189,10 @@ class HATextAICoordinator(DataUpdateCoordinator):
# Maximum history file size (1 MB) from const.py # Maximum history file size (1 MB) from const.py
self._max_history_file_size = MAX_HISTORY_FILE_SIZE self._max_history_file_size = MAX_HISTORY_FILE_SIZE
# Asynchronous file initialization self.context_messages = context_messages
hass.async_create_task(self.async_initialize_history_file())
_LOGGER.info(f"Initialized HA Text AI coordinator with instance: {instance_name}") _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 @property
def last_response(self) -> Dict[str, Any]: def last_response(self) -> Dict[str, Any]:
""" """
@@ -549,25 +533,57 @@ class HATextAICoordinator(DataUpdateCoordinator):
return 0 return 0
def _sync_write_history_entry(self, entry: dict) -> None: def _sync_write_history_entry(self, entry: dict) -> None:
"""Synchronous method to write history entry.""" """Synchronous method to write history entry with enhanced error handling."""
try: try:
history = [] history = []
if os.path.exists(self._history_file): if os.path.exists(self._history_file):
with open(self._history_file, 'r') as f: try:
content = f.read() with open(self._history_file, 'r', encoding='utf-8') as f:
if content: content = f.read()
history = json.loads(content) if content.strip():
history = json.loads(content)
except (json.JSONDecodeError, UnicodeDecodeError) as e:
_LOGGER.warning(f"Corrupted history file, creating new: {e}")
# Backup corrupted file
backup_path = f"{self._history_file}.corrupted.{dt_util.utcnow().strftime('%Y%m%d_%H%M%S')}"
try:
os.rename(self._history_file, backup_path)
_LOGGER.info(f"Corrupted history backed up to: {backup_path}")
except OSError:
pass
except PermissionError as e:
_LOGGER.error(f"Permission denied reading history file: {e}")
raise
except OSError as e:
_LOGGER.error(f"OS error reading history file: {e}")
raise
history.append(entry) history.append(entry)
if len(history) > self.max_history_size: if len(history) > self.max_history_size:
history = history[-self.max_history_size:] history = history[-self.max_history_size:]
with open(self._history_file, 'w') as f: # Write with atomic operation
json.dump(history, f, indent=2) temp_file = f"{self._history_file}.tmp"
try:
with open(temp_file, 'w', encoding='utf-8') as f:
json.dump(history, f, indent=2, ensure_ascii=False)
os.replace(temp_file, self._history_file)
except PermissionError as e:
_LOGGER.error(f"Permission denied writing history file: {e}")
raise
except OSError as e:
_LOGGER.error(f"OS error writing history file: {e}")
# Clean up temp file if it exists
try:
os.remove(temp_file)
except OSError:
pass
raise
except Exception as e: except Exception as e:
_LOGGER.error(f"Synchronous history entry writing failed: {e}") _LOGGER.error(f"Synchronous history entry writing failed: {e}")
raise
async def _rotate_history(self) -> None: async def _rotate_history(self) -> None:
"""Rotate conversation history with file management.""" """Rotate conversation history with file management."""
@@ -796,46 +812,6 @@ class HATextAICoordinator(DataUpdateCoordinator):
"normalized_name": self.normalized_name, "normalized_name": self.normalized_name,
} }
def _calculate_context_tokens(self, messages: List[Dict[str, str]], model: Optional[str] = None) -> int:
total_tokens = 0
# Compile regular expressions for performance
number_pattern = re.compile(r'[0-9]')
special_char_pattern = re.compile(r'[^\w\s]')
whitespace_pattern = re.compile(r'\s+')
try:
for message in messages:
text = message.get('content', '')
if text:
# Normalize whitespace
text = whitespace_pattern.sub(' ', text.strip())
# Advanced token estimation heuristics
words = text.split()
for word in words:
# Complexity-based token calculation
if len(word) > 8: # Long words
total_tokens += 2
elif number_pattern.search(word): # Words with numbers
total_tokens += 1.5
elif special_char_pattern.search(word): # Words with special characters
total_tokens += 1.5
elif word.isupper(): # Acronyms and technical terms
total_tokens += 1.5
else: # Regular words
total_tokens += 1
# Additional correction for technical texts
total_tokens = math.ceil(total_tokens * 1.2)
return int(total_tokens)
except Exception as e:
_LOGGER.error(f"Token calculation error: {e}. Processed {len(messages)} messages.")
# Safe fallback with minimal token estimation
return len(messages) * 100
async def async_ask_question( async def async_ask_question(
self, self,
question: str, question: str,
@@ -844,6 +820,8 @@ class HATextAICoordinator(DataUpdateCoordinator):
max_tokens: Optional[int] = None, max_tokens: Optional[int] = None,
system_prompt: Optional[str] = None, system_prompt: Optional[str] = None,
context_messages: Optional[int] = None, context_messages: Optional[int] = None,
structured_output: bool = False,
json_schema: Optional[str] = None,
) -> dict: ) -> dict:
""" """
Process a question with optional parameters. Process a question with optional parameters.
@@ -859,12 +837,15 @@ class HATextAICoordinator(DataUpdateCoordinator):
max_tokens: Optional maximum response length max_tokens: Optional maximum response length
system_prompt: Optional system-level instruction system_prompt: Optional system-level instruction
context_messages: Optional number of context messages to include 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: Returns:
Full response dictionary from the AI Full response dictionary from the AI
""" """
return await self.async_process_question( 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( async def async_process_question(
@@ -875,10 +856,10 @@ class HATextAICoordinator(DataUpdateCoordinator):
max_tokens: Optional[int] = None, max_tokens: Optional[int] = None,
system_prompt: Optional[str] = None, system_prompt: Optional[str] = None,
context_messages: Optional[int] = None, context_messages: Optional[int] = None,
structured_output: bool = False,
json_schema: Optional[str] = None,
) -> dict: ) -> dict:
""" """Process question with context management."""
Enhanced question processing with intelligent token management.
"""
if self.client is None: if self.client is None:
raise HomeAssistantError("AI client not initialized") raise HomeAssistantError("AI client not initialized")
@@ -900,62 +881,25 @@ class HATextAICoordinator(DataUpdateCoordinator):
if temp_system_prompt: if temp_system_prompt:
messages.append({"role": "system", "content": temp_system_prompt}) messages.append({"role": "system", "content": temp_system_prompt})
# Context history management # Add context from history
context_history = self._conversation_history[-temp_context_messages:] context_history = self._conversation_history[-temp_context_messages:]
# Comprehensive token calculation
context_tokens = self._calculate_context_tokens(
[{"content": entry["question"]} for entry in context_history] +
[{"content": entry["response"]} for entry in context_history] +
[{"content": question}],
temp_model
)
# Dynamic token allocation
available_tokens = max(0, temp_max_tokens - context_tokens)
# Context trimming if over token limit
if context_tokens > temp_max_tokens:
_LOGGER.warning(
f"Token limit exceeded. "
f"Context: {context_tokens}, "
f"Max: {temp_max_tokens}"
)
# Intelligent context reduction
while context_tokens > temp_max_tokens // 2 and context_history:
context_history.pop(0)
context_tokens = self._calculate_context_tokens(
[{"content": entry["question"]} for entry in context_history] +
[{"content": entry["response"]} for entry in context_history] +
[{"content": question}],
temp_model
)
# Rebuild messages with trimmed context
for entry in context_history: for entry in context_history:
messages.append({"role": "user", "content": entry["question"]}) messages.append({"role": "user", "content": entry["question"]})
messages.append({"role": "assistant", "content": entry["response"]}) messages.append({"role": "assistant", "content": entry["response"]})
# Add current question
messages.append({"role": "user", "content": question}) messages.append({"role": "user", "content": question})
# Detailed token logging # Process message
_LOGGER.debug(
f"Token Analysis: "
f"Context Tokens: {context_tokens}, "
f"Max Tokens: {temp_max_tokens}, "
f"Available Tokens: {available_tokens}"
)
# Prepare API call with dynamic token management
kwargs = { kwargs = {
"model": temp_model, "model": temp_model,
"temperature": temp_temperature, "temperature": temp_temperature,
"max_tokens": min(temp_max_tokens, available_tokens), "max_tokens": temp_max_tokens,
"messages": messages, "messages": messages,
"structured_output": structured_output,
"json_schema": json_schema,
} }
# Process message
response = await self.async_process_message(question, **kwargs) response = await self.async_process_message(question, **kwargs)
# Update metrics # Update metrics
@@ -968,7 +912,7 @@ class HATextAICoordinator(DataUpdateCoordinator):
return response return response
except Exception as err: except Exception as err:
self._handle_error(err) await self._handle_error(err)
raise HomeAssistantError(f"Failed to process question: {err}") raise HomeAssistantError(f"Failed to process question: {err}")
finally: finally:
@@ -978,55 +922,52 @@ class HATextAICoordinator(DataUpdateCoordinator):
async def async_process_message(self, question: str, **kwargs) -> dict: async def async_process_message(self, question: str, **kwargs) -> dict:
"""Process message using the AI client.""" """Process message using the AI client."""
try: try:
async with asyncio.timeout(60): # 60 second timeout structured_output = kwargs.pop("structured_output", False)
if self.is_anthropic: json_schema = kwargs.pop("json_schema", None)
response = await self._process_anthropic_message(question, **kwargs)
else: async with asyncio.timeout(self.api_timeout):
response = await self._process_openai_message(question, **kwargs) # 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 = { self.last_response = {
"timestamp": dt_util.utcnow().isoformat(), "timestamp": timestamp,
"question": question, "question": question,
"response": response["content"], "response": response["content"],
"model": kwargs.get("model", self.model), "model": model_used,
"instance": self.instance_name, "instance": self.instance_name,
"normalized_name": self.normalized_name, "normalized_name": self.normalized_name,
"error": None, "error": None,
} }
return response return enhanced_response
except asyncio.TimeoutError: except asyncio.TimeoutError:
raise HomeAssistantError("Request timed out") raise HomeAssistantError("Request timed out")
except Exception as err: except Exception as err:
self._handle_error(err) await self._handle_error(err)
raise raise
async def _process_anthropic_message(self, question: str, **kwargs) -> dict: async def _process_openai_message(self, question: str, structured_output: bool = False,
"""Process message using Anthropic API.""" json_schema: Optional[str] = None, **kwargs) -> dict:
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:
"""Process message using OpenAI API.""" """Process message using OpenAI API."""
try: try:
response = await self.client.create( response = await self.client.create(
@@ -1034,6 +975,8 @@ class HATextAICoordinator(DataUpdateCoordinator):
messages=kwargs["messages"], messages=kwargs["messages"],
temperature=kwargs["temperature"], temperature=kwargs["temperature"],
max_tokens=kwargs["max_tokens"], max_tokens=kwargs["max_tokens"],
structured_output=structured_output,
json_schema=json_schema,
) )
return { return {
@@ -1132,15 +1075,84 @@ class HATextAICoordinator(DataUpdateCoordinator):
_LOGGER.error(f"Error clearing history: {e}") _LOGGER.error(f"Error clearing history: {e}")
_LOGGER.debug(traceback.format_exc()) _LOGGER.debug(traceback.format_exc())
async def async_get_history(self) -> List[Dict[str, str]]: async def async_get_history(
"""Get conversation history.""" self,
return self._conversation_history 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: async def async_set_system_prompt(self, prompt: str) -> None:
"""Set system prompt.""" """Set system prompt."""
self._system_prompt = prompt self._system_prompt = prompt
await self.async_update_ha_state() await self.async_update_ha_state()
def _check_memory_available(self) -> bool:
"""Check if enough memory is available."""
try:
memory = psutil.virtual_memory()
# Log the total and available memory
_LOGGER.debug("Total memory: %s, Available memory: %s", memory.total, memory.available)
if memory.available > 1024 * 1024 * 100: # 100MB
_LOGGER.debug("Sufficient memory available: %s bytes", memory.available)
return True
else:
_LOGGER.warning("Insufficient memory available: %s bytes", memory.available)
return False
except Exception as e:
_LOGGER.error("Error checking memory availability: %s", e)
return True # Assume memory is available if check fails
async def async_shutdown(self) -> None: async def async_shutdown(self) -> None:
"""Shutdown coordinator.""" """Shutdown coordinator."""
_LOGGER.debug(f"Shutting down coordinator for {self.instance_name}") _LOGGER.debug(f"Shutting down coordinator for {self.instance_name}")
+10 -8
View File
@@ -13,16 +13,18 @@
"loggers": ["custom_components.ha_text_ai"], "loggers": ["custom_components.ha_text_ai"],
"mqtt": [], "mqtt": [],
"quality_scale": "silver", "quality_scale": "silver",
"requirements": [ "requirements": [
"openai>=1.12.0", "aiofiles>=23.0.0",
"anthropic>=0.8.0", "aiohttp>=3.8.0",
"aiohttp>=3.8.0", "anthropic>=0.8.0",
"async-timeout>=4.0.0", "async-timeout>=4.0.0",
"certifi>=2024.2.2" "certifi>=2024.2.2",
], "google-genai>=1.16.0",
"openai>=1.12.0"
],
"single_config_entry": false, "single_config_entry": false,
"ssdp": [], "ssdp": [],
"usb": [], "usb": [],
"version": "2.1.0", "version": "2.3.0",
"zeroconf": [] "zeroconf": []
} }
+1
View File
@@ -9,6 +9,7 @@ Sensor platform for HA Text AI.
import logging import logging
import math import math
from typing import Any, Dict from typing import Any, Dict
from datetime import datetime, timedelta
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorEntity, SensorEntity,
+22 -3
View File
@@ -3,6 +3,7 @@ ask_question:
description: >- description: >-
Send a question to the AI model and receive a detailed response. Send a question to the AI model and receive a detailed response.
The response will be stored in the conversation history and can be retrieved later. The response will be stored in the conversation history and can be retrieved later.
This service now returns response data directly, eliminating the need to read from sensors.
fields: fields:
instance: instance:
name: Instance name: Instance
@@ -63,16 +64,34 @@ ask_question:
max_tokens: max_tokens:
name: Max Tokens name: Max Tokens
description: Maximum length of the response (1-4096 tokens) description: Maximum length of the response (tokens)
required: false required: false
default: 1000 default: 1000
selector: selector:
number: number:
min: 1 min: 1
max: 4096 max: 100000
step: 1 step: 1
mode: box 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: clear_history:
name: Clear History name: Clear History
description: >- description: >-
@@ -134,7 +153,7 @@ get_history:
required: false required: false
default: false default: false
selector: selector:
boolean: boolean: {}
sort_order: sort_order:
name: Sort Order name: Sort Order
@@ -19,8 +19,9 @@
"model": "Zu verwendendes AI-Modell", "model": "Zu verwendendes AI-Modell",
"api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)", "api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)",
"temperature": "Kreativität der Antwort (0-2, niedriger = fokussierter)", "temperature": "Kreativität der Antwort (0-2, niedriger = fokussierter)",
"max_tokens": "Maximale Länge der Antwort (1-4096 Token)", "max_tokens": "Maximale Länge der Antwort (1-100000 Token)",
"request_interval": "Minimale Zeit zwischen Anfragen (0,1-60 Sekunden)", "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)", "context_messages": "Anzahl der zu behaltenden Kontextnachrichten (1-20)",
"max_history_size": "Maximale Größe des Gesprächsverlaufs (1-100)" "max_history_size": "Maximale Größe des Gesprächsverlaufs (1-100)"
} }
@@ -33,10 +34,11 @@
"api_key": "API-Schlüssel zur Authentifizierung", "api_key": "API-Schlüssel zur Authentifizierung",
"model": "Zu verwendendes AI-Modell", "model": "Zu verwendendes AI-Modell",
"temperature": "Kreativität der Antwort (0-2, niedriger = fokussierter)", "temperature": "Kreativität der Antwort (0-2, niedriger = fokussierter)",
"max_tokens": "Maximale Länge der Antwort (1-4096 Token)", "max_tokens": "Maximale Länge der Antwort (1-100000 Token)",
"api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)", "api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)",
"api_provider": "API-Anbieter", "api_provider": "API-Anbieter",
"request_interval": "Minimale Zeit zwischen Anfragen (0,1-60 Sekunden)", "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)", "context_messages": "Anzahl der zu behaltenden Kontextnachrichten (1-20)",
"max_history_size": "Maximale Größe des Gesprächsverlaufs (1-100)" "max_history_size": "Maximale Größe des Gesprächsverlaufs (1-100)"
} }
@@ -72,13 +74,23 @@
"options": { "options": {
"step": { "step": {
"init": { "init": {
"title": "Instanzeinstellungen aktualisieren", "title": "Anbieter auswählen",
"description": "Ändern Sie die Einstellungen für diese AI-Assistenteninstanz.", "description": "Wählen Sie den AI-Anbieter für diese Instanz. Die Integration wird nach dem Speichern der Änderungen neu geladen.",
"data": { "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", "model": "AI-Modell",
"temperature": "Kreativität der Antwort (0-2)", "temperature": "Kreativität der Antwort (0-2)",
"max_tokens": "Maximale Länge der Antwort (1-4096)", "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)", "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)" "max_history_size": "Maximale Größe des Gesprächsverlaufs (1-100)"
} }
@@ -88,15 +100,17 @@
"selector": { "selector": {
"api_provider": { "api_provider": {
"options": { "options": {
"openai": "OpenAI (kompatibel)", "openai": "OpenAI (compatible)",
"anthropic": "Anthropic (kompatibel)" "anthropic": "Anthropic (compatible)",
"deepseek": "DeepSeek",
"gemini": "Google Gemini"
} }
} }
}, },
"services": { "services": {
"ask_question": { "ask_question": {
"name": "Frage stellen (HA Text AI)", "name": "Frage stellen (HA Text AI)",
"description": "Stellen Sie eine Frage an das AI-Modell und erhalten Sie eine detaillierte Antwort. Die Antwort wird im Gesprächsverlauf gespeichert und kann später abgerufen werden.", "description": "Stellen Sie eine Frage an das AI-Modell und erhalten Sie eine detaillierte Antwort. Dieser Service gibt jetzt Antwortdaten direkt zurück, wodurch separate Textsensoren und die 255-Zeichen-Begrenzung überflüssig werden. Die Antwort wird auch im Gesprächsverlauf gespeichert.",
"fields": { "fields": {
"instance": { "instance": {
"name": "Instanz", "name": "Instanz",
@@ -124,7 +138,15 @@
}, },
"max_tokens": { "max_tokens": {
"name": "Max Tokens", "name": "Max Tokens",
"description": "Maximale Länge der Antwort (1-4096 Token)" "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."
} }
} }
}, },
@@ -19,8 +19,9 @@
"model": "AI model to use", "model": "AI model to use",
"api_endpoint": "Custom API endpoint URL (optional)", "api_endpoint": "Custom API endpoint URL (optional)",
"temperature": "Response creativity (0-2, lower = more focused)", "temperature": "Response creativity (0-2, lower = more focused)",
"max_tokens": "Maximum response length (1-4096 tokens)", "max_tokens": "Maximum response length (1-100000 tokens)",
"request_interval": "Minimum time between requests (0.1-60 seconds)", "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)", "context_messages": "Number of context messages to retain (1-20)",
"max_history_size": "Maximum conversation history size (1-100)" "max_history_size": "Maximum conversation history size (1-100)"
} }
@@ -33,10 +34,11 @@
"api_key": "API key for authentication", "api_key": "API key for authentication",
"model": "AI model to use", "model": "AI model to use",
"temperature": "Response creativity (0-2, lower = more focused)", "temperature": "Response creativity (0-2, lower = more focused)",
"max_tokens": "Maximum response length (1-4096 tokens)", "max_tokens": "Maximum response length (1-100000 tokens)",
"api_endpoint": "Custom API endpoint URL (optional)", "api_endpoint": "Custom API endpoint URL (optional)",
"api_provider": "API Provider", "api_provider": "API Provider",
"request_interval": "Minimum time between requests (0.1-60 seconds)", "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)", "context_messages": "Number of context messages to retain (1-20)",
"max_history_size": "Maximum conversation history size (1-100)" "max_history_size": "Maximum conversation history size (1-100)"
} }
@@ -72,13 +74,23 @@
"options": { "options": {
"step": { "step": {
"init": { "init": {
"title": "Update Instance Settings", "title": "Select Provider",
"description": "Modify settings for this AI assistant instance.", "description": "Choose the AI provider for this instance. The integration will reload after saving changes.",
"data": { "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", "model": "AI model",
"temperature": "Response creativity (0-2)", "temperature": "Response creativity (0-2)",
"max_tokens": "Maximum response length (1-4096)", "max_tokens": "Maximum response length (1-100000)",
"request_interval": "Minimum request interval (0.1-60 seconds)", "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)", "context_messages": "Number of previous messages to include in context (1-20)",
"max_history_size": "Maximum conversation history size (1-100)" "max_history_size": "Maximum conversation history size (1-100)"
} }
@@ -86,17 +98,19 @@
} }
}, },
"selector": { "selector": {
"api_provider": { "api_provider": {
"options": { "options": {
"openai": "OpenAI (compatible)", "openai": "OpenAI (compatible)",
"anthropic": "Anthropic (compatible)" "anthropic": "Anthropic (compatible)",
"deepseek": "DeepSeek",
"gemini": "Google Gemini"
}
} }
} },
},
"services": { "services": {
"ask_question": { "ask_question": {
"name": "Ask Question (HA Text AI)", "name": "Ask Question (HA Text AI)",
"description": "Send a question to the AI model and receive a detailed response. The response will be stored in the conversation history and can be retrieved later.", "description": "Send a question to the AI model and receive a detailed response. This service now returns response data directly, eliminating the need for separate text sensors and the 255-character limitation. The response will also be stored in the conversation history.",
"fields": { "fields": {
"instance": { "instance": {
"name": "Instance", "name": "Instance",
@@ -124,7 +138,15 @@
}, },
"max_tokens": { "max_tokens": {
"name": "Max Tokens", "name": "Max Tokens",
"description": "Maximum length of the response (1-4096 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."
} }
} }
}, },
@@ -19,8 +19,9 @@
"model": "Modelo de IA a utilizar", "model": "Modelo de IA a utilizar",
"api_endpoint": "URL del endpoint de API personalizado (opcional)", "api_endpoint": "URL del endpoint de API personalizado (opcional)",
"temperature": "Creatividad de la respuesta (0-2, menor = más enfocado)", "temperature": "Creatividad de la respuesta (0-2, menor = más enfocado)",
"max_tokens": "Longitud máxima de la respuesta (1-4096 tokens)", "max_tokens": "Longitud máxima de la respuesta (1-100000 tokens)",
"request_interval": "Tiempo mínimo entre solicitudes (0.1-60 segundos)", "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)", "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)" "max_history_size": "Tamaño máximo del historial de conversación (1-100)"
} }
@@ -33,10 +34,11 @@
"api_key": "Clave API para autenticación", "api_key": "Clave API para autenticación",
"model": "Modelo de IA a utilizar", "model": "Modelo de IA a utilizar",
"temperature": "Creatividad de la respuesta (0-2, menor = más enfocado)", "temperature": "Creatividad de la respuesta (0-2, menor = más enfocado)",
"max_tokens": "Longitud máxima de la respuesta (1-4096 tokens)", "max_tokens": "Longitud máxima de la respuesta (1-100000 tokens)",
"api_endpoint": "URL del endpoint de API personalizado (opcional)", "api_endpoint": "URL del endpoint de API personalizado (opcional)",
"api_provider": "Proveedor de API", "api_provider": "Proveedor de API",
"request_interval": "Tiempo mínimo entre solicitudes (0.1-60 segundos)", "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)", "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)" "max_history_size": "Tamaño máximo del historial de conversación (1-100)"
} }
@@ -72,13 +74,23 @@
"options": { "options": {
"step": { "step": {
"init": { "init": {
"title": "Actualizar configuración de la instancia", "title": "Seleccionar proveedor",
"description": "Modifica la configuración para esta instancia de asistente de IA.", "description": "Elige el proveedor de IA para esta instancia. La integración se recargará después de guardar los cambios.",
"data": { "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", "model": "Modelo de IA",
"temperature": "Creatividad de la respuesta (0-2)", "temperature": "Creatividad de la respuesta (0-2)",
"max_tokens": "Longitud máxima de la respuesta (1-4096)", "max_tokens": "Longitud máxima de la respuesta (1-100000)",
"request_interval": "Intervalo mínimo de solicitud (0.1-60 segundos)", "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)", "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)" "max_history_size": "Tamaño máximo del historial de conversación (1-100)"
} }
@@ -89,14 +101,16 @@
"api_provider": { "api_provider": {
"options": { "options": {
"openai": "OpenAI (compatible)", "openai": "OpenAI (compatible)",
"anthropic": "Anthropic (compatible)" "anthropic": "Anthropic (compatible)",
"deepseek": "DeepSeek",
"gemini": "Google Gemini"
} }
} }
}, },
"services": { "services": {
"ask_question": { "ask_question": {
"name": "Hacer Pregunta (HA Text AI)", "name": "Hacer Pregunta (HA Text AI)",
"description": "Envía una pregunta al modelo de IA y recibe una respuesta detallada. La respuesta se almacenará en el historial de conversación y se podrá recuperar más tarde.", "description": "Envía una pregunta al modelo de IA y recibe una respuesta detallada. Este servicio ahora devuelve datos de respuesta directamente, eliminando la necesidad de sensores de texto separados y la limitación de 255 caracteres. La respuesta también se almacenará en el historial de conversación.",
"fields": { "fields": {
"instance": { "instance": {
"name": "Instancia", "name": "Instancia",
@@ -124,7 +138,15 @@
}, },
"max_tokens": { "max_tokens": {
"name": "Máx. Tokens", "name": "Máx. Tokens",
"description": "Longitud máxima de la respuesta (1-4096 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": { "config": {
"step": { "step": {
"provider": {
"title": "एआई प्रदाता चुनें",
"description": "इस उदाहरण के लिए किस एआई सेवा प्रदाता का उपयोग करना है, चुनें।",
"data": {
"api_provider": "एपीआई प्रदाता",
"context_messages": "रखने के लिए संदर्भ संदेशों की संख्या (1-20)",
"max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)"
}
},
"provider": { "provider": {
"title": "प्रदाता सेटिंग्स", "title": "प्रदाता सेटिंग्स",
"description": "आपके द्वारा चुने गए एआई प्रदाता के लिए कनेक्शन विवरण प्रदान करें।", "description": "आपके द्वारा चुने गए एआई प्रदाता के लिए कनेक्शन विवरण प्रदान करें।",
@@ -19,8 +10,9 @@
"model": "उपयोग करने के लिए एआई मॉडल", "model": "उपयोग करने के लिए एआई मॉडल",
"api_endpoint": "कस्टम एपीआई एंडपॉइंट यूआरएल (वैकल्पिक)", "api_endpoint": "कस्टम एपीआई एंडपॉइंट यूआरएल (वैकल्पिक)",
"temperature": "प्रतिक्रिया की रचनात्मकता (0-2, कम = अधिक केंद्रित)", "temperature": "प्रतिक्रिया की रचनात्मकता (0-2, कम = अधिक केंद्रित)",
"max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-4096 टोकन)", "max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-100000 टोकन)",
"request_interval": "अनुरोधों के बीच न्यूनतम समय (0.1-60 सेकंड)", "request_interval": "अनुरोधों के बीच न्यूनतम समय (0.1-60 सेकंड)",
"api_timeout": "एपीआई अनुरोध टाइमआउट सेकंड में (5-600)",
"context_messages": "रखने के लिए संदर्भ संदेशों की संख्या (1-20)", "context_messages": "रखने के लिए संदर्भ संदेशों की संख्या (1-20)",
"max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)" "max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)"
} }
@@ -33,10 +25,11 @@
"api_key": "प्रमाणीकरण के लिए एपीआई कुंजी", "api_key": "प्रमाणीकरण के लिए एपीआई कुंजी",
"model": "उपयोग करने के लिए एआई मॉडल", "model": "उपयोग करने के लिए एआई मॉडल",
"temperature": "प्रतिक्रिया की रचनात्मकता (0-2, कम = अधिक केंद्रित)", "temperature": "प्रतिक्रिया की रचनात्मकता (0-2, कम = अधिक केंद्रित)",
"max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-4096 टोकन)", "max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-100000 टोकन)",
"api_endpoint": "कस्टम एपीआई एंडपॉइंट यूआरएल (वैकल्पिक)", "api_endpoint": "कस्टम एपीआई एंडपॉइंट यूआरएल (वैकल्पिक)",
"api_provider": "एपीआई प्रदाता", "api_provider": "एपीआई प्रदाता",
"request_interval": "अनुरोधों के बीच न्यूनतम समय (0.1-60 सेकंड)", "request_interval": "अनुरोधों के बीच न्यूनतम समय (0.1-60 सेकंड)",
"api_timeout": "एपीआई अनुरोध टाइमआउट सेकंड में (5-600)",
"context_messages": "रखने के लिए संदर्भ संदेशों की संख्या (1-20)", "context_messages": "रखने के लिए संदर्भ संदेशों की संख्या (1-20)",
"max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)" "max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)"
} }
@@ -72,13 +65,23 @@
"options": { "options": {
"step": { "step": {
"init": { "init": {
"title": "उदाहरण सेटिंग्स अपडेट करें", "title": "प्रदाता चुनें",
"description": "इस एआई सहायक उदाहरण के लिए सेटिंग्स संशोधित करें।", "description": "इस उदाहरण के लिए एआई प्रदाता चुनें। परिवर्तन सहेजने के बाद एकीकरण पुनः लोड होगा।",
"data": { "data": {
"api_provider": "एपीआई प्रदाता"
}
},
"settings": {
"title": "कनेक्शन और मॉडल सेटिंग्स",
"description": "एपीआई क्रेडेंशियल और मॉडल पैरामीटर कॉन्फ़िगर करें। एकीकरण पुनः लोड होने के बाद परिवर्तन प्रभावी होंगे।",
"data": {
"api_key": "एपीआई कुंजी",
"api_endpoint": "एपीआई एंडपॉइंट यूआरएल",
"model": "एआई मॉडल", "model": "एआई मॉडल",
"temperature": "प्रतिक्रिया की रचनात्मकता (0-2)", "temperature": "प्रतिक्रिया की रचनात्मकता (0-2)",
"max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-4096)", "max_tokens": "प्रतिक्रिया की अधिकतम लंबाई (1-100000)",
"request_interval": "न्यूनतम अनुरोध अंतराल (0.1-60 सेकंड)", "request_interval": "न्यूनतम अनुरोध अंतराल (0.1-60 सेकंड)",
"api_timeout": "एपीआई अनुरोध टाइमआउट सेकंड में (5-600)",
"context_messages": "संदर्भ में शामिल करने के लिए पिछले संदेशों की संख्या (1-20)", "context_messages": "संदर्भ में शामिल करने के लिए पिछले संदेशों की संख्या (1-20)",
"max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)" "max_history_size": "अधिकतम बातचीत इतिहास आकार (1-100)"
} }
@@ -89,14 +92,16 @@
"api_provider": { "api_provider": {
"options": { "options": {
"openai": "OpenAI (अनुकूलित)", "openai": "OpenAI (अनुकूलित)",
"anthropic": "Anthropic (अनुकूलित)" "anthropic": "Anthropic (अनुकूलित)",
"deepseek": "DeepSeek",
"gemini": "Google Gemini"
} }
} }
}, },
"services": { "services": {
"ask_question": { "ask_question": {
"name": "प्रश्न पूछें (HA Text AI)", "name": "प्रश्न पूछें (HA Text AI)",
"description": "एआई मॉडल को एक प्रश्न भेजें और विस्तृत प्रतिक्रिया प्राप्त करें। प्रतिक्रिया बातचीत के इतिहास में संग्रहीत क जाएगी और बाद में पुनर्प्राप्त की जा सकती है।", "description": "AI मॉडल को प्रश्न भेजें और विस्तृत उत्तर प्राप्त करें। यह सेवा अब प्रत्यक्ष रूप से प्रतिक्रिया डेटा वापस करती है, अलग टेक्स्ट सेंसर की आवश्यकता और 255 वर्ण की सीमा को समाप्त करती है। प्रतिक्रिया को बातचीत के इतिहास में भी संग्रहीत किया जाएग।",
"fields": { "fields": {
"instance": { "instance": {
"name": "उदाहरण", "name": "उदाहरण",
@@ -124,7 +129,15 @@
}, },
"max_tokens": { "max_tokens": {
"name": "अधिकतम टोकन", "name": "अधिकतम टोकन",
"description": "प्रतिक्रिया की अधिकतम लंबाई (1-4096 टोकन)" "description": "प्रतिक्रिया की अधिकतम लंबाई (1-100000 टोकन)"
},
"structured_output": {
"name": "संरचित आउटपुट",
"description": "JSON संरचित आउटपुट मोड सक्षम करें। सक्षम होने पर, AI प्रदान किए गए स्कीमा से मेल खाने वाले वैध JSON के साथ प्रतिक्रिया देगा।"
},
"json_schema": {
"name": "JSON स्कीमा",
"description": "अपेक्षित प्रतिक्रिया की संरचना को परिभाषित करने वाला JSON स्कीमा। structured_output सक्षम होने पर आवश्यक।"
} }
} }
}, },
@@ -19,8 +19,9 @@
"model": "Modello AI da utilizzare", "model": "Modello AI da utilizzare",
"api_endpoint": "URL dell'endpoint API personalizzato (opzionale)", "api_endpoint": "URL dell'endpoint API personalizzato (opzionale)",
"temperature": "Creatività della risposta (0-2, più basso = più focalizzato)", "temperature": "Creatività della risposta (0-2, più basso = più focalizzato)",
"max_tokens": "Lunghezza massima della risposta (1-4096 token)", "max_tokens": "Lunghezza massima della risposta (1-100000 token)",
"request_interval": "Tempo minimo tra le richieste (0.1-60 secondi)", "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)", "context_messages": "Numero di messaggi di contesto da mantenere (1-20)",
"max_history_size": "Dimensione massima della cronologia delle conversazioni (1-100)" "max_history_size": "Dimensione massima della cronologia delle conversazioni (1-100)"
} }
@@ -33,10 +34,11 @@
"api_key": "Chiave API per l'autenticazione", "api_key": "Chiave API per l'autenticazione",
"model": "Modello AI da utilizzare", "model": "Modello AI da utilizzare",
"temperature": "Creatività della risposta (0-2, più basso = più focalizzato)", "temperature": "Creatività della risposta (0-2, più basso = più focalizzato)",
"max_tokens": "Lunghezza massima della risposta (1-4096 token)", "max_tokens": "Lunghezza massima della risposta (1-100000 token)",
"api_endpoint": "URL dell'endpoint API personalizzato (opzionale)", "api_endpoint": "URL dell'endpoint API personalizzato (opzionale)",
"api_provider": "Fornitore API", "api_provider": "Fornitore API",
"request_interval": "Tempo minimo tra le richieste (0.1-60 secondi)", "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)", "context_messages": "Numero di messaggi di contesto da mantenere (1-20)",
"max_history_size": "Dimensione massima della cronologia delle conversazioni (1-100)" "max_history_size": "Dimensione massima della cronologia delle conversazioni (1-100)"
} }
@@ -72,13 +74,23 @@
"options": { "options": {
"step": { "step": {
"init": { "init": {
"title": "Aggiorna impostazioni dell'istanza", "title": "Seleziona fornitore",
"description": "Modifica le impostazioni per questa istanza di assistente AI.", "description": "Scegli il fornitore AI per questa istanza. L'integrazione verrà ricaricata dopo aver salvato le modifiche.",
"data": { "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", "model": "Modello AI",
"temperature": "Creatività della risposta (0-2)", "temperature": "Creatività della risposta (0-2)",
"max_tokens": "Lunghezza massima della risposta (1-4096)", "max_tokens": "Lunghezza massima della risposta (1-100000)",
"request_interval": "Intervallo minimo di richiesta (0.1-60 secondi)", "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)", "context_messages": "Numero di messaggi precedenti da includere nel contesto (1-20)",
"max_history_size": "Dimensione massima della cronologia delle conversazioni (1-100)" "max_history_size": "Dimensione massima della cronologia delle conversazioni (1-100)"
} }
@@ -89,14 +101,16 @@
"api_provider": { "api_provider": {
"options": { "options": {
"openai": "OpenAI (compatibile)", "openai": "OpenAI (compatibile)",
"anthropic": "Anthropic (compatibile)" "anthropic": "Anthropic (compatibile)",
"deepseek": "DeepSeek",
"gemini": "Google Gemini"
} }
} }
}, },
"services": { "services": {
"ask_question": { "ask_question": {
"name": "Fai una domanda (HA Text AI)", "name": "Fai una domanda (HA Text AI)",
"description": "Invia una domanda al modello AI e ricevi una risposta dettagliata. La risposta sarà memorizzata nella cronologia delle conversazioni e potrà essere recuperata in seguito.", "description": "Invia una domanda al modello AI e ricevi una risposta dettagliata. Questo servizio ora restituisce i dati di risposta direttamente, eliminando la necessità di sensori di testo separati e la limitazione di 255 caratteri. La risposta sarà anche memorizzata nella cronologia delle conversazioni.",
"fields": { "fields": {
"instance": { "instance": {
"name": "Istanze", "name": "Istanze",
@@ -124,7 +138,15 @@
}, },
"max_tokens": { "max_tokens": {
"name": "Token massimi", "name": "Token massimi",
"description": "Lunghezza massima della risposta (1-4096 token)" "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."
} }
} }
}, },
@@ -19,8 +19,9 @@
"model": "Модель ИИ для использования", "model": "Модель ИИ для использования",
"api_endpoint": "Пользовательский URL-адрес конечной точки API (необязательно)", "api_endpoint": "Пользовательский URL-адрес конечной точки API (необязательно)",
"temperature": "Креативность ответа (0-2, меньше = более сфокусированно)", "temperature": "Креативность ответа (0-2, меньше = более сфокусированно)",
"max_tokens": "Максимальная длина ответа (1-4096 токенов)", "max_tokens": "Максимальная длина ответа (1-100000 токенов)",
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)", "request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
"api_timeout": "Таймаут API-запроса в секундах (5-600)",
"context_messages": "Количество сохраняемых контекстных сообщений (1-20)", "context_messages": "Количество сохраняемых контекстных сообщений (1-20)",
"max_history_size": "Максимальный размер истории разговора (1-100)" "max_history_size": "Максимальный размер истории разговора (1-100)"
} }
@@ -33,10 +34,11 @@
"api_key": "API-ключ для аутентификации", "api_key": "API-ключ для аутентификации",
"model": "Модель ИИ для использования", "model": "Модель ИИ для использования",
"temperature": "Креативность ответа (0-2, меньше = более сфокусированно)", "temperature": "Креативность ответа (0-2, меньше = более сфокусированно)",
"max_tokens": "Максимальная длина ответа (1-4096 токенов)", "max_tokens": "Максимальная длина ответа (1-100000 токенов)",
"api_endpoint": "Пользовательский URL-адрес конечной точки API (необязательно)", "api_endpoint": "Пользовательский URL-адрес конечной точки API (необязательно)",
"api_provider": "Провайдер API", "api_provider": "Провайдер API",
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)", "request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
"api_timeout": "Таймаут API-запроса в секундах (5-600)",
"context_messages": "Количество сохраняемых контекстных сообщений (1-20)", "context_messages": "Количество сохраняемых контекстных сообщений (1-20)",
"max_history_size": "Максимальный размер истории разговора (1-100)" "max_history_size": "Максимальный размер истории разговора (1-100)"
} }
@@ -72,13 +74,23 @@
"options": { "options": {
"step": { "step": {
"init": { "init": {
"title": "Обновление настроек экземпляра", "title": "Выбор провайдера",
"description": "Измените настройки для этого экземпляра ИИ-помощника.", "description": "Выберите провайдера ИИ для этого экземпляра. Интеграция перезагрузится после сохранения изменений.",
"data": { "data": {
"api_provider": "Провайдер API"
}
},
"settings": {
"title": "Настройки подключения и модели",
"description": "Настройте учётные данные API и параметры модели. Изменения вступят в силу после перезагрузки интеграции.",
"data": {
"api_key": "API-ключ",
"api_endpoint": "URL конечной точки API",
"model": "Модель ИИ", "model": "Модель ИИ",
"temperature": "Креативность ответа (0-2)", "temperature": "Креативность ответа (0-2)",
"max_tokens": "Максимальная длина ответа (1-4096)", "max_tokens": "Максимальная длина ответа (1-100000)",
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)", "request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
"api_timeout": "Таймаут API-запроса в секундах (5-600)",
"context_messages": "Количество предыдущих сообщений для включения в контекст (1-20)", "context_messages": "Количество предыдущих сообщений для включения в контекст (1-20)",
"max_history_size": "Максимальный размер истории разговора (1-100)" "max_history_size": "Максимальный размер истории разговора (1-100)"
} }
@@ -89,14 +101,16 @@
"api_provider": { "api_provider": {
"options": { "options": {
"openai": "OpenAI (совместимый)", "openai": "OpenAI (совместимый)",
"anthropic": "Anthropic (совместимый)" "anthropic": "Anthropic (совместимый)",
"deepseek": "DeepSeek",
"gemini": "Google Gemini"
} }
} }
}, },
"services": { "services": {
"ask_question": { "ask_question": {
"name": "Задать вопрос (HA Text AI)", "name": "Задать вопрос (HA Text AI)",
"description": "Отправить вопрос модели ИИ и получить подробный ответ. Ответ будет сохранен в истории разговора и может быть получен позже.", "description": "Отправить вопрос модели ИИ и получить подробный ответ. Сервис теперь возвращает данные ответа напрямую, устраняя необходимость в отдельных текстовых сенсорах и ограничение в 255 символов. Ответ также будет сохранен в истории разговора.",
"fields": { "fields": {
"instance": { "instance": {
"name": "Экземпляр", "name": "Экземпляр",
@@ -124,7 +138,15 @@
}, },
"max_tokens": { "max_tokens": {
"name": "Максимум токенов", "name": "Максимум токенов",
"description": "Максимальная длина ответа (1-4096 токенов)" "description": "Максимальная длина ответа (1-100000 токенов)"
},
"structured_output": {
"name": "Структурированный вывод",
"description": "Включить режим структурированного JSON-вывода. При включении ИИ будет отвечать валидным JSON, соответствующим указанной схеме."
},
"json_schema": {
"name": "JSON Schema",
"description": "JSON-схема, определяющая структуру ожидаемого ответа. Обязательна при включении structured_output."
} }
} }
}, },
@@ -1,15 +1,6 @@
{ {
"config": { "config": {
"step": { "step": {
"provider": {
"title": "Изаберите AI провајдера",
"description": "Изаберите који AI сервис провајдер да користите за ову инстанцу.",
"data": {
"api_provider": "API провајдер",
"context_messages": "Број контекстуалних порука које треба задржати (1-20)",
"max_history_size": "Максимална величина историје разговора (1-100)"
}
},
"provider": { "provider": {
"title": "Подешавања провајдера", "title": "Подешавања провајдера",
"description": "Обезбедите детаље о вези за изабраног AI провајдера.", "description": "Обезбедите детаље о вези за изабраног AI провајдера.",
@@ -19,8 +10,9 @@
"model": "AI модел који ће се користити", "model": "AI модел који ће се користити",
"api_endpoint": "Прилагођени URL API крајње тачке (опционо)", "api_endpoint": "Прилагођени URL API крајње тачке (опционо)",
"temperature": "Креативност одговора (0-2, нижа = фокусираније)", "temperature": "Креативност одговора (0-2, нижа = фокусираније)",
"max_tokens": "Максимална дужина одговора (1-4096 токена)", "max_tokens": "Максимална дужина одговора (1-100000 токена)",
"request_interval": "Минимално време између захтева (0.1-60 секунди)", "request_interval": "Минимално време између захтева (0.1-60 секунди)",
"api_timeout": "Временско ограничење API захтева у секундама (5-600)",
"context_messages": "Број контекстуалних порука које треба задржати (1-20)", "context_messages": "Број контекстуалних порука које треба задржати (1-20)",
"max_history_size": "Максимална величина историје разговора (1-100)" "max_history_size": "Максимална величина историје разговора (1-100)"
} }
@@ -33,10 +25,11 @@
"api_key": "API кључ за аутентификацију", "api_key": "API кључ за аутентификацију",
"model": "AI модел који ће се користити", "model": "AI модел који ће се користити",
"temperature": "Креативност одговора (0-2, нижа = фокусираније)", "temperature": "Креативност одговора (0-2, нижа = фокусираније)",
"max_tokens": "Максимална дужина одговора (1-4096 токена)", "max_tokens": "Максимална дужина одговора (1-100000 токена)",
"api_endpoint": "Прилагођени URL API крајње тачке (опционо)", "api_endpoint": "Прилагођени URL API крајње тачке (опционо)",
"api_provider": "API провајдер", "api_provider": "API провајдер",
"request_interval": "Минимално време између захтева (0.1-60 секунди)", "request_interval": "Минимално време између захтева (0.1-60 секунди)",
"api_timeout": "Временско ограничење API захтева у секундама (5-600)",
"context_messages": "Број контекстуалних порука које треба задржати (1-20)", "context_messages": "Број контекстуалних порука које треба задржати (1-20)",
"max_history_size": "Максимална величина историје разговора (1-100)" "max_history_size": "Максимална величина историје разговора (1-100)"
} }
@@ -72,13 +65,23 @@
"options": { "options": {
"step": { "step": {
"init": { "init": {
"title": "Ажурирајте подешавања инстанце", "title": "Изаберите провајдера",
"description": "Измените подешавања за ову AI асистент инстанцу.", "description": "Изаберите AI провајдера за ову инстанцу. Интеграција ће се поново учитати након чувања измена.",
"data": { "data": {
"api_provider": "API провајдер"
}
},
"settings": {
"title": "Подешавања везе и модела",
"description": "Конфигуришите API акредитиве и параметре модела. Промене ће ступити на снагу након поновног учитавања интеграције.",
"data": {
"api_key": "API кључ",
"api_endpoint": "URL API крајње тачке",
"model": "AI модел", "model": "AI модел",
"temperature": "Креативност одговора (0-2)", "temperature": "Креативност одговора (0-2)",
"max_tokens": "Максимална дужина одговора (1-4096)", "max_tokens": "Максимална дужина одговора (1-100000)",
"request_interval": "Минимално време између захтева (0.1-60 секунди)", "request_interval": "Минимално време између захтева (0.1-60 секунди)",
"api_timeout": "Временско ограничење API захтева у секундама (5-600)",
"context_messages": "Број претходних порука које треба укључити у контекст (1-20)", "context_messages": "Број претходних порука које треба укључити у контекст (1-20)",
"max_history_size": "Максимална величина историје разговора (1-100)" "max_history_size": "Максимална величина историје разговора (1-100)"
} }
@@ -89,14 +92,16 @@
"api_provider": { "api_provider": {
"options": { "options": {
"openai": "OpenAI (компатибилан)", "openai": "OpenAI (компатибилан)",
"anthropic": "Anthropic (компатибилан)" "anthropic": "Anthropic (компатибилан)",
"deepseek": "DeepSeek",
"gemini": "Google Gemini"
} }
} }
}, },
"services": { "services": {
"ask_question": { "ask_question": {
"name": "Поставите питање (HA Text AI)", "name": "Поставите питање (HA Text AI)",
"description": "Пошаљите питање AI моделу и примите детаљан одговор. Одговор ће бити сачуван у историји разговора и може се касније повратити.", "description": "Пошаљите питање AI моделу и добијте детаљан одговор. Овај сервис сада враћа податке одговора директно, елиминишући потребу за засебним текстуалним сензорима и ограничење од 255 карактера. Одговор ће такође бити сачуван у историји разговора.",
"fields": { "fields": {
"instance": { "instance": {
"name": "Инстанца", "name": "Инстанца",
@@ -124,7 +129,15 @@
}, },
"max_tokens": { "max_tokens": {
"name": "Максимални токени", "name": "Максимални токени",
"description": "Максимална дужина одговора (1-4096 токена)" "description": "Максимална дужина одговора (1-100000 токена)"
},
"structured_output": {
"name": "Структурисани излаз",
"description": "Омогући JSON структурисани излаз. Када је омогућено, AI ће одговарати валидним JSON-ом који одговара датој шеми."
},
"json_schema": {
"name": "JSON шема",
"description": "JSON шема која дефинише структуру очекиваног одговора. Обавезна када је structured_output омогућен."
} }
} }
}, },
@@ -1,15 +1,6 @@
{ {
"config": { "config": {
"step": { "step": {
"provider": {
"title": "选择AI提供者",
"description": "选择要用于此实例的AI服务提供者。",
"data": {
"api_provider": "API提供者",
"context_messages": "保留的上下文消息数量(1-20",
"max_history_size": "最大对话历史大小(1-100"
}
},
"provider": { "provider": {
"title": "提供者设置", "title": "提供者设置",
"description": "提供所选AI提供者的连接详细信息。", "description": "提供所选AI提供者的连接详细信息。",
@@ -19,8 +10,9 @@
"model": "要使用的AI模型", "model": "要使用的AI模型",
"api_endpoint": "自定义API端点URL(可选)", "api_endpoint": "自定义API端点URL(可选)",
"temperature": "响应创造力(0-2,越低越专注)", "temperature": "响应创造力(0-2,越低越专注)",
"max_tokens": "最大响应长度(1-4096个标记)", "max_tokens": "最大响应长度(1-100000个标记)",
"request_interval": "请求之间的最小时间(0.1-60秒)", "request_interval": "请求之间的最小时间(0.1-60秒)",
"api_timeout": "API请求超时时间(5-600秒)",
"context_messages": "保留的上下文消息数量(1-20", "context_messages": "保留的上下文消息数量(1-20",
"max_history_size": "最大对话历史大小(1-100" "max_history_size": "最大对话历史大小(1-100"
} }
@@ -33,10 +25,11 @@
"api_key": "用于身份验证的API密钥", "api_key": "用于身份验证的API密钥",
"model": "要使用的AI模型", "model": "要使用的AI模型",
"temperature": "响应创造力(0-2,越低越专注)", "temperature": "响应创造力(0-2,越低越专注)",
"max_tokens": "最大响应长度(1-4096个标记)", "max_tokens": "最大响应长度(1-100000个标记)",
"api_endpoint": "自定义API端点URL(可选)", "api_endpoint": "自定义API端点URL(可选)",
"api_provider": "API提供者", "api_provider": "API提供者",
"request_interval": "请求之间的最小时间(0.1-60秒)", "request_interval": "请求之间的最小时间(0.1-60秒)",
"api_timeout": "API请求超时时间(5-600秒)",
"context_messages": "保留的上下文消息数量(1-20", "context_messages": "保留的上下文消息数量(1-20",
"max_history_size": "最大对话历史大小(1-100" "max_history_size": "最大对话历史大小(1-100"
} }
@@ -72,13 +65,23 @@
"options": { "options": {
"step": { "step": {
"init": { "init": {
"title": "更新实例设置", "title": "选择提供者",
"description": "修改此AI助手实例的设置。", "description": "选择此实例的AI提供者。保存更改后集成将重新加载。",
"data": { "data": {
"api_provider": "API提供者"
}
},
"settings": {
"title": "连接和模型设置",
"description": "配置API凭据和模型参数。更改将在集成重新加载后生效。",
"data": {
"api_key": "API密钥",
"api_endpoint": "API端点URL",
"model": "AI模型", "model": "AI模型",
"temperature": "响应创造力(0-2", "temperature": "响应创造力(0-2",
"max_tokens": "最大响应长度(1-4096", "max_tokens": "最大响应长度(1-100000",
"request_interval": "最小请求间隔(0.1-60秒)", "request_interval": "最小请求间隔(0.1-60秒)",
"api_timeout": "API请求超时时间(5-600秒)",
"context_messages": "要包含在上下文中的先前消息数量(1-20)", "context_messages": "要包含在上下文中的先前消息数量(1-20)",
"max_history_size": "最大对话历史大小(1-100" "max_history_size": "最大对话历史大小(1-100"
} }
@@ -89,14 +92,16 @@
"api_provider": { "api_provider": {
"options": { "options": {
"openai": "OpenAI(兼容)", "openai": "OpenAI(兼容)",
"anthropic": "Anthropic(兼容)" "anthropic": "Anthropic(兼容)",
"deepseek": "DeepSeek",
"gemini": "Google Gemini"
} }
} }
}, },
"services": { "services": {
"ask_question": { "ask_question": {
"name": "提问 (HA Text AI)", "name": "提问 (HA Text AI)",
"description": "向AI模型发送问题并接收详细响应。响应将存储在对话历史中,可以稍后检索。", "description": "向AI模型发送问题并获得详细回答。此服务现在直接返回响应数据,消除了对单独文本传感器的需要和255字符限制。响应将存储在对话历史中。",
"fields": { "fields": {
"instance": { "instance": {
"name": "实例", "name": "实例",
@@ -124,7 +129,15 @@
}, },
"max_tokens": { "max_tokens": {
"name": "最大标记数", "name": "最大标记数",
"description": "响应的最大长度(1-4096个标记)" "description": "响应的最大长度(1-100000个标记)"
},
"structured_output": {
"name": "结构化输出",
"description": "启用JSON结构化输出模式。启用后,AI将以符合提供的模式的有效JSON进行响应。"
},
"json_schema": {
"name": "JSON模式",
"description": "定义预期响应结构的JSON模式。启用structured_output时必需。"
} }
} }
}, },
+2 -2
View File
@@ -1,5 +1,5 @@
{ {
"name": "HA text AI", "name": "HA Text AI",
"render_readme": true, "render_readme": true,
"homeassistant": "2024.11.0" "homeassistant": "2024.12.0"
} }