Compare commits

..
81 Commits
Author SHA1 Message Date
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
SMKRV 82e1f0c4f9 Release v2.1.0 2024-12-13 00:06:08 +03:00
SMKRV 5c16eee6e4 fix: Read version from manifest.json 2024-12-12 16:03:15 +03:00
SMKRV e988d445a4 - Fixed version reading from manifest.json implementation 2024-12-11 22:01:01 +03:00
SMKRV f9bfb9ab7f fix: correct sw_version syntax in device_info
- Fixed version reading from manifest.json implementation
2024-12-11 21:57:27 +03:00
SMKRV 92dd1bc110 ~ 2024-12-11 00:00:12 +03:00
SMKRV 17d547325a refactor(docs): updated README services examples with more detailed configuration 2024-12-10 23:33:32 +03:00
SMKRV b8cb70217c refactor(docs): updated README shields 2024-12-10 23:27:49 +03:00
SMKRV 530d04f25d refactor(docs): updated README shields 2024-12-10 23:27:18 +03:00
SMKRV b71083b9bf bump to version 2.0.9 2024-12-10 17:25:47 +03:00
SMKRV f9f7d10f7f refactor(docs): updated README images 2024-12-10 17:21:41 +03:00
SMKRV 9f7cb20621 refactor(docs): updated README images 2024-12-10 17:19:52 +03:00
SMKRV 6fc3b23365 refactor(docs): updated README images 2024-12-10 17:18:55 +03:00
SMKRV 15c717fcb0 fix: Display only last Q&A in sensor state to prevent data truncation
- Show only the latest question and answer in sensor state
- Keep full conversation history in attributes
- Fix truncation issues in Home Assistant UI
- Maintain backwards compatibility
- No configuration changes required
2024-12-10 17:02:48 +03:00
SMKRV be06fddce1 fix: Display only last Q&A in sensor state to prevent data truncation
- Show only the latest question and answer in sensor state
- Keep full conversation history in attributes
- Fix truncation issues in Home Assistant UI
- Maintain backwards compatibility
- No configuration changes required
2024-12-10 16:19:17 +03:00
SMKRV 5f0bd861a7 docs(readme): Included note that the integration has been submitted to HACS store and is currently pending review in pull request: [pull request #2896](hacs/default#2896). 2024-12-10 00:00:18 +03:00
SMKRV 428aee46c8 docs(readme): Included note that the integration has been submitted to HACS store and is currently pending review in pull request: [pull request #2896](hacs/default#2896). 2024-12-09 23:59:51 +03:00
SMKRV 561bcf0b1d docs(Code of Conduct): Add Code of Conduct to promote community guidelines
- Implement Contributor Covenant Code of Conduct v1.4
- Establish clear expectations for community interactions
- Define standards of acceptable and unacceptable behavior
- Provide framework for reporting and addressing issues
- Emphasize inclusivity and respect for all contributors
2024-12-09 16:52:44 +03:00
SMKRV 2b1e42c665 docs(readme): Included note that the integration has been submitted to HACS store and is currently pending review in pull request: [pull request #2893](https://github.com/hacs/default/pull/2893). 2024-12-09 15:29:53 +03:00
SMKRV 2d68f29ab5 bump to version 2.0.8 2024-12-09 00:41:28 +03:00
SMKRV 19d4a93bca bump to version 2.0.8 2024-12-09 00:41:08 +03:00
SMKRV fb75c5f44e bump to version 2.0.9 2024-12-09 00:39:28 +03:00
SMKRV 52af987985 bump to version 2.0.9 2024-12-09 00:38:59 +03:00
SMKRV 162a30acdd bump to version 2.0.8-beta 2024-12-07 00:48:17 +03:00
SMKRV 898a6fc638 bump to version 2.0.8-beta 2024-12-07 00:43:53 +03:00
SMKRV 38e2362be4 bump to version 2.0.8-beta 2024-12-06 16:51:32 +03:00
SMKRV cea912d0b5 bump to version 2.0.8-beta 2024-12-06 16:51:09 +03:00
SMKRV 5ec00040c2 bump to version 2.0.8-beta 2024-12-06 16:50:26 +03:00
SMKRV 8d28fc4a0d bump to version 2.0.8-beta 2024-12-06 16:46:11 +03:00
SMKRV 7cc6587724 bump to version 2.0.8-beta 2024-12-06 16:35:26 +03:00
SMKRV bacf76e0a9 bump to version 2.0.8-beta 2024-12-06 16:33:46 +03:00
SMKRV f86c7bfd57 refactor(docs): relocate README images from misc/ to assets/ 2024-12-06 16:13:27 +03:00
SMKRV 2aaf340575 refactor(docs): relocate README images from misc/ to assets/ 2024-12-06 16:11:13 +03:00
SMKRV bae11ba85c refactor(docs): relocate README images from misc/ to assets/ 2024-12-06 16:10:04 +03:00
SMKRV 9eb7d8912c fix: sensor history attribute calculation 2024-12-06 12:26:56 +03:00
SMKRV d29535245f Release v2.0.7-beta 2024-12-06 12:00:23 +03:00
SMKRV ca3ae982b0 feat(performance): Optimize system resources and token estimation
- Improve JSON history file processing
- Add memory and disk space validation
- Enhance parallel request handling
- Refine token counting heuristics
2024-12-06 03:14:52 +03:00
SMKRV 0c4399b46c feat(performance): Optimize system resources and token estimation
- Improve JSON history file processing
- Add memory and disk space validation
- Enhance parallel request handling
- Refine token counting heuristics
2024-12-06 02:53:41 +03:00
29 changed files with 1496 additions and 451 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:
+2
View File
@@ -1,4 +1,6 @@
name: Validate name: Validate
permissions:
contents: read
on: on:
push: push:
-40
View File
@@ -1,40 +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
+128
View File
@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
issue tracker.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+268 -40
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) ![/README.md](https://img.shields.io/badge/language-English-green?style=flat-square) ![/README_RU.md](https://img.shields.io/badge/language-Russian-green?style=flat-square) ![](https://img.shields.io/badge/language-Deutch-green?style=flat-square) ![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/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/e03b5315c2cc7b489a6e1175da728075275ed614/misc/icons/logo.png" alt="HA Text AI" style="width: 80%; max-width: 640px; max-height: 150px; 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]
> 🚧 BETA VERSION > 🤝 Community Driven: for more details on the integration,
> Expect: potential bugs, frequent changes, incomplete features. > 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)**
> 🤝 Community Driven
> >
> <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](misc/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
@@ -89,28 +91,63 @@ Transform your smart home experience with powerful AI assistance powered by mult
</details> </details>
#### 🌐 Translations
| Code | Language | Status |
|------|----------|--------|
| 🇩🇪 de | Deutsch | Full |
| 🇬🇧 en | English | Primary |
| 🇪🇸 es | Español | Full |
| 🇮🇳 hi | हिन्दी | Full |
| 🇮🇹 it | Italiano | Full |
| 🇷🇺 ru | Русский | Full |
| 🇷🇸 sr | Српски | Full |
| 🇨🇳 zh | 中文 | Full |
## 📋 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
- Python 3.9 or newer - Python 3.9 or newer
- Stable internet connection - Stable internet connection
## 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 - 📏 **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>
@@ -136,20 +173,29 @@ To be compatible, a provider should support:
- JSON request/response format - JSON request/response format
- Standard authentication method - Standard authentication method
- Similar model parameter handling - Similar model parameter handling
</details> </details>
## ⚡ 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" **Alternative Method (Custom Repository):**
8. Restart Home Assistant 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
@@ -174,7 +220,7 @@ To be compatible, a provider should support:
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
@@ -191,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
``` ```
@@ -202,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
@@ -220,30 +266,58 @@ 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
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
```yaml ```yaml
service: ha_text_ai.set_system_prompt service: ha_text_ai.set_system_prompt
data: data:
instance: sensor.ha_text_ai_gpt
prompt: | prompt: |
You are a home automation expert focused on: You are a home automation expert focused on:
1. Energy efficiency 1. Energy efficiency
@@ -255,14 +329,163 @@ data:
### clear_history ### clear_history
```yaml ```yaml
service: ha_text_ai.clear_history service: ha_text_ai.clear_history
data:
instance: sensor.ha_text_ai_gpt
``` ```
### get_history ### get_history
```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
```
## 🚀 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
@@ -299,6 +522,7 @@ automation:
- service: ha_text_ai.ask_question - service: ha_text_ai.ask_question
data: data:
question: "Home automation advice" question: "Home automation advice"
instance: sensor.ha_text_ai_gpt
- service: notify.mobile - service: notify.mobile
data: data:
message: > message: >
@@ -318,7 +542,7 @@ automation:
- 🤖 **Model and Provider Information**: Tracking current AI model and service provider - 🤖 **Model and Provider Information**: Tracking current AI model and service provider
- 🚦 **System Status**: Real-time API and processing readiness - 🚦 **System Status**: Real-time API and processing readiness
- 📊 **Performance Metrics**: Request success rates and response times - 📊 **Performance Metrics**: Request success rates and response times
- 💬 **Conversation Tracking**: Token usage and interaction history (for more precise token counting, install `tiktoken`; otherwise, a fallback estimation method is automatically used) - 💬 **Conversation Tracking**: Token usage and interaction history are estimated using a heuristic method based on word count and specific word characteristics, which may differ from actual token usage.
- 🕒 **Last Interaction Details**: Recent query and response tracking - 🕒 **Last Interaction Details**: Recent query and response tracking
- ❤️ **System Health**: Error monitoring and service uptime - ❤️ **System Health**: Error monitoring and service uptime
@@ -384,7 +608,7 @@ automation:
# Number of entries in current history file # Number of entries in current history file
{{ state_attr('sensor.ha_text_ai_gpt', 'History size') }} # 0 {{ state_attr('sensor.ha_text_ai_gpt', 'History size') }} # 0
# Last few conversation entries (limited to 3 for performance) # Last few conversation entries (limited to 1 for performance)
{{ state_attr('sensor.ha_text_ai_gpt', 'conversation_history') }} # [...] {{ state_attr('sensor.ha_text_ai_gpt', 'conversation_history') }} # [...]
``` ```
@@ -414,7 +638,7 @@ automation:
### History Storage ### History Storage
Conversation history stored in `.storage/ha_text_ai_history/` directory: Conversation history stored in `.storage/ha_text_ai_history/` directory:
- Each instance has its own history file - Each instance has its own history file (JSON)
- Files are automatically rotated when size limit is reached - Files are automatically rotated when size limit is reached
- Archived history files are timestamped - Archived history files are timestamped
- Default maximum file size: 1MB - Default maximum file size: 1MB
@@ -429,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.
@@ -443,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.
@@ -507,11 +734,12 @@ 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/f2a41aaa2cd88adaf6929ba5f228205a315efdfa/custom_components/ha_text_ai/icons/dark_icon%402x.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 ❤️ and Claude 3.5 Sonnet for the Home Assistant Community Made with ❤️ for the Home Assistant Community
[Report Bug](https://github.com/smkrv/ha-text-ai/issues) · [Request Feature](https://github.com/smkrv/ha-text-ai/issues) [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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Before

Width:  |  Height:  |  Size: 618 KiB

After

Width:  |  Height:  |  Size: 618 KiB

Before

Width:  |  Height:  |  Size: 923 KiB

After

Width:  |  Height:  |  Size: 923 KiB

Before

Width:  |  Height:  |  Size: 339 KiB

After

Width:  |  Height:  |  Size: 339 KiB

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.0 MiB

+75 -22
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,
@@ -80,6 +87,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,11 +116,11 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
# Initialize domain data storage # Initialize domain data storage
hass.data.setdefault(DOMAIN, {}) hass.data.setdefault(DOMAIN, {})
async def async_ask_question(call: ServiceCall) -> None: async def async_ask_question(call: ServiceCall) -> dict:
"""Handle ask_question service.""" """Handle ask_question service with response data."""
try: try:
coordinator = get_coordinator_by_instance(hass, call.data["instance"]) coordinator = get_coordinator_by_instance(hass, call.data["instance"])
await coordinator.async_ask_question( response = await coordinator.async_ask_question(
question=call.data["question"], question=call.data["question"],
model=call.data.get("model"), model=call.data.get("model"),
temperature=call.data.get("temperature"), temperature=call.data.get("temperature"),
@@ -118,9 +128,34 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
system_prompt=call.data.get("system_prompt"), system_prompt=call.data.get("system_prompt"),
context_messages=call.data.get("context_messages"), context_messages=call.data.get("context_messages"),
) )
# Return structured response data
return {
"response_text": response.get("content", ""),
"tokens_used": response.get("tokens", {}).get("total", 0),
"prompt_tokens": response.get("tokens", {}).get("prompt", 0),
"completion_tokens": response.get("tokens", {}).get("completion", 0),
"model_used": response.get("model", call.data.get("model", coordinator.model)),
"instance": call.data["instance"],
"question": call.data["question"],
"timestamp": response.get("timestamp"),
"success": True
}
except Exception as err: except Exception as err:
_LOGGER.error("Error asking question: %s", str(err)) _LOGGER.error("Error asking question: %s", str(err))
raise HomeAssistantError(f"Failed to process question: {str(err)}") # Return error response
return {
"response_text": "",
"tokens_used": 0,
"prompt_tokens": 0,
"completion_tokens": 0,
"model_used": call.data.get("model", ""),
"instance": call.data["instance"],
"question": call.data["question"],
"timestamp": datetime.now().isoformat(),
"success": False,
"error": str(err)
}
async def async_clear_history(call: ServiceCall) -> None: async def async_clear_history(call: ServiceCall) -> None:
"""Handle clear_history service.""" """Handle clear_history service."""
@@ -137,7 +172,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 +195,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 +269,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
@@ -263,22 +312,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_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 # Get configuration (merge data with options to apply any runtime changes)
config = {**entry.data, **entry.options}
session = aiohttp_client.async_get_clientsession(hass) session = aiohttp_client.async_get_clientsession(hass)
api_provider = entry.data.get(CONF_API_PROVIDER) api_provider = config.get(CONF_API_PROVIDER)
model = entry.data.get(CONF_MODEL, DEFAULT_MODEL) model = config.get(CONF_MODEL, DEFAULT_MODEL)
endpoint = entry.data.get( endpoint = config.get(
CONF_API_ENDPOINT, CONF_API_ENDPOINT,
DEFAULT_OPENAI_ENDPOINT if api_provider == API_PROVIDER_OPENAI DEFAULT_OPENAI_ENDPOINT if api_provider == API_PROVIDER_OPENAI
else DEFAULT_ANTHROPIC_ENDPOINT else DEFAULT_ANTHROPIC_ENDPOINT
).rstrip('/') ).rstrip('/')
api_key = entry.data[CONF_API_KEY] api_key = entry.data[CONF_API_KEY] # API key stays in data, not in options
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 +343,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 +354,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 +368,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}")
+214 -16
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))
@@ -109,19 +136,53 @@ class APIClient:
return await self._create_anthropic_completion( return await self._create_anthropic_completion(
model, messages, temperature, max_tokens model, messages, temperature, max_tokens
) )
elif self.api_provider == API_PROVIDER_DEEPSEEK:
return await self._create_deepseek_completion(
model, messages, temperature, max_tokens
)
elif self.api_provider == API_PROVIDER_GEMINI:
return await self._create_gemini_completion(
model, messages, temperature, max_tokens
)
else: else:
return await self._create_openai_completion( return await self._create_openai_completion(
model, messages, temperature, max_tokens model, messages, temperature, max_tokens
) )
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,
) -> 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
}
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,
@@ -206,6 +267,143 @@ 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,
) -> 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
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']}]
})
# Create configuration
def create_config():
from google.genai import types
config = types.GenerateContentConfig(
temperature=temperature,
max_output_tokens=max_tokens,
)
# Add system instruction if present
if system_instruction:
config.system_instruction = system_instruction.strip()
return config
config = await asyncio.to_thread(create_config)
def generate_content():
# For single message without history, use generate_content
if len(contents) <= 1:
# If we have no content yet, create a simple prompt
if not contents:
prompt = "I need your assistance."
else:
prompt = contents[0]["parts"][0]["text"]
return client.models.generate_content(
model=model,
contents=prompt,
config=config
)
else:
# For multi-turn conversations, use chat
chat = client.chats.create(model=model, config=config)
# Send all messages in sequence
for content in contents:
if content["role"] == "user":
response = chat.send_message(content["parts"][0]["text"])
# We don't send assistant messages as they're already part of the history
return response
response = await asyncio.to_thread(generate_content)
# Extract response text
def extract_response():
response_text = response.text if hasattr(response, 'text') else ""
# Try to get token usage if available
usage = {}
if hasattr(response, 'usage_metadata'):
usage = {
"prompt_tokens": getattr(response.usage_metadata, 'prompt_token_count', 0),
"completion_tokens": getattr(response.usage_metadata, 'candidates_token_count', 0),
"total_tokens": getattr(response.usage_metadata, 'total_token_count', 0)
}
else:
# Estimate token count as fallback
usage = {
"prompt_tokens": len(" ".join([m["content"] for m in messages]).split()) // 3,
"completion_tokens": len(response_text.split()) // 3,
"total_tokens": 0 # Will be calculated below
}
usage["total_tokens"] = usage["prompt_tokens"] + usage["completion_tokens"]
return response_text, usage
response_text, usage = await asyncio.to_thread(extract_response)
return {
"choices": [{
"message": {
"content": response_text
}
}],
"usage": usage
}
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")
+234 -27
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),
} }
@@ -296,13 +489,20 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
return self.async_create_entry(title="", data=user_input) 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}
provider = current_data.get(CONF_API_PROVIDER)
default_model = (
DEFAULT_DEEPSEEK_MODEL if provider == API_PROVIDER_DEEPSEEK else
DEFAULT_GEMINI_MODEL if provider == API_PROVIDER_GEMINI else
DEFAULT_MODEL
)
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.Optional(
CONF_MODEL, CONF_MODEL,
default=current_data.get(CONF_MODEL, DEFAULT_MODEL) default=current_data.get(CONF_MODEL, default_model)
): str, ): str,
vol.Optional( vol.Optional(
CONF_TEMPERATURE, CONF_TEMPERATURE,
@@ -325,6 +525,13 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
vol.Coerce(float), vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL) vol.Range(min=MIN_REQUEST_INTERVAL)
), ),
vol.Optional(
CONF_API_TIMEOUT,
default=current_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( vol.Optional(
CONF_CONTEXT_MESSAGES, CONF_CONTEXT_MESSAGES,
default=current_data.get( default=current_data.get(
+41 -3
View File
@@ -6,10 +6,14 @@ Constants for the HA text AI integration.
@github: https://github.com/smkrv/ha-text-ai @github: https://github.com/smkrv/ha-text-ai
@source: https://github.com/smkrv/ha-text-ai @source: https://github.com/smkrv/ha-text-ai
""" """
import os
import json
from typing import Final 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"
@@ -19,15 +23,37 @@ 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
MANIFEST_PATH = os.path.join(os.path.dirname(__file__), "manifest.json")
try:
with open(MANIFEST_PATH) as manifest_file:
manifest = json.load(manifest_file)
VERSION = manifest.get("version", "unknown")
except FileNotFoundError:
VERSION = "unknown"
_LOGGER.warning("manifest.json not found at %s", MANIFEST_PATH)
except json.JSONDecodeError as err:
VERSION = "unknown"
_LOGGER.error("Error decoding JSON from manifest.json: %s", err)
except Exception as err:
VERSION = "unknown"
_LOGGER.error("Error reading manifest.json: %s", 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"
@@ -35,6 +61,7 @@ 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"
@@ -47,25 +74,32 @@ 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"
DEFAULT_CONTEXT_MESSAGES: Final = 5 DEFAULT_CONTEXT_MESSAGES: Final = 5
TRUNCATION_INDICATOR = " ... "
# Parameter constraints # Parameter constraints
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
@@ -202,6 +236,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),
+239 -172
View File
@@ -37,27 +37,16 @@ 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,
MAX_HISTORY_FILE_SIZE, MAX_HISTORY_FILE_SIZE,
TRUNCATION_INDICATOR,
) )
_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."""
@@ -88,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
@@ -127,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
@@ -173,7 +164,6 @@ class HATextAICoordinator(DataUpdateCoordinator):
self.available = True self.available = True
self._state = STATE_READY self._state = STATE_READY
self._conversation_history = [] # Full conversation history
self._start_time = dt_util.utcnow() self._start_time = dt_util.utcnow()
@@ -182,13 +172,13 @@ class HATextAICoordinator(DataUpdateCoordinator):
hass.config.path(".storage"), hass.config.path(".storage"),
"ha_text_ai_history" "ha_text_ai_history"
) )
hass.async_create_task(self._create_history_dir())
hass.async_create_task(self._check_history_directory()) # Initialize all async tasks in proper order
self.hass.async_create_task(self._create_history_dir())
hass.async_create_task(self._migrate_history_from_txt_to_json()) self.hass.async_create_task(self._check_history_directory())
self.hass.async_create_task(self._initialize_metrics())
hass.async_create_task(self._initialize_metrics()) self.hass.async_create_task(self.async_initialize_history_file())
self.hass.async_create_task(self._migrate_history_from_txt_to_json())
# History file path using instance name # History file path using instance name
self._history_file = os.path.join( self._history_file = os.path.join(
@@ -273,18 +263,17 @@ class HATextAICoordinator(DataUpdateCoordinator):
_LOGGER.error(f"Error checking file existence for {path}: {e}") _LOGGER.error(f"Error checking file existence for {path}: {e}")
return False return False
async def _create_history_dir(self): async def _create_history_dir(self) -> None:
""" """
Asynchronously create history directory. Asynchronously create history directory.
Creates the directory for storing history files
without blocking the event loop.
""" """
try: try:
await self.hass.async_add_executor_job( def mkdir_sync():
lambda: os.makedirs(self._history_dir, exist_ok=True) os.makedirs(self._history_dir, exist_ok=True)
)
_LOGGER.debug(f"Directory creation details: exist_ok=True") await self.hass.async_add_executor_job(mkdir_sync)
_LOGGER.debug(f"History directory created/verified: {self._history_dir}")
except PermissionError: except PermissionError:
_LOGGER.error(f"Permission denied when creating history directory: {self._history_dir}") _LOGGER.error(f"Permission denied when creating history directory: {self._history_dir}")
raise raise
@@ -405,27 +394,38 @@ class HATextAICoordinator(DataUpdateCoordinator):
async def async_initialize_history_file(self) -> None: async def async_initialize_history_file(self) -> None:
""" """
Asynchronously initialize history file. Asynchronously initialize history file and load existing history.
Creates the file and writes an initialization timestamp
without blocking the event loop using Home Assistant's executor.
""" """
try: try:
await self.hass.async_add_executor_job(self._sync_initialize_history_file) await self._create_history_dir()
if await self._file_exists(self._history_file):
# Load existing history
async with AsyncFileHandler(self._history_file, 'r') as f:
content = await f.read()
if content:
history = json.loads(content)
# Validate and load history
if isinstance(history, list):
self._conversation_history = history[-self.max_history_size:]
_LOGGER.debug(
f"Loaded {len(self._conversation_history)} history entries "
f"for {self.instance_name}"
)
else:
# Create new history file
async with AsyncFileHandler(self._history_file, 'w') as f:
await f.write(json.dumps([]))
await self._check_history_size()
await self.async_update_ha_state()
_LOGGER.debug(f"History file initialized: {self._history_file}")
except Exception as e: except Exception as e:
_LOGGER.error(f"Could not initialize history file: {e}") _LOGGER.error(f"Could not initialize history file: {e}")
_LOGGER.debug(traceback.format_exc()) _LOGGER.debug(traceback.format_exc())
async def _sync_initialize_history_file(self) -> None:
try:
if not await self._file_exists(self._history_file):
def write_empty_history():
with open(self._history_file, 'w') as f:
json.dump([], f)
await self.hass.async_add_executor_job(write_empty_history)
except Exception as e:
_LOGGER.error(f"History file initialization failed: {e}")
# Size check to _update_history method # Size check to _update_history method
async def _update_history(self, question: str, response: dict) -> None: async def _update_history(self, question: str, response: dict) -> None:
"""Update conversation history with size validation.""" """Update conversation history with size validation."""
@@ -509,6 +509,15 @@ class HATextAICoordinator(DataUpdateCoordinator):
_LOGGER.error(f"Error writing history entry: {e}") _LOGGER.error(f"Error writing history entry: {e}")
_LOGGER.debug(traceback.format_exc()) _LOGGER.debug(traceback.format_exc())
async def _check_history_size(self) -> None:
"""Verify and adjust history size if needed."""
if len(self._conversation_history) > self.max_history_size:
_LOGGER.warning(
f"History size ({len(self._conversation_history)}) exceeds "
f"maximum ({self.max_history_size}). Trimming..."
)
self._conversation_history = self._conversation_history[-self.max_history_size:]
async def _check_file_size(self, file_path: str) -> int: async def _check_file_size(self, file_path: str) -> int:
""" """
Check file size asynchronously. Check file size asynchronously.
@@ -530,25 +539,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."""
@@ -565,7 +606,10 @@ class HATextAICoordinator(DataUpdateCoordinator):
Asynchronously check history directory permissions and writability. Asynchronously check history directory permissions and writability.
""" """
try: try:
# Test write permission in a separate thread # First ensure directory exists
await self._create_history_dir()
# Then test write permission in a separate thread
test_file_path = os.path.join(self._history_dir, ".write_test") test_file_path = os.path.join(self._history_dir, ".write_test")
await self.hass.async_add_executor_job(self._sync_test_directory_write, test_file_path) await self.hass.async_add_executor_job(self._sync_test_directory_write, test_file_path)
@@ -626,15 +670,14 @@ class HATextAICoordinator(DataUpdateCoordinator):
async def _async_update_data(self) -> Dict[str, Any]: async def _async_update_data(self) -> Dict[str, Any]:
"""Update coordinator data with improved error handling and performance.""" """Update coordinator data with improved error handling and performance."""
try: try:
async with asyncio.Semaphore(1): async with asyncio.Semaphore(1):
current_state = self._get_current_state() current_state = self._get_current_state()
limited_history = await self._get_limited_history() # Get limited history with info
history_data = await self._get_limited_history()
metrics = await self._get_current_metrics() metrics = await self._get_current_metrics()
if metrics is None: if metrics is None:
metrics = {} metrics = {}
@@ -649,47 +692,68 @@ class HATextAICoordinator(DataUpdateCoordinator):
"uptime": self._calculate_uptime(), "uptime": self._calculate_uptime(),
"system_prompt": self._get_truncated_system_prompt(), "system_prompt": self._get_truncated_system_prompt(),
"history_size": len(self._conversation_history), "history_size": len(self._conversation_history),
"conversation_history": limited_history, "conversation_history": history_data["entries"],
"history_info": history_data["info"],
"normalized_name": self.normalized_name, "normalized_name": self.normalized_name,
} }
await self._validate_update_data(data) await self._validate_update_data(data)
return data return data
except asyncio.CancelledError:
_LOGGER.warning("Update was cancelled")
raise
except Exception as err: except Exception as err:
_LOGGER.error(f"Error updating data: {err}", exc_info=True) _LOGGER.error(f"Error updating data: {err}", exc_info=True)
return self._get_safe_initial_state() return self._get_safe_initial_state()
async def _get_limited_history(self) -> List[Dict[str, Any]]: async def _get_limited_history(self) -> Dict[str, Any]:
"""Get limited conversation history with size constraints.""" """Get limited conversation history showing only last Q&A."""
limited_history = [] limited_history = []
if self._conversation_history: if self._conversation_history:
for entry in self._conversation_history[-3:]: last_entry = self._conversation_history[-1]
limited_entry = { limited_entry = {
"timestamp": entry["timestamp"], "timestamp": last_entry["timestamp"],
"question": self._truncate_text(entry["question"]), "question": last_entry["question"][:4096] + (TRUNCATION_INDICATOR if len(last_entry["question"]) > 4096 else ""),
"response": self._truncate_text(entry["response"]), "response": last_entry["response"][:4096] + (TRUNCATION_INDICATOR if len(last_entry["response"]) > 4096 else ""),
} }
limited_history.append(limited_entry) limited_history.append(limited_entry)
return limited_history
history_info = {
"total_entries": len(self._conversation_history),
"displayed_entries": len(limited_history),
"full_history_available": True,
"history_path": self._history_file,
}
return {
"entries": limited_history,
"full_history": self._conversation_history,
"info": history_info
}
async def _get_sanitized_last_response(self) -> Dict[str, Any]:
"""Get sanitized version of last response with truncation indicators."""
response = self.last_response.copy()
if "response" in response:
original_response = response["response"]
is_response_truncated = len(original_response) > 4096
response["response"] = original_response[:4096] + (TRUNCATION_INDICATOR if is_response_truncated else "")
response["is_response_truncated"] = is_response_truncated
response["full_response_length"] = len(original_response)
if "question" in response:
original_question = response["question"]
is_question_truncated = len(original_question) > 4096
response["question"] = original_question[:4096] + (TRUNCATION_INDICATOR if is_question_truncated else "")
response["is_question_truncated"] = is_question_truncated
response["full_question_length"] = len(original_question)
return response
def _truncate_text(self, text: str, max_length: int = MAX_ATTRIBUTE_SIZE) -> str: def _truncate_text(self, text: str, max_length: int = MAX_ATTRIBUTE_SIZE) -> str:
"""Safely truncate text to maximum length.""" """Safely truncate text to maximum length."""
return text[:max_length] if text else "" return text[:max_length] if text else ""
async def _get_sanitized_last_response(self) -> Dict[str, Any]:
"""Get sanitized version of last response."""
response = self.last_response.copy()
if "response" in response:
response["response"] = self._truncate_text(response["response"])
if "question" in response:
response["question"] = self._truncate_text(response["question"])
return response
def _calculate_uptime(self) -> float: def _calculate_uptime(self) -> float:
"""Calculate current uptime in seconds.""" """Calculate current uptime in seconds."""
return (dt_util.utcnow() - self._start_time).total_seconds() return (dt_util.utcnow() - self._start_time).total_seconds()
@@ -754,46 +818,6 @@ class HATextAICoordinator(DataUpdateCoordinator):
"normalized_name": self.normalized_name, "normalized_name": self.normalized_name,
} }
def _calculate_context_tokens(self, messages: List[Dict[str, str]]) -> 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,
@@ -834,9 +858,7 @@ class HATextAICoordinator(DataUpdateCoordinator):
system_prompt: Optional[str] = None, system_prompt: Optional[str] = None,
context_messages: Optional[int] = None, context_messages: Optional[int] = 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")
@@ -858,62 +880,23 @@ 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,
} }
# Process message
response = await self.async_process_message(question, **kwargs) response = await self.async_process_message(question, **kwargs)
# Update metrics # Update metrics
@@ -936,29 +919,44 @@ 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 async with asyncio.timeout(self.api_timeout):
if self.is_anthropic: if self.is_anthropic:
response = await self._process_anthropic_message(question, **kwargs) response = await self._process_anthropic_message(question, **kwargs)
else: else:
response = await self._process_openai_message(question, **kwargs) response = await self._process_openai_message(question, **kwargs)
# Add timestamp and model information to response
timestamp = dt_util.utcnow().isoformat()
model_used = kwargs.get("model", self.model)
# Enhance response with metadata
enhanced_response = {
"content": response["content"],
"tokens": response.get("tokens", {}),
"model": model_used,
"timestamp": timestamp,
"instance": self.instance_name,
"question": question,
"success": True
}
self.last_response = { self.last_response = {
"timestamp": dt_util.utcnow().isoformat(), "timestamp": timestamp,
"question": question, "question": question,
"response": response["content"], "response": response["content"],
"model": kwargs.get("model", self.model), "model": model_used,
"instance": self.instance_name, "instance": self.instance_name,
"normalized_name": self.normalized_name, "normalized_name": self.normalized_name,
"error": None, "error": None,
} }
return response return enhanced_response
except asyncio.TimeoutError: except asyncio.TimeoutError:
raise HomeAssistantError("Request timed out") raise HomeAssistantError("Request timed out")
except Exception as err: except Exception as err:
self._handle_error(err) await self._handle_error(err)
raise raise
async def _process_anthropic_message(self, question: str, **kwargs) -> dict: async def _process_anthropic_message(self, question: str, **kwargs) -> dict:
@@ -1090,15 +1088,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}")
+9 -8
View File
@@ -13,16 +13,17 @@
"loggers": ["custom_components.ha_text_ai"], "loggers": ["custom_components.ha_text_ai"],
"mqtt": [], "mqtt": [],
"quality_scale": "silver", "quality_scale": "silver",
"requirements": [ "requirements": [
"openai>=1.12.0", "openai>=1.12.0",
"anthropic>=0.8.0", "anthropic>=0.8.0",
"aiohttp>=3.8.0", "google-genai>=1.16.0",
"async-timeout>=4.0.0", "aiohttp>=3.8.0",
"certifi>=2024.2.2" "async-timeout>=4.0.0",
], "certifi>=2024.2.2"
],
"single_config_entry": false, "single_config_entry": false,
"ssdp": [], "ssdp": [],
"usb": [], "usb": [],
"version": "2.0.6-beta", "version": "2.2.0",
"zeroconf": [] "zeroconf": []
} }
+3 -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,
@@ -68,6 +69,7 @@ from .const import (
DEFAULT_NAME_PREFIX, DEFAULT_NAME_PREFIX,
CONF_MAX_HISTORY_SIZE, CONF_MAX_HISTORY_SIZE,
MAX_ATTRIBUTE_SIZE, MAX_ATTRIBUTE_SIZE,
VERSION,
) )
from .coordinator import HATextAICoordinator from .coordinator import HATextAICoordinator
@@ -154,7 +156,7 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
name=self._attr_name, name=self._attr_name,
manufacturer="Community", manufacturer="Community",
model=f"{model} ({api_provider} provider)", model=f"{model} ({api_provider} provider)",
sw_version="1.0.0", sw_version=VERSION,
) )
_LOGGER.debug( _LOGGER.debug(
+3 -2
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,13 +64,13 @@ 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
@@ -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)"
} }
@@ -77,8 +79,9 @@
"data": { "data": {
"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": "Minimale 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 +91,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 +129,7 @@
}, },
"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)"
} }
} }
}, },
@@ -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)"
} }
@@ -77,8 +79,9 @@
"data": { "data": {
"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 +89,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 +129,7 @@
}, },
"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)"
} }
} }
}, },
@@ -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)"
} }
@@ -77,8 +79,9 @@
"data": { "data": {
"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 +92,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 (IA de Texto de HA)", "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 +129,7 @@
}, },
"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)"
} }
} }
}, },
@@ -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)"
} }
@@ -77,8 +70,9 @@
"data": { "data": {
"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 +83,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": "प्रश्न पूछें (एचए टेक्स्ट एआई)", "name": "प्रश्न पूछें (HA Text AI)",
"description": "एआई मॉडल को एक प्रश्न भेजें और विस्तृत प्रतिक्रिया प्राप्त करें। प्रतिक्रिया बातचीत के इतिहास में संग्रहीत क जाएगी और बाद में पुनर्प्राप्त की जा सकती है।", "description": "AI मॉडल को प्रश्न भेजें और विस्तृत उत्तर प्राप्त करें। यह सेवा अब प्रत्यक्ष रूप से प्रतिक्रिया डेटा वापस करती है, अलग टेक्स्ट सेंसर की आवश्यकता और 255 वर्ण की सीमा को समाप्त करती है। प्रतिक्रिया को बातचीत के इतिहास में भी संग्रहीत किया जाएग।",
"fields": { "fields": {
"instance": { "instance": {
"name": "उदाहरण", "name": "उदाहरण",
@@ -124,7 +120,7 @@
}, },
"max_tokens": { "max_tokens": {
"name": "अधिकतम टोकन", "name": "अधिकतम टोकन",
"description": "प्रतिक्रिया की अधिकतम लंबाई (1-4096 टोकन)" "description": "प्रतिक्रिया की अधिकतम लंबाई (1-100000 टोकन)"
} }
} }
}, },
@@ -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)"
} }
@@ -77,8 +79,9 @@
"data": { "data": {
"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 +92,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 +129,7 @@
}, },
"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)"
} }
} }
}, },
@@ -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)"
} }
@@ -77,8 +79,9 @@
"data": { "data": {
"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 +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)", "name": "Задать вопрос (HA Text AI)",
"description": "Отправить вопрос модели ИИ и получить подробный ответ. Ответ будет сохранен в истории разговора и может быть получен позже.", "description": "Отправить вопрос модели ИИ и получить подробный ответ. Сервис теперь возвращает данные ответа напрямую, устраняя необходимость в отдельных текстовых сенсорах и ограничение в 255 символов. Ответ также будет сохранен в истории разговора.",
"fields": { "fields": {
"instance": { "instance": {
"name": "Экземпляр", "name": "Экземпляр",
@@ -124,7 +129,7 @@
}, },
"max_tokens": { "max_tokens": {
"name": "Максимум токенов", "name": "Максимум токенов",
"description": "Максимальная длина ответа (1-4096 токенов)" "description": "Максимальная длина ответа (1-100000 токенов)"
} }
} }
}, },
@@ -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)"
} }
@@ -77,8 +70,9 @@
"data": { "data": {
"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 +83,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 +120,7 @@
}, },
"max_tokens": { "max_tokens": {
"name": "Максимални токени", "name": "Максимални токени",
"description": "Максимална дужина одговора (1-4096 токена)" "description": "Максимална дужина одговора (1-100000 токена)"
} }
} }
}, },
@@ -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"
} }
@@ -77,8 +70,9 @@
"data": { "data": {
"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 +83,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文本AI", "name": "提问 (HA Text AI)",
"description": "向AI模型发送问题并接收详细响应。响应将存储在对话历史中,可以稍后检索。", "description": "向AI模型发送问题并获得详细回答。此服务现在直接返回响应数据,消除了对单独文本传感器的需要和255字符限制。响应将存储在对话历史中。",
"fields": { "fields": {
"instance": { "instance": {
"name": "实例", "name": "实例",
@@ -124,7 +120,7 @@
}, },
"max_tokens": { "max_tokens": {
"name": "最大标记数", "name": "最大标记数",
"description": "响应的最大长度(1-4096个标记)" "description": "响应的最大长度(1-100000个标记)"
} }
} }
}, },
+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"
} }
+27 -29
View File
@@ -1,32 +1,30 @@
``` ```
ha-text-ai/ ha_text_ai/
├── custom_components ├── __init__.py
│   └── ha_text_ai ├── api_client.py
│   ├── __init__.py ├── config_flow.py
│   ├── api_client.py ├── const.py
│   ├── config_flow.py ├── coordinator.py
│   ├── const.py ├── icons
│   ├── coordinator.py │   ├── dark_icon.png
│   ├── icons │   ├── dark_icon@2x.png
│   │   ├── dark_icon.png │   ├── dark_logo.png
│   │   ├── dark_icon@2x.png │   ├── dark_logo@2x.png
│   │   ├── dark_logo.png │   ├── icon.png
│   │   ├── dark_logo@2x.png │   ├── icon@2x.png
│   │   ├── icon.png │   ├── logo.png
│   │   ├── icon@2x.png │   └── logo@2x.png
│   │   ├── logo.png ├── manifest.json
│   │   └── logo@2x.png ├── sensor.py
│   ├── manifest.json ├── services.yaml
│   ├── sensor.py └── translations
│   ├── services.yaml ├── de.json
│   ── translations ── en.json
│   ├── de.json ├── es.json
│   ├── en.json ├── hi.json
│   ├── es.json ├── it.json
│   ├── hi.json ├── ru.json
│   ├── it.json ├── sr.json
│   ── ru.json ── zh.json
│   ├── sr.json
│   └── zh.json
``` ```