Compare commits

...
189 Commits
Author SHA1 Message Date
SMKRV 29f3ae5592 Release v2.0.0 2024-11-25 17:10:38 +03:00
SMKRV 107d2a64fc Release v2.0.0 2024-11-25 17:09:39 +03:00
SMKRV e24bb884ef Release v2.0.0 2024-11-25 17:08:23 +03:00
SMKRV 107a2ef962 Release v2.0.0 2024-11-25 17:04:53 +03:00
SMKRV 9d58f2cf1e Release v2.0.0 2024-11-25 17:04:18 +03:00
SMKRV 29f6860fe1 Release v2.0.0 2024-11-25 17:03:29 +03:00
SMKRV fa89026e05 Release v2.0.0 2024-11-25 17:00:29 +03:00
SMKRV 9968452c46 Release v2.0.0 2024-11-25 16:57:14 +03:00
SMKRV 9665634013 Release v2.0.0 2024-11-25 16:55:12 +03:00
SMKRV 76d10ba8fb Release v2.0.0 2024-11-25 16:54:42 +03:00
SMKRV e9ea10203e Release v2.0.0 2024-11-25 16:54:07 +03:00
SMKRV 92a4c2da02 Release v2.0.0 2024-11-25 16:53:38 +03:00
SMKRV b6d8eb98f6 Release v2.0.0 2024-11-25 16:52:20 +03:00
SMKRV b6b01bccd7 Release v2.0.0 2024-11-25 16:51:42 +03:00
SMKRV ace2339b4f Release v2.0.0 2024-11-25 16:46:27 +03:00
SMKRV 166c1f9c9c Release v2.0.0 2024-11-25 16:45:43 +03:00
SMKRV e4039a08bc Release v2.0.0 2024-11-25 16:45:05 +03:00
SMKRV 1da5b5941d Release v2.0.0 2024-11-25 16:44:14 +03:00
SMKRV 6683f12c80 Release v2.0.0 2024-11-25 16:42:51 +03:00
SMKRV 2277f48e46 Release v2.0.0 2024-11-25 16:40:55 +03:00
SMKRV d206bde15a Release v2.0.0 2024-11-25 16:37:57 +03:00
SMKRV af16d03915 Release v2.0.0 2024-11-25 16:34:36 +03:00
SMKRV bf26cd3cfb Release v2.0.0 2024-11-25 16:25:58 +03:00
SMKRV 094062773a Release v2.0.0 2024-11-25 16:18:54 +03:00
SMKRV 4cd95813bc Release v2.0.0 2024-11-25 15:51:08 +03:00
SMKRV c2064f0b64 Release v2.0.0 2024-11-25 15:42:04 +03:00
SMKRV fafd927610 Release v2.0.0 2024-11-25 14:59:44 +03:00
SMKRV 7f46380054 Release v2.0.0 2024-11-25 02:05:13 +03:00
SMKRV 351a8b18dd Release v2.0.0 2024-11-25 02:03:29 +03:00
SMKRV 39833b333f Release v2.0.0 2024-11-25 01:52:37 +03:00
SMKRV 888a41375b Release v2.0.0 2024-11-25 01:43:06 +03:00
SMKRV 823abb22e4 Release v2.0.0 2024-11-25 01:20:44 +03:00
SMKRV a3c88309b4 Release v2.0.0 2024-11-25 01:10:32 +03:00
SMKRV beebc7e194 Release v2.0.0 2024-11-25 00:58:02 +03:00
SMKRV 3f8f22ac61 Release v2.0.0 2024-11-25 00:50:44 +03:00
SMKRV 12c95d0e92 Release v2.0.0 2024-11-25 00:08:07 +03:00
SMKRV b00f600cd9 Release v2.0.0 2024-11-25 00:05:15 +03:00
SMKRV a4925fc943 Release v2.0.0 2024-11-24 23:43:36 +03:00
SMKRV 28248ac3c4 Release v2.0.0 2024-11-24 23:23:37 +03:00
SMKRV d2b5626977 Release v2.0.0 2024-11-24 23:17:41 +03:00
SMKRV b61429b52d Release v2.0.0 2024-11-24 23:14:48 +03:00
SMKRV d05b39d8ae Release v2.0.0 2024-11-24 22:49:11 +03:00
SMKRV 6b3b0f1bd6 Release v2.0.0 2024-11-24 21:38:22 +03:00
SMKRV feb679ae77 Release v2.0.0 2024-11-24 20:17:01 +03:00
SMKRV 9779e5552d Release v2.0.0 2024-11-24 20:12:03 +03:00
SMKRV fbd187dc29 Release v2.0.0 2024-11-24 19:37:01 +03:00
SMKRV e8b6116439 Release v2.0.0 2024-11-24 19:24:30 +03:00
SMKRV fcd3e79cb7 Release v2.0.0 2024-11-24 18:36:37 +03:00
SMKRV d03078cfd4 Release v2.0.0 2024-11-24 17:55:39 +03:00
SMKRV b94d859849 Release v2.0.0 2024-11-24 17:46:39 +03:00
SMKRV dc4fcdf578 Release v2.0.0 2024-11-24 17:41:48 +03:00
SMKRV da4c40017d Release v2.0.0 2024-11-24 17:34:51 +03:00
SMKRV ac420b6495 Release v2.0.0 2024-11-24 17:27:49 +03:00
SMKRV 5791601c7e Release v2.0.0 2024-11-24 17:09:23 +03:00
SMKRV eb149184c3 Release v2.0.0 2024-11-24 16:56:39 +03:00
SMKRV d410073c64 Release v2.0.0 2024-11-24 16:45:03 +03:00
SMKRV b373f6c513 Release v2.0.0 2024-11-24 16:29:57 +03:00
SMKRV 18395a2265 Release v2.0.0 2024-11-24 13:52:27 +03:00
SMKRV 2e4c63ba7d Release v2.0.0 2024-11-24 13:51:04 +03:00
SMKRV 9fdf7c4642 Release v2.0.0 2024-11-24 03:03:01 +03:00
SMKRV 053a9050b6 Release v2.0.0 2024-11-24 02:58:04 +03:00
SMKRV 7efabdfa70 Release v2.0.0 2024-11-24 02:52:41 +03:00
SMKRV e5077969e9 Release v2.0.0 2024-11-24 02:32:08 +03:00
SMKRV 2688da5a82 Release v2.0.0 2024-11-24 02:20:29 +03:00
SMKRV 4f46d077df Release v2.0.0 2024-11-24 01:54:09 +03:00
SMKRV bde856c576 Release v2.0.0 2024-11-24 01:18:09 +03:00
SMKRV 083cb9f730 Release v2.0.0 2024-11-24 01:03:51 +03:00
SMKRV 2644d720e7 Release v2.0.0 2024-11-24 01:01:51 +03:00
SMKRV 2fe84ab801 Release v2.0.0 2024-11-24 00:56:33 +03:00
SMKRV ca1d79f848 Release v2.0.0 2024-11-24 00:29:45 +03:00
SMKRV e52572beaa Release v2.0.0 2024-11-24 00:16:27 +03:00
SMKRV 0f77a98d76 Release v2.0.0 2024-11-23 23:46:55 +03:00
SMKRV ebede3d56b Release v2.0.0 2024-11-23 23:42:33 +03:00
SMKRV 1692f5519f Release v2.0.0 2024-11-23 23:33:53 +03:00
SMKRV dad1aa1c45 Release v2.0.0 2024-11-23 23:30:39 +03:00
SMKRV 5d2244db6e Release v2.0.0 2024-11-23 23:21:32 +03:00
SMKRV 8a15cfe4b4 Release v2.0.0 2024-11-23 23:17:41 +03:00
SMKRV 500c7fbe30 Release v2.0.0 2024-11-23 21:55:34 +03:00
SMKRV 894b600b09 Release v2.0.0 2024-11-23 21:39:28 +03:00
SMKRV 97f0b30cd6 Release v2.0.0 2024-11-23 21:31:27 +03:00
SMKRV 9855e8a561 Release v2.0.0 2024-11-23 20:12:10 +03:00
SMKRV c2b259ade1 Release v2.0.0 2024-11-23 19:51:21 +03:00
SMKRV cfc185117c Release v2.0.0 2024-11-23 19:42:10 +03:00
SMKRV 9e89920e79 Release v2.0.0 2024-11-23 19:03:06 +03:00
SMKRV af190da333 Misc 2024-11-23 18:59:27 +03:00
SMKRV 3e3ec45b19 Release v2.0.0 2024-11-23 18:58:30 +03:00
SMKRV f0fe593d78 Release v2.0.0 2024-11-23 18:57:02 +03:00
SMKRV 8646118a27 Release v2.0.0 2024-11-23 18:50:48 +03:00
SMKRV 31f21b3a6b Release v2.0.0 2024-11-23 03:06:13 +03:00
SMKRV 12d87e30e1 Release v2.0.0 2024-11-23 03:02:35 +03:00
SMKRV 6ecc3f72d1 Release v2.0.0 2024-11-23 02:21:10 +03:00
SMKRV e8c40dc6b8 Release v2.0.0 2024-11-23 01:45:53 +03:00
SMKRV 0279517a42 Release v2.0.0 2024-11-23 01:34:05 +03:00
SMKRV 072eab1703 Release v2.0.0 2024-11-23 01:21:42 +03:00
SMKRV 3b38a6dd29 Release v2.0.0 2024-11-23 01:09:38 +03:00
SMKRV 7f8d8be5fb Release v2.0.0 2024-11-23 00:59:09 +03:00
SMKRV 3aa6b6a2ef Release v2.0.0 2024-11-23 00:51:57 +03:00
SMKRV d870cfbba6 Release v2.0.0 2024-11-23 00:36:58 +03:00
SMKRV 13a9e1a5d7 Release v2.0.0 2024-11-22 19:18:10 +03:00
SMKRV 06aba7e692 Release v2.0.0 2024-11-22 18:59:10 +03:00
SMKRV c73ff02bfb Release v2.0.0 2024-11-22 18:48:19 +03:00
SMKRV ed03170817 Release v2.0.0 2024-11-22 18:46:22 +03:00
SMKRV 3079994a77 Release v2.0.0 2024-11-22 18:44:38 +03:00
SMKRV 41b37f9edf Release v2.0.0 2024-11-22 18:41:40 +03:00
SMKRV 8d65d3ef4e Release v2.0.0 2024-11-22 18:39:29 +03:00
SMKRV 357c8b8be4 Release v2.0.0 2024-11-22 18:37:55 +03:00
SMKRV a47b343f93 Release v2.0.0 2024-11-22 18:16:15 +03:00
SMKRV 4388e0f2e1 Release v2.0.0 2024-11-22 17:55:46 +03:00
SMKRV f2adff1d85 Release v2.0.0 2024-11-22 17:45:41 +03:00
SMKRV ad51da7950 Release v2.0.0 2024-11-22 17:31:48 +03:00
SMKRV 7610a71829 Release v2.0.0 2024-11-22 17:20:07 +03:00
SMKRV 8800f226d2 Release v2.0.0 2024-11-22 17:19:40 +03:00
SMKRV ba16932b44 Release v2.0.0 2024-11-22 17:10:42 +03:00
SMKRV 81394345b4 Misc 2024-11-22 17:02:56 +03:00
SMKRV 72621d9d0e Release v2.0.0 2024-11-22 16:58:22 +03:00
SMKRV d35ded6502 Release v2.0.0 2024-11-22 16:46:40 +03:00
SMKRV b17e5c3db2 Release v2.0.0 2024-11-22 16:29:26 +03:00
SMKRV 365c4df2a4 Release v2.0.0 2024-11-22 16:16:58 +03:00
SMKRV ab79d05e96 Release v2.0.0 2024-11-22 16:08:33 +03:00
SMKRV a208004fe0 Release v2.0.0 2024-11-22 15:57:13 +03:00
SMKRV 51c714df25 Release v2.0.0 2024-11-22 15:49:47 +03:00
SMKRV 87b45df180 Release v2.0.0 2024-11-22 15:44:45 +03:00
SMKRV accb15a92a Release v2.0.0 2024-11-22 15:38:29 +03:00
SMKRV a421825050 Release v2.0.0 2024-11-22 15:25:01 +03:00
SMKRV c55112c8f7 Release v2.0.0 2024-11-22 15:08:45 +03:00
SMKRV accbc5635e Release v2.0.0 2024-11-22 13:51:23 +03:00
SMKRV 558ff7d141 Release v2.0.0 2024-11-22 13:39:01 +03:00
SMKRV fa70a0a4ab Release v2.0.0 2024-11-22 12:28:49 +03:00
SMKRV 50b414a904 Release v2.0.0 2024-11-22 12:23:28 +03:00
SMKRV 80c91039e1 Release v2.0.0 2024-11-22 12:17:06 +03:00
SMKRV 966e01e7e2 Release v2.0.0 2024-11-22 12:02:26 +03:00
SMKRV 82ffce1e25 Release v2.0.0 2024-11-22 11:50:47 +03:00
SMKRV c7257cc00a Release v2.0.0 2024-11-22 11:39:43 +03:00
SMKRV ab877c2e9a Release v2.0.0 2024-11-22 11:39:27 +03:00
SMKRV c6dcf307fd Release v2.0.0 2024-11-22 11:38:14 +03:00
SMKRV 30d69e7ed1 Release v2.0.0 2024-11-22 11:36:34 +03:00
SMKRV bb8195c0d1 Release v2.0.0 2024-11-22 10:16:46 +03:00
SMKRV a813302e86 Release v2.0.0 2024-11-22 02:45:31 +03:00
SMKRV 0f643664f7 Release v2.0.0 2024-11-22 02:39:40 +03:00
SMKRV 665537cb6a Release v2.0.0 2024-11-22 02:34:46 +03:00
SMKRV 766b9293cf Release v2.0.0 2024-11-22 02:34:05 +03:00
SMKRV 6e014e30d9 Release v2.0.0 2024-11-22 02:33:34 +03:00
SMKRV 30fc8ad1df Release v2.0.0 2024-11-22 02:33:07 +03:00
SMKRV bcad939a3d Release v2.0.0 2024-11-22 02:32:18 +03:00
SMKRV e153df85fb Release v2.0.0 2024-11-22 02:31:02 +03:00
SMKRV 26908b5d81 Release v2.0.0 2024-11-22 02:30:17 +03:00
SMKRV 0ddca8ff58 Release v2.0.0 2024-11-22 02:28:03 +03:00
SMKRV fd285d969e Release v2.0.0 2024-11-22 02:25:27 +03:00
SMKRV 10bebe5eb5 Release v2.0.0 2024-11-22 02:24:45 +03:00
SMKRV fef5e81033 Release v2.0.0 2024-11-22 02:23:04 +03:00
SMKRV a325ae180a Release v2.0.0 2024-11-22 02:20:10 +03:00
SMKRV df1ae9cd55 Release v2.0.0 2024-11-22 02:04:20 +03:00
SMKRV d5380b4195 Release v2.0.0 2024-11-22 02:03:24 +03:00
SMKRV 9e12ada4fa Release v2.0.0 2024-11-22 02:03:06 +03:00
SMKRV bb3240f1b3 Release v2.0.0 2024-11-22 02:00:42 +03:00
SMKRV 7780ac89ef Release v2.0.0 2024-11-22 01:57:21 +03:00
SMKRV c58eae695e Release v2.0.0 2024-11-22 01:56:36 +03:00
SMKRV 45244dcaa9 Release v2.0.0 2024-11-22 01:43:00 +03:00
SMKRV 53b15fa74c Release v2.0.0 2024-11-22 01:34:47 +03:00
SMKRV 9341b02f4b Release v2.0.0 2024-11-21 19:21:04 +03:00
SMKRV 158db522a8 Release v2.0.0 2024-11-21 19:16:21 +03:00
SMKRV f3b76c0bc1 Release v2.0.0 2024-11-21 19:04:28 +03:00
SMKRV d11f961566 Release v2.0.0 2024-11-21 18:55:11 +03:00
SMKRV 9d7f81d042 Release v2.0.0 2024-11-21 18:30:35 +03:00
SMKRV c95e1a829c Release v2.0.0 2024-11-21 18:24:39 +03:00
SMKRV 7bd06e7b88 Release v2.0.0 2024-11-21 18:17:57 +03:00
SMKRV 6e64b6feac Release v2.0.0 2024-11-21 18:12:33 +03:00
SMKRV f6c0e6265e Release v2.0.0 2024-11-21 18:05:45 +03:00
SMKRV 662ce701ca Release v2.0.0 2024-11-21 18:00:33 +03:00
SMKRV 72bbfb3f58 Release v2.0.0 2024-11-21 17:30:59 +03:00
SMKRV 9306c12fcd Release v2.0.0 2024-11-21 17:18:13 +03:00
SMKRV 9f1ea70c9f Release v2.0.0 2024-11-21 17:06:19 +03:00
SMKRV 98913b359a Release v2.0.0 2024-11-21 16:40:55 +03:00
SMKRV a4905f0778 Release v2.0.0 2024-11-21 16:17:51 +03:00
SMKRV e603231633 Release v2.0.0 2024-11-21 15:35:52 +03:00
SMKRV 345463322a Release v2.0.0 2024-11-21 15:15:50 +03:00
SMKRV c298866e3c Release v2.0.0 2024-11-21 14:55:27 +03:00
SMKRV 75e97ac652 Release v2.0.0 2024-11-21 14:28:17 +03:00
SMKRV 149ec16d57 Release v2.0.0 2024-11-21 13:56:22 +03:00
SMKRV 31e30d94aa Release v2.0.0 2024-11-20 14:29:17 +03:00
SMKRV 326f876410 Handles the VSE GPT API endpoint correctly 2024-11-20 12:23:26 +03:00
SMKRV 58a7ae4229 Resoved blocking SSL verification issue in
coordinator.py

 API endpoint handling in config_flow.py
 changes

 Added support for the custom models in const.py

 Requirements in manifest.json updated
2024-11-20 12:07:30 +03:00
SMKRV 8f796ad9dc Retry constants 2024-11-20 01:40:29 +03:00
SMKRV 8be860007a Retry constants
MAX_RETRIES: Final = 3
  RETRY_DELAY: Final = 1.0
2024-11-20 01:39:19 +03:00
SMKRV 1985e201b4 Updated 2024-11-20 01:29:14 +03:00
SMKRV 4e4b41661b Release v2.0.0 2024-11-20 01:25:01 +03:00
SMKRV b24a99a11f Release v2.0.0 2024-11-20 01:24:44 +03:00
SMKRV 69b151f990 Release v2.0.0 2024-11-20 01:11:23 +03:00
SMKRV 2d4eb59d6c Release v2.0.0 2024-11-20 01:09:19 +03:00
20 changed files with 15084 additions and 1612 deletions
+35 -4
View File
@@ -1,8 +1,39 @@
# Python
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
*$py.class *$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 .DS_Store
.env Thumbs.db
.venv *.psd
venv/ *.zip
ENV/
+110
View File
@@ -0,0 +1,110 @@
# 🤝 Contributing Guide
We welcome contributions to the HA Text AI project! This document will help you contribute to the project's development.
## 🌟 How to Contribute
### 1. Preparation
1. Fork the Repository
- Go to the repository page on GitHub
- Click the "Fork" button in the top right corner
2. Clone Your Fork
```bash
git clone https://github.com/YOUR_USERNAME/ha-text-ai.git
cd ha-text-ai
```
3. Set Up Remote Repositories
```bash
git remote add upstream https://github.com/smkrv/ha-text-ai.git
```
### 2. Creating a Development Branch
```bash
# Update the main branch
git checkout main
git pull upstream main
# Create a new branch for your feature
git checkout -b feature/short-description-of-changes
```
### 3. Development
- Follow the project's coding standards
- Write clean and understandable code
- Add comments when necessary
- Create unit tests for new functionality
### 4. Committing Changes
```bash
# Add modified files
git add .
# Create a meaningful commit
git commit -m "Feat: Add [short feature description]"
```
### 5. Commit Message Style
Use the following prefixes:
- `Feat:` - new feature
- `Fix:` - bug fixes
- `Docs:` - documentation updates
- `Style:` - formatting changes
- `Refactor:` - code refactoring
- `Test:` - adding tests
- `Chore:` - project maintenance
### 6. Pushing Changes
```bash
# Push changes to your fork
git push origin feature/short-description-of-changes
```
### 7. Creating a Pull Request (PR)
1. Go to your fork on GitHub
2. Click "New Pull Request"
3. Select the base branch `main` of the original repository
4. Fill out the PR description:
- Brief description of changes
- Motivation for changes
- Screenshots (if applicable)
### 8. Review Process
- Project maintainers will review your PR
- There may be comments and requests for changes
- After approval, the PR will be merged
## 🛠 Code Requirements
- Follow PEP 8 for Python
- Write clear and self-documenting code
- Add type hints
- Cover code with tests
## 🐛 Found a Bug?
1. Check existing Issues
2. Create a new Issue with:
- Bug description
- Reproduction steps
- Home Assistant version
- Plugin version
## 📜 License
By contributing to the project, you agree to the [project's license](LICENSE).
## 🤔 Questions?
If you have any questions, create an Issue or contact the project maintainers.
**Thank you for your contribution!** 🎉
+1
View File
@@ -19,3 +19,4 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
+69 -24
View File
@@ -4,6 +4,9 @@
![GitHub release](https://img.shields.io/github/release/smkrv/ha-text-ai.svg?style=flat-square) ![GitHub downloads](https://img.shields.io/github/downloads/smkrv/ha-text-ai/total.svg?style=flat-square) ![GitHub stars](https://img.shields.io/github/stars/smkrv/ha-text-ai.svg?style=social) ![GitHub last commit](https://img.shields.io/github/last-commit/smkrv/ha-text-ai.svg?style=flat-square) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT) [![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg?style=flat-square)](https://github.com/hacs/integration) ![GitHub release](https://img.shields.io/github/release/smkrv/ha-text-ai.svg?style=flat-square) ![GitHub downloads](https://img.shields.io/github/downloads/smkrv/ha-text-ai/total.svg?style=flat-square) ![GitHub stars](https://img.shields.io/github/stars/smkrv/ha-text-ai.svg?style=social) ![GitHub last commit](https://img.shields.io/github/last-commit/smkrv/ha-text-ai.svg?style=flat-square) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT) [![hacs_badge](https://img.shields.io/badge/HACS-Custom-41BDF5.svg?style=flat-square)](https://github.com/hacs/integration)
<img src="https://github.com/smkrv/ha-text-ai/blob/3e3ec45b195c92989434fde40ae110027f4ea124/misc/icons/icon.png" alt="HA Text AI" width="140"/>
### Advanced AI Integration for Home Assistant with multi-provider support
</div> </div>
<p align="center"> <p align="center">
@@ -11,6 +14,10 @@ Transform your smart home experience with powerful AI assistance powered by mult
</p> </p>
--- ---
> [!NOTE]
> 🚧 ALPHA VERSION 🚧
> Expect: potential bugs, frequent changes, incomplete features.
> 🤝 Community Driven
## 🌟 Features ## 🌟 Features
@@ -19,36 +26,43 @@ Transform your smart home experience with powerful AI assistance powered by mult
- Anthropic Claude integration - Anthropic Claude integration
- Custom API endpoints - Custom API endpoints
- Flexible model selection - Flexible model selection
- 💬 **Advanced Language Processing**: - 💬 **Advanced Language Processing**:
- Context-aware responses - Context-aware responses
- Multi-turn conversations - Multi-turn conversations
- Custom system instructions - Custom system instructions
- Natural conversation flow - Natural conversation flow
- 📝 **Enhanced Memory Management**: - 📝 **Enhanced Memory Management**:
- Persistent conversation history - Persistent conversation history
- Context-aware responses - Context-aware responses
- Customizable history limits - Customizable history limits
- Model-specific filtering - Model-specific filtering
-**Performance Optimization**: -**Performance Optimization**:
- Efficient token usage - Efficient token usage
- Smart rate limiting - Smart rate limiting
- Response caching - Response caching
- Request interval control - Request interval control
- 🎯 **Advanced Customization**: - 🎯 **Advanced Customization**:
- Per-request model selection - Per-request model selection
- Adjustable parameters - Adjustable parameters
- Custom system prompts - Custom system prompts
- Temperature control - Temperature control
- 🔒 **Enhanced Security**: - 🔒 **Enhanced Security**:
- Secure API key storage - Secure API key storage
- Rate limiting protection - Rate limiting protection
- Error handling - Error handling
- Usage monitoring - Usage monitoring
- 🎨 **Improved User Experience**: - 🎨 **Improved User Experience**:
- Intuitive configuration UI - Intuitive configuration UI
- Detailed sensor attributes - Detailed sensor attributes
- Rich service interface - Rich service interface
- Model selection UI - Model selection UI
- 🔄 **Automation Integration**: - 🔄 **Automation Integration**:
- Event-driven responses - Event-driven responses
- Conditional logic support - Conditional logic support
@@ -57,25 +71,58 @@ Transform your smart home experience with powerful AI assistance powered by mult
## 📋 Prerequisites ## 📋 Prerequisites
- Home Assistant 2023.8.0 or newer - Home Assistant 2023.11 or later
- API key from supported providers: - 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/))
- OpenRouter ([Get key](https://openrouter.ai/keys))
- Python 3.9 or newer - Python 3.9 or newer
- Stable internet connection - Stable internet connection
### Configuration Options
- API Provider (OpenAI/Anthropic)
- API Key (provider-specific)
- Model Selection (flexible, provider-specific models)
- Temperature (Creativity control, 0.0-2.0)
- Max Tokens (Response length limit)
- Request Interval (API call throttling)
- Custom API Endpoint (optional)
#### ⓘ Potentially Compatible Providers
The integration is designed to be flexible and may work with other providers offering OpenAI-compatible APIs:
- Groq
- Together AI
- Perplexity AI
- Mistral AI
- Google AI
- Local AI servers (like Ollama)
- Custom OpenAI-compatible endpoints
#### Additional Notes
- Not all providers guarantee full compatibility
- Performance may vary between providers
- Check individual provider's documentation
- Ensure your API key has sufficient credits/quota
#### Provider Compatibility Requirements
To be compatible, a provider should support:
- OpenAI-like REST API structure
- JSON request/response format
- Standard authentication method
- Similar model parameter handling
## ⚡ Installation ## ⚡ Installation
### HACS Installation (Recommended) ### HACS Installation (Recommended)
1. Open HACS in Home Assistant <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>
2. Click on the three dots in the top right corner 1. Open HACS in Home Assistant
3. Select "Custom repositories" 2. Click on "Integrations"
4. Add `https://github.com/smkrv/ha-text-ai` as Integration 3. Click "..." in top right corner
5. Click "Add" 4. Select "Custom repositories"
6. Click on "Integrations" in HACS 5. Add repository URL: `https://github.com/smkrv/ha-text-ai`
7. Search for "HA Text AI" 6. Choose "Integration" as category
8. Click "Download" 7. Click "Download"
9. Restart Home Assistant 8. Restart Home Assistant
### Manual Installation ### Manual Installation
1. Download the latest release 1. Download the latest release
@@ -94,8 +141,9 @@ Transform your smart home experience with powerful AI assistance powered by mult
### Via YAML ### Via YAML
```yaml ```yaml
ha_text_ai: ha_text_ai:
api_provider: openai # or anthropic
api_key: !secret ai_api_key api_key: !secret ai_api_key
model: gpt-3.5-turbo # or claude-3-sonnet model: gpt-4o-mini
temperature: 0.7 temperature: 0.7
max_tokens: 1000 max_tokens: 1000
request_interval: 1.0 request_interval: 1.0
@@ -115,6 +163,7 @@ data:
model: "claude-3-sonnet" # optional model: "claude-3-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
system_prompt: "You are a sleep optimization expert" # optional system_prompt: "You are a sleep optimization expert" # optional
``` ```
@@ -140,38 +189,34 @@ service: ha_text_ai.clear_history
service: ha_text_ai.get_history service: ha_text_ai.get_history
data: data:
limit: 5 # optional limit: 5 # optional
filter_model: "gpt-4" # optional filter_model: "gpt-4o" # optional
``` ```
## 📘 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: Currently OpenAI (GPT models) and Anthropic (Claude models) are supported, with more providers planned.
**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-3.5-Turbo or Claude-3-Sonnet for most queries, implement caching, and optimize token usage.
**Q: Can I use custom models?** **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.
**Q: Can I use custom models?**
A: Yes, you can configure custom endpoints and use any compatible model by specifying it in the configuration. A: Yes, you can configure custom endpoints and use any compatible model by specifying it in the configuration.
**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: How can I reduce API costs?**
A: Use GPT-3.5-Turbo for most queries, implement caching, and optimize token usage. A: Use GPT-3.5-Turbo for most queries, implement caching, and optimize token usage.
**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, API keys are stored securely and data is transmitted via encrypted connections. **Q: How do context messages work?**
A: Context messages allow the AI to remember and reference previous conversation history. By default, 5 previous messages are included, but you can customize this from 1 to 20 messages to control the conversation depth and token usage.
**Q: Can I use custom models?**
A: Yes, configure custom endpoints and models via configuration options.
## 🤝 Contributing ## 🤝 Contributing
+197 -277
View File
@@ -1,20 +1,24 @@
"""The HA Text AI integration.""" """The HA Text AI integration."""
import logging from __future__ import annotations
from typing import Any, Dict, Optional
import asyncio
import voluptuous as vol
import json
from datetime import datetime
from homeassistant.config_entries import ConfigEntry import logging
from homeassistant.const import CONF_API_KEY import os
from homeassistant.core import HomeAssistant, ServiceCall, callback import shutil
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError from datetime import datetime, timedelta
from homeassistant.helpers import aiohttp_client from typing import Any, Dict
from homeassistant.helpers import config_validation as cv
import voluptuous as vol
from async_timeout import timeout from async_timeout import timeout
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import aiohttp_client
from .coordinator import HATextAICoordinator from .coordinator import HATextAICoordinator
from .api_client import APIClient
from .const import ( from .const import (
DOMAIN, DOMAIN,
PLATFORMS, PLATFORMS,
@@ -23,75 +27,161 @@ from .const import (
CONF_MAX_TOKENS, CONF_MAX_TOKENS,
CONF_API_ENDPOINT, CONF_API_ENDPOINT,
CONF_REQUEST_INTERVAL, CONF_REQUEST_INTERVAL,
CONF_API_PROVIDER,
CONF_CONTEXT_MESSAGES,
API_PROVIDER_OPENAI,
API_PROVIDER_ANTHROPIC,
DEFAULT_MODEL, DEFAULT_MODEL,
DEFAULT_TEMPERATURE, DEFAULT_TEMPERATURE,
DEFAULT_MAX_TOKENS, DEFAULT_MAX_TOKENS,
DEFAULT_API_ENDPOINT, DEFAULT_OPENAI_ENDPOINT,
DEFAULT_ANTHROPIC_ENDPOINT,
DEFAULT_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL,
API_VERSION, DEFAULT_CONTEXT_MESSAGES,
API_MODELS_PATH,
API_CHAT_PATH,
API_TIMEOUT, API_TIMEOUT,
API_RETRY_COUNT, SERVICE_ASK_QUESTION,
API_BACKOFF_FACTOR, SERVICE_CLEAR_HISTORY,
LOGGER_NAME, SERVICE_GET_HISTORY,
STATE_ERROR, SERVICE_SET_SYSTEM_PROMPT,
STATE_READY,
STATE_PROCESSING,
STATE_RATE_LIMITED,
STATE_MAINTENANCE,
STATE_DISCONNECTED,
STATE_RETRYING,
STATE_QUEUED,
STATE_UPDATING,
SUPPORTED_MODELS,
EVENT_RESPONSE_RECEIVED,
EVENT_ERROR_OCCURRED,
EVENT_STATE_CHANGED,
) )
_LOGGER = logging.getLogger(LOGGER_NAME) _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
# Service validation schemas
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({ SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
vol.Required("instance"): cv.string,
vol.Required("question"): cv.string, vol.Required("question"): cv.string,
vol.Optional("system_prompt"): cv.string, vol.Optional("system_prompt"): cv.string,
vol.Optional("model"): vol.In(SUPPORTED_MODELS), vol.Optional("model"): cv.string,
vol.Optional("temperature"): vol.All( vol.Optional("temperature"): cv.positive_float,
vol.Coerce(float), vol.Range(min=0, max=2) vol.Optional("max_tokens"): cv.positive_int,
), vol.Optional("context_messages"): cv.positive_int,
vol.Optional("max_tokens"): vol.All(
vol.Coerce(int), vol.Range(min=1, max=4096)
),
vol.Optional("priority"): vol.Boolean,
})
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
vol.Optional("limit", default=10): vol.All(
vol.Coerce(int), vol.Range(min=1, max=100)
),
vol.Optional("filter_model"): vol.In(SUPPORTED_MODELS),
vol.Optional("start_date"): cv.datetime,
vol.Optional("include_metadata"): vol.Boolean,
}) })
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({ SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
vol.Required("instance"): cv.string,
vol.Required("prompt"): cv.string, vol.Required("prompt"): cv.string,
}) })
async def async_check_api(session, endpoint: str, headers: dict, is_anthropic: bool = False) -> bool: SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
vol.Required("instance"): cv.string,
vol.Optional("limit"): cv.positive_int,
vol.Optional("filter_model"): cv.string,
})
def get_coordinator_by_instance(hass: HomeAssistant, instance: str) -> HATextAICoordinator:
"""Get coordinator by instance name."""
if instance.startswith("sensor."):
instance = instance.replace("sensor.ha_text_ai_", "", 1)
for entry_id, coord in hass.data[DOMAIN].items():
if isinstance(coord, HATextAICoordinator) and coord.instance_name.lower() == instance.lower():
return coord
raise HomeAssistantError(f"Instance {instance} not found")
async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
"""Set up the HA Text AI component."""
hass.data.setdefault(DOMAIN, {})
try:
source = os.path.join(os.path.dirname(__file__), 'icons', 'icon.svg')
dest_dir = os.path.join(hass.config.path('www'), 'icons')
os.makedirs(dest_dir, exist_ok=True)
dest = os.path.join(dest_dir, 'icon.svg')
if not os.path.exists(dest):
shutil.copyfile(source, dest)
except Exception as ex:
_LOGGER.warning("Failed to copy custom icon: %s", str(ex))
async def async_ask_question(call: ServiceCall) -> None:
"""Handle ask_question service."""
try:
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
await coordinator.async_ask_question(
question=call.data["question"],
model=call.data.get("model"),
temperature=call.data.get("temperature"),
max_tokens=call.data.get("max_tokens"),
system_prompt=call.data.get("system_prompt"),
context_messages=call.data.get("context_messages"),
)
except Exception as err:
_LOGGER.error("Error asking question: %s", str(err))
raise HomeAssistantError(f"Failed to process question: {str(err)}")
async def async_clear_history(call: ServiceCall) -> None:
"""Handle clear_history service."""
try:
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
await coordinator.async_clear_history()
except Exception as err:
_LOGGER.error("Error clearing history: %s", str(err))
raise HomeAssistantError(f"Failed to clear history: {str(err)}")
async def async_get_history(call: ServiceCall) -> list:
"""Handle get_history service."""
try:
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
return await coordinator.async_get_history(
limit=call.data.get("limit"),
filter_model=call.data.get("filter_model")
)
except Exception as err:
_LOGGER.error("Error getting history: %s", str(err))
raise HomeAssistantError(f"Failed to get history: {str(err)}")
async def async_set_system_prompt(call: ServiceCall) -> None:
"""Handle set_system_prompt service."""
try:
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
await coordinator.async_set_system_prompt(call.data["prompt"])
except Exception as err:
_LOGGER.error("Error setting system prompt: %s", str(err))
raise HomeAssistantError(f"Failed to set system prompt: {str(err)}")
hass.services.async_register(
DOMAIN,
SERVICE_ASK_QUESTION,
async_ask_question,
schema=SERVICE_SCHEMA_ASK_QUESTION
)
hass.services.async_register(
DOMAIN,
SERVICE_CLEAR_HISTORY,
async_clear_history,
schema=vol.Schema({vol.Required("instance"): cv.string})
)
hass.services.async_register(
DOMAIN,
SERVICE_GET_HISTORY,
async_get_history,
schema=SERVICE_SCHEMA_GET_HISTORY
)
hass.services.async_register(
DOMAIN,
SERVICE_SET_SYSTEM_PROMPT,
async_set_system_prompt,
schema=SERVICE_SCHEMA_SET_SYSTEM_PROMPT
)
return True
async def async_check_api(session, endpoint: str, headers: dict, provider: str) -> bool:
"""Check API availability for different providers.""" """Check API availability for different providers."""
try: try:
if is_anthropic: if provider == API_PROVIDER_ANTHROPIC:
check_url = f"{endpoint}/v1/models" check_url = f"{endpoint}/v1/models"
else: else: # OpenAI
check_url = f"{endpoint}/{API_VERSION}/{API_MODELS_PATH}" 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 == 200: if response.status in [200, 404]:
return True return True
elif response.status == 401: elif response.status == 401:
raise ConfigEntryNotReady("Invalid API key") raise ConfigEntryNotReady("Invalid API key")
@@ -105,32 +195,25 @@ async def async_check_api(session, endpoint: str, headers: dict, is_anthropic: b
_LOGGER.error("API check error: %s", str(ex)) _LOGGER.error("API check error: %s", str(ex))
return False return False
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
"""Set up the HA Text AI component."""
hass.data.setdefault(DOMAIN, {
"coordinators": {},
"metrics": {
"total_requests": 0,
"total_tokens": 0,
"errors": {},
"model_usage": {},
}
})
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up HA Text AI from a config entry.""" """Set up HA Text AI from a config entry."""
try: try:
if CONF_API_PROVIDER not in entry.data:
_LOGGER.error("API provider not specified")
raise ConfigEntryNotReady("API provider is required")
session = aiohttp_client.async_get_clientsession(hass) session = aiohttp_client.async_get_clientsession(hass)
api_provider = entry.data.get(CONF_API_PROVIDER)
# Determine API type based on model
model = entry.data.get(CONF_MODEL, DEFAULT_MODEL) model = entry.data.get(CONF_MODEL, DEFAULT_MODEL)
is_anthropic = any(model.startswith("claude") for model in SUPPORTED_MODELS) endpoint = entry.data.get(
CONF_API_ENDPOINT,
DEFAULT_OPENAI_ENDPOINT if api_provider == API_PROVIDER_OPENAI
else DEFAULT_ANTHROPIC_ENDPOINT
).rstrip('/')
api_key = entry.data[CONF_API_KEY] api_key = entry.data[CONF_API_KEY]
endpoint = entry.data.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT).rstrip('/') instance_name = entry.data.get(CONF_NAME, entry.entry_id)
is_anthropic = api_provider == API_PROVIDER_ANTHROPIC
# Configure headers based on API type
headers = { headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
"Accept": "application/json" "Accept": "application/json"
@@ -142,234 +225,71 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
else: else:
headers["Authorization"] = f"Bearer {api_key}" headers["Authorization"] = f"Bearer {api_key}"
# Check API with retries if not await async_check_api(session, endpoint, headers, api_provider):
for attempt in range(API_RETRY_COUNT): raise ConfigEntryNotReady("API connection failed")
if await async_check_api(session, endpoint, headers, is_anthropic):
break
if attempt < API_RETRY_COUNT - 1:
delay = API_BACKOFF_FACTOR * (2 ** attempt)
await asyncio.sleep(delay)
else:
raise ConfigEntryNotReady("Failed to connect to API")
# Create and initialize coordinator _LOGGER.debug("Creating API client for %s with endpoint %s", api_provider, endpoint)
coordinator = HATextAICoordinator(
hass, api_client = APIClient(
api_key=api_key,
endpoint=endpoint,
model=model,
temperature=entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
max_tokens=entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
request_interval=entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL),
session=session, session=session,
is_anthropic=is_anthropic endpoint=endpoint,
headers=headers,
api_provider=api_provider,
model=model,
) )
# Initialize coordinator coordinator = HATextAICoordinator(
hass=hass,
client=api_client,
model=model,
update_interval=entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL),
instance_name=instance_name,
max_tokens=entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
temperature=entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
is_anthropic=is_anthropic,
context_messages=entry.data.get(
CONF_CONTEXT_MESSAGES,
DEFAULT_CONTEXT_MESSAGES
),
)
coordinator.data = coordinator._initial_state.copy()
_LOGGER.debug(f"Initial state set for coordinator {instance_name}")
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
# Check coordinator status hass.data.setdefault(DOMAIN, {})
if coordinator.endpoint_status == "auth_error":
raise ConfigEntryNotReady("Authentication failed")
elif coordinator.endpoint_status == "rate_limited":
_LOGGER.warning("API rate limited during setup")
elif coordinator.endpoint_status == "maintenance":
raise ConfigEntryNotReady("API is in maintenance mode")
elif coordinator.endpoint_status == "error":
raise ConfigEntryNotReady("API error during setup")
elif not coordinator.last_update_success:
raise ConfigEntryNotReady("Failed to initialize coordinator")
hass.data[DOMAIN][entry.entry_id] = coordinator hass.data[DOMAIN][entry.entry_id] = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# Register event handlers
@callback
def handle_state_change(event):
"""Handle state changes."""
if event.data.get("entity_id").startswith(f"{DOMAIN}."):
_LOGGER.debug("State changed: %s", event.data)
hass.bus.async_listen(EVENT_STATE_CHANGED, handle_state_change)
# Register services
async def async_ask_question(call: ServiceCall) -> None:
"""Handle the ask_question service call."""
question = call.data.get("question", "")
if not question:
_LOGGER.error("No question provided in service call")
return
request_params = {}
for param in ["system_prompt", "model", "temperature", "max_tokens"]:
if param in call.data:
request_params[param] = call.data[param]
try:
await coordinator.async_ask_question(question, **request_params)
except Exception as err:
_LOGGER.error("Error asking question: %s", str(err))
async def async_clear_history(call: ServiceCall) -> None:
"""Handle the clear_history service call."""
try:
coordinator._responses.clear()
await coordinator.async_refresh()
_LOGGER.info("History cleared successfully")
except Exception as err:
_LOGGER.error("Error clearing history: %s", str(err))
async def async_get_history(call: ServiceCall) -> dict:
"""Handle the get_history service call."""
try:
limit = min(int(call.data.get("limit", 10)), 100)
filter_model = str(call.data.get("filter_model", ""))
start_date = call.data.get("start_date")
include_metadata = call.data.get("include_metadata", False)
responses = coordinator._responses
metrics = {
"total_requests": coordinator.request_count,
"total_tokens": coordinator.tokens_used,
"api_version": coordinator.api_version,
"endpoint_status": coordinator.endpoint_status,
"error_count": coordinator.error_count
}
filtered_responses = responses.copy()
if filter_model:
filtered_responses = {
k: v for k, v in filtered_responses.items()
if v.get("model") == filter_model
}
if start_date:
filtered_responses = {
k: v for k, v in filtered_responses.items()
if v.get("timestamp") >= start_date
}
if not include_metadata:
filtered_responses = {
k: {
"question": v["question"],
"response": v["response"],
"timestamp": v["timestamp"]
} for k, v in filtered_responses.items()
}
sorted_responses = dict(
sorted(
filtered_responses.items(),
key=lambda x: x[1]["timestamp"],
reverse=True
)[:limit]
)
return {
"metrics": metrics,
"responses": sorted_responses
}
except Exception as err:
_LOGGER.error("Error getting history: %s", str(err))
return {}
async def async_set_system_prompt(call: ServiceCall) -> None:
"""Handle the set_system_prompt service call."""
try:
prompt = str(call.data.get("prompt", "")).strip()
if prompt:
coordinator.system_prompt = prompt
_LOGGER.info("System prompt updated successfully")
else:
_LOGGER.error("Empty prompt provided")
except Exception as err:
_LOGGER.error("Error setting system prompt: %s", str(err))
# Register services with validation
hass.services.async_register(
DOMAIN,
"ask_question",
async_ask_question,
schema=SERVICE_SCHEMA_ASK_QUESTION
)
hass.services.async_register(
DOMAIN,
"clear_history",
async_clear_history
)
hass.services.async_register(
DOMAIN,
"get_history",
async_get_history,
schema=SERVICE_SCHEMA_GET_HISTORY
)
hass.services.async_register(
DOMAIN,
"set_system_prompt",
async_set_system_prompt,
schema=SERVICE_SCHEMA_SET_SYSTEM_PROMPT
)
_LOGGER.info( _LOGGER.info(
"Successfully set up HA Text AI with model: %s", "Successfully set up %s instance '%s' with model %s",
entry.data.get(CONF_MODEL, DEFAULT_MODEL) api_provider,
instance_name,
model
) )
return True return True
except Exception as ex: except Exception as ex:
_LOGGER.exception("Setup error: %s", str(ex)) _LOGGER.exception("Setup error: %s", str(ex))
raise ConfigEntryNotReady from ex raise ConfigEntryNotReady(f"Setup error: {str(ex)}") from ex
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry.""" """Unload a config entry."""
try: try:
coordinator = hass.data[DOMAIN].get(entry.entry_id) if entry.entry_id in hass.data[DOMAIN]:
if coordinator: coordinator = hass.data[DOMAIN][entry.entry_id]
# Clear queue and history
coordinator._responses.clear() if hasattr(coordinator.client, 'shutdown'):
while not coordinator._question_queue.empty(): await coordinator.client.shutdown()
try:
coordinator._question_queue.get_nowait()
coordinator._question_queue.task_done()
except Exception:
pass
# Close connection
await coordinator.async_shutdown() await coordinator.async_shutdown()
# Remove services
for service in ["ask_question", "clear_history", "get_history", "set_system_prompt"]:
hass.services.async_remove(DOMAIN, service)
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id) hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
except Exception as ex: except Exception as ex:
_LOGGER.exception("Error unloading entry: %s", str(ex)) _LOGGER.exception("Error unloading entry: %s", str(ex))
return False return False
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s", entry.version)
if entry.version == 1:
new = {**entry.data}
# Migrate settings
if CONF_MODEL in new and new[CONF_MODEL] not in SUPPORTED_MODELS:
new[CONF_MODEL] = DEFAULT_MODEL
entry.version = 2
hass.config_entries.async_update_entry(entry, data=new)
return True
+180
View File
@@ -0,0 +1,180 @@
"""API Client for HA Text AI."""
import logging
import asyncio
from typing import Any, Dict, List, Optional
from aiohttp import ClientSession, ClientTimeout
from async_timeout import timeout
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .const import (
API_TIMEOUT,
API_RETRY_COUNT,
API_PROVIDER_ANTHROPIC,
MIN_TEMPERATURE,
MAX_TEMPERATURE,
MIN_MAX_TOKENS,
MAX_MAX_TOKENS,
)
_LOGGER = logging.getLogger(__name__)
class APIClient:
"""API Client for OpenAI and Anthropic."""
def __init__(
self,
session: ClientSession,
endpoint: str,
headers: Dict[str, str],
api_provider: str,
model: str,
) -> None:
"""Initialize API client."""
self.session = session
self.endpoint = endpoint
self.headers = headers
self.api_provider = api_provider
self.model = model
self.timeout = ClientTimeout(total=API_TIMEOUT)
def _validate_parameters(
self,
temperature: float,
max_tokens: int,
) -> None:
"""Validate API parameters."""
if not MIN_TEMPERATURE <= temperature <= MAX_TEMPERATURE:
raise ValueError(
f"Temperature must be between {MIN_TEMPERATURE} and {MAX_TEMPERATURE}"
)
if not MIN_MAX_TOKENS <= max_tokens <= MAX_MAX_TOKENS:
raise ValueError(
f"Max tokens must be between {MIN_MAX_TOKENS} and {MAX_MAX_TOKENS}"
)
async def _make_request(
self,
url: str,
payload: Dict[str, Any],
) -> Dict[str, Any]:
"""Make API request with retry logic."""
for attempt in range(API_RETRY_COUNT):
try:
async with timeout(API_TIMEOUT):
async with self.session.post(
url,
json=payload,
headers=self.headers,
timeout=self.timeout
) as response:
if response.status != 200:
error_data = await response.json()
raise HomeAssistantError(f"API error: {error_data}")
return await response.json()
except asyncio.TimeoutError:
if attempt == API_RETRY_COUNT - 1:
raise HomeAssistantError("API request timed out")
await asyncio.sleep(1 * (attempt + 1))
except Exception as e:
if attempt == API_RETRY_COUNT - 1:
raise
_LOGGER.warning("API request failed, retrying: %s", str(e))
await asyncio.sleep(1 * (attempt + 1))
async def create(
self,
model: str,
messages: List[Dict[str, str]],
temperature: float,
max_tokens: int,
) -> Dict[str, Any]:
"""Create completion using appropriate API."""
try:
self._validate_parameters(temperature, max_tokens)
if self.api_provider == API_PROVIDER_ANTHROPIC:
return await self._create_anthropic_completion(
model, messages, temperature, max_tokens
)
else:
return await self._create_openai_completion(
model, messages, temperature, max_tokens
)
except Exception as e:
_LOGGER.error("API request failed: %s", str(e))
raise HomeAssistantError(f"API request failed: {str(e)}")
async def _create_openai_completion(
self,
model: str,
messages: List[Dict[str, str]],
temperature: float,
max_tokens: int,
) -> Dict[str, Any]:
"""Create completion using OpenAI API."""
url = f"{self.endpoint}/chat/completions"
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens,
}
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_anthropic_completion(
self,
model: str,
messages: List[Dict[str, str]],
temperature: float,
max_tokens: int,
) -> Dict[str, Any]:
"""Create completion using Anthropic API."""
url = f"{self.endpoint}/v1/messages"
# Convert messages to Anthropic format
system_prompt = next(
(msg["content"] for msg in messages if msg["role"] == "system"),
None
)
conversation = [msg for msg in messages if msg["role"] != "system"]
payload = {
"model": model,
"messages": conversation,
"max_tokens": max_tokens,
"temperature": temperature,
}
if system_prompt:
payload["system"] = system_prompt
data = await self._make_request(url, payload)
return {
"choices": [
{
"message": {
"content": data["content"][0]["text"]
}
}
],
"usage": {
"prompt_tokens": data["usage"]["input_tokens"],
"completion_tokens": data["usage"]["output_tokens"],
"total_tokens": data["usage"]["input_tokens"] + data["usage"]["output_tokens"]
}
}
+185 -226
View File
@@ -1,17 +1,14 @@
"""Config flow for HA text AI integration.""" """Config flow for HA text AI integration."""
from typing import Any, Dict, Optional, Tuple import logging
import voluptuous as vol from typing import Any, Dict, Optional
import asyncio
import aiohttp
from async_timeout import timeout
from urllib.parse import urlparse, urljoin
import voluptuous as vol
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY from homeassistant.const import CONF_API_KEY, CONF_NAME
import homeassistant.helpers.config_validation as cv
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import selector
from .const import ( from .const import (
DOMAIN, DOMAIN,
@@ -20,269 +17,231 @@ from .const import (
CONF_MAX_TOKENS, CONF_MAX_TOKENS,
CONF_API_ENDPOINT, CONF_API_ENDPOINT,
CONF_REQUEST_INTERVAL, CONF_REQUEST_INTERVAL,
CONF_API_PROVIDER,
CONF_CONTEXT_MESSAGES,
API_PROVIDER_OPENAI,
API_PROVIDER_ANTHROPIC,
API_PROVIDERS,
DEFAULT_MODEL, DEFAULT_MODEL,
DEFAULT_TEMPERATURE, DEFAULT_TEMPERATURE,
DEFAULT_MAX_TOKENS, DEFAULT_MAX_TOKENS,
DEFAULT_API_ENDPOINT,
DEFAULT_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL,
DEFAULT_OPENAI_ENDPOINT,
DEFAULT_ANTHROPIC_ENDPOINT,
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,
API_VERSION,
API_MODELS_PATH,
ERROR_INVALID_API_KEY,
ERROR_CANNOT_CONNECT,
ERROR_UNKNOWN,
ERROR_INVALID_MODEL,
ERROR_RATE_LIMIT,
ERROR_API_ERROR,
ERROR_TIMEOUT,
) )
import logging
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): cv.string,
vol.Optional(
CONF_TEMPERATURE,
default=DEFAULT_TEMPERATURE
): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
),
vol.Optional(
CONF_MAX_TOKENS,
default=DEFAULT_MAX_TOKENS
): vol.All(
vol.Coerce(int),
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
),
vol.Optional(
CONF_API_ENDPOINT,
default=DEFAULT_API_ENDPOINT
): cv.string,
vol.Optional(
CONF_REQUEST_INTERVAL,
default=DEFAULT_REQUEST_INTERVAL
): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL)
),
}
)
async def validate_api_connection(
hass,
api_key: str,
endpoint: str,
model: str,
retry_count: int = 3,
retry_delay: float = 1.0
) -> Tuple[bool, str, list]:
"""Validate API connection with retry logic."""
session = async_get_clientsession(hass)
headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json; charset=utf-8",
"Accept": "application/json; charset=utf-8",
"Accept-Charset": "utf-8"
}
base_url = endpoint.rstrip('/')
if not base_url.endswith(f"/{API_VERSION}"):
base_url = f"{base_url}/{API_VERSION}"
models_url = f"{base_url}/{API_MODELS_PATH}"
_LOGGER.debug("Attempting to connect to: %s", models_url)
for attempt in range(retry_count):
try:
async with timeout(10):
async with session.get(models_url, headers=headers) as response:
if response.status == 200:
data = await response.json()
model_ids = [m["id"] for m in data.get("data", [])]
_LOGGER.debug("Available models: %s", ", ".join(model_ids))
if model not in model_ids:
_LOGGER.warning(
"Model %s not found in available models: %s",
model,
", ".join(model_ids)
)
return False, ERROR_INVALID_MODEL, model_ids
return True, "", model_ids
elif response.status == 401:
_LOGGER.error("Authentication failed")
return False, ERROR_INVALID_API_KEY, []
elif response.status == 429:
_LOGGER.error("Rate limit exceeded")
return False, ERROR_RATE_LIMIT, []
else:
response_text = await response.text()
_LOGGER.error(
"API error: %s - %s",
response.status,
response_text
)
return False, ERROR_API_ERROR, []
except asyncio.TimeoutError:
_LOGGER.warning(
"Timeout during API validation (attempt %d/%d)",
attempt + 1,
retry_count
)
if attempt == retry_count - 1:
return False, ERROR_TIMEOUT, []
await asyncio.sleep(retry_delay)
except aiohttp.ClientError as err:
_LOGGER.error("Connection error: %s", str(err))
return False, ERROR_CANNOT_CONNECT, []
except Exception as err:
_LOGGER.exception("Unexpected error during validation: %s", str(err))
return False, ERROR_UNKNOWN, []
return False, ERROR_UNKNOWN, []
class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for HA text AI.""" """Handle a config flow for HA text AI."""
VERSION = 1 VERSION = 1
async def async_step_user( def __init__(self) -> None:
self, """Initialize flow."""
user_input: Optional[Dict[str, Any]] = None self._errors = {}
) -> FlowResult: self._data = {}
self._provider = None
async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
"""Handle the initial step.""" """Handle the initial step."""
errors: Dict[str, str] = {} if user_input is None:
return self.async_show_form(
if user_input is not None: step_id="user",
try: data_schema=vol.Schema({
endpoint = user_input.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT) vol.Required(CONF_API_PROVIDER): selector.SelectSelector(
try: selector.SelectSelectorConfig(
result = urlparse(endpoint) options=API_PROVIDERS,
if not all([result.scheme, result.netloc]): translation_key="api_provider"
errors["base"] = "invalid_url_format"
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors
) )
except Exception as e: ),
_LOGGER.error("URL parsing error: %s", str(e)) })
errors["base"] = "invalid_url_format" )
return self.async_show_form(
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA,
errors=errors
)
validated_input = STEP_USER_DATA_SCHEMA(user_input) self._provider = user_input[CONF_API_PROVIDER]
return await self.async_step_provider()
is_valid, error_code, available_models = await validate_api_connection( async def async_step_provider(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
self.hass, """Handle provider configuration step."""
validated_input[CONF_API_KEY], if user_input is None:
endpoint, default_endpoint = (
validated_input[CONF_MODEL] DEFAULT_OPENAI_ENDPOINT if self._provider == API_PROVIDER_OPENAI
) else DEFAULT_ANTHROPIC_ENDPOINT
)
if is_valid: suggested_name = f"HA Text AI {len(self._async_current_entries()) + 1}"
await self.async_set_unique_id(validated_input[CONF_API_KEY])
self._abort_if_unique_id_configured()
return self.async_create_entry( return self.async_show_form(
title="HA Text AI", step_id="provider",
data=validated_input data_schema=vol.Schema({
) vol.Required(CONF_NAME, default=suggested_name): str,
vol.Required(CONF_API_KEY): str,
vol.Required(CONF_MODEL, default=DEFAULT_MODEL): str,
vol.Required(CONF_API_ENDPOINT, default=default_endpoint): str,
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
),
vol.Optional(CONF_MAX_TOKENS, default=DEFAULT_MAX_TOKENS): vol.All(
vol.Coerce(int),
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
),
vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL)
),
vol.Optional(
CONF_CONTEXT_MESSAGES,
default=DEFAULT_CONTEXT_MESSAGES
): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=20)
),
}),
errors=self._errors
)
errors["base"] = error_code instance_name = user_input[CONF_NAME]
if error_code == ERROR_INVALID_MODEL: await self._async_validate_name(instance_name)
_LOGGER.warning( if self._errors:
"Selected model %s not found in available models: %s", return await self.async_step_provider()
validated_input[CONF_MODEL],
", ".join(available_models)
)
except vol.Invalid as err: if not await self._async_validate_api(user_input):
_LOGGER.error("Validation error: %s", str(err)) return await self.async_step_provider()
errors["base"] = "invalid_input"
return self.async_show_form( return await self._create_entry(user_input)
step_id="user",
data_schema=STEP_USER_DATA_SCHEMA, async def _async_validate_name(self, name: str) -> bool:
errors=errors, """Validate that the name is unique."""
description_placeholders={ for entry in self._async_current_entries():
"default_model": DEFAULT_MODEL, if entry.data.get(CONF_NAME) == name:
"default_endpoint": DEFAULT_API_ENDPOINT, self._errors["name"] = "name_exists"
return False
return True
async def _async_validate_api(self, user_input: Dict[str, Any]) -> bool:
"""Validate API connection."""
try:
session = async_get_clientsession(self.hass)
headers = self._get_api_headers(user_input)
endpoint = user_input[CONF_API_ENDPOINT].rstrip('/')
check_url = (
f"{endpoint}/v1/models" if self._provider == API_PROVIDER_ANTHROPIC
else f"{endpoint}/models"
)
async with session.get(check_url, headers=headers) as response:
if response.status == 401:
self._errors["base"] = "invalid_auth"
return False
elif response.status not in [200, 404]:
self._errors["base"] = "cannot_connect"
return False
return True
except Exception as err:
_LOGGER.error("API validation error: %s", str(err))
self._errors["base"] = "cannot_connect"
return False
def _get_api_headers(self, user_input: Dict[str, Any]) -> Dict[str, str]:
"""Get API headers based on provider."""
api_key = user_input[CONF_API_KEY]
if self._provider == API_PROVIDER_ANTHROPIC:
return {
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
"Content-Type": "application/json"
} }
return {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
async def _create_entry(self, user_input: Dict[str, Any]) -> FlowResult:
"""Create the config entry."""
instance_name = user_input[CONF_NAME]
unique_id = f"{DOMAIN}_{instance_name}_{self._provider}".lower().replace(" ", "_")
return self.async_create_entry(
title=instance_name,
data={
CONF_API_PROVIDER: self._provider,
CONF_NAME: instance_name,
**user_input,
"unique_id": unique_id,
CONF_CONTEXT_MESSAGES: user_input.get(
CONF_CONTEXT_MESSAGES,
DEFAULT_CONTEXT_MESSAGES)
}
) )
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow( def async_get_options_flow(config_entry: config_entries.ConfigEntry) -> config_entries.OptionsFlow:
config_entry: config_entries.ConfigEntry,
) -> config_entries.OptionsFlow:
"""Get the options flow for this handler.""" """Get the options flow for this handler."""
return OptionsFlowHandler(config_entry) return OptionsFlowHandler(config_entry)
class OptionsFlowHandler(config_entries.OptionsFlow): class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handle options flow for HA text AI.""" """Handle options flow."""
def __init__(self, config_entry: config_entries.ConfigEntry) -> None: def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
"""Initialize options flow.""" """Initialize options flow."""
self.config_entry = config_entry self.config_entry = config_entry
async def async_step_init( async def async_step_init(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
self, """Manage the options."""
user_input: Optional[Dict[str, Any]] = None
) -> FlowResult:
"""Handle options flow."""
if user_input is not None: if user_input is not None:
return self.async_create_entry(title="", data=user_input) return self.async_create_entry(title="", data=user_input)
options_schema = vol.Schema({ current_data = {**self.config_entry.data, **self.config_entry.options}
vol.Optional(
CONF_TEMPERATURE,
default=self.config_entry.options.get(
CONF_TEMPERATURE, DEFAULT_TEMPERATURE
),
): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
),
vol.Optional(
CONF_MAX_TOKENS,
default=self.config_entry.options.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=self.config_entry.options.get(
CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL
),
): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL)
),
})
return self.async_show_form( return self.async_show_form(
step_id="init", step_id="init",
data_schema=options_schema, data_schema=vol.Schema({
vol.Optional(
CONF_MODEL,
default=current_data.get(CONF_MODEL, DEFAULT_MODEL)
): str,
vol.Optional(
CONF_TEMPERATURE,
default=current_data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)
): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
),
vol.Optional(
CONF_MAX_TOKENS,
default=current_data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)
): vol.All(
vol.Coerce(int),
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
),
vol.Optional(
CONF_REQUEST_INTERVAL,
default=current_data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)
): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL)
),
vol.Optional(
CONF_CONTEXT_MESSAGES,
default=current_data.get(
CONF_CONTEXT_MESSAGES,
DEFAULT_CONTEXT_MESSAGES
)
): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=20)
),
})
) )
+125 -108
View File
@@ -1,39 +1,47 @@
"""Constants for the HA text AI integration.""" """Constants for the HA text AI integration."""
from typing import Final from typing import Final
from homeassistant.const import Platform import voluptuous as vol
from homeassistant.const import Platform, CONF_API_KEY, CONF_NAME
from homeassistant.helpers import config_validation as cv
# Domain and platforms # Domain and platforms
DOMAIN: Final = "ha_text_ai" DOMAIN: Final = "ha_text_ai"
PLATFORMS: Final = [Platform.SENSOR] PLATFORMS: Final = [Platform.SENSOR]
# Provider configuration
CONF_API_PROVIDER: Final = "api_provider"
API_PROVIDER_OPENAI: Final = "openai"
API_PROVIDER_ANTHROPIC: Final = "anthropic"
API_PROVIDERS: Final = [
API_PROVIDER_OPENAI,
API_PROVIDER_ANTHROPIC
]
# Default endpoints
DEFAULT_OPENAI_ENDPOINT: Final = "https://api.openai.com/v1"
DEFAULT_ANTHROPIC_ENDPOINT: Final = "https://api.anthropic.com"
# Configuration constants # Configuration constants
CONF_MODEL: Final = "model" CONF_MODEL: Final = "model"
CONF_TEMPERATURE: Final = "temperature" 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_INSTANCE: Final = "instance"
# Model constants CONF_MAX_HISTORY_SIZE: Final = "max_history_size"
SUPPORTED_MODELS: Final = [ CONF_IS_ANTHROPIC: Final = "is_anthropic"
"gpt-3.5-turbo", CONF_CONTEXT_MESSAGES: Final = "context_messages"
"gpt-3.5-turbo-16k",
"gpt-4",
"gpt-4-32k",
"gpt-4-1106-preview",
"claude-3-sonnet",
"claude-3-opus"
]
# Default values # Default values
DEFAULT_MODEL: Final = "gpt-3.5-turbo" DEFAULT_MODEL: Final = "gpt-4o-mini"
DEFAULT_TEMPERATURE: Final = 0.1 DEFAULT_TEMPERATURE: Final = 0.1
DEFAULT_MAX_TOKENS: Final = 1000 DEFAULT_MAX_TOKENS: Final = 1000
DEFAULT_API_ENDPOINT: Final = "https://api.openai.com"
DEFAULT_REQUEST_INTERVAL: Final = 1.0 DEFAULT_REQUEST_INTERVAL: Final = 1.0
DEFAULT_TIMEOUT: Final = 30 DEFAULT_TIMEOUT: Final = 30
DEFAULT_QUEUE_SIZE: Final = 100 DEFAULT_MAX_HISTORY: Final = 50
DEFAULT_HISTORY_LIMIT: Final = 50 DEFAULT_NAME: Final = "HA Text AI"
DEFAULT_RETRY_COUNT: Final = 3 DEFAULT_CONTEXT_MESSAGES: Final = 5
# Parameter constraints # Parameter constraints
MIN_TEMPERATURE: Final = 0.0 MIN_TEMPERATURE: Final = 0.0
@@ -41,25 +49,11 @@ MAX_TEMPERATURE: Final = 2.0
MIN_MAX_TOKENS: Final = 1 MIN_MAX_TOKENS: Final = 1
MAX_MAX_TOKENS: Final = 4096 MAX_MAX_TOKENS: Final = 4096
MIN_REQUEST_INTERVAL: Final = 0.1 MIN_REQUEST_INTERVAL: Final = 0.1
MIN_TIMEOUT: Final = 5 MAX_REQUEST_INTERVAL: Final = 60.0
MAX_TIMEOUT: Final = 120
MAX_PROMPT_LENGTH: Final = 1000
MAX_HISTORY_LIMIT: Final = 100
# API constants # API constants
API_VERSION: Final = "v1"
API_MODELS_PATH: Final = "models"
API_CHAT_PATH: Final = "chat/completions"
API_TIMEOUT: Final = 30 API_TIMEOUT: Final = 30
API_RETRY_COUNT: Final = 3 API_RETRY_COUNT: Final = 3
API_BACKOFF_FACTOR: Final = 1.5
API_MAX_RETRIES: Final = 3
# History constants
HISTORY_FILTER_MODEL: Final = "filter_model"
HISTORY_FILTER_DATE: Final = "start_date"
HISTORY_SORT_ORDER: Final = "sort_order"
HISTORY_INCLUDE_METADATA: Final = "include_metadata"
# Service names # Service names
SERVICE_ASK_QUESTION: Final = "ask_question" SERVICE_ASK_QUESTION: Final = "ask_question"
@@ -67,33 +61,48 @@ SERVICE_CLEAR_HISTORY: Final = "clear_history"
SERVICE_GET_HISTORY: Final = "get_history" SERVICE_GET_HISTORY: Final = "get_history"
SERVICE_SET_SYSTEM_PROMPT: Final = "set_system_prompt" SERVICE_SET_SYSTEM_PROMPT: Final = "set_system_prompt"
# Service descriptions
SERVICE_ASK_QUESTION_DESCRIPTION: Final = "Ask a question to the AI model"
SERVICE_CLEAR_HISTORY_DESCRIPTION: Final = "Clear conversation history"
SERVICE_GET_HISTORY_DESCRIPTION: Final = "Get conversation history"
SERVICE_SET_SYSTEM_PROMPT_DESCRIPTION: Final = "Set system prompt for AI model"
# Attribute keys # Attribute keys
ATTR_QUESTION: Final = "question" ATTR_QUESTION: Final = "question"
ATTR_RESPONSE: Final = "response" ATTR_RESPONSE: Final = "response"
ATTR_LAST_UPDATED: Final = "last_updated" ATTR_INSTANCE: Final = "instance"
ATTR_MODEL: Final = "model" ATTR_MODEL: Final = "model"
ATTR_TEMPERATURE: Final = "temperature" ATTR_TEMPERATURE: Final = "temperature"
ATTR_MAX_TOKENS: Final = "max_tokens" ATTR_MAX_TOKENS: Final = "max_tokens"
ATTR_TOTAL_RESPONSES: Final = "total_responses"
ATTR_SYSTEM_PROMPT: Final = "system_prompt" ATTR_SYSTEM_PROMPT: Final = "system_prompt"
ATTR_RESPONSE_TIME: Final = "response_time"
ATTR_QUEUE_SIZE: Final = "queue_size"
ATTR_API_STATUS: Final = "api_status" ATTR_API_STATUS: Final = "api_status"
ATTR_ERROR_COUNT: Final = "error_count" ATTR_ERROR_COUNT: Final = "error_count"
ATTR_CONVERSATION_HISTORY: Final = "conversation_history"
# Sensor attributes
ATTR_TOTAL_RESPONSES: Final = "total_responses"
ATTR_TOTAL_ERRORS: Final = "total_errors"
ATTR_AVG_RESPONSE_TIME: Final = "average_response_time"
ATTR_LAST_REQUEST_TIME: Final = "last_request_time"
ATTR_LAST_ERROR: Final = "last_error" ATTR_LAST_ERROR: Final = "last_error"
ATTR_IS_PROCESSING: Final = "is_processing"
ATTR_IS_RATE_LIMITED: Final = "is_rate_limited"
ATTR_IS_MAINTENANCE: Final = "is_maintenance"
ATTR_API_VERSION: Final = "api_version" ATTR_API_VERSION: Final = "api_version"
ATTR_ENDPOINT_STATUS: Final = "endpoint_status" ATTR_ENDPOINT_STATUS: Final = "endpoint_status"
ATTR_REQUEST_COUNT: Final = "request_count" ATTR_PERFORMANCE_METRICS: Final = "performance_metrics"
ATTR_TOKENS_USED: Final = "tokens_used" ATTR_HISTORY_SIZE: Final = "history_size"
ATTR_RETRY_COUNT: Final = "retry_count" ATTR_UPTIME: Final = "uptime"
ATTR_QUEUE_POSITION: Final = "queue_position" ATTR_API_PROVIDER: Final = "api_provider"
ATTR_ESTIMATED_WAIT: Final = "estimated_wait" ATTR_METRICS: Final = "metrics"
ATTR_STATE: Final = "state"
ATTR_LAST_RESPONSE: Final = "last_response"
ATTR_ERROR: Final = "error"
ATTR_TIMESTAMP: Final = "timestamp"
# Sensor metrics
METRIC_TOTAL_TOKENS: Final = "total_tokens"
METRIC_PROMPT_TOKENS: Final = "prompt_tokens"
METRIC_COMPLETION_TOKENS: Final = "completion_tokens"
METRIC_SUCCESSFUL_REQUESTS: Final = "successful_requests"
METRIC_FAILED_REQUESTS: Final = "failed_requests"
METRIC_AVERAGE_LATENCY: Final = "average_latency"
METRIC_MAX_LATENCY: Final = "max_latency"
METRIC_MIN_LATENCY: Final = "min_latency"
# Error messages # Error messages
ERROR_INVALID_API_KEY: Final = "invalid_api_key" ERROR_INVALID_API_KEY: Final = "invalid_api_key"
@@ -104,81 +113,89 @@ ERROR_RATE_LIMIT: Final = "rate_limit_exceeded"
ERROR_CONTEXT_LENGTH: Final = "context_length_exceeded" ERROR_CONTEXT_LENGTH: Final = "context_length_exceeded"
ERROR_API_ERROR: Final = "api_error" ERROR_API_ERROR: Final = "api_error"
ERROR_TIMEOUT: Final = "timeout_error" ERROR_TIMEOUT: Final = "timeout_error"
ERROR_QUEUE_FULL: Final = "queue_full" ERROR_INVALID_INSTANCE: Final = "invalid_instance"
ERROR_INVALID_PROMPT: Final = "invalid_prompt" ERROR_NAME_EXISTS: Final = "name_exists"
ERROR_INVALID_PARAMETERS: Final = "invalid_parameters"
ERROR_SERVICE_UNAVAILABLE: Final = "service_unavailable"
# Configuration descriptions
CONF_MODEL_DESCRIPTION: Final = "AI model to use for responses"
CONF_TEMPERATURE_DESCRIPTION: Final = "Temperature for response generation (0-2)"
CONF_MAX_TOKENS_DESCRIPTION: Final = "Maximum tokens in response (1-4096)"
CONF_API_ENDPOINT_DESCRIPTION: Final = "API endpoint URL"
CONF_REQUEST_INTERVAL_DESCRIPTION: Final = "Minimum time between API requests (seconds)"
# Entity attributes descriptions
ATTR_QUESTION_DESCRIPTION: Final = "Last question asked"
ATTR_RESPONSE_DESCRIPTION: Final = "Last response received"
ATTR_LAST_UPDATED_DESCRIPTION: Final = "Time of last update"
ATTR_MODEL_DESCRIPTION: Final = "Current AI model in use"
ATTR_TEMPERATURE_DESCRIPTION: Final = "Current temperature setting"
ATTR_MAX_TOKENS_DESCRIPTION: Final = "Current max tokens setting"
ATTR_TOTAL_RESPONSES_DESCRIPTION: Final = "Total number of responses"
ATTR_SYSTEM_PROMPT_DESCRIPTION: Final = "Current system prompt"
ATTR_RESPONSE_TIME_DESCRIPTION: Final = "Time taken for last response"
ATTR_QUEUE_SIZE_DESCRIPTION: Final = "Current size of question queue"
ATTR_API_STATUS_DESCRIPTION: Final = "Current API connection status"
ATTR_ERROR_COUNT_DESCRIPTION: Final = "Total number of errors"
ATTR_LAST_ERROR_DESCRIPTION: Final = "Last error message"
ATTR_API_VERSION_DESCRIPTION: Final = "Current API version"
ATTR_ENDPOINT_STATUS_DESCRIPTION: Final = "Current endpoint status"
ATTR_REQUEST_COUNT_DESCRIPTION: Final = "Total number of API requests"
ATTR_TOKENS_USED_DESCRIPTION: Final = "Total tokens used"
# Entity attributes # Entity attributes
ENTITY_NAME: Final = "HA Text AI"
ENTITY_ICON: Final = "mdi:robot" ENTITY_ICON: Final = "mdi:robot"
ENTITY_ICON_ERROR: Final = "mdi:robot-dead" ENTITY_ICON_ERROR: Final = "mdi:robot-dead"
ENTITY_ICON_PROCESSING: Final = "mdi:robot-excited" ENTITY_ICON_PROCESSING: Final = "mdi:robot-excited"
ENTITY_ICON_OFFLINE: Final = "mdi:robot-off"
ENTITY_ICON_QUEUE: Final = "mdi:robot-confused"
# Translation keys
TRANSLATION_KEY_CONFIG: Final = "config"
TRANSLATION_KEY_OPTIONS: Final = "options"
TRANSLATION_KEY_ERROR: Final = "error"
TRANSLATION_KEY_STATE: Final = "state"
TRANSLATION_KEY_SERVICES: Final = "services"
# State attributes # State attributes
STATE_READY: Final = "ready" STATE_READY: Final = "ready"
STATE_PROCESSING: Final = "processing" STATE_PROCESSING: Final = "processing"
STATE_ERROR: Final = "error" STATE_ERROR: Final = "error"
STATE_DISCONNECTED: Final = "disconnected"
STATE_RATE_LIMITED: Final = "rate_limited"
STATE_INITIALIZING: Final = "initializing" STATE_INITIALIZING: Final = "initializing"
STATE_MAINTENANCE: Final = "maintenance" STATE_MAINTENANCE: Final = "maintenance"
STATE_RETRYING: Final = "retrying" STATE_RATE_LIMITED: Final = "rate_limited"
STATE_QUEUED: Final = "queued" STATE_DISCONNECTED: Final = "disconnected"
STATE_UPDATING: Final = "updating"
# Logging
LOGGER_NAME: Final = "custom_components.ha_text_ai"
LOG_LEVEL_DEFAULT: Final = "INFO"
# Queue constants
QUEUE_TIMEOUT: Final = 5
QUEUE_MAX_SIZE: Final = 100
# Service schema constants
SCHEMA_QUESTION: Final = "question"
SCHEMA_MODEL: Final = "model"
SCHEMA_TEMPERATURE: Final = "temperature"
SCHEMA_MAX_TOKENS: Final = "max_tokens"
SCHEMA_PROMPT: Final = "prompt"
SCHEMA_LIMIT: Final = "limit"
# Event names # Event names
EVENT_RESPONSE_RECEIVED: Final = f"{DOMAIN}_response_received" EVENT_RESPONSE_RECEIVED: Final = f"{DOMAIN}_response_received"
EVENT_ERROR_OCCURRED: Final = f"{DOMAIN}_error_occurred" EVENT_ERROR_OCCURRED: Final = f"{DOMAIN}_error_occurred"
EVENT_STATE_CHANGED: Final = f"{DOMAIN}_state_changed" EVENT_STATE_CHANGED: Final = f"{DOMAIN}_state_changed"
# Service schema constants
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
vol.Required(CONF_INSTANCE): cv.string,
vol.Required("question"): cv.string,
vol.Optional("system_prompt"): cv.string,
vol.Optional("model"): cv.string,
vol.Optional("temperature"): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
),
vol.Optional("max_tokens"): vol.All(
vol.Coerce(int),
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
),
vol.Optional("context_messages"): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=20)
)
})
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
vol.Required(CONF_INSTANCE): cv.string,
vol.Required("prompt"): cv.string
})
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
vol.Required(CONF_INSTANCE): cv.string,
vol.Optional("limit", default=10): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=100)
),
vol.Optional("filter_model"): cv.string
})
# Configuration schema
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
vol.Required(CONF_NAME): cv.string,
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_API_PROVIDER): vol.In(API_PROVIDERS),
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): cv.string,
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
),
vol.Optional(CONF_MAX_TOKENS, default=DEFAULT_MAX_TOKENS): vol.All(
vol.Coerce(int),
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
),
vol.Optional(CONF_API_ENDPOINT): cv.string,
vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_REQUEST_INTERVAL, max=MAX_REQUEST_INTERVAL)
),
vol.Optional(CONF_MAX_HISTORY_SIZE, default=DEFAULT_MAX_HISTORY): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=100),
),
vol.Optional(CONF_CONTEXT_MESSAGES, default=DEFAULT_CONTEXT_MESSAGES): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=20)
)
})
}, extra=vol.ALLOW_EXTRA)
+310 -551
View File
@@ -1,618 +1,377 @@
"""Data coordinator for HA text AI.""" """The HA Text AI coordinator."""
import asyncio from __future__ import annotations
import logging import logging
from datetime import timedelta from datetime import datetime, timedelta
from typing import Any, Dict, Optional, List from typing import Any, Dict, List, Optional
import time
from openai import AsyncOpenAI, APIError, AuthenticationError, RateLimitError
from anthropic import AsyncAnthropic
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
import async_timeout from homeassistant.exceptions import HomeAssistantError
from .const import ( from .const import (
DOMAIN, DOMAIN,
DEFAULT_TIMEOUT, STATE_READY,
MAX_QUEUE_SIZE, STATE_PROCESSING,
MAX_RETRIES, STATE_ERROR,
RETRY_DELAY, STATE_RATE_LIMITED,
SUPPORTED_MODELS, STATE_MAINTENANCE,
DEFAULT_MAX_TOKENS,
DEFAULT_TEMPERATURE,
DEFAULT_MAX_HISTORY,
DEFAULT_CONTEXT_MESSAGES,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class HATextAICoordinator(DataUpdateCoordinator): class HATextAICoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from the API."""
def __init__( def __init__(
self, self,
hass: HomeAssistant, hass: HomeAssistant,
api_key: str, client: Any,
endpoint: str,
model: str, model: str,
temperature: float, update_interval: int,
max_tokens: int, instance_name: str,
request_interval: float, max_tokens: int = DEFAULT_MAX_TOKENS,
session: Optional[Any] = None, temperature: float = DEFAULT_TEMPERATURE,
max_history_size: int = DEFAULT_MAX_HISTORY,
context_messages: int = DEFAULT_CONTEXT_MESSAGES,
is_anthropic: bool = False, is_anthropic: bool = False,
) -> None: ) -> None:
"""Initialize coordinator. """Initialize coordinator."""
self.instance_name = instance_name
self.hass = hass
self.client = client
self.model = model
self.temperature = temperature
self.max_tokens = max_tokens
self.max_history_size = max_history_size
self.is_anthropic = is_anthropic
# Initialize with default state
self._initial_state = {
"state": STATE_READY,
"metrics": {
"total_tokens": 0,
"prompt_tokens": 0,
"completion_tokens": 0,
"successful_requests": 0,
"failed_requests": 0,
"total_errors": 0,
"average_latency": 0,
"max_latency": 0,
"min_latency": float('inf'),
},
"last_response": {
"timestamp": dt_util.utcnow().isoformat(),
"question": "",
"response": "",
"model": model,
"instance": instance_name,
"error": None
},
"is_processing": False,
"is_rate_limited": False,
"is_maintenance": False,
"endpoint_status": "ready",
"uptime": 0,
"system_prompt": None,
"history_size": 0,
"conversation_history": [],
}
update_interval_td = timedelta(seconds=update_interval)
Args:
hass: HomeAssistant instance
api_key: API key for the service
endpoint: API endpoint URL
model: Model name to use
temperature: Temperature parameter for generation
max_tokens: Maximum tokens to generate
request_interval: Interval between requests
session: Optional session object
is_anthropic: Whether to use Anthropic API
"""
super().__init__( super().__init__(
hass, hass,
_LOGGER, _LOGGER,
name=DOMAIN, name=instance_name,
update_interval=timedelta(seconds=request_interval), update_interval=update_interval_td,
) )
self._validate_params(api_key, temperature, max_tokens) # Register instance
self.hass.data.setdefault(DOMAIN, {})
self.hass.data[DOMAIN][instance_name] = self
self.context_messages = context_messages
self.api_key = api_key self._system_prompt = None
self.endpoint = endpoint self._conversation_history = []
self.model = model self._performance_metrics = self._initial_state["metrics"].copy()
self.temperature = float(temperature)
self.max_tokens = int(max_tokens)
self._question_queue = asyncio.PriorityQueue(maxsize=MAX_QUEUE_SIZE)
self._responses: Dict[str, Any] = {}
self.system_prompt: Optional[str] = None
self._is_ready = False
self._is_processing = False self._is_processing = False
self._is_rate_limited = False self._is_rate_limited = False
self._is_maintenance = False self._is_maintenance = False
self._error_count = 0 self.endpoint_status = "ready"
self._MAX_ERRORS = 3 self.last_response = self._initial_state["last_response"].copy()
self._request_count = 0 self._start_time = dt_util.utcnow()
self._tokens_used = 0
self._api_version = "v1"
self._endpoint_status = "disconnected"
self._performance_metrics: Dict[str, Any] = {
"avg_response_time": 0,
"total_errors": 0,
"success_rate": 100,
"requests_per_minute": 0,
}
self._last_request_time = 0
self._is_anthropic = is_anthropic
if is_anthropic: _LOGGER.info(f"Initialized HA Text AI coordinator with instance: {instance_name}")
self.client = AsyncAnthropic(api_key=self.api_key)
else:
self.client = AsyncOpenAI(
api_key=self.api_key,
base_url=self.endpoint,
http_client=session,
)
def _validate_params(self, api_key: str, temperature: float, max_tokens: int) -> None:
"""Validate initialization parameters."""
if not api_key:
raise ValueError("API key is required")
if not isinstance(temperature, (int, float)) or not 0 <= temperature <= 2:
raise ValueError("Temperature must be between 0 and 2")
if not isinstance(max_tokens, int) or max_tokens < 1:
raise ValueError("Max tokens must be a positive integer")
async def _async_update_data(self) -> Dict[str, Any]: async def _async_update_data(self) -> Dict[str, Any]:
"""Update data via API.""" """Update data via library."""
if self._question_queue.empty():
return self._responses
try: try:
async with async_timeout.timeout(DEFAULT_TIMEOUT): current_state = self._get_current_state()
self._is_processing = True _LOGGER.debug(f"Updating data for {self.instance_name}, current state: {current_state}")
priority, question_data = await self._question_queue.get()
question = question_data["question"]
params = question_data["params"]
try: data = {
response_data = await self._make_api_call( "state": current_state,
question, "metrics": self._performance_metrics,
model=params.get("model"), "last_response": self.last_response,
temperature=params.get("temperature"), "is_processing": self._is_processing,
max_tokens=params.get("max_tokens"), "is_rate_limited": self._is_rate_limited,
system_prompt=params.get("system_prompt") "is_maintenance": self._is_maintenance,
) "endpoint_status": self.endpoint_status,
"uptime": (dt_util.utcnow() - self._start_time).total_seconds(),
self._update_metrics(response_data) "system_prompt": self._system_prompt,
self._responses[question] = { "history_size": len(self._conversation_history),
"question": question, "conversation_history": self._conversation_history,
"response": response_data["response"],
"error": None,
"timestamp": dt_util.utcnow(),
"model": response_data["model"],
"temperature": params.get("temperature", self.temperature),
"max_tokens": params.get("max_tokens", self.max_tokens),
"response_time": response_data.get("response_time"),
"tokens": response_data.get("tokens", 0),
"priority": priority
}
self._error_count = 0
self._is_ready = True
self._endpoint_status = "connected"
self._request_count += 1
self._tokens_used += response_data.get("tokens", 0)
self._last_request_time = time.time()
# Fire event for successful response
self.hass.bus.async_fire(f"{DOMAIN}_response_received", {
"question": question,
"model": response_data["model"],
"tokens": response_data.get("tokens", 0)
})
_LOGGER.debug("Response received for question: %s", question)
except Exception as err:
await self._handle_api_error(question, err)
finally:
self._is_processing = False
self._question_queue.task_done()
return self._responses
except asyncio.TimeoutError as err:
_LOGGER.error("Timeout while processing question")
await self._handle_timeout_error()
return self._responses
async def _handle_api_error(self, question: str, error: Exception) -> None:
"""Handle API errors with retry logic."""
self._error_count += 1
self._performance_metrics["total_errors"] += 1
error_msg = str(error)
if isinstance(error, AuthenticationError):
error_msg = "Authentication failed - invalid API key"
self._is_ready = False
self._endpoint_status = "auth_error"
elif isinstance(error, RateLimitError):
error_msg = "Rate limit exceeded"
self._is_rate_limited = True
self._endpoint_status = "rate_limited"
# Implement exponential backoff
await asyncio.sleep(RETRY_DELAY * (2 ** (self._error_count - 1)))
elif isinstance(error, APIError):
if "maintenance" in str(error).lower():
self._is_maintenance = True
self._endpoint_status = "maintenance"
error_msg = f"API error: {error}"
else:
self._endpoint_status = "error"
self._responses[question] = {
"question": question,
"response": None,
"error": error_msg,
"timestamp": dt_util.utcnow(),
"model": self.model,
"temperature": self.temperature,
"max_tokens": self.max_tokens
}
# Fire error event
self.hass.bus.async_fire(f"{DOMAIN}_error_occurred", {
"error_type": type(error).__name__,
"error_message": error_msg,
"question": question
})
_LOGGER.error("API error (%s): %s", type(error).__name__, error_msg)
if self._error_count >= self._MAX_ERRORS:
_LOGGER.warning(
"Multiple errors occurred (%d). Coordinator needs attention.",
self._error_count
)
def _update_metrics(self, response_data: Dict[str, Any]) -> None:
"""Update performance metrics."""
response_time = response_data.get("response_time", 0)
current_avg = self._performance_metrics["avg_response_time"]
self._performance_metrics["avg_response_time"] = (
(current_avg * self._request_count + response_time) /
(self._request_count + 1)
)
total_requests = self._request_count + 1
self._performance_metrics["success_rate"] = (
(total_requests - self._performance_metrics["total_errors"]) /
total_requests * 100
)
# Calculate requests per minute
if self._last_request_time:
time_diff = time.time() - self._last_request_time
if time_diff > 0:
self._performance_metrics["requests_per_minute"] = 60 / time_diff
async def _make_api_call(
self,
question: str,
model: Optional[str] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
system_prompt: Optional[str] = None
) -> Dict[str, Any]:
"""Make API call to the selected service."""
try:
start_time = dt_util.utcnow()
if self._is_anthropic:
response = await self._make_anthropic_call(
question, model, temperature, max_tokens, system_prompt
)
else:
response = await self._make_openai_call(
question, model, temperature, max_tokens, system_prompt
)
response_time = (dt_util.utcnow() - start_time).total_seconds()
return {
**response,
"response_time": response_time
} }
# Validate data
if not isinstance(data, dict):
raise ValueError("Invalid data format")
_LOGGER.debug(f"Updated data for {self.instance_name}: {data}")
return data
except Exception as err: except Exception as err:
_LOGGER.error("Error in API call: %s", err) _LOGGER.error(f"Error updating data for {self.instance_name}: {err}")
raise return self._initial_state
async def _make_anthropic_call( async def async_update_ha_state(self) -> None:
self, """Update Home Assistant state."""
question: str, try:
model: Optional[str], _LOGGER.debug(f"Requesting state update for {self.instance_name}")
temperature: Optional[float], await self.async_request_refresh()
max_tokens: Optional[int],
system_prompt: Optional[str]
) -> Dict[str, Any]:
"""Make API call to Anthropic."""
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": question})
completion = await self.client.messages.create( # Force update of all entities
model=model or self.model, for entity_id in self.hass.states.async_entity_ids():
messages=messages, if entity_id.startswith(f"sensor.ha_text_ai_{self.instance_name}"):
temperature=temperature if temperature is not None else self.temperature, self.hass.states.async_set(entity_id, self._get_current_state())
max_tokens=max_tokens if max_tokens is not None else self.max_tokens,
)
return { except Exception as err:
"response": completion.content[0].text, _LOGGER.error(f"Error updating HA state for {self.instance_name}: {err}")
"model": completion.model,
"tokens": completion.usage.total_tokens if hasattr(completion, 'usage') else 0
}
async def _make_openai_call( def _get_current_state(self) -> str:
self, """Get current state based on internal flags."""
question: str, if self._is_processing:
model: Optional[str], return STATE_PROCESSING
temperature: Optional[float], elif self._is_rate_limited:
max_tokens: Optional[int], return STATE_RATE_LIMITED
system_prompt: Optional[str] elif self._is_maintenance:
) -> Dict[str, Any]: return STATE_MAINTENANCE
"""Make API call to OpenAI.""" elif self.last_response.get("error"):
messages = [] return STATE_ERROR
if system_prompt: return STATE_READY
messages.append({"role": "system", "content": system_prompt})
messages.append({"role": "user", "content": question})
completion = await self.client.chat.completions.create( def _calculate_context_tokens(self, messages: List[Dict[str, str]]) -> int:
model=model or self.model, try:
messages=messages, if self.is_anthropic and hasattr(self.client, 'count_tokens'):
temperature=temperature if temperature is not None else self.temperature, return sum(self.client.count_tokens(msg['content']) for msg in messages)
max_tokens=max_tokens if max_tokens is not None else self.max_tokens,
)
return { return sum(len(msg['content']) // 4 for msg in messages)
"response": completion.choices[0].message.content, except Exception as e:
"model": completion.model, _LOGGER.warning(f"Error calculating context tokens: {e}")
"tokens": completion.usage.total_tokens if hasattr(completion, 'usage') else 0 return 0
}
async def async_ask_question( async def async_ask_question(
self, self,
question: str, question: str,
system_prompt: Optional[str] = None,
model: Optional[str] = None, model: Optional[str] = None,
temperature: Optional[float] = None, temperature: Optional[float] = None,
max_tokens: Optional[int] = None, max_tokens: Optional[int] = None,
priority: bool = False system_prompt: Optional[str] = None,
) -> None: context_messages: Optional[int] = None,
"""Add question to queue with priority support.""" ) -> dict:
if not self._is_ready and self._error_count >= self._MAX_ERRORS: """Process a question with optional parameters."""
_LOGGER.warning("Coordinator is not ready due to previous errors") return await self.async_process_question(
return question, model, temperature, max_tokens, system_prompt, context_messages
)
question_data = { async def async_process_question(
"question": question, self,
"params": { question: str,
"system_prompt": system_prompt, model: Optional[str] = None,
"model": model, temperature: Optional[float] = None,
"temperature": temperature, max_tokens: Optional[int] = None,
"max_tokens": max_tokens system_prompt: Optional[str] = None,
context_messages: Optional[int] = None,
) -> dict:
temp_context_messages = context_messages or self.context_messages
if not question:
raise ValueError("Question cannot be empty")
_LOGGER.debug(f"Processing question for instance {self.instance_name}")
try:
self._is_processing = True
await self.async_update_ha_state()
temp_model = model or self.model
temp_temperature = temperature or self.temperature
temp_max_tokens = max_tokens or self.max_tokens
temp_system_prompt = system_prompt or self._system_prompt
start_time = dt_util.utcnow()
messages = []
if temp_system_prompt:
if self.is_anthropic:
system_content = f"\n\nHuman: {temp_system_prompt}\n\nAssistant: I understand and will follow these instructions."
messages.append({"role": "user", "content": system_content})
else:
messages.append({"role": "system", "content": temp_system_prompt})
# Add conversation history
context_history = self._conversation_history[-temp_context_messages:]
for entry in context_history:
messages.append({"role": "user", "content": entry["question"]})
messages.append({"role": "assistant", "content": entry["response"]})
messages.append({"role": "user", "content": question})
kwargs = {
"model": temp_model,
"temperature": temp_temperature,
"max_tokens": temp_max_tokens,
"messages": messages,
}
response = await self.async_process_message(question, **kwargs)
# Update metrics
end_time = dt_util.utcnow()
latency = (end_time - start_time).total_seconds()
self._update_metrics(latency, response)
# Update history
self._update_history(question, response)
return response
except Exception as err:
self._handle_error(err)
raise HomeAssistantError(f"Failed to process question: {err}")
finally:
self._is_processing = False
await self.async_update_ha_state()
async def async_process_message(self, question: str, **kwargs) -> dict:
"""Process message using the AI client."""
try:
if self.is_anthropic:
response = await self._process_anthropic_message(question, **kwargs)
else:
response = await self._process_openai_message(question, **kwargs)
self.last_response = {
"timestamp": dt_util.utcnow().isoformat(),
"question": question,
"response": response["content"],
"model": kwargs.get("model", self.model),
"instance": self.instance_name,
"error": None
}
return response
except Exception as err:
self._handle_error(err)
raise
async def _process_anthropic_message(self, question: str, **kwargs) -> dict:
"""Process message using Anthropic API."""
response = await self.client.messages.create(
model=kwargs["model"],
max_tokens=kwargs["max_tokens"],
messages=kwargs["messages"],
temperature=kwargs["temperature"],
)
return {
"content": response.content[0].text,
"tokens": {
"prompt": response.usage.input_tokens,
"completion": response.usage.output_tokens,
"total": response.usage.input_tokens + response.usage.output_tokens
} }
} }
# Priority: 0 for high priority, 1 for normal async def _process_openai_message(self, question: str, **kwargs) -> dict:
priority_level = 0 if priority else 1 """Process message using OpenAI API."""
try: try:
await self._question_queue.put((priority_level, question_data)) response = await self.client.create(
await self.async_refresh() model=kwargs["model"],
except asyncio.QueueFull: messages=kwargs["messages"],
_LOGGER.error("Question queue is full. Try again later.") temperature=kwargs["temperature"],
raise RuntimeError("Queue is full") max_tokens=kwargs["max_tokens"],
async def async_shutdown(self) -> None:
"""Shutdown the coordinator."""
try:
while not self._question_queue.empty():
try:
self._question_queue.get_nowait()
self._question_queue.task_done()
except asyncio.QueueEmpty:
break
if hasattr(self.client, 'close'):
await self.client.close()
self._is_ready = False
self._endpoint_status = "disconnected"
# Final metrics update
self._update_final_metrics()
except Exception as err:
_LOGGER.error("Error during shutdown: %s", err)
def _update_final_metrics(self) -> None:
"""Update final metrics before shutdown."""
if self._request_count > 0:
self._performance_metrics["final_success_rate"] = (
(self._request_count - self._performance_metrics["total_errors"]) /
self._request_count * 100
)
self._performance_metrics["total_requests"] = self._request_count
self._performance_metrics["total_tokens"] = self._tokens_used
@property
def performance_metrics(self) -> Dict[str, Any]:
"""Return current performance metrics."""
return self._performance_metrics
@property
def queue_size(self) -> int:
"""Return current queue size."""
return self._question_queue.qsize()
@property
def is_queue_full(self) -> bool:
"""Return whether queue is full."""
return self._question_queue.full()
@property
def is_ready(self) -> bool:
"""Return if coordinator is ready."""
return self._is_ready and self._error_count < self._MAX_ERRORS
@property
def is_processing(self) -> bool:
"""Return if coordinator is processing."""
return self._is_processing
@property
def is_rate_limited(self) -> bool:
"""Return if coordinator is rate limited."""
return self._is_rate_limited
@property
def is_maintenance(self) -> bool:
"""Return if API is in maintenance."""
return self._is_maintenance
@property
def error_count(self) -> int:
"""Return current error count."""
return self._error_count
@property
def request_count(self) -> int:
"""Return total request count."""
return self._request_count
@property
def tokens_used(self) -> int:
"""Return total tokens used."""
return self._tokens_used
@property
def api_version(self) -> str:
"""Return API version."""
return self._api_version
@property
def endpoint_status(self) -> str:
"""Return endpoint status."""
return self._endpoint_status
@property
def responses(self) -> Dict[str, Any]:
"""Return all responses."""
return self._responses
@property
def last_response(self) -> Optional[Dict[str, Any]]:
"""Return the last response."""
if not self._responses:
return None
return next(iter(self._responses.values()))
def reset_error_count(self) -> None:
"""Reset error counter."""
self._error_count = 0
self._is_rate_limited = False
self._is_maintenance = False
if not self._is_ready:
self._is_ready = True
self._endpoint_status = "connected"
async def clear_queue(self) -> None:
"""Clear the question queue."""
try:
while not self._question_queue.empty():
try:
self._question_queue.get_nowait()
self._question_queue.task_done()
except asyncio.QueueEmpty:
break
except Exception as err:
_LOGGER.error("Error clearing queue: %s", err)
async def clear_history(self) -> None:
"""Clear response history."""
self._responses.clear()
await self.async_refresh()
def get_response(self, question: str) -> Optional[Dict[str, Any]]:
"""Get specific response by question."""
return self._responses.get(question)
def get_recent_responses(self, limit: int = 10) -> List[Dict[str, Any]]:
"""Get most recent responses."""
return list(sorted(
self._responses.values(),
key=lambda x: x["timestamp"],
reverse=True
))[:limit]
async def retry_failed_requests(self) -> None:
"""Retry failed requests."""
failed_requests = [
(q, r) for q, r in self._responses.items()
if r.get("error") is not None
]
for question, response in failed_requests:
await self.async_ask_question(
question,
system_prompt=response.get("system_prompt"),
model=response.get("model"),
temperature=response.get("temperature"),
max_tokens=response.get("max_tokens"),
priority=True
) )
def update_system_prompt(self, new_prompt: str) -> None: return {
"""Update system prompt.""" "content": response["choices"][0]["message"]["content"],
self.system_prompt = new_prompt "tokens": {
_LOGGER.info("System prompt updated") "prompt": response["usage"]["prompt_tokens"],
"completion": response["usage"]["completion_tokens"],
"total": response["usage"]["total_tokens"]
}
}
except Exception as e:
_LOGGER.error(f"Error in OpenAI API call: {str(e)}")
raise
async def health_check(self) -> Dict[str, Any]: def _update_metrics(self, latency: float, response: dict) -> None:
"""Perform health check.""" """Update performance metrics."""
health_status = { metrics = self._performance_metrics
"is_ready": self.is_ready, tokens = response.get("tokens", {})
"is_processing": self.is_processing,
"is_rate_limited": self.is_rate_limited,
"is_maintenance": self.is_maintenance,
"error_count": self.error_count,
"endpoint_status": self.endpoint_status,
"queue_size": self.queue_size,
"request_count": self.request_count,
"tokens_used": self.tokens_used,
"performance_metrics": self.performance_metrics
}
return health_status metrics["total_tokens"] += tokens.get("total", 0)
metrics["prompt_tokens"] += tokens.get("prompt", 0)
metrics["completion_tokens"] += tokens.get("completion", 0)
metrics["successful_requests"] += 1
async def _handle_timeout_error(self) -> None: metrics["average_latency"] = (
"""Handle timeout errors.""" (metrics["average_latency"] * (metrics["successful_requests"] - 1) + latency)
self._error_count += 1 / metrics["successful_requests"]
self._endpoint_status = "timeout" )
self._performance_metrics["total_errors"] += 1 metrics["max_latency"] = max(metrics["max_latency"], latency)
metrics["min_latency"] = min(metrics["min_latency"], latency)
if not self._question_queue.empty(): def _update_history(self, question: str, response: dict) -> None:
await self.clear_queue() """Update conversation history."""
self._conversation_history.append({
# Fire timeout event "timestamp": dt_util.utcnow().isoformat(),
self.hass.bus.async_fire(f"{DOMAIN}_timeout_error", { "question": question,
"error_count": self._error_count, "response": response["content"]
"endpoint_status": self._endpoint_status
}) })
def export_metrics(self) -> Dict[str, Any]: while len(self._conversation_history) > self.max_history_size:
"""Export all metrics and statistics.""" self._conversation_history.pop(0)
return {
"performance": self._performance_metrics, def _handle_error(self, error: Exception) -> None:
"requests": { """Handle error and update metrics."""
"total": self._request_count, self._performance_metrics["total_errors"] += 1
"successful": self._request_count - self._performance_metrics["total_errors"], self._performance_metrics["failed_requests"] += 1
"failed": self._performance_metrics["total_errors"]
}, self.last_response = {
"tokens": { "timestamp": dt_util.utcnow().isoformat(),
"total_used": self._tokens_used, "question": "",
"average_per_request": self._tokens_used / self._request_count if self._request_count > 0 else 0 "response": "",
}, "model": self.model,
"status": { "instance": self.instance_name,
"is_ready": self.is_ready, "error": str(error)
"endpoint_status": self._endpoint_status,
"error_count": self._error_count
},
"queue": {
"size": self.queue_size,
"is_full": self.is_queue_full
}
} }
async def validate_model(self, model: str) -> bool: async def async_clear_history(self) -> None:
"""Validate if model is supported.""" """Clear conversation history."""
return model in SUPPORTED_MODELS self._conversation_history = []
await self.async_update_ha_state()
async def estimate_tokens(self, text: str) -> int: async def async_get_history(self) -> List[Dict[str, str]]:
"""Estimate token count for text.""" """Get conversation history."""
# Простая оценка: примерно 4 символа на токен return self._conversation_history
return len(text) // 4
def get_rate_limit_info(self) -> Dict[str, Any]: async def async_set_system_prompt(self, prompt: str) -> None:
"""Get rate limit information.""" """Set system prompt."""
return { self._system_prompt = prompt
"is_rate_limited": self._is_rate_limited, await self.async_update_ha_state()
"retry_after": RETRY_DELAY * (2 ** (self._error_count - 1)) if self._is_rate_limited else 0
}
async def optimize_queue(self) -> None:
"""Optimize queue by removing duplicate requests."""
if self._question_queue.empty():
return
seen_questions = set()
optimized_queue = asyncio.PriorityQueue(maxsize=MAX_QUEUE_SIZE)
while not self._question_queue.empty():
try:
priority, question_data = self._question_queue.get_nowait()
question = question_data["question"]
if question not in seen_questions:
seen_questions.add(question)
await optimized_queue.put((priority, question_data))
self._question_queue.task_done()
except asyncio.QueueEmpty:
break
self._question_queue = optimized_queue
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 678 KiB

+11 -25
View File
@@ -1,42 +1,28 @@
{ {
"domain": "ha_text_ai", "domain": "ha_text_ai",
"name": "HA Text AI", "name": "HA Text AI",
"after_dependencies": ["http"],
"bluetooth": [],
"codeowners": ["@smkrv"], "codeowners": ["@smkrv"],
"config_flow": true, "config_flow": true,
"dependencies": [], "dependencies": [],
"documentation": "https://github.com/smkrv/ha-text-ai", "documentation": "https://github.com/smkrv/ha-text-ai",
"homepage": "https://github.com/smkrv/ha-text-ai", "integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"issue_tracker": "https://github.com/smkrv/ha-text-ai/issues", "issue_tracker": "https://github.com/smkrv/ha-text-ai/issues",
"loggers": ["custom_components.ha_text_ai"],
"mqtt": [],
"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", "aiohttp>=3.8.0",
"async-timeout>=4.0.0" "async-timeout>=4.0.0",
"certifi>=2024.2.2"
], ],
"version": "2.0.0", "single_config_entry": false,
"homeassistant": "2024.1.0",
"quality_scale": "silver",
"integration_type": "service",
"translations": [
"en"
],
"zeroconf": [],
"ssdp": [], "ssdp": [],
"usb": [], "usb": [],
"bluetooth": [], "version": "2.0.0",
"mqtt": [], "zeroconf": []
"loggers": ["custom_components.ha_text_ai"],
"after_dependencies": ["http"],
"title": "Text AI Assistant",
"description": "AI-powered text assistant supporting multiple models including GPT and Claude",
"render_readme": true,
"tags": [
"ai",
"automation",
"chat",
"assistant",
"openai",
"claude"
]
} }
+221 -162
View File
@@ -1,50 +1,65 @@
"""Sensor platform for HA text AI.""" """Sensor platform for HA Text AI."""
from datetime import datetime
import logging import logging
from typing import Any, Dict, Optional import math
from typing import Any, Dict
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
SensorEntity, SensorEntity,
SensorStateClass, SensorEntityDescription,
SensorDeviceClass,
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from homeassistant.util import slugify
from .const import ( from .const import (
DOMAIN, DOMAIN,
ATTR_QUESTION, CONF_MODEL,
ATTR_RESPONSE, CONF_API_PROVIDER,
ATTR_LAST_UPDATED,
ATTR_MODEL,
ATTR_TEMPERATURE,
ATTR_MAX_TOKENS,
ATTR_TOTAL_RESPONSES, ATTR_TOTAL_RESPONSES,
ATTR_SYSTEM_PROMPT, ATTR_TOTAL_ERRORS,
ATTR_QUEUE_SIZE, ATTR_AVG_RESPONSE_TIME,
ATTR_API_STATUS, ATTR_LAST_REQUEST_TIME,
ATTR_ERROR_COUNT,
ATTR_LAST_ERROR, ATTR_LAST_ERROR,
ATTR_RESPONSE_TIME, ATTR_IS_PROCESSING,
ATTR_IS_RATE_LIMITED,
ATTR_IS_MAINTENANCE,
ATTR_API_VERSION, ATTR_API_VERSION,
ATTR_ENDPOINT_STATUS, ATTR_ENDPOINT_STATUS,
ATTR_REQUEST_COUNT, ATTR_PERFORMANCE_METRICS,
ATTR_TOKENS_USED, ATTR_HISTORY_SIZE,
ENTITY_ICON, ATTR_UPTIME,
ENTITY_ICON_ERROR, ATTR_API_PROVIDER,
ENTITY_ICON_PROCESSING, ATTR_MODEL,
ATTR_SYSTEM_PROMPT,
ATTR_API_STATUS,
ATTR_RESPONSE,
ATTR_QUESTION,
ATTR_CONVERSATION_HISTORY,
METRIC_TOTAL_TOKENS,
METRIC_PROMPT_TOKENS,
METRIC_COMPLETION_TOKENS,
METRIC_SUCCESSFUL_REQUESTS,
METRIC_FAILED_REQUESTS,
METRIC_AVERAGE_LATENCY,
METRIC_MAX_LATENCY,
METRIC_MIN_LATENCY,
STATE_READY, STATE_READY,
STATE_PROCESSING, STATE_PROCESSING,
STATE_ERROR, STATE_ERROR,
STATE_DISCONNECTED,
STATE_RATE_LIMITED,
STATE_INITIALIZING, STATE_INITIALIZING,
STATE_MAINTENANCE, STATE_MAINTENANCE,
STATE_RATE_LIMITED,
STATE_DISCONNECTED,
ENTITY_ICON,
ENTITY_ICON_ERROR,
ENTITY_ICON_PROCESSING,
) )
from .coordinator import HATextAICoordinator from .coordinator import HATextAICoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -54,17 +69,19 @@ async def async_setup_entry(
entry: ConfigEntry, entry: ConfigEntry,
async_add_entities: AddEntitiesCallback, async_add_entities: AddEntitiesCallback,
) -> None: ) -> None:
"""Set up the HA text AI sensor.""" """Set up the HA Text AI sensor."""
coordinator = hass.data[DOMAIN][entry.entry_id] coordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([HATextAISensor(coordinator, entry)], True) instance_name = coordinator.instance_name
_LOGGER.debug(f"Setting up sensor with instance: {instance_name}")
sensor = HATextAISensor(coordinator, entry)
async_add_entities([sensor], True)
class HATextAISensor(CoordinatorEntity, SensorEntity): class HATextAISensor(CoordinatorEntity, SensorEntity):
"""HA text AI Sensor.""" """HA Text AI Sensor."""
_attr_has_entity_name = True coordinator: HATextAICoordinator
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_device_class = SensorDeviceClass.TIMESTAMP
def __init__( def __init__(
self, self,
@@ -73,163 +90,205 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator) super().__init__(coordinator)
self._config_entry = config_entry self._config_entry = config_entry
self._instance_name = coordinator.instance_name
self._conversation_history = []
self._system_prompt = None
self._attr_name = f"HA Text AI {self._instance_name}"
self.entity_id = f"sensor.ha_text_ai_{slugify(self._instance_name)}"
self._attr_unique_id = f"{config_entry.entry_id}" self._attr_unique_id = f"{config_entry.entry_id}"
self._attr_name = "Last Response"
self._attr_suggested_display_precision = 0 self.entity_description = SensorEntityDescription(
key=f"ha_text_ai_{self._instance_name}",
entity_registry_enabled_default=True,
)
self._current_state = STATE_INITIALIZING
self._error_count = 0 self._error_count = 0
self._last_error = None self._last_error = None
self._state = STATE_INITIALIZING self._last_update = None
self._attr_device_info = { self._is_processing = False
"identifiers": {(DOMAIN, self._attr_unique_id)}, self._last_response = {}
"name": "HA Text AI", self._metrics = {}
"manufacturer": "Community",
"model": coordinator.model,
"sw_version": coordinator.api_version,
}
@property model = config_entry.data.get(CONF_MODEL, "Unknown")
def icon(self) -> str: api_provider = config_entry.data.get(CONF_API_PROVIDER, "Unknown")
"""Return the icon based on the current state."""
if self._state == STATE_PROCESSING:
return ENTITY_ICON_PROCESSING
elif self._state in [STATE_ERROR, STATE_DISCONNECTED, STATE_RATE_LIMITED]:
return ENTITY_ICON_ERROR
return ENTITY_ICON
@property self._attr_device_info = DeviceInfo(
def state(self) -> StateType: identifiers={(DOMAIN, self._attr_unique_id)},
"""Return the state of the sensor.""" name=self._attr_name, # Используем имя сенсора
if not self.coordinator.data or not self.coordinator.last_update_success: manufacturer="Community",
return None model=f"{model} ({api_provider} provider)",
sw_version="1.0.0",
)
try: _LOGGER.debug(f"Initialized sensor: {self.entity_id} for instance: {self._instance_name}")
if self.coordinator.data and isinstance(self.coordinator.data, dict):
last_update = self.coordinator.data.get("last_update")
if isinstance(last_update, datetime):
return dt_util.as_local(last_update)
return last_update
return None
except Exception as err:
_LOGGER.error("Error getting state: %s", err, exc_info=True)
return None
@property
def extra_state_attributes(self) -> Dict[str, Any]:
"""Return entity specific state attributes."""
attributes = {
ATTR_TOTAL_RESPONSES: 0,
ATTR_MODEL: self.coordinator.model,
ATTR_TEMPERATURE: self.coordinator.temperature,
ATTR_MAX_TOKENS: self.coordinator.max_tokens,
ATTR_SYSTEM_PROMPT: self.coordinator.system_prompt,
ATTR_QUEUE_SIZE: self.coordinator._question_queue.qsize(),
ATTR_API_STATUS: self._state,
ATTR_ERROR_COUNT: self._error_count,
ATTR_LAST_ERROR: self._last_error,
ATTR_API_VERSION: self.coordinator.api_version,
ATTR_ENDPOINT_STATUS: self.coordinator.endpoint_status,
ATTR_REQUEST_COUNT: self.coordinator.request_count,
ATTR_TOKENS_USED: self.coordinator.tokens_used,
}
if not self.coordinator.data:
return attributes
try:
history = list(self.coordinator._responses.items())
if history:
last_question, last_data = history[-1]
if isinstance(last_data, dict):
last_response = last_data.get("response", "")
last_updated = last_data.get("timestamp") or self.coordinator.data.get("last_update")
response_time = last_data.get("response_time")
model = last_data.get("model", self.coordinator.model)
temperature = last_data.get("temperature", self.coordinator.temperature)
max_tokens = last_data.get("max_tokens", self.coordinator.max_tokens)
error = last_data.get("error")
if error:
self._last_error = error
self._state = STATE_ERROR
else:
last_response = str(last_data)
last_updated = self.coordinator.data.get("last_update")
response_time = None
model = self.coordinator.model
temperature = self.coordinator.temperature
max_tokens = self.coordinator.max_tokens
if isinstance(last_updated, datetime):
last_updated = dt_util.as_local(last_updated)
attributes.update({
ATTR_QUESTION: last_question,
ATTR_RESPONSE: last_response,
ATTR_LAST_UPDATED: last_updated,
ATTR_TOTAL_RESPONSES: len(history),
ATTR_MODEL: model,
ATTR_TEMPERATURE: temperature,
ATTR_MAX_TOKENS: max_tokens,
})
if response_time is not None:
attributes[ATTR_RESPONSE_TIME] = response_time
return attributes
except Exception as err:
_LOGGER.error("Error getting attributes: %s", err, exc_info=True)
self._error_count += 1
self._last_error = str(err)
self._state = STATE_ERROR
return attributes
@property @property
def available(self) -> bool: def available(self) -> bool:
"""Return if entity is available.""" """Return if entity is available."""
return self.coordinator.last_update_success return (
self.coordinator.last_update_success
and self.coordinator.data is not None
and self._current_state != STATE_DISCONNECTED
)
def _sanitize_value(self, value: Any) -> Any:
"""Sanitize values for JSON serialization."""
if isinstance(value, float):
if math.isinf(value) or math.isnan(value):
return None
return value
def _sanitize_attributes(self, attributes: Dict[str, Any]) -> Dict[str, Any]:
"""Sanitize all attributes for JSON serialization."""
return {
key: self._sanitize_value(value)
for key, value in attributes.items()
if value is not None
}
@property
def native_value(self) -> StateType:
"""Return the native value of the sensor."""
if not self.coordinator.last_update_success or not self.coordinator.data:
self._current_state = STATE_DISCONNECTED
return self._current_state
status = self.coordinator.data.get("state", STATE_READY)
self._current_state = status
return status
@property
def icon(self) -> str:
"""Return the icon based on the current state."""
if self._current_state == STATE_ERROR:
return ENTITY_ICON_ERROR
elif self._current_state == STATE_PROCESSING:
return ENTITY_ICON_PROCESSING
return ENTITY_ICON
@property
def extra_state_attributes(self) -> Dict[str, Any]:
"""Return entity specific state attributes."""
if not self.coordinator.data:
return {}
try:
data = self.coordinator.data
attributes = {
ATTR_MODEL: self._config_entry.data.get(CONF_MODEL, "Unknown"),
ATTR_API_PROVIDER: self._config_entry.data.get(CONF_API_PROVIDER, "Unknown"),
ATTR_API_STATUS: self._current_state,
ATTR_TOTAL_ERRORS: self._error_count,
ATTR_LAST_ERROR: self._last_error,
"instance_name": self._instance_name,
ATTR_SYSTEM_PROMPT: data.get("system_prompt"),
ATTR_IS_PROCESSING: data.get("is_processing", False),
ATTR_IS_RATE_LIMITED: data.get("is_rate_limited", False),
ATTR_IS_MAINTENANCE: data.get("is_maintenance", False),
ATTR_ENDPOINT_STATUS: data.get("endpoint_status", "unknown"),
ATTR_UPTIME: data.get("uptime", 0),
ATTR_HISTORY_SIZE: data.get("history_size", 0),
ATTR_CONVERSATION_HISTORY: data.get("conversation_history", []),
}
# Add metrics
metrics = data.get("metrics", {})
if isinstance(metrics, dict):
self._metrics = metrics
attributes.update({
METRIC_TOTAL_TOKENS: metrics.get("total_tokens", 0),
METRIC_PROMPT_TOKENS: metrics.get("prompt_tokens", 0),
METRIC_COMPLETION_TOKENS: metrics.get("completion_tokens", 0),
METRIC_SUCCESSFUL_REQUESTS: metrics.get("successful_requests", 0),
METRIC_FAILED_REQUESTS: metrics.get("failed_requests", 0),
METRIC_AVERAGE_LATENCY: metrics.get("average_latency", 0),
METRIC_MAX_LATENCY: metrics.get("max_latency", 0),
METRIC_MIN_LATENCY: metrics.get("min_latency", float("inf")),
})
# Add last response
last_response = data.get("last_response", {})
if isinstance(last_response, dict):
self._last_response = last_response
attributes.update({
ATTR_RESPONSE: last_response.get("response", ""),
ATTR_QUESTION: last_response.get("question", ""),
"last_model": last_response.get("model", ""),
"last_timestamp": last_response.get("timestamp", ""),
"last_error": last_response.get("error"),
})
# Add performance metrics if available
if ATTR_PERFORMANCE_METRICS in data:
attributes[ATTR_PERFORMANCE_METRICS] = data[ATTR_PERFORMANCE_METRICS]
# Add API version if available
if ATTR_API_VERSION in data:
attributes[ATTR_API_VERSION] = data[ATTR_API_VERSION]
return self._sanitize_attributes(attributes)
except Exception as err:
_LOGGER.error("Error preparing attributes: %s", err, exc_info=True)
return {}
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""When entity is added to hass.""" """When entity is added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
self._handle_coordinator_update() self._handle_coordinator_update()
self._state = STATE_READY _LOGGER.debug(f"Entity {self.entity_id} added to Home Assistant")
def _handle_coordinator_update(self) -> None: def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator.""" """Handle updated data from the coordinator."""
try: try:
if self.coordinator.data: data = self.coordinator.data
if self.coordinator._is_ready: if not self.coordinator.last_update_success or not data:
if self.coordinator._is_processing: self._current_state = STATE_DISCONNECTED
self._state = STATE_PROCESSING _LOGGER.warning(f"No data available for {self.entity_id}")
elif self.coordinator._is_rate_limited: self.async_write_ha_state()
self._state = STATE_RATE_LIMITED return
elif self.coordinator._is_maintenance:
self._state = STATE_MAINTENANCE
else:
self._state = STATE_READY
else:
self._state = STATE_DISCONNECTED
else:
self._state = STATE_DISCONNECTED
# Обновляем счетчик ошибок только если статус изменился на ошибку self._is_processing = data.get("is_processing", False)
if self._state == STATE_ERROR:
# Update conversation history and system prompt
self._conversation_history = data.get("conversation_history", [])
self._system_prompt = data.get("system_prompt")
# Update state based on conditions
if self._is_processing:
self._current_state = STATE_PROCESSING
elif data.get("is_rate_limited"):
self._current_state = STATE_RATE_LIMITED
elif data.get("is_maintenance"):
self._current_state = STATE_MAINTENANCE
elif data.get("error"):
self._current_state = STATE_ERROR
self._last_error = data["error"]
self._error_count += 1 self._error_count += 1
else:
self._current_state = data.get("state", STATE_READY)
# Update last update timestamp
self._last_update = dt_util.utcnow()
_LOGGER.debug(
f"Updated {self.entity_id} state to: {self._current_state} "
f"(available: {self.available})"
)
except Exception as err: except Exception as err:
_LOGGER.error("Error handling update: %s", err, exc_info=True) self._current_state = STATE_ERROR
self._error_count += 1
self._last_error = str(err) self._last_error = str(err)
self._state = STATE_ERROR self._error_count += 1
_LOGGER.error(
"Error handling update for %s: %s",
self.entity_id,
err,
exc_info=True
)
self.async_write_ha_state() self.async_write_ha_state()
async def async_reset_error_count(self) -> None:
"""Reset the error counter."""
self._error_count = 0
self._last_error = None
self.async_write_ha_state()
+36 -166
View File
@@ -3,69 +3,56 @@ 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.
Response time may vary based on model selection and server load.
Supports various AI models with different capabilities and pricing.
fields: fields:
instance:
name: Instance
description: Name of the HA Text AI instance to use
required: true
selector:
entity:
integration: ha_text_ai
domain: sensor
question: question:
name: Question name: Question
description: >- description: Your question or prompt for the AI assistant
Your question or prompt for the AI assistant. Be specific and clear for better results.
You can ask about home automation, technical advice, or general questions.
For complex queries, consider breaking them into smaller parts.
The system will maintain conversation context for follow-up questions.
required: true required: true
example: |
What automations would you recommend for a smart kitchen?
Consider energy efficiency, convenience, and integration with:
- Smart lighting
- Appliance control
- Temperature monitoring
- Voice commands
selector: selector:
text: text:
multiline: true multiline: true
type: text type: text
system_prompt:
name: System Prompt
description: Optional system prompt to set context for this specific question
required: false
selector:
text:
multiline: true
context_messages:
name: Context Messages
description: Number of previous messages to include in context (1-20)
required: false
default: 5
selector:
number:
min: 1
max: 20
step: 1
mode: box
model: model:
name: Model name: Model
description: >- description: "Select AI model to use (optional, overrides default setting)"
Select an AI model to use (optional, overrides default setting).
Different models have different capabilities, context limits, and response characteristics:
- GPT-3.5 Turbo: Fast, efficient, good for most tasks
- GPT-4: More capable, better reasoning, slower
- Claude-3: Advanced capabilities, longer context
Note: More capable models may have longer response times and higher API costs.
required: false required: false
example: "gpt-3.5-turbo" selector:
default: "gpt-3.5-turbo" text:
selector: multiline: false
select:
custom_value: true
options:
- label: "GPT-3.5 Turbo (Fast & Efficient)"
value: "gpt-3.5-turbo"
- label: "GPT-3.5 Turbo 16K (Extended)"
value: "gpt-3.5-turbo-16k"
- label: "GPT-4 (Most Capable)"
value: "gpt-4"
- label: "GPT-4 32K (Extended Context)"
value: "gpt-4-32k"
- label: "GPT-4 Turbo (Latest)"
value: "gpt-4-1106-preview"
- label: "Claude-3 Sonnet (Balanced)"
value: "claude-3-sonnet"
- label: "Claude-3 Opus (Most Advanced)"
value: "claude-3-opus"
mode: dropdown
temperature: temperature:
name: Temperature name: Temperature
description: >- description: Controls response creativity (0.0-2.0)
Controls response creativity (0.0-2.0):
0.0-0.3: Focused, consistent responses (best for technical/factual queries)
0.4-0.7: Balanced responses (recommended for most uses)
0.8-2.0: More creative, varied responses (best for brainstorming)
Note: Higher values may produce less predictable results.
required: false required: false
default: 0.7 default: 0.7
selector: selector:
@@ -74,17 +61,10 @@ ask_question:
max: 2.0 max: 2.0
step: 0.1 step: 0.1
mode: slider mode: slider
unit_of_measurement: ""
max_tokens: max_tokens:
name: Max Tokens name: Max Tokens
description: >- description: Maximum length of the response (1-4096 tokens)
Maximum length of the response (1-4096 tokens).
Recommended ranges:
- Short (256-512): Quick answers, status updates
- Medium (512-1024): Detailed explanations
- Long (1024-4096): Complex analysis
Note: Token limits vary by model. Actual length may be shorter.
required: false required: false
default: 1000 default: 1000
selector: selector:
@@ -93,113 +73,3 @@ ask_question:
max: 4096 max: 4096
step: 1 step: 1
mode: box mode: box
system_prompt:
name: System Prompt
description: >-
Optional system prompt to set context for this specific question.
This will temporarily override the default system prompt.
Use this to specify expertise areas, response format, or special instructions.
required: false
example: "You are a home automation expert focused on energy efficiency and security"
selector:
text:
multiline: true
clear_history:
name: Clear History
description: >-
Delete all stored questions and responses from the conversation history.
This action cannot be undone. Consider using 'get_history' first if you need to backup the data.
System prompt settings and configuration will be preserved.
fields: {}
get_history:
name: Get History
description: >-
Retrieve recent conversation history, including:
- Questions and responses
- Timestamps and response times
- Models used and token counts
- Temperature and other settings
Results are ordered from newest to oldest.
fields:
limit:
name: Limit
description: >-
Number of most recent conversations to return (1-100).
Higher values return more history but may take longer to process.
Default: 10 conversations
required: false
default: 10
selector:
number:
min: 1
max: 100
step: 1
mode: box
filter_model:
name: Filter by Model
description: >-
Only return conversations using a specific AI model.
Leave empty to show all models.
required: false
selector:
select:
custom_value: true
options:
- label: "All Models"
value: ""
- label: "GPT-3.5 Turbo"
value: "gpt-3.5-turbo"
- label: "GPT-4"
value: "gpt-4"
- label: "GPT-4 Turbo"
value: "gpt-4-1106-preview"
- label: "Claude-3 Sonnet"
value: "claude-3-sonnet"
- label: "Claude-3 Opus"
value: "claude-3-opus"
mode: dropdown
start_date:
name: Start Date
description: >-
Optional start date for filtering history.
Format: YYYY-MM-DD
required: false
selector:
datetime:
type: date
set_system_prompt:
name: Set System Prompt
description: >-
Set default system behavior instructions for all future conversations.
This defines how the AI should behave and respond to questions.
The prompt will be used for all models unless overridden per question.
fields:
prompt:
name: System Prompt
description: >-
Instructions that define how the AI should behave and respond.
Be specific about:
- Desired expertise and knowledge areas
- Response tone and style
- Output format preferences
- Special handling instructions
Maximum length: 1000 characters
required: true
example: |
You are a home automation expert assistant. Focus on:
1. Practical and efficient solutions
2. Energy-saving recommendations
3. Integration with popular smart home platforms
4. Security and privacy considerations
Provide detailed but concise responses with clear steps when applicable.
Format complex responses with bullet points or numbered lists.
Include warnings about potential risks or limitations.
selector:
text:
multiline: true
@@ -0,0 +1,261 @@
{
"config": {
"step": {
"provider": {
"title": "KI-Anbieter auswählen",
"description": "Wählen Sie den KI-Dienst-Anbieter für diese Instanz",
"data": {
"api_provider": "API-Anbieter",
"context_messages": "Anzahl der zu speichernden Kontextnachrichten (1-20)"
}
},
"user": {
"title": "HA Text AI Instanz konfigurieren",
"description": "Richten Sie eine neue KI-Assistenten-Instanz mit Ihrem ausgewählten Anbieter ein",
"data": {
"name": "Instanzname (z.B. 'GPT Assistent', 'Claude Helfer')",
"api_key": "API-Schlüssel für Authentifizierung",
"model": "Zu verwendendes KI-Modell",
"temperature": "Antwort-Kreativität (0-2, niedriger = fokussierter)",
"max_tokens": "Maximale Antwortlänge (1-4096 Token)",
"api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)",
"request_interval": "Minimale Zeit zwischen Anfragen (0,1-60 Sekunden)",
"context_messages": "Anzahl der zu speichernden Kontextnachrichten (1-20)"
}
}
},
"error": {
"name_exists": "Eine Instanz mit diesem Namen existiert bereits",
"invalid_name": "Ungültiger Instanzname",
"invalid_auth": "Authentifizierung fehlgeschlagen - überprüfen Sie Ihren API-Schlüssel",
"invalid_api_key": "Ungültiger API-Schlüssel - bitte überprüfen Sie Ihre Anmeldedaten",
"cannot_connect": "Verbindung zum API-Dienst fehlgeschlagen",
"invalid_model": "Ausgewähltes Modell ist nicht verfügbar",
"rate_limit": "Anfragelimit überschritten",
"context_length": "Kontextlänge überschritten",
"rate_limit_exceeded": "API-Anfragelimit überschritten",
"maintenance": "Dienst ist in Wartung",
"invalid_response": "Ungültige API-Antwort erhalten",
"api_error": "API-Dienst-Fehler aufgetreten",
"timeout": "Anfrage-Zeitüberschreitung",
"invalid_instance": "Ungültige Instanz angegeben",
"unknown": "Unerwarteter Fehler aufgetreten"
}
},
"options": {
"step": {
"init": {
"title": "Instanzeinstellungen aktualisieren",
"description": "Einstellungen für diese KI-Assistenten-Instanz ändern",
"data": {
"model": "KI-Modell",
"temperature": "Antwort-Kreativität (0-2)",
"max_tokens": "Maximale Antwortlänge (1-4096)",
"request_interval": "Minimales Anfragen-Intervall (0,1-60 Sekunden)",
"context_messages": "Anzahl der vorherigen Nachrichten, die in den Kontext einbezogen werden sollen (1-20)"
}
}
}
},
"services": {
"ask_question": {
"name": "Frage stellen (HA Text AI)",
"description": "Senden Sie eine Frage an das KI-Modell und erhalten Sie eine detaillierte Antwort. Die Antwort wird in der Gesprächshistorie gespeichert und kann später abgerufen werden.",
"fields": {
"instance": {
"name": "Instanz",
"description": "Name der zu verwendenden HA Text AI Instanz"
},
"question": {
"name": "Frage",
"description": "Ihre Frage oder Eingabeaufforderung für den KI-Assistenten"
},
"context_messages": {
"name": "Kontextnachrichten",
"description": "Anzahl der vorherigen Nachrichten, die in den Kontext einbezogen werden sollen (1-20)"
},
"system_prompt": {
"name": "Systemaufforderung",
"description": "Optionale Systemaufforderung zur Kontexteinstellung für diese spezifische Frage"
},
"model": {
"name": "Modell",
"description": "Wählen Sie das zu verwendende KI-Modell (optional, überschreibt Standardeinstellung)"
},
"temperature": {
"name": "Temperatur",
"description": "Steuert die Antwort-Kreativität (0,0-2,0)"
},
"max_tokens": {
"name": "Max. Token",
"description": "Maximale Länge der Antwort (1-4096 Token)"
}
}
},
"clear_history": {
"name": "Verlauf löschen",
"description": "Alle gespeicherten Fragen und Antworten aus dem Gesprächsverlauf löschen",
"fields": {
"instance": {
"name": "Instanz",
"description": "Name der HA Text AI Instanz, für die der Verlauf gelöscht werden soll"
}
}
},
"get_history": {
"name": "Verlauf abrufen",
"description": "Gesprächsverlauf mit optionaler Filterung und Sortierung abrufen",
"fields": {
"instance": {
"name": "Instanz",
"description": "Name der HA Text AI Instanz, aus der der Verlauf abgerufen werden soll"
},
"limit": {
"name": "Limit",
"description": "Anzahl der zurückzugebenden Gespräche (1-100)"
},
"filter_model": {
"name": "Modell filtern",
"description": "Gespräche nach bestimmtem KI-Modell filtern"
},
"start_date": {
"name": "Startdatum",
"description": "Gespräche ab diesem Datum/Zeitpunkt filtern"
},
"include_metadata": {
"name": "Metadaten einbeziehen",
"description": "Zusätzliche Informationen wie verwendete Token, Antwortzeit usw. einbeziehen"
},
"sort_order": {
"name": "Sortierreihenfolge",
"description": "Sortierreihenfolge der Ergebnisse (neueste oder älteste zuerst)"
}
}
},
"set_system_prompt": {
"name": "Systemaufforderung festlegen",
"description": "Standardmäßige Systemverhaltensinstruktionen für alle zukünftigen Gespräche festlegen",
"fields": {
"instance": {
"name": "Instanz",
"description": "Name der HA Text AI Instanz, für die die Systemaufforderung festgelegt werden soll"
},
"prompt": {
"name": "Systemaufforderung",
"description": "Anweisungen, die definieren, wie sich die KI verhalten und antworten soll"
}
}
}
},
"entity": {
"sensor": {
"ha_text_ai": {
"name": "{name}",
"state": {
"ready": "Bereit",
"processing": "Verarbeitung",
"error": "Fehler",
"disconnected": "Getrennt",
"rate_limited": "Anfragelimit",
"maintenance": "Wartung",
"initializing": "Initialisierung",
"retrying": "Wiederholung",
"queued": "In Warteschlange"
},
"state_attributes": {
"question": {
"name": "Letzte Frage"
},
"response": {
"name": "Letzte Antwort"
},
"model": {
"name": "Aktuelles Modell"
},
"temperature": {
"name": "Temperatur"
},
"max_tokens": {
"name": "Max. Token"
},
"system_prompt": {
"name": "Systemaufforderung"
},
"response_time": {
"name": "Letzte Antwortzeit"
},
"total_responses": {
"name": "Gesamte Antworten"
},
"error_count": {
"name": "Fehleranzahl"
},
"last_error": {
"name": "Letzter Fehler"
},
"api_status": {
"name": "API-Status"
},
"tokens_used": {
"name": "Insgesamt verwendete Token"
},
"average_response_time": {
"name": "Durchschnittliche Antwortzeit"
},
"last_request_time": {
"name": "Letzte Anforderungszeit"
},
"is_processing": {
"name": "Verarbeitungsstatus"
},
"is_rate_limited": {
"name": "Status Anfragelimit"
},
"is_maintenance": {
"name": "Wartungsstatus"
},
"api_version": {
"name": "API-Version"
},
"endpoint_status": {
"name": "Endpunktstatus"
},
"performance_metrics": {
"name": "Leistungsmetriken"
},
"history_size": {
"name": "Verlaufsgröße"
},
"uptime": {
"name": "Betriebszeit"
},
"total_tokens": {
"name": "Gesamte Token"
},
"prompt_tokens": {
"name": "Prompt-Token"
},
"completion_tokens": {
"name": "Abschluss-Token"
},
"successful_requests": {
"name": "Erfolgreiche Anfragen"
},
"failed_requests": {
"name": "Fehlgeschlagene Anfragen"
},
"average_latency": {
"name": "Durchschnittliche Latenz"
},
"max_latency": {
"name": "Maximale Latenz"
},
"min_latency": {
"name": "Minimale Latenz"
}
}
}
}
}
}
+177 -64
View File
@@ -1,145 +1,258 @@
{ {
"config": { "config": {
"step": { "step": {
"user": { "provider": {
"title": "Set up HA Text AI", "title": "Select AI Provider",
"description": "Configure your AI integration for various providers (OpenAI, Anthropic, etc.). Different models have different capabilities and pricing.", "description": "Choose which AI service provider to use for this instance",
"data": { "data": {
"api_key": "API key for authentication (required)", "api_provider": "API Provider",
"model": "AI model to use (e.g., gpt-3.5-turbo, gpt-4, claude-3-sonnet, claude-3-opus)", "context_messages": "Number of context messages to retain (1-20)"
"temperature": "Response creativity (0-2, lower = more focused and consistent)", }
},
"user": {
"title": "Configure HA Text AI Instance",
"description": "Set up a new AI assistant instance with your selected provider",
"data": {
"name": "Instance name (e.g., 'GPT Assistant', 'Claude Helper')",
"api_key": "API key for authentication",
"model": "AI model to use",
"temperature": "Response creativity (0-2, lower = more focused)",
"max_tokens": "Maximum response length (1-4096 tokens)", "max_tokens": "Maximum response length (1-4096 tokens)",
"api_endpoint": "Custom API endpoint URL (default varies by provider)", "api_endpoint": "Custom API endpoint URL (optional)",
"request_interval": "Minimum time between requests in seconds (min: 0.1)", "request_interval": "Minimum time between requests (0.1-60 seconds)",
"system_prompt": "Default instructions for AI behavior and expertise" "context_messages": "Number of context messages to retain (1-20)"
} }
} }
}, },
"error": { "error": {
"name_exists": "An instance with this name already exists",
"invalid_name": "Invalid instance name",
"invalid_auth": "Authentication failed - check your API key", "invalid_auth": "Authentication failed - check your API key",
"invalid_api_key": "Invalid API key - please verify your credentials", "invalid_api_key": "Invalid API key - please verify your credentials",
"cannot_connect": "Connection failed - check endpoint and network status", "cannot_connect": "Failed to connect to API service",
"invalid_model": "Model unavailable or not supported by the API", "invalid_model": "Selected model is not available",
"rate_limit": "Rate limit exceeded - please reduce request frequency", "rate_limit": "Rate limit exceeded",
"context_length": "Input exceeds maximum context length for model", "context_length": "Context length exceeded",
"api_error": "API service error - check provider status", "rate_limit_exceeded": "API rate limit exceeded",
"timeout": "Request timeout - server not responding", "maintenance": "Service is under maintenance",
"queue_full": "Request queue full - try again later", "invalid_response": "Invalid API response received",
"invalid_prompt": "Invalid system prompt format or length", "api_error": "API service error occurred",
"invalid_url_format": "Invalid API endpoint URL format", "timeout": "Request timed out",
"invalid_input": "Invalid configuration parameters", "invalid_instance": "Invalid instance specified",
"unknown": "Unexpected error - check logs for details" "unknown": "Unexpected error occurred"
} }
}, },
"options": { "options": {
"step": { "step": {
"init": { "init": {
"title": "HA Text AI Settings", "title": "Update Instance Settings",
"description": "Adjust your AI integration parameters and behavior", "description": "Modify settings for this AI assistant instance",
"data": { "data": {
"model": "Select AI model for responses (capabilities vary)", "model": "AI model",
"temperature": "Response creativity (0-2, affects variation)", "temperature": "Response creativity (0-2)",
"max_tokens": "Maximum response length in tokens", "max_tokens": "Maximum response length (1-4096)",
"request_interval": "Minimum seconds between requests (rate limiting)", "request_interval": "Minimum request interval (0.1-60 seconds)",
"system_prompt": "Default AI behavior and expertise instructions", "context_messages": "Number of previous messages to include in context (1-20)"
"timeout": "Request timeout in seconds (default: 30)",
"retry_count": "Number of retry attempts for failed requests",
"queue_size": "Maximum pending requests in queue"
} }
} }
} }
}, },
"services": { "services": {
"ask_question": { "ask_question": {
"name": "Ask Question", "name": "Ask Question (HA Text AI)",
"description": "Send a question to the AI model and get a detailed response. Supports context awareness for follow-up questions.", "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.",
"fields": { "fields": {
"instance": {
"name": "Instance",
"description": "Name of the HA Text AI instance to use"
},
"question": { "question": {
"name": "Question", "name": "Question",
"description": "Your question or prompt for the AI model. Be specific for better results." "description": "Your question or prompt for the AI assistant"
},
"context_messages": {
"name": "Context Messages",
"description": "Number of previous messages to include in context (1-20)"
}, },
"system_prompt": { "system_prompt": {
"name": "System Prompt", "name": "System Prompt",
"description": "Optional behavior instructions for this specific question" "description": "Optional system prompt to set context for this specific question"
}, },
"model": { "model": {
"name": "Model", "name": "Model",
"description": "Optional specific AI model for this request (overrides default)" "description": "Select AI model to use (optional, overrides default setting)"
}, },
"temperature": { "temperature": {
"name": "Temperature", "name": "Temperature",
"description": "Optional creativity setting (0-2, affects response variation)" "description": "Controls response creativity (0.0-2.0)"
}, },
"max_tokens": { "max_tokens": {
"name": "Max Tokens", "name": "Max Tokens",
"description": "Optional maximum response length (1-4096 tokens)" "description": "Maximum length of the response (1-4096 tokens)"
} }
} }
}, },
"clear_history": { "clear_history": {
"name": "Clear History", "name": "Clear History",
"description": "Delete all stored conversation history, responses, and metadata" "description": "Delete all stored questions and responses from the conversation history",
"fields": {
"instance": {
"name": "Instance",
"description": "Name of the HA Text AI instance to clear history for"
}
}
}, },
"get_history": { "get_history": {
"name": "Get History", "name": "Get History",
"description": "Retrieve conversation history with detailed metadata including timestamps", "description": "Retrieve conversation history with optional filtering and sorting",
"fields": { "fields": {
"instance": {
"name": "Instance",
"description": "Name of the HA Text AI instance to get history from"
},
"limit": { "limit": {
"name": "Limit", "name": "Limit",
"description": "Maximum number of conversations to return (1-100)" "description": "Number of conversations to return (1-100)"
}, },
"filter_model": { "filter_model": {
"name": "Filter Model", "name": "Filter Model",
"description": "Show only responses from a specific model" "description": "Filter conversations by specific AI model"
}, },
"start_date": { "start_date": {
"name": "Start Date", "name": "Start Date",
"description": "Filter conversations from this date (YYYY-MM-DD)" "description": "Filter conversations starting from this date/time"
},
"include_metadata": {
"name": "Include Metadata",
"description": "Include additional information like tokens used, response time, etc."
},
"sort_order": {
"name": "Sort Order",
"description": "Sort order for results (newest or oldest first)"
} }
} }
}, },
"set_system_prompt": { "set_system_prompt": {
"name": "Set System Prompt", "name": "Set System Prompt",
"description": "Update default AI behavior and expertise instructions", "description": "Set default system behavior instructions for all future conversations",
"fields": { "fields": {
"instance": {
"name": "Instance",
"description": "Name of the HA Text AI instance to set system prompt for"
},
"prompt": { "prompt": {
"name": "System Prompt", "name": "System Prompt",
"description": "Instructions for AI behavior, expertise, and response style" "description": "Instructions that define how the AI should behave and respond"
} }
} }
} }
}, },
"state": {
"ready": "Ready for requests",
"processing": "Processing request",
"error": "Error occurred",
"disconnected": "API disconnected",
"rate_limited": "Rate limit reached",
"initializing": "Starting up",
"retrying": "Retrying request",
"queued": "Request queued"
},
"entity": { "entity": {
"sensor": { "sensor": {
"status": { "ha_text_ai": {
"name": "AI Status", "name": "{name}",
"state": { "state": {
"ready": "Ready", "ready": "Ready",
"processing": "Processing", "processing": "Processing",
"error": "Error", "error": "Error",
"disconnected": "Disconnected", "disconnected": "Disconnected",
"rate_limited": "Rate Limited", "rate_limited": "Rate Limited",
"maintenance": "Maintenance",
"initializing": "Initializing", "initializing": "Initializing",
"retrying": "Retrying", "retrying": "Retrying",
"queued": "Queued" "queued": "Queued"
} },
}, "state_attributes": {
"last_response": { "question": {
"name": "Last Response", "name": "Last Question"
"state": { },
"success": "Success", "response": {
"error": "Error", "name": "Last Response"
"timeout": "Timeout" },
"model": {
"name": "Current Model"
},
"temperature": {
"name": "Temperature"
},
"max_tokens": {
"name": "Max Tokens"
},
"system_prompt": {
"name": "System Prompt"
},
"response_time": {
"name": "Last Response Time"
},
"total_responses": {
"name": "Total Responses"
},
"error_count": {
"name": "Error Count"
},
"last_error": {
"name": "Last Error"
},
"api_status": {
"name": "API Status"
},
"tokens_used": {
"name": "Total Tokens Used"
},
"average_response_time": {
"name": "Average Response Time"
},
"last_request_time": {
"name": "Last Request Time"
},
"is_processing": {
"name": "Processing Status"
},
"is_rate_limited": {
"name": "Rate Limited Status"
},
"is_maintenance": {
"name": "Maintenance Status"
},
"api_version": {
"name": "API Version"
},
"endpoint_status": {
"name": "Endpoint Status"
},
"performance_metrics": {
"name": "Performance Metrics"
},
"history_size": {
"name": "History Size"
},
"uptime": {
"name": "Uptime"
},
"total_tokens": {
"name": "Total Tokens"
},
"prompt_tokens": {
"name": "Prompt Tokens"
},
"completion_tokens": {
"name": "Completion Tokens"
},
"successful_requests": {
"name": "Successful Requests"
},
"failed_requests": {
"name": "Failed Requests"
},
"average_latency": {
"name": "Average Latency"
},
"max_latency": {
"name": "Maximum Latency"
},
"min_latency": {
"name": "Minimum Latency"
}
} }
} }
} }
@@ -0,0 +1,260 @@
{
"config": {
"step": {
"provider": {
"title": "Выбор провайдера ИИ",
"description": "Выберите сервис ИИ для этого экземпляра",
"data": {
"api_provider": "Провайдер API",
"context_messages": "Количество сообщений в контексте (1-20)"
}
},
"user": {
"title": "Настройка экземпляра HA Text AI",
"description": "Настройте нового помощника ИИ с выбранным провайдером",
"data": {
"name": "Название экземпляра (например, 'GPT Помощник', 'Claude Ассистент')",
"api_key": "API-ключ для аутентификации",
"model": "Модель ИИ для использования",
"temperature": "Креативность ответов (0-2, меньше = более сфокусированно)",
"max_tokens": "Максимальная длина ответа (1-4096 токенов)",
"api_endpoint": "Пользовательский URL-адрес API (необязательно)",
"request_interval": "Минимальный интервал между запросами (0.1-60 секунд)",
"context_messages": "Количество сохраняемых контекстных сообщений (1-20)"
}
}
},
"error": {
"name_exists": "Экземпляр с таким именем уже существует",
"invalid_name": "Некорректное имя экземпляра",
"invalid_auth": "Ошибка аутентификации - проверьте API-ключ",
"invalid_api_key": "Неверный API-ключ - проверьте учетные данные",
"cannot_connect": "Не удалось подключиться к сервису API",
"invalid_model": "Выбранная модель недоступна",
"rate_limit": "Превышен лимит запросов",
"context_length": "Превышена длина контекста",
"rate_limit_exceeded": "Превышен лимит API",
"maintenance": "Сервис на техническом обслуживании",
"invalid_response": "Получен некорректный ответ API",
"api_error": "Произошла ошибка сервиса API",
"timeout": "Время ожидания истекло",
"invalid_instance": "Указан неверный экземпляр",
"unknown": "Произошла непредвиденная ошибка"
}
},
"options": {
"step": {
"init": {
"title": "Обновление настроек экземпляра",
"description": "Измените настройки для этого помощника ИИ",
"data": {
"model": "Модель ИИ",
"temperature": "Креативность ответов (0-2)",
"max_tokens": "Максимальная длина ответа (1-4096)",
"request_interval": "Минимальный интервал запросов (0.1-60 секунд)",
"context_messages": "Количество предыдущих сообщений для включения в контекст (1-20)"
}
}
}
},
"services": {
"ask_question": {
"name": "Задать вопрос (HA Text AI)",
"description": "Отправьте вопрос модели ИИ и получите подробный ответ. Ответ будет сохранен в истории беседы и может быть извлечен позже.",
"fields": {
"instance": {
"name": "Экземпляр",
"description": "Название экземпляра HA Text AI для использования"
},
"question": {
"name": "Вопрос",
"description": "Ваш вопрос или запрос помощнику ИИ"
},
"context_messages": {
"name": "Контекстные сообщения",
"description": "Количество предыдущих сообщений для включения в контекст (1-20)"
},
"system_prompt": {
"name": "Системный промпт",
"description": "Необязательный системный промпт для установки контекста для этого конкретного вопроса"
},
"model": {
"name": "Модель",
"description": "Выберите модель ИИ (необязательно, переопределяет настройки по умолчанию)"
},
"temperature": {
"name": "Температура",
"description": "Управляет креативностью ответа (0.0-2.0)"
},
"max_tokens": {
"name": "Максимальное количество токенов",
"description": "Максимальная длина ответа (1-4096 токенов)"
}
}
},
"clear_history": {
"name": "Очистить историю",
"description": "Удалить все сохраненные вопросы и ответы из истории беседы",
"fields": {
"instance": {
"name": "Экземпляр",
"description": "Название экземпляра HA Text AI для очистки истории"
}
}
},
"get_history": {
"name": "Получить историю",
"description": "Получить историю беседы с дополнительной фильтрацией и сортировкой",
"fields": {
"instance": {
"name": "Экземпляр",
"description": "Название экземпляра HA Text AI для получения истории"
},
"limit": {
"name": "Лимит",
"description": "Количество возвращаемых бесед (1-100)"
},
"filter_model": {
"name": "Фильтр модели",
"description": "Фильтрация бесед по конкретной модели ИИ"
},
"start_date": {
"name": "Начальная дата",
"description": "Фильтрация бесед, начиная с указанной даты/времени"
},
"include_metadata": {
"name": "Включить метаданные",
"description": "Включить дополнительную информацию, например, использованные токены, время ответа и т.д."
},
"sort_order": {
"name": "Порядок сортировки",
"description": "Порядок результатов (сначала новые или старые)"
}
}
},
"set_system_prompt": {
"name": "Установить системный промпт",
"description": "Установить инструкции по умолчанию для поведения ИИ во всех будущих разговорах",
"fields": {
"instance": {
"name": "Экземпляр",
"description": "Название экземпляра HA Text AI для установки системного промпта"
},
"prompt": {
"name": "Системный промпт",
"description": "Инструкции, определяющие, как ИИ должен вести себя и отвечать"
}
}
}
},
"entity": {
"sensor": {
"ha_text_ai": {
"name": "{name}",
"state": {
"ready": "Готов",
"processing": "Обработка",
"error": "Ошибка",
"disconnected": "Отключен",
"rate_limited": "Лимит запросов",
"maintenance": "Техническое обслуживание",
"initializing": "Инициализация",
"retrying": "Повторная попытка",
"queued": "В очереди"
},
"state_attributes": {
"question": {
"name": "Последний вопрос"
},
"response": {
"name": "Последний ответ"
},
"model": {
"name": "Текущая модель"
},
"temperature": {
"name": "Температура"
},
"max_tokens": {
"name": "Максимальное количество токенов"
},
"system_prompt": {
"name": "Системный промпт"
},
"response_time": {
"name": "Время последнего ответа"
},
"total_responses": {
"name": "Всего ответов"
},
"error_count": {
"name": "Количество ошибок"
},
"last_error": {
"name": "Последняя ошибка"
},
"api_status": {
"name": "Статус API"
},
"tokens_used": {
"name": "Всего использовано токенов"
},
"average_response_time": {
"name": "Среднее время ответа"
},
"last_request_time": {
"name": "Время последнего запроса"
},
"is_processing": {
"name": "Статус обработки"
},
"is_rate_limited": {
"name": "Статус лимита запросов"
},
"is_maintenance": {
"name": "Статус обслуживания"
},
"api_version": {
"name": "Версия API"
},
"endpoint_status": {
"name": "Статус эндпоинта"
},
"performance_metrics": {
"name": "Показатели производительности"
},
"history_size": {
"name": "Размер истории"
},
"uptime": {
"name": "Время работы"
},
"total_tokens": {
"name": "Всего токенов"
},
"prompt_tokens": {
"name": "Токены промпта"
},
"completion_tokens": {
"name": "Токены завершения"
},
"successful_requests": {
"name": "Успешные запросы"
},
"failed_requests": {
"name": "Неудачные запросы"
},
"average_latency": {
"name": "Средняя задержка"
},
"max_latency": {
"name": "Максимальная задержка"
},
"min_latency": {
"name": "Минимальная задержка"
}
}
}
}
}
}
+1 -1
View File
@@ -1,9 +1,9 @@
{ {
"name": "HA text AI", "name": "HA text AI",
"render_readme": true, "render_readme": true,
"icon": "mdi:robot",
"domains": ["sensor"], "domains": ["sensor"],
"homeassistant": "2024.11.0", "homeassistant": "2024.11.0",
"icon": "mdi:brain",
"version": "2.0.0", "version": "2.0.0",
"documentation": "https://github.com/smkrv/ha-text-ai" "documentation": "https://github.com/smkrv/ha-text-ai"
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 618 KiB

-3
View File
@@ -1,3 +0,0 @@
pytest
pytest-asyncio
homeassistant
+3 -1
View File
@@ -9,9 +9,11 @@ ha-text-ai/
│ ├── manifest.json │ ├── manifest.json
│ ├── sensor.py │ ├── sensor.py
│ ├── services.yaml │ ├── services.yaml
── const.py ── const.py
│ └── api_client.py
└── strings/ └── strings/
├── en.json ├── en.json
├── de.json
└── ru.json └── ru.json
``` ```