Compare commits

...
264 Commits
Author SHA1 Message Date
SMKRV d3ef31f551 Release v2.0.2-beta 2024-11-28 23:27:39 +03:00
SMKRV ff0c0369e8 YAML configuration explained 2024-11-27 18:03:30 +03:00
SMKRV b0dafe081b YAML configuration explained 2024-11-27 16:58:00 +03:00
SMKRV a88b5d01c1 Vesion updated 2024-11-27 16:52:49 +03:00
SMKRV e7d5e62671 YAML configuration explained 2024-11-27 16:43:20 +03:00
SMKRV 5f41b9489d YAML configuration explained 2024-11-27 16:34:55 +03:00
SMKRV 958e241e0e YAML configuration explained 2024-11-27 16:30:58 +03:00
smkrvandGitHub 4b3efc0b6f Update README_RU.md 2024-11-27 01:36:11 +03:00
smkrvandGitHub 1592fc2371 Delete socia_logo.png 2024-11-26 23:50:38 +03:00
smkrvandGitHub d5a6613428 Update README.md 2024-11-26 18:00:45 +03:00
SMKRV 9324473c9e Banner chaged 2024-11-26 17:49:08 +03:00
SMKRV 456e797cca Banner changed 2024-11-26 17:47:49 +03:00
SMKRV f960b9f9b4 Misc 2024-11-26 17:47:17 +03:00
SMKRV 8449b73423 Banner changed 2024-11-26 17:46:51 +03:00
SMKRV f19ba9aac9 Banner changed 2024-11-26 17:45:59 +03:00
SMKRV 524849f6a9 Logo 2024-11-26 17:44:13 +03:00
SMKRV 0a64a9abe0 Misc 2024-11-26 16:52:35 +03:00
SMKRV adb4127f5a Markdown changes 2024-11-26 16:38:56 +03:00
SMKRV f463593180 Markdown changes 2024-11-26 16:35:49 +03:00
SMKRV e53f257977 Markdown changes 2024-11-26 16:31:44 +03:00
SMKRV 87199e856a markdown changes 2024-11-26 16:27:50 +03:00
SMKRV bcf7cfbf76 Markdown changes 2024-11-26 16:25:22 +03:00
SMKRV 9289e1388f Markdown changes 2024-11-26 16:23:35 +03:00
SMKRV 84db9b7bb8 Markdown changes 2024-11-26 16:22:39 +03:00
SMKRV bd1098a181 Markdown changes 2024-11-26 16:22:27 +03:00
SMKRV 6fd3db0063 Markdown changes 2024-11-26 16:21:56 +03:00
SMKRV bf52217cd5 Markdown changes 2024-11-26 16:21:45 +03:00
SMKRV b0cf5b3c61 Markdown changes 2024-11-26 16:21:22 +03:00
SMKRV 347c1675ca Markdown changes 2024-11-26 16:20:07 +03:00
SMKRV 961ae2d34f Markdown changes 2024-11-26 16:19:45 +03:00
SMKRV 14827c3adc Markdown changes 2024-11-26 15:45:19 +03:00
SMKRV 4a88453abc Markdown changes 2024-11-26 15:44:47 +03:00
SMKRV bc33d38f5e Markdown changes 2024-11-26 15:42:52 +03:00
SMKRV 657a5ede86 Markdown changes 2024-11-26 15:41:46 +03:00
SMKRV dd046fe3e8 Markdown changes 2024-11-26 15:40:02 +03:00
SMKRV 7391dbf5b1 Markdown changes 2024-11-26 15:37:06 +03:00
SMKRV a0ccb86bd4 Markdown changes 2024-11-26 15:36:06 +03:00
SMKRV c0eb5b165d Vesrion changed 2024-11-26 15:31:34 +03:00
SMKRV 9812babc7e Русский перевод README 2024-11-26 15:31:06 +03:00
SMKRV c61c4570b0 Русский перевод README 2024-11-26 15:30:07 +03:00
SMKRV d618feffff docs(readme): Enhance attribute descriptions with detailed English comments
- Add comprehensive explanations for HA Text AI sensor attributes
- Improve readability of README.md documentation
- Provide context and usage details for each sensor attribute
- Translate comments to English with technical clarity

Changes include:
* Detailed descriptions for Model and Provider Information
* Expanded System Status attribute explanations
* Clarified Performance Metrics comments
* Added context for Conversation and Token Usage
* Improved Last Interaction Details descriptions
* Enhanced System Health attribute documentation
2024-11-26 15:01:40 +03:00
SMKRV 0fdc3c93d3 Release v2.0.0-alpha 2024-11-26 14:02:37 +03:00
SMKRV d6e76f7805 Validate HACS 2024-11-26 13:16:10 +03:00
SMKRV b05afe1085 Release v2.0.0-alpha 2024-11-26 02:08:55 +03:00
SMKRV 2a4911f5f8 Release v2.0.0-alpha 2024-11-26 02:04:23 +03:00
SMKRV 208074d845 Release v2.0.0-alpha 2024-11-26 01:20:08 +03:00
SMKRV 616ff2c3fe 💡 Support the Project 2024-11-26 00:14:26 +03:00
SMKRV 987c939956 Support the Project 2024-11-26 00:12:43 +03:00
SMKRV 1615cc744e Support the Project 2024-11-26 00:12:10 +03:00
SMKRV 2d793a4b25 Support the Project 2024-11-26 00:11:55 +03:00
SMKRV aeeb4d5504 Support the Project 2024-11-26 00:09:46 +03:00
SMKRV daec801073 Misc 2024-11-25 23:50:33 +03:00
SMKRV e4916a7b7c Misc 2024-11-25 23:49:42 +03:00
SMKRV a08abd76e3 Misc 2024-11-25 23:48:55 +03:00
SMKRV a2be709608 Misc 2024-11-25 23:48:16 +03:00
SMKRV 69002ef926 Misc 2024-11-25 23:45:28 +03:00
SMKRV 9ade5a7194 misc 2024-11-25 23:43:05 +03:00
SMKRV 9b3f4f605b Sensor Attributes 2024-11-25 17:59:17 +03:00
SMKRV b862968d01 Release v2.0.0-alpha 2024-11-25 17:31:42 +03:00
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
SMKRV 5037622fb6 Release v2.0.0 2024-11-20 01:02:27 +03:00
SMKRV 962a089bf4 Stability improvements 2024-11-19 23:38:07 +03:00
SMKRV 4c56565b66 Stability improvements 2024-11-19 23:30:28 +03:00
SMKRV 8432038f09 Stability improvements 2024-11-19 23:21:54 +03:00
SMKRV 929d916d41 Bugfixes 2024-11-19 19:39:05 +03:00
SMKRV 675975d951 Minor changes 2024-11-19 19:31:26 +03:00
SMKRV 524ec87395 Minor changes 2024-11-19 19:30:42 +03:00
SMKRV 94f8193996 last_update_success_time > last_update_success 2024-11-19 19:28:01 +03:00
SMKRV 7c3fcf73c3 Text edits 2024-11-19 19:23:03 +03:00
SMKRV 20bbf89679 Changes 2024-11-19 19:20:52 +03:00
SMKRV 86dca52d07 Small changes 2024-11-19 19:18:16 +03:00
SMKRV 78d1561e74 Markdown 2024-11-19 19:17:04 +03:00
SMKRV 054e4af258 Quick bugfix 2024-11-19 19:15:57 +03:00
SMKRV dc03faa97e docs: update HACS installation for custom
repository

  - Change HACS badge from Default to Custom
  - Add custom repository installation steps
  - Update installation instructions
  - Revise documentation format
2024-11-19 19:03:51 +03:00
SMKRV 9a7635c2ae Release v1.1.0 2024-11-19 18:55:14 +03:00
SMKRV df3d79c20c feat: add multi-provider support,
config improvements
2024-11-19 18:53:51 +03:00
25 changed files with 3028 additions and 919 deletions
+17
View File
@@ -0,0 +1,17 @@
name: Validate
on:
push:
pull_request:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
jobs:
validate-hacs:
runs-on: "ubuntu-latest"
steps:
- name: HACS validation
uses: "hacs/action@main"
with:
category: "integration"
+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.
+295 -117
View File
@@ -1,73 +1,132 @@
# 🤖 HA Text AI for Home Assistant # 🤖 HA Text AI for Home Assistant
<div align="center"> <div align="center">
![GitHub release](https://img.shields.io/github/release/smkrv/ha-text-ai.svg?style=flat-square) ![GitHub release](https://img.shields.io/github/release/smkrv/ha-text-ai.svg?style=flat-square) ![GitHub last commit](https://img.shields.io/github/last-commit/smkrv/ha-text-ai.svg?style=flat-square) [![License: 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) ![/README.md](https://img.shields.io/badge/language-English-green?style=flat-square) ![/README_RU.md](https://img.shields.io/badge/language-Russian-green?style=flat-square) ![](https://img.shields.io/badge/language-Deutch-green?style=flat-square)
![GitHub 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-Default-orange.svg?style=flat-square)](https://github.com/hacs/integration)
[![Community Forum](https://img.shields.io/badge/Community-Forum-blue.svg?style=flat-square)](https://community.home-assistant.io/t/ha-text-ai-integration)
<img src="https://github.com/smkrv/ha-text-ai/blob/524849f6a945ec62c2cf6a6b7ecd9a28b37bf0fa/misc/icons/logo.jpg" alt="HA Text AI" height="160"/>
### Advanced AI Integration for Home Assistant with LLM multi-provider support
</div> </div>
<p align="center"> <p align="center">
Transform your smart home experience with powerful AI assistance powered by OpenAI's GPT models. Get intelligent responses, automate complex scenarios, and enhance your home automation with natural language processing. Transform your smart home experience with powerful AI assistance powered by multiple AI providers including OpenAI GPT and Anthropic Claude models. Get intelligent responses, automate complex scenarios, and enhance your home automation with advanced natural language processing.
</p> </p>
--- ---
> [!IMPORTANT]
> 🚧 ALPHA VERSION 🚧
> Expect: potential bugs, frequent changes, incomplete features.
> 🤝 Community Driven
>
> <a href="https://community.home-assistant.io/t/ha-text-ai-transforming-home-automation-with-multi-provider-language-models/799741"><img src="https://img.shields.io/badge/Community-blue?style=for-the-badge&logo=homeassistant&logoColor=white&color=03a9f4"/></a>
## 🌟 Features ## 🌟 Features
- 🧠 **Advanced AI Integration**: - 🧠 **Multi-Provider AI Integration**:
- Support for latest GPT models - Support for OpenAI GPT models
- Anthropic Claude integration
- Custom API endpoints
- Flexible model selection
- 💬 **Advanced Language Processing**:
- Context-aware responses - Context-aware responses
- Multi-turn conversations - Multi-turn conversations
- 💬 **Natural Language Control**: - Custom system instructions
- Control devices using everyday language
- Get detailed explanations and recommendations
- Natural conversation flow - Natural conversation flow
- 📝 **Smart Memory Management**:
- 📝 **Enhanced Memory Management**:
- Persistent conversation history - Persistent conversation history
- Context-aware responses - Context-aware responses
- Customizable history limits - Customizable history limits
-**Performance Optimized**: - Model-specific filtering
-**Performance Optimization**:
- Efficient token usage - Efficient token usage
- Rate limit handling - Smart rate limiting
- Response caching - Response caching
- Request interval control
- 🎯 **Advanced Customization**: - 🎯 **Advanced Customization**:
- Adjustable response parameters - Per-request model selection
- Adjustable parameters
- Custom system prompts - Custom system prompts
- Model selection per request - Temperature control
- 🔒 **Enhanced Security**: - 🔒 **Enhanced Security**:
- Secure API key storage - Secure API key storage
- Rate limiting protection - Rate limiting protection
- Error handling - Error handling
- 🎨 **User Experience**: - Usage monitoring
- 🎨 **Improved User Experience**:
- Intuitive configuration UI - Intuitive configuration UI
- Detailed sensor attributes - Detailed sensor attributes
- Rich service interface - Rich service interface
- Model selection UI
- 🔄 **Automation Integration**: - 🔄 **Automation Integration**:
- Event-driven responses - Event-driven responses
- Conditional logic support - Conditional logic support
- Template compatibility - Template compatibility
- Model-specific automation
## 📋 Prerequisites ## 📋 Prerequisites
- Home Assistant 2023.8.0 or newer - Home Assistant 2024.11 or later
- OpenAI API key ([Get one here](https://platform.openai.com/account/api-keys)) - Active API key from:
- OpenAI ([Get key](https://platform.openai.com/account/api-keys))
- 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)
<a href="https://my.home-assistant.io/redirect/hacs_repository/?owner=smkrv&repository=ha-text-ai&category=Integration"><img src="https://my.home-assistant.io/badges/hacs_repository.svg" width="170" height="auto"></a>
1. Open HACS in Home Assistant 1. Open HACS in Home Assistant
2. Click the "+" button 2. Click on "Integrations"
3. Search for "HA Text AI" 3. Click "..." in top right corner
4. Click "Install" 4. Select "Custom repositories"
5. Restart Home Assistant 5. Add repository URL: `https://github.com/smkrv/ha-text-ai`
6. Choose "Integration" as category
7. Click "Download"
8. Restart Home Assistant
### Manual Installation ### Manual Installation
1. Download the latest release 1. Download the latest release
@@ -84,16 +143,61 @@ Transform your smart home experience with powerful AI assistance powered by Open
4. Follow the configuration steps 4. Follow the configuration steps
### Via YAML ### Via YAML
### Platform Configuration (Global Settings)
```yaml ```yaml
ha_text_ai: ha_text_ai:
api_key: !secret openai_api_key api_provider: openai # Required
model: gpt-3.5-turbo api_key: !secret ai_api_key # Required
temperature: 0.7 model: gpt-4o-mini # Strongly recommended
max_tokens: 1000 temperature: 0.7 # Optional
request_interval: 1.0 max_tokens: 1000 # Optional
api_endpoint: https://api.openai.com/v1 # optional request_interval: 1.0 # Optional
api_endpoint: https://api.openai.com/v1 # Required
system_prompt: | # Optional
You are a home automation expert assistant.
Focus on practical and efficient solutions.
``` ```
### Sensor Configuration
```yaml
sensor:
- platform: ha_text_ai
name: "My AI Assistant" # Required, unique identifier
api_provider: openai # Optional (inherits from platform)
model: "gpt-4o-mini" # Optional
temperature: 0.7 # Optional
max_tokens: 1000 # Optional
```
### 📋 Configuration Parameters
#### Platform Configuration
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `api_provider` | String | ✅ | - | AI service provider (openai, anthropic) |
| `api_key` | String | ✅ | - | Authentication key for AI service |
| `model` | String | ⚠️ | Provider default | Strongly recommended: Specific AI model to use. If not specified, the provider's default model will be used |
| `temperature` | Float | ❌ | 0.7 | Response creativity level (0.0-2.0) |
| `max_tokens` | Integer | ❌ | 1000 | Maximum response length |
| `request_interval` | Float | ❌ | 1.0 | Delay between API requests |
| `api_endpoint` | URL | ⚠️ | Provider default | Custom API endpoint |
| `system_prompt` | String | ❌ | - | Default context for AI interactions |
#### Sensor Configuration
| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `platform` | String | ✅ | - | Must be `ha_text_ai` |
| `name` | String | ✅ | - | Unique sensor identifier |
| `api_provider` | String | ❌ | Platform setting | Override global provider |
| `model` | String | ⚠️ | Platform setting | Recommended: Override global model. If not specified, uses platform or provider default |
| `temperature` | Float | ❌ | Platform setting | Override global temperature |
| `max_tokens` | Integer | ❌ | Platform setting | Override global max tokens |
## 🛠️ Available Services ## 🛠️ Available Services
### ask_question ### ask_question
@@ -101,9 +205,11 @@ ha_text_ai:
service: ha_text_ai.ask_question service: ha_text_ai.ask_question
data: data:
question: "What's the optimal temperature for sleeping?" question: "What's the optimal temperature for sleeping?"
model: "gpt-4o" # 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
``` ```
### set_system_prompt ### set_system_prompt
@@ -128,115 +234,171 @@ 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-4o" # optional
``` ```
## 🔧 Advanced Examples ### 🏷️ HA Text AI Sensor Naming Convention
### Smart Energy Management #### Character Restrictions
- Only lowercase letters (a-z)
- Numbers (0-9)
- Underscore (_)
- Maximum length: 50 characters (including `ha_text_ai_` prefix (14 characters)
#### Sensor Name Structure
```yaml
# Always starts with 'sensor.ha_text_ai_'
# You define only the part after the underscore
sensor.ha_text_ai_YOUR_UNIQUE_SUFFIX
# Examples:
sensor.ha_text_ai_gpt # GPT-based sensor
sensor.ha_text_ai_claude # Claude-based sensor
sensor.ha_text_ai_gpt # Custom suffix
```
#### Response Retrieval
```yaml
# Use your specific sensor name
{{ state_attr('sensor.ha_text_ai_gpt', 'response') }}
```
#### Practical Usage
```yaml ```yaml
automation: automation:
alias: "AI Energy Optimization" - alias: "AI Response with Custom Sensor"
trigger: action:
platform: time_pattern - service: ha_text_ai.ask_question
hours: "/2" data:
action: question: "Home automation advice"
- service: ha_text_ai.ask_question - service: notify.mobile
data: data:
question: > message: >
Current power usage: {{ states('sensor.total_power') }}W AI Tip:
Temperature: {{ states('sensor.indoor_temperature') }}°C {{ state_attr('sensor.ha_text_ai_gpt', 'response') }}
Time: {{ now().strftime('%H:%M') }}
Occupancy: {{ states('binary_sensor.occupancy') }}
Analyze current energy usage and suggest optimizations
considering comfort and efficiency.
temperature: 0.3
max_tokens: 200
- service: notify.mobile_app
data:
message: "{{ states.sensor.ha_text_ai.attributes.response }}"
``` ```
### Contextual Lighting Control ### 💡 Naming Rules
- Prefix is always `sensor.ha_text_ai_`
- Add your unique identifier after the underscore
- Use lowercase
- No spaces allowed
- Keep it descriptive but concise
### 🔍 HA Text AI Sensor Attributes
#### Model and Provider Information
```yaml ```yaml
automation: # Name of the AI model currently in use (e.g., latest version of GPT)
alias: "AI Lighting Assistant" {{ state_attr('sensor.ha_text_ai_gpt', 'Model') }} # gpt-4o
trigger:
platform: state
entity_id: binary_sensor.motion
variables:
context: >
Time: {{ now().strftime('%H:%M') }}
Light Level: {{ states('sensor.illuminance') }}
Room: {{ trigger.to_state.attributes.room }}
Activity: {{ states('input_select.current_activity') }}
Weather: {{ states('weather.home') }}
action:
- service: ha_text_ai.ask_question
data:
question: >
Based on this context:
{{ context }}
Suggest optimal lighting settings for current conditions. # Service provider for the AI model (determines API endpoint and authentication)
model: gpt-3.5-turbo {{ state_attr('sensor.ha_text_ai_gpt', 'Api provider') }} # openai
temperature: 0.4
- service: scene.turn_on # Previous or alternative model configuration
data: {{ state_attr('sensor.ha_text_ai_gpt', 'Last model') }} # gpt-4o
entity_id: >
{{ states.sensor.ha_text_ai.attributes.response | regex_findall('scene\.[a-z_]+') | first }}
``` ```
## 📊 Performance Optimization #### System Status
```yaml
# Current operational readiness of the AI service API
{{ state_attr('sensor.ha_text_ai_gpt', 'Api status') }} # ready
### Token Usage # Indicates if a request is currently being processed
- Use focused system prompts {{ state_attr('sensor.ha_text_ai_gpt', 'Is processing') }} # false
- Implement response caching
- Clear history periodically
- Monitor token usage
### Response Time # Shows if the API has hit its request rate limit
- Adjust request_interval {{ state_attr('sensor.ha_text_ai_gpt', 'Is rate limited') }} # false
- Use faster models for simple queries
- Implement timeout handling
- Cache frequent responses
### Memory Management # Status of the specific API endpoint being used
- Set appropriate history limits {{ state_attr('sensor.ha_text_ai_gpt', 'Endpoint status') }} # ready
- Clear unused contexts ```
- Monitor memory usage
- Use efficient data structures
## ❗ Troubleshooting #### Performance Metrics
```yaml
# Total number of successfully completed API requests
{{ state_attr('sensor.ha_text_ai_gpt', 'Successful requests') }} # 0
### API Issues # Number of API requests that encountered errors
- Verify API key validity {{ state_attr('sensor.ha_text_ai_gpt', 'Failed requests') }} # 0
- Check rate limits
- Monitor usage quotas
- Test endpoint accessibility
### Performance Issues # Mean time taken to receive a response from the AI service
- Reduce max_tokens {{ state_attr('sensor.ha_text_ai_gpt', 'Average latency') }} # 0
- Increase request_interval
- Clear conversation history # Maximum time taken for a single request-response cycle
- Check network connectivity {{ state_attr('sensor.ha_text_ai_gpt', 'Max latency') }} # 0
```
#### Conversation and Token Usage
```yaml
# Number of previous interactions stored in conversation context
{{ state_attr('sensor.ha_text_ai_gpt', 'History size') }} # 0
# Total number of tokens used across all interactions
{{ state_attr('sensor.ha_text_ai_gpt', 'Total tokens') }} # 0
# Tokens used in the input prompts
{{ state_attr('sensor.ha_text_ai_gpt', 'Prompt tokens') }} # 0
# Tokens used in the AI's generated responses
{{ state_attr('sensor.ha_text_ai_gpt', 'Completion tokens') }} # 0
```
#### Last Interaction Details
```yaml
# Most recent complete response generated by the AI service
{{ state_attr('sensor.ha_text_ai_gpt', 'Response') }} # Last AI response
# The most recently processed user query or prompt
{{ state_attr('sensor.ha_text_ai_gpt', 'Question') }} # Last asked question
# Precise moment when the last interaction occurred (useful for tracking and logging)
{{ state_attr('sensor.ha_text_ai_gpt', 'Last timestamp') }} # Timestamp
```
#### System Health
```yaml
# Cumulative count of all errors encountered during AI service interactions
{{ state_attr('sensor.ha_text_ai_gpt', 'Total errors') }} # 0
# Indicates if the AI service is currently undergoing scheduled or emergency maintenance
{{ state_attr('sensor.ha_text_ai_gpt', 'Is maintenance') }} # false
# Total continuous operational time of the AI service (in hours or days)
{{ state_attr('sensor.ha_text_ai_gpt', 'Uptime') }} # 547,58
```
### 💡 Pro Tips
- Always check attribute existence
- Use these attributes for monitoring and automation
- Some values might be 0 or empty initially
### Integration Issues
- Verify HA version compatibility
- Check component dependencies
- Review log files
- Update configuration
## 📘 FAQ ## 📘 FAQ
**Q: Which AI providers are supported?**
A: Currently OpenAI (GPT models) and Anthropic (Claude models) are supported, with more providers planned.
**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.
**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.
**Q: How do I switch between different AI providers?**
A: Simply change the model parameter in your configuration or service calls to use the desired provider's model.
**Q: How can I reduce API costs?** **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, API keys are stored securely and data is transmitted via encrypted connections. A: Yes, your data is secure. The system operates entirely on your local machine, keeping your data under your control. API keys are stored securely and all external communications use encrypted connections.
**Q: Can I use custom models?** **Q: How do context messages work?**
A: Yes, configure custom endpoints and models via configuration options. 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.
## 🤝 Contributing ## 🤝 Contributing
@@ -252,11 +414,27 @@ Contributions welcome! Please read our [Contributing Guide](CONTRIBUTING.md).
MIT License - see [LICENSE](LICENSE) for details. MIT License - see [LICENSE](LICENSE) for details.
## 💡 Support the Project
The best support is:
- Sharing feedback
- Contributing ideas
- Recommending to friends
- Reporting issues
- Star the repository
If you want to say thanks financially, you can send a small token of appreciation in USDT:
**USDT Wallet (TRC10/TRC20):**
`TXC9zYHYPfWUGi4Sv4R1ctTBGScXXQk5HZ`
*Open-source is built by community passion!* 🚀
--- ---
<div align="center"> <div align="center">
Made with ❤️ for the Home Assistant Community Made with ❤️ and Claude 3.5 Sonnet for the Home Assistant Community
[Report Bug](https://github.com/smkrv/ha-text-ai/issues) · [Request Feature](https://github.com/smkrv/ha-text-ai/issues) [Report Bug](https://github.com/smkrv/ha-text-ai/issues) · [Request Feature](https://github.com/smkrv/ha-text-ai/issues)
+245 -52
View File
@@ -1,16 +1,24 @@
"""The HA Text AI integration.""" """The HA Text AI integration."""
from __future__ import annotations
import logging import logging
from typing import Any import os
import shutil
from datetime import datetime, timedelta
from typing import Any, Dict
import voluptuous as vol
from async_timeout import timeout
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers import config_validation as cv 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,
@@ -19,88 +27,273 @@ 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,
DEFAULT_CONTEXT_MESSAGES,
API_TIMEOUT,
SERVICE_ASK_QUESTION,
SERVICE_CLEAR_HISTORY,
SERVICE_GET_HISTORY,
SERVICE_SET_SYSTEM_PROMPT,
DEFAULT_MAX_HISTORY,
CONF_MAX_HISTORY_SIZE,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
vol.Required("instance"): cv.string,
vol.Required("question"): cv.string,
vol.Optional("system_prompt"): cv.string,
vol.Optional("model"): cv.string,
vol.Optional("temperature"): cv.positive_float,
vol.Optional("max_tokens"): cv.positive_int,
vol.Optional("context_messages"): cv.positive_int,
})
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
vol.Required("instance"): cv.string,
vol.Required("prompt"): cv.string,
})
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.""" """Set up the HA Text AI component."""
hass.data.setdefault(DOMAIN, {}) 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.png')
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 return True
async def async_check_api(session, endpoint: str, headers: dict, provider: str) -> bool:
"""Check API availability for different providers."""
try:
if provider == API_PROVIDER_ANTHROPIC:
check_url = f"{endpoint}/v1/models"
else: # OpenAI
check_url = f"{endpoint}/models"
async with timeout(API_TIMEOUT):
async with session.get(check_url, headers=headers) as response:
if response.status in [200, 404]:
return True
elif response.status == 401:
raise ConfigEntryNotReady("Invalid API key")
elif response.status == 429:
_LOGGER.warning("Rate limit exceeded during API check")
return False
else:
_LOGGER.error("API check failed with status: %d", response.status)
return False
except Exception as ex:
_LOGGER.error("API check error: %s", str(ex))
return False
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."""
_LOGGER.debug(f"Setting up HA Text AI entry: {entry.data}")
try: try:
if CONF_API_PROVIDER not in entry.data:
_LOGGER.error("API provider not specified")
raise ConfigEntryNotReady("API provider is required")
# Get configuration
session = aiohttp_client.async_get_clientsession(hass) session = aiohttp_client.async_get_clientsession(hass)
api_provider = entry.data.get(CONF_API_PROVIDER)
model = entry.data.get(CONF_MODEL, DEFAULT_MODEL)
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]
instance_name = entry.data.get(CONF_NAME, entry.entry_id)
request_interval = entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)
max_tokens = entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)
temperature = entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)
max_history_size = entry.data.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY)
context_messages = entry.data.get(CONF_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES)
is_anthropic = api_provider == API_PROVIDER_ANTHROPIC
headers = {
"Content-Type": "application/json",
"Accept": "application/json"
}
if is_anthropic:
headers["x-api-key"] = api_key
headers["anthropic-version"] = "2023-06-01"
else:
headers["Authorization"] = f"Bearer {api_key}"
if not await async_check_api(session, endpoint, headers, api_provider):
raise ConfigEntryNotReady("API connection failed")
_LOGGER.debug("Creating API client for %s with endpoint %s", api_provider, endpoint)
api_client = APIClient(
session=session,
endpoint=endpoint,
headers=headers,
api_provider=api_provider,
model=model,
)
coordinator = HATextAICoordinator( coordinator = HATextAICoordinator(
hass, hass=hass,
api_key=entry.data[CONF_API_KEY], client=api_client,
endpoint=entry.data.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT), model=model,
model=entry.data.get(CONF_MODEL, DEFAULT_MODEL), update_interval=request_interval,
temperature=entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE), instance_name=instance_name,
max_tokens=entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS), max_tokens=max_tokens,
request_interval=entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL), temperature=temperature,
session=session, max_history_size=max_history_size,
context_messages=context_messages,
is_anthropic=is_anthropic,
) )
try: _LOGGER.debug(f"Created coordinator for {instance_name}")
await coordinator.async_config_entry_first_refresh()
except Exception as refresh_ex:
_LOGGER.error("Failed to refresh coordinator: %s", str(refresh_ex))
return False
if not coordinator.last_update_success:
_LOGGER.error("Failed to communicate with OpenAI API")
return False
# Store coordinator
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator hass.data[DOMAIN][entry.entry_id] = coordinator
try: _LOGGER.debug(f"Stored coordinator in hass.data[{DOMAIN}][{entry.entry_id}]")
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
except Exception as setup_ex:
_LOGGER.error("Failed to setup platforms: %s", str(setup_ex))
return False
_LOGGER.info( # Set up platforms
"Successfully set up HA Text AI with model: %s", await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.data.get(CONF_MODEL, DEFAULT_MODEL)
) _LOGGER.debug(f"Setup completed for {instance_name}")
return True return True
except Exception as ex: except Exception as err:
_LOGGER.exception("Unexpected error setting up entry: %s", str(ex)) _LOGGER.exception(f"Error setting up HA Text AI: {err}")
return False raise
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:
if entry.entry_id not in hass.data.get(DOMAIN, {}): if entry.entry_id in hass.data[DOMAIN]:
return True coordinator = hass.data[DOMAIN][entry.entry_id]
if hasattr(coordinator.client, 'shutdown'):
await coordinator.client.shutdown()
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
coordinator = hass.data[DOMAIN].pop(entry.entry_id)
await coordinator.async_shutdown() await coordinator.async_shutdown()
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_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Reload config entry."""
try:
await async_unload_entry(hass, entry)
await async_setup_entry(hass, entry)
except Exception as ex:
_LOGGER.exception("Error reloading entry: %s", str(ex))
+200
View File
@@ -0,0 +1,200 @@
"""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."""
_LOGGER.debug(f"API Request: URL={url}, Payload={payload}")
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:
_LOGGER.debug(f"Response status: {response.status}")
if response.status != 200:
error_data = await response.json()
_LOGGER.error(f"API error: {error_data}")
raise HomeAssistantError(f"API error: {error_data}")
return await response.json()
except asyncio.TimeoutError:
_LOGGER.warning(f"Timeout on attempt {attempt + 1}")
if attempt == API_RETRY_COUNT - 1:
raise HomeAssistantError("API request timed out")
await asyncio.sleep(1 * (attempt + 1))
except Exception as e:
_LOGGER.warning(f"API request failed on attempt {attempt + 1}: {str(e)}")
if attempt == API_RETRY_COUNT - 1:
raise
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"
system_prompt = None
filtered_messages = []
for msg in messages:
if msg['role'] == 'system':
if system_prompt is None:
system_prompt = msg['content']
else:
system_prompt += f" {msg['content']}"
else:
filtered_messages.append(msg)
payload = {
"model": model,
"messages": filtered_messages,
"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"],
},
}
async def check_connection(self) -> bool:
"""Check API connection."""
try:
await self._make_request(self.endpoint, {"test": "connection"})
return True
except Exception as e:
_LOGGER.error(f"Connection check failed: {str(e)}")
return False
async def shutdown(self) -> None:
"""Shutdown API client."""
_LOGGER.debug("Shutting down API client")
await self.session.close()
+283 -197
View File
@@ -1,19 +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 ssl
import certifi
import asyncio
from async_timeout import timeout
import aiohttp
from urllib.parse import urlparse
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 openai import AsyncOpenAI from homeassistant.data_entry_flow import FlowResult
from openai import OpenAIError, APIError, APIConnectionError, AuthenticationError, RateLimitError from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import selector
from .const import ( from .const import (
DOMAIN, DOMAIN,
@@ -22,235 +17,326 @@ 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,
MAX_TEMPERATURE,
MIN_MAX_TOKENS,
MAX_MAX_TOKENS,
MIN_REQUEST_INTERVAL,
DEFAULT_NAME_PREFIX,
DEFAULT_MAX_HISTORY,
CONF_MAX_HISTORY_SIZE,
) )
import logging
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Create SSL context at module level
SSL_CONTEXT = ssl.create_default_context(cafile=certifi.where())
STEP_USER_DATA_SCHEMA = vol.Schema({ def normalize_name(name: str) -> str:
vol.Required(CONF_API_KEY): str, """Normalize name to conform to HA naming convention using underscores."""
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): str, normalized = ''.join(c if c.isalnum() or c == '_' else '_' for c in name)
vol.Optional( normalized = '_'.join(filter(None, normalized.split('_')))
CONF_TEMPERATURE, return normalized.lower()
default=DEFAULT_TEMPERATURE
): vol.All(
vol.Coerce(float),
vol.Range(min=0, max=2)
),
vol.Optional(
CONF_MAX_TOKENS,
default=DEFAULT_MAX_TOKENS
): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=4096)
),
vol.Optional(CONF_API_ENDPOINT, default=DEFAULT_API_ENDPOINT): str,
vol.Optional(
CONF_REQUEST_INTERVAL,
default=DEFAULT_REQUEST_INTERVAL
): vol.All(
vol.Coerce(float),
vol.Range(min=0.1)
),
})
async def validate_api_connection(
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."""
for attempt in range(retry_count):
try:
async with timeout(10):
client = AsyncOpenAI(
api_key=api_key,
base_url=endpoint,
)
models = await client.models.list()
model_ids = [model.id for model in models.data]
if model not in model_ids:
_LOGGER.warning(
"Model %s not found in available models: %s",
model,
", ".join(model_ids)
)
return False, "invalid_model", model_ids
return True, "", model_ids
except asyncio.TimeoutError:
_LOGGER.warning(
"Timeout during API validation (attempt %d/%d)",
attempt + 1,
retry_count
)
if attempt == retry_count - 1:
return False, "timeout", []
await asyncio.sleep(retry_delay)
except AuthenticationError as err:
_LOGGER.error("Authentication error: %s", str(err))
return False, "invalid_auth", []
except RateLimitError as err:
_LOGGER.error("Rate limit exceeded: %s", str(err))
return False, "rate_limit", []
except APIConnectionError as err:
_LOGGER.error("API connection error: %s", str(err))
return False, "cannot_connect", []
except APIError as err:
_LOGGER.error("API error: %s", str(err))
return False, "api_error", []
except Exception as err:
_LOGGER.exception("Unexpected error during validation: %s", str(err))
return False, "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 = {}
) -> Dict[str, Any]: 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({
# Validate URL format vol.Required(CONF_API_PROVIDER): selector.SelectSelector(
endpoint = user_input.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT) selector.SelectSelectorConfig(
try: options=API_PROVIDERS,
result = urlparse(endpoint) translation_key="api_provider"
if not all([result.scheme, result.netloc]):
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
)
# Validate input data self._provider = user_input[CONF_API_PROVIDER]
user_input = STEP_USER_DATA_SCHEMA(user_input) 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:
user_input[CONF_API_KEY], """Handle provider configuration step."""
endpoint, self._errors = {}
user_input[CONF_MODEL]
if user_input is None:
default_endpoint = (
DEFAULT_OPENAI_ENDPOINT if self._provider == API_PROVIDER_OPENAI
else DEFAULT_ANTHROPIC_ENDPOINT
)
return self.async_show_form(
step_id="provider",
data_schema=vol.Schema({
vol.Required(CONF_NAME, default="my_assistant"): 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)
),
vol.Optional(
CONF_MAX_HISTORY_SIZE,
default=DEFAULT_MAX_HISTORY
): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=100)
),
})
)
input_copy = user_input.copy()
try:
normalized_name = self._validate_and_normalize_name(input_copy[CONF_NAME])
input_copy[CONF_NAME] = normalized_name
except ValueError as e:
return self.async_show_form(
step_id="provider",
data_schema=vol.Schema({
vol.Required(CONF_NAME, default=input_copy[CONF_NAME]): str,
vol.Required(CONF_API_KEY, default=input_copy[CONF_API_KEY]): str,
vol.Required(CONF_MODEL, default=input_copy[CONF_MODEL]): str,
vol.Required(CONF_API_ENDPOINT, default=input_copy[CONF_API_ENDPOINT]): str,
vol.Optional(CONF_TEMPERATURE, default=input_copy.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.All(
vol.Coerce(float),
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
),
}),
errors={"name": str(e)}
)
try:
if not await self._async_validate_api(input_copy):
return self.async_show_form(
step_id="provider",
data_schema=vol.Schema({
}),
errors=self._errors
) )
except Exception as e:
return self.async_show_form(
step_id="provider",
data_schema=vol.Schema({
}),
errors={"base": str(e)}
)
if is_valid: return await self._create_entry(input_copy)
await self.async_set_unique_id(user_input[CONF_API_KEY])
self._abort_if_unique_id_configured()
return self.async_create_entry( def _validate_and_normalize_name(self, name: str) -> str:
title="HA text AI", """
data=user_input Validate and normalize name with detailed error handling.
)
errors["base"] = error_code Raises:
if error_code == "invalid_model": ValueError: If name is invalid
_LOGGER.warning(
"Selected model %s not found in available models: %s",
user_input[CONF_MODEL],
", ".join(available_models)
)
except vol.Invalid as err: Returns:
_LOGGER.error("Validation error: %s", str(err)) Normalized name
errors["base"] = "invalid_input" """
if not name:
raise ValueError("empty")
return self.async_show_form( name = name.strip()
step_id="user", normalized = ''.join(
data_schema=STEP_USER_DATA_SCHEMA, c if c.isalnum() or c in ' _' else '_' # Only allow underscores
errors=errors, for c in name
description_placeholders={ )
"default_model": DEFAULT_MODEL,
"default_endpoint": DEFAULT_API_ENDPOINT, normalized = normalized.replace(' ', '_').lower()
for entry in self._async_current_entries():
if entry.data.get(CONF_NAME, "") == normalized:
raise ValueError("name_exists")
normalized = normalized[:50]
if not normalized:
raise ValueError("empty")
return normalized
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 with comprehensive data preservation."""
instance_name = user_input[CONF_NAME]
normalized_name = normalize_name(instance_name)
unique_id = f"{DOMAIN}_{normalized_name}_{self._provider}".lower()
entry_data = {
CONF_API_PROVIDER: self._provider,
CONF_NAME: instance_name,
"normalized_name": normalized_name,
CONF_API_KEY: user_input.get(CONF_API_KEY),
CONF_API_ENDPOINT: user_input.get(CONF_API_ENDPOINT),
"unique_id": unique_id,
CONF_MODEL: user_input.get(CONF_MODEL, DEFAULT_MODEL),
CONF_TEMPERATURE: user_input.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
CONF_MAX_TOKENS: user_input.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
CONF_REQUEST_INTERVAL: user_input.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL),
CONF_CONTEXT_MESSAGES: user_input.get(CONF_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES),
CONF_MAX_HISTORY_SIZE: user_input.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY),
}
for key, value in user_input.items():
if key not in entry_data:
entry_data[key] = value
_LOGGER.debug(f"Creating config entry with data: {entry_data}")
return self.async_create_entry(
title=instance_name,
data=entry_data
) )
@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
) -> Dict[str, Any]:
"""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
),
description={"suggested_value": DEFAULT_TEMPERATURE},
): vol.All(
vol.Coerce(float),
vol.Range(min=0, max=2)
),
vol.Optional(
CONF_MAX_TOKENS,
default=self.config_entry.options.get(
CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS
),
description={"suggested_value": DEFAULT_MAX_TOKENS},
): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=4096)
),
vol.Optional(
CONF_REQUEST_INTERVAL,
default=self.config_entry.options.get(
CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL
),
description={"suggested_value": DEFAULT_REQUEST_INTERVAL},
): vol.All(
vol.Coerce(float),
vol.Range(min=0.1)
),
})
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)
),
vol.Optional(
CONF_MAX_HISTORY_SIZE,
default=current_data.get(
CONF_MAX_HISTORY_SIZE,
DEFAULT_MAX_HISTORY
)
): vol.All(
vol.Coerce(int),
vol.Range(min=1, max=100)
),
})
) )
+135 -74
View File
@@ -1,10 +1,26 @@
"""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: list[str] = ["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"
@@ -12,16 +28,21 @@ 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"
CONF_MAX_HISTORY_SIZE: Final = "max_history_size" # Correct constant name
CONF_IS_ANTHROPIC: Final = "is_anthropic"
CONF_CONTEXT_MESSAGES: Final = "context_messages"
# Default values # Default values
DEFAULT_MODEL: Final = "gpt-3.5-turbo" DEFAULT_MODEL: Final = "gpt-4o-mini"
DEFAULT_TEMPERATURE: Final = 0.7 DEFAULT_TEMPERATURE: Final = 0.1
DEFAULT_MAX_TOKENS: Final = 1000 DEFAULT_MAX_TOKENS: Final = 1000
DEFAULT_API_ENDPOINT: Final = "https://api.openai.com/v1"
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_NAME_PREFIX = "ha_text_ai"
DEFAULT_CONTEXT_MESSAGES: Final = 5
# Parameter constraints # Parameter constraints
MIN_TEMPERATURE: Final = 0.0 MIN_TEMPERATURE: Final = 0.0
@@ -29,8 +50,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
# API constants
API_TIMEOUT: Final = 30
API_RETRY_COUNT: Final = 3
# Service names # Service names
SERVICE_ASK_QUESTION: Final = "ask_question" SERVICE_ASK_QUESTION: Final = "ask_question"
@@ -38,26 +62,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_ENDPOINT_STATUS: Final = "endpoint_status"
ATTR_PERFORMANCE_METRICS: Final = "performance_metrics"
ATTR_HISTORY_SIZE: Final = "history_size"
ATTR_UPTIME: Final = "uptime"
ATTR_API_PROVIDER: Final = "api_provider"
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"
@@ -68,74 +114,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"
# 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"
# 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"
# 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"
# Logging STATE_RATE_LIMITED: Final = "rate_limited"
LOGGER_NAME: Final = "custom_components.ha_text_ai" STATE_DISCONNECTED: Final = "disconnected"
LOG_LEVEL_DEFAULT: Final = "INFO"
# Queue constants
QUEUE_TIMEOUT: Final = 5
QUEUE_MAX_SIZE: Final = 100
# API constants
API_TIMEOUT: Final = 30
API_RETRY_COUNT: Final = 3
API_BACKOFF_FACTOR: Final = 1.5
# 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( # Correct usage
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)
+518 -157
View File
@@ -1,194 +1,555 @@
"""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 import traceback
from typing import Any, Dict, Optional from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional
from openai import AsyncOpenAI, APIError, AuthenticationError, RateLimitError
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
import async_timeout from homeassistant.util import dt as dt_util
from homeassistant.exceptions import HomeAssistantError
from homeassistant.const import CONF_NAME
from .config_flow import normalize_name
from .const import DOMAIN from .const import (
DOMAIN,
STATE_READY,
STATE_PROCESSING,
STATE_ERROR,
STATE_RATE_LIMITED,
STATE_MAINTENANCE,
DEFAULT_MAX_TOKENS,
DEFAULT_TEMPERATURE,
DEFAULT_MAX_HISTORY,
DEFAULT_CONTEXT_MESSAGES,
DEFAULT_NAME_PREFIX,
CONF_MAX_HISTORY_SIZE,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
class HATextAICoordinator(DataUpdateCoordinator): class HATextAICoordinator(DataUpdateCoordinator):
"""Class to manage fetching data from the API.""" """The HA Text AI coordinator."""
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,
) -> None: ) -> None:
"""Initialize.""" """Initialize coordinator."""
self.instance_name = instance_name
self.normalized_name = None
# Use the normalize_name function from config_flow to ensure consistency
from .config_flow import normalize_name
self.normalized_name = normalize_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,
"normalized_name": self.normalized_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)
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._is_processing = False
self.max_tokens = int(max_tokens) self._is_rate_limited = False
self._question_queue = asyncio.Queue() self._is_maintenance = False
self._responses: Dict[str, Any] = {} self.endpoint_status = "ready"
self.system_prompt: Optional[str] = None self.last_response = self._initial_state["last_response"].copy()
self._is_ready = False self._start_time = dt_util.utcnow()
self._error_count = 0
self._MAX_ERRORS = 3
self.client = AsyncOpenAI( _LOGGER.info(
api_key=self.api_key, f"Initialized HA Text AI coordinator with instance: {instance_name}"
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 OpenAI API.""" """Update data via library."""
if self._question_queue.empty():
return self._responses
try: try:
async with async_timeout.timeout(30): current_state = self._get_current_state()
question = await self._question_queue.get() _LOGGER.debug(
try: f"Updating data for {self.instance_name}, current state: {current_state}"
response_content = await self._make_api_call(question)
self._responses[question] = {
"question": question,
"response": response_content,
"error": None,
"timestamp": self.hass.loop.time()
}
self._error_count = 0
self._is_ready = True
_LOGGER.debug("Response received for question: %s", question)
except Exception as err:
self._handle_api_error(question, err)
finally:
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
def _handle_api_error(self, question: str, error: Exception) -> None:
"""Handle API errors."""
self._error_count += 1
error_msg = str(error)
if isinstance(error, AuthenticationError):
error_msg = "Authentication failed - invalid API key"
self._is_ready = False
elif isinstance(error, RateLimitError):
error_msg = "Rate limit exceeded"
elif isinstance(error, APIError):
error_msg = f"API error: {error}"
self._responses[question] = {
"question": question,
"response": None,
"error": error_msg,
"timestamp": self.hass.loop.time()
}
_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
) )
async def _handle_timeout_error(self) -> None: data = {
"""Handle timeout errors.""" "state": current_state,
self._error_count += 1 "metrics": self._performance_metrics,
if not self._question_queue.empty(): "last_response": self.last_response,
try: "is_processing": self._is_processing,
# Clear the queue if we have timeout issues "is_rate_limited": self._is_rate_limited,
while not self._question_queue.empty(): "is_maintenance": self._is_maintenance,
self._question_queue.get_nowait() "endpoint_status": self.endpoint_status,
self._question_queue.task_done() "uptime": (dt_util.utcnow() - self._start_time).total_seconds(),
except Exception as err: "system_prompt": self._system_prompt,
_LOGGER.error("Error clearing question queue: %s", err) "history_size": len(self._conversation_history),
"conversation_history": self._conversation_history,
"normalized_name": self.normalized_name,
}
async def _make_api_call(self, question: str) -> str: # Validate data
"""Make API call to OpenAI.""" if not isinstance(data, dict):
try: raise ValueError("Invalid data format")
messages = []
if self.system_prompt:
messages.append({"role": "system", "content": self.system_prompt})
messages.append({"role": "user", "content": question})
completion = await self.client.chat.completions.create( _LOGGER.debug(f"Updated data for {self.instance_name}: {data}")
model=self.model, return data
messages=messages,
temperature=self.temperature,
max_tokens=self.max_tokens,
)
return completion.choices[0].message.content
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}")
return self._initial_state
async def async_update_ha_state(self) -> None:
"""Update Home Assistant state."""
try:
_LOGGER.debug(
f"Requesting state update for {self.instance_name} (normalized: {self.normalized_name})"
)
await self.async_request_refresh()
# Force update of all entities
entity_id_base = f"sensor.ha_text_ai_{self.normalized_name.lower()}"
for entity_id in self.hass.states.async_entity_ids():
if entity_id.startswith(entity_id_base):
self.hass.states.async_set(entity_id, self._get_current_state())
except Exception as err:
_LOGGER.error(f"Error updating HA state for {self.instance_name}: {err}")
def _get_current_state(self) -> str:
"""Get current state based on internal flags."""
if self._is_processing:
return STATE_PROCESSING
elif self._is_rate_limited:
return STATE_RATE_LIMITED
elif self._is_maintenance:
return STATE_MAINTENANCE
elif self.last_response.get("error"):
return STATE_ERROR
return STATE_READY
def _calculate_context_tokens(self, messages: List[Dict[str, str]], model: str = None) -> int:
"""
Estimate tokens for conversation context.
Args:
messages: List of message dictionaries
model: Optional model name for provider-specific estimation
Returns:
Estimated number of tokens
"""
try:
# Anthropic specific token counting
if self.is_anthropic and hasattr(self.client, 'count_tokens'):
return sum(self.client.count_tokens(msg['content']) for msg in messages)
def estimate_tokens(text: str) -> int:
"""
Flexible token estimation algorithm.
Heuristics:
- Count words
- Estimate special characters
- Fallback to character-based estimation
"""
# Word-based estimation
words = len(text.split())
# Special character handling
special_chars = sum(1 for char in text if not char.isalnum())
# Character-based fallback
char_tokens = len(text) // 4
# Combine estimations with bias towards words
total_tokens = (words * 1.5) + (special_chars * 0.5) + char_tokens
return max(int(total_tokens), words)
# Calculate total tokens across all messages
total_tokens = sum(estimate_tokens(msg['content']) for msg in messages)
# Logging for debugging
_LOGGER.debug(
f"Token Estimation: "
f"Messages: {len(messages)}, "
f"Estimated Tokens: {total_tokens}"
)
return total_tokens
except Exception as e:
# Safe fallback with detailed logging
_LOGGER.warning(
f"Token estimation failed. "
f"Error: {e}. "
f"Using conservative estimation."
)
# Conservative token estimation
return len(messages) * 100
async def async_ask_question(
self,
question: str,
model: Optional[str] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
system_prompt: Optional[str] = None,
context_messages: Optional[int] = None,
) -> dict:
"""
Process a question with optional parameters.
This method is a direct wrapper around async_process_question,
allowing flexible AI interaction with optional model, temperature,
and context customization.
Args:
question: The input question or prompt
model: Optional AI model to use
temperature: Optional response creativity level
max_tokens: Optional maximum response length
system_prompt: Optional system-level instruction
context_messages: Optional number of context messages to include
Returns:
Full response dictionary from the AI
"""
return await self.async_process_question(
question, model, temperature, max_tokens, system_prompt, context_messages
)
async def async_process_question(
self,
question: str,
model: Optional[str] = None,
temperature: Optional[float] = None,
max_tokens: Optional[int] = None,
system_prompt: Optional[str] = None,
context_messages: Optional[int] = None,
) -> dict:
"""
Enhanced question processing with intelligent token management.
"""
try:
self._is_processing = True
await self.async_update_ha_state()
temp_context_messages = context_messages or self.context_messages
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 timing
start_time = dt_util.utcnow()
# Prepare messages with system prompt
messages = []
if temp_system_prompt:
messages.append({"role": "system", "content": temp_system_prompt})
# Context history management
context_history = self._conversation_history[-temp_context_messages:]
# Comprehensive token calculation
context_tokens = self._calculate_context_tokens(
[{"content": entry["question"]} for entry in context_history] +
[{"content": entry["response"]} for entry in context_history] +
[{"content": question}],
temp_model
)
# Dynamic token allocation
available_tokens = max(0, temp_max_tokens - context_tokens)
# Context trimming if over token limit
if context_tokens > temp_max_tokens:
_LOGGER.warning(
f"Token limit exceeded. "
f"Context: {context_tokens}, "
f"Max: {temp_max_tokens}"
)
# Intelligent context reduction
while context_tokens > temp_max_tokens // 2 and context_history:
context_history.pop(0)
context_tokens = self._calculate_context_tokens(
[{"content": entry["question"]} for entry in context_history] +
[{"content": entry["response"]} for entry in context_history] +
[{"content": question}],
temp_model
)
# Rebuild messages with trimmed context
for entry in context_history:
messages.append({"role": "user", "content": entry["question"]})
messages.append({"role": "assistant", "content": entry["response"]})
messages.append({"role": "user", "content": question})
# Detailed token logging
_LOGGER.debug(
f"Token Analysis: "
f"Context Tokens: {context_tokens}, "
f"Max Tokens: {temp_max_tokens}, "
f"Available Tokens: {available_tokens}"
)
# Prepare API call with dynamic token management
kwargs = {
"model": temp_model,
"temperature": temp_temperature,
"max_tokens": min(temp_max_tokens, available_tokens),
"messages": messages,
}
# Process message
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 raise
async def async_ask_question(self, question: str) -> None: async def _process_anthropic_message(self, question: str, **kwargs) -> dict:
"""Add question to queue.""" """Process message using Anthropic API."""
if not self._is_ready and self._error_count >= self._MAX_ERRORS: try:
_LOGGER.warning("Coordinator is not ready due to previous errors") _LOGGER.debug(f"Anthropic API call: model={kwargs['model']}, max_tokens={kwargs['max_tokens']}")
return response = await self.client.messages.create(
model=kwargs["model"],
max_tokens=kwargs["max_tokens"],
messages=kwargs["messages"],
temperature=kwargs["temperature"],
)
_LOGGER.debug(f"Anthropic response: tokens={response.usage}")
return {
"content": response.content[0].text,
"tokens": {
"prompt": response.usage.input_tokens,
"completion": response.usage.output_tokens,
"total": response.usage.input_tokens + response.usage.output_tokens,
},
}
except Exception as e:
_LOGGER.error(f"Anthropic API error: {str(e)}")
raise
await self._question_queue.put(question) async def _process_openai_message(self, question: str, **kwargs) -> dict:
await self.async_refresh() """Process message using OpenAI API."""
try:
response = await self.client.create(
model=kwargs["model"],
messages=kwargs["messages"],
temperature=kwargs["temperature"],
max_tokens=kwargs["max_tokens"],
)
return {
"content": response["choices"][0]["message"]["content"],
"tokens": {
"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
def _update_metrics(self, latency: float, response: dict) -> None:
"""Update performance metrics."""
metrics = self._performance_metrics
tokens = response.get("tokens", {})
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
metrics["average_latency"] = (
(metrics["average_latency"] * (metrics["successful_requests"] - 1) + latency)
/ metrics["successful_requests"]
)
metrics["max_latency"] = max(metrics["max_latency"], latency)
metrics["min_latency"] = min(metrics["min_latency"], latency)
def _update_history(self, question: str, response: dict) -> None:
"""Update conversation history."""
self._conversation_history.append(
{
"timestamp": dt_util.utcnow().isoformat(),
"question": question,
"response": response["content"],
}
)
while len(self._conversation_history) > self.max_history_size:
self._conversation_history.pop(0)
def _handle_error(self, error: Exception) -> None:
"""
Enhanced error handling with comprehensive diagnostics.
Captures detailed error information, tracks error metrics,
and provides context for troubleshooting AI processing issues.
"""
self._performance_metrics["total_errors"] += 1
self._performance_metrics["failed_requests"] += 1
error_details = {
"timestamp": dt_util.utcnow().isoformat(),
"model": self.model,
"instance": self.instance_name,
"error_message": str(error),
"error_type": type(error).__name__,
"traceback": traceback.format_exc() if _LOGGER.isEnabledFor(logging.DEBUG) else None,
}
# Specific error type handling
error_mapping = {
HomeAssistantError: {"is_ha_error": True},
ConnectionError: {
"is_connection_error": True,
"is_rate_limited": True
},
TimeoutError: {"is_timeout": True},
PermissionError: {"is_permission_denied": True},
ValueError: {"is_validation_error": True}
}
for error_type, error_flags in error_mapping.items():
if isinstance(error, error_type):
error_details.update(error_flags)
break
# Update system state based on error type
if error_details.get("is_rate_limited"):
self._is_rate_limited = True
_LOGGER.warning(f"Rate limit detected for {self.instance_name}")
if error_details.get("is_connection_error"):
self.endpoint_status = "unavailable"
self.last_response = error_details
_LOGGER.error(f"AI Processing Error: {error_details}")
# Optional: Add more sophisticated error tracking or notification logic
if _LOGGER.isEnabledFor(logging.DEBUG):
_LOGGER.debug(f"Full Error Traceback: {error_details['traceback']}")
async def async_clear_history(self) -> None:
"""Clear conversation history."""
self._conversation_history = []
await self.async_update_ha_state()
async def async_get_history(self) -> List[Dict[str, str]]:
"""Get conversation history."""
return self._conversation_history
async def async_set_system_prompt(self, prompt: str) -> None:
"""Set system prompt."""
self._system_prompt = prompt
await self.async_update_ha_state()
async def async_shutdown(self) -> None: async def async_shutdown(self) -> None:
"""Shutdown the coordinator.""" """Shutdown coordinator."""
try: _LOGGER.debug(f"Shutting down coordinator for {self.instance_name}")
# Clear the queue self.hass.data[DOMAIN].pop(self.instance_name, None)
while not self._question_queue.empty():
self._question_queue.get_nowait()
self._question_queue.task_done()
await self.client.close()
self._is_ready = False
except Exception as err:
_LOGGER.error("Error during shutdown: %s", err)
@property
def is_ready(self) -> bool:
"""Return if coordinator is ready."""
return self._is_ready
@property
def error_count(self) -> int:
"""Return current error count."""
return self._error_count
def reset_error_count(self) -> None:
"""Reset error counter."""
self._error_count = 0
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

+17 -3
View File
@@ -1,14 +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/wiki", "documentation": "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",
"requirements": ["openai>=1.0.0"], "loggers": ["custom_components.ha_text_ai"],
"mqtt": [],
"quality_scale": "silver",
"requirements": [
"openai>=1.12.0",
"anthropic>=0.8.0",
"aiohttp>=3.8.0",
"async-timeout>=4.0.0",
"certifi>=2024.2.2"
],
"single_config_entry": false,
"ssdp": [], "ssdp": [],
"version": "1.0.10", "usb": [],
"version": "2.0.2-alpha",
"zeroconf": [] "zeroconf": []
} }
+266 -115
View File
@@ -1,64 +1,101 @@
"""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,
ENTITY_ICON, ATTR_IS_RATE_LIMITED,
ENTITY_ICON_ERROR, ATTR_IS_MAINTENANCE,
ENTITY_ICON_PROCESSING, ATTR_API_VERSION,
ATTR_ENDPOINT_STATUS,
ATTR_PERFORMANCE_METRICS,
ATTR_HISTORY_SIZE,
ATTR_UPTIME,
ATTR_API_PROVIDER,
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_RATE_LIMITED,
STATE_DISCONNECTED,
ENTITY_ICON,
ENTITY_ICON_ERROR,
ENTITY_ICON_PROCESSING,
DEFAULT_NAME_PREFIX,
CONF_MAX_HISTORY_SIZE,
) )
from .coordinator import HATextAICoordinator from .coordinator import HATextAICoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
async def async_setup_entry( async def async_setup_entry(
hass: HomeAssistant, hass: HomeAssistant,
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] _LOGGER.debug(f"Starting sensor setup for entry: {entry.entry_id}")
async_add_entities([HATextAISensor(coordinator, entry)], True)
try:
coordinator = hass.data[DOMAIN][entry.entry_id]
_LOGGER.debug(f"Found coordinator for entry {entry.entry_id}")
instance_name = coordinator.instance_name
_LOGGER.debug(f"Setting up sensor with instance: {instance_name}")
sensor = HATextAISensor(coordinator, entry)
_LOGGER.debug(f"Created sensor instance: {sensor.entity_id}")
async_add_entities([sensor], True)
_LOGGER.debug(f"Added sensor entity: {sensor.entity_id}")
except Exception as err:
_LOGGER.exception(f"Error setting up sensor: {err}")
raise
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,
@@ -66,116 +103,230 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
config_entry: ConfigEntry, config_entry: ConfigEntry,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
_LOGGER.debug(f"Initializing sensor with config entry: {config_entry.data}")
super().__init__(coordinator) super().__init__(coordinator)
self._config_entry = config_entry self._config_entry = config_entry
self._instance_name = coordinator.instance_name
self._normalized_name = coordinator.normalized_name
_LOGGER.debug(f"Instance name: {self._instance_name}")
_LOGGER.debug(f"Normalized name: {self._normalized_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_{self._normalized_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 _LOGGER.debug(f"Created sensor with entity_id: {self.entity_id}")
_LOGGER.debug(f"Sensor name: {self._attr_name}")
_LOGGER.debug(f"Unique ID: {self._attr_unique_id}")
self.entity_description = SensorEntityDescription(
key=f"ha_text_ai_{self._normalized_name.lower()}",
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._is_processing = False
self._last_response = {}
self._metrics = {}
@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_time: manufacturer="Community",
return None model=f"{model} ({api_provider} provider)",
sw_version="1.0.0",
)
try: _LOGGER.debug(
if isinstance(self.coordinator.last_update_success_time, datetime): f"Initialized sensor: {self.entity_id} for instance: {self._instance_name}"
return dt_util.as_local(self.coordinator.last_update_success_time) )
return self.coordinator.last_update_success_time
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,
}
if not self.coordinator.data:
return attributes
try:
history = list(self.coordinator.data.items())
if history:
last_question, last_data = history[-1]
# Handle different response formats
if isinstance(last_data, dict):
last_response = last_data.get("response", "")
last_updated = last_data.get("timestamp", self.coordinator.last_update_success_time)
response_time = last_data.get("response_time")
else:
last_response = str(last_data)
last_updated = self.coordinator.last_update_success_time
response_time = None
# Convert timestamp to local time if needed
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),
})
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,
"normalized_name": self._normalized_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
self._state = STATE_READY if not self.coordinator.last_update_success or not data:
self._current_state = STATE_DISCONNECTED
_LOGGER.warning(f"No data available for {self.entity_id}")
self.async_write_ha_state()
return
self._is_processing = data.get("is_processing", False)
# 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
else: else:
self._state = STATE_DISCONNECTED 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()
+97 -82
View File
@@ -3,59 +3,55 @@ 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.
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.
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: slider
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 and token limits.
Note: More capable models may have longer response times and higher API costs.
required: false required: false
example: "gpt-3.5-turbo"
default: "gpt-3.5-turbo"
selector: selector:
select: text: {}
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"
mode: dropdown
temperature: temperature:
name: Temperature name: Temperature
description: >- description: Controls response creativity (0.0-2.0)
Controls response creativity (0-2):
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:
@@ -64,17 +60,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. Higher values allow longer responses but use more API tokens.
Recommended ranges:
- Short responses (256-512): Quick answers, status updates
- Medium responses (512-1024): Detailed explanations, instructions
- Long responses (1024-4096): Complex analysis, multiple examples
Note: Actual response length may be shorter based on content.
required: false required: false
default: 1000 default: 1000
selector: selector:
@@ -87,23 +76,33 @@ ask_question:
clear_history: clear_history:
name: Clear History name: Clear History
description: >- description: >-
Delete all stored questions and responses from the conversation history. 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. fields:
System prompt settings will be preserved. instance:
fields: {} name: Instance
description: Name of the HA Text AI instance to clear history for
required: true
selector:
entity:
integration: ha_text_ai
domain: sensor
get_history: get_history:
name: Get History name: Get History
description: >- description: Retrieve conversation history with optional filtering and sorting
Retrieve recent conversation history, including questions, responses, and timestamps.
Results are ordered from newest to oldest and include metadata like model used and response times.
fields: fields:
instance:
name: Instance
description: Name of the HA Text AI instance to get history from
required: true
selector:
entity:
integration: ha_text_ai
domain: sensor
limit: limit:
name: Limit name: Limit
description: >- description: Number of conversations to return (1-100)
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 required: false
default: 10 default: 10
selector: selector:
@@ -114,41 +113,57 @@ get_history:
mode: box mode: box
filter_model: filter_model:
name: Filter by Model name: Filter Model
description: >- description: Filter conversations by specific AI model
Only return conversations using a specific AI model.
Leave empty to show all models.
required: false required: false
selector:
text:
multiline: false
start_date:
name: Start Date
description: Filter conversations starting from this date/time
required: false
selector:
text:
multiline: false
include_metadata:
name: Include Metadata
description: Include additional information like tokens used, response time, etc.
required: false
default: false
selector:
boolean:
sort_order:
name: Sort Order
description: Sort order for results (newest or oldest first)
required: false
default: newest
selector: selector:
select: select:
options: options:
- label: "All Models" - newest
value: "" - oldest
- label: "GPT-3.5 Turbo"
value: "gpt-3.5-turbo"
- label: "GPT-4"
value: "gpt-4"
mode: dropdown
set_system_prompt: set_system_prompt:
fields: name: Set System Prompt
description: Set default system behavior instructions for all future conversations
fields:
instance:
name: Instance
description: Name of the HA Text AI instance to set system prompt for
required: true
selector:
entity:
integration: ha_text_ai
domain: sensor
prompt: prompt:
name: System Prompt name: System Prompt
description: >- description: Instructions that define how the AI should behave and respond
Instructions that define how the AI should behave and respond.
Be specific about the desired expertise, tone, and format of responses.
Maximum length: 1000 characters.
required: true 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: selector:
text: text:
multiline: true multiline: true
type: text
@@ -0,0 +1,266 @@
{
"config": {
"step": {
"provider": {
"title": "KI-Anbieter auswählen",
"description": "Wählen Sie den KI-Dienstanbieter für diese Instanz aus.",
"data": {
"api_provider": "API-Anbieter",
"context_messages": "Anzahl der zu behaltenden Kontextnachrichten (1-20)"
}
},
"user": {
"title": "HA Text AI-Instanz konfigurieren",
"description": "Richten Sie eine neue KI-Assistenteninstanz mit Ihrem ausgewählten Anbieter ein.",
"data": {
"name": "Instanzname (z. B. 'GPT-Assistent', 'Claude-Helfer')",
"api_key": "API-Schlüssel zur Authentifizierung",
"model": "Zu verwendendes KI-Modell",
"temperature": "Antwortkreativität (0-2, niedriger = fokussierter)",
"max_tokens": "Maximale Antwortlänge (1-4096 Token)",
"api_endpoint": "Benutzerdefinierte API-Endpunkt-URL (optional)",
"api_provider": "API-Anbieter",
"request_interval": "Mindestzeit zwischen Anfragen (0,1-60 Sekunden)",
"context_messages": "Anzahl der zu behaltenden Kontextnachrichten (1-20)",
"max_history_size": "Maximale Größe des Konversationsverlaufs (1-100)"
}
}
},
"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 - überprüfen Sie Ihre Anmeldedaten",
"cannot_connect": "Verbindung zum API-Dienst fehlgeschlagen",
"invalid_model": "Das ausgewählte Modell ist nicht verfügbar",
"rate_limit": "Ratenlimit überschritten",
"context_length": "Kontextlänge überschritten",
"rate_limit_exceeded": "API-Ratenlimit überschritten",
"maintenance": "Dienst befindet sich in der Wartung",
"invalid_response": "Ungültige API-Antwort empfangen",
"api_error": "Fehler im API-Dienst aufgetreten",
"timeout": "Anfrage ist abgelaufen",
"invalid_instance": "Ungültige Instanz angegeben",
"unknown": "Es ist ein unerwarteter Fehler aufgetreten",
"empty": "Name darf nicht leer sein",
"invalid_characters": "Name darf nur Buchstaben, Zahlen, Leerzeichen, Unterstriche und Bindestriche enthalten",
"name_too_long": "Name darf maximal 50 Zeichen lang sein"
}
},
"options": {
"step": {
"init": {
"title": "Instanzeinstellungen aktualisieren",
"description": "Ändern Sie die Einstellungen für diese KI-Assistenteninstanz.",
"data": {
"model": "KI-Modell",
"temperature": "Antwortkreativität (0-2)",
"max_tokens": "Maximale Antwortlänge (1-4096)",
"request_interval": "Mindestzeitraum zwischen Anfragen (0,1-60 Sekunden)",
"context_messages": "Anzahl der vorherigen Nachrichten, die im Kontext enthalten sein sollen (1-20)",
"max_history_size": "Maximale Größe des Konversationsverlaufs (1-100)"
}
}
}
},
"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 im Konversationsverlauf 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 Aufforderung an den KI-Assistenten"
},
"context_messages": {
"name": "Kontextnachrichten",
"description": "Anzahl der vorherigen Nachrichten, die im Kontext enthalten sein sollen (1-20)"
},
"system_prompt": {
"name": "Systemprompt",
"description": "Optionaler Systemprompt, um den Kontext für diese spezielle Frage festzulegen"
},
"model": {
"name": "Modell",
"description": "Wählen Sie das zu verwendende KI-Modell aus (optional, überschreibt die Standardeinstellung)"
},
"temperature": {
"name": "Temperatur",
"description": "Steuert die Antwortkreativitä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 Konversationsverlauf löschen",
"fields": {
"instance": {
"name": "Instanz",
"description": "Name der HA Text AI-Instanz, deren Verlauf gelöscht werden soll"
}
}
},
"get_history": {
"name": "Verlauf abrufen",
"description": "Konversationsverlauf mit optionalem Filtern und Sortieren abrufen",
"fields": {
"instance": {
"name": "Instanz",
"description": "Name der HA Text AI-Instanz, von der der Verlauf abgerufen werden soll"
},
"limit": {
"name": "Limit",
"description": "Anzahl der zurückzugebenden Konversationen (1-100)"
},
"filter_model": {
"name": "Modell filtern",
"description": "Konversationen nach einem bestimmten KI-Modell filtern"
},
"start_date": {
"name": "Startdatum",
"description": "Konversationen ab diesem Datum/Uhrzeit filtern"
},
"include_metadata": {
"name": "Metadaten einschließen",
"description": "Zusätzliche Informationen wie verwendete Token, Antwortzeit usw. einschließen"
},
"sort_order": {
"name": "Sortierreihenfolge",
"description": "Sortierreihenfolge für die Ergebnisse (neueste oder älteste zuerst)"
}
}
},
"set_system_prompt": {
"name": "Systemprompt festlegen",
"description": "Legen Sie Standardanweisungen für das Systemverhalten für alle zukünftigen Konversationen fest",
"fields": {
"instance": {
"name": "Instanz",
"description": "Name der HA Text AI-Instanz, für die der Systemprompt festgelegt werden soll"
},
"prompt": {
"name": "Systemprompt",
"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": "Ratenlimit",
"maintenance": "Wartung",
"initializing": "Initialisierung",
"retrying": "Wiederholen",
"queued": "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": "Systemprompt"
},
"response_time": {
"name": "Letzte Antwortzeit"
},
"total_responses": {
"name": "Gesamtzahl der Antworten"
},
"error_count": {
"name": "Fehleranzahl"
},
"last_error": {
"name": "Letzter Fehler"
},
"api_status": {
"name": "API-Status"
},
"tokens_used": {
"name": "Verwendete Token insgesamt"
},
"average_response_time": {
"name": "Durchschnittliche Antwortzeit"
},
"last_request_time": {
"name": "Zeitpunkt der letzten Anfrage"
},
"is_processing": {
"name": "Verarbeitungsstatus"
},
"is_rate_limited": {
"name": "Ratenlimit-Status"
},
"is_maintenance": {
"name": "Wartungsstatus"
},
"api_version": {
"name": "API-Version"
},
"endpoint_status": {
"name": "Endpunkt-Status"
},
"performance_metrics": {
"name": "Leistungsmetriken"
},
"history_size": {
"name": "Verlaufsgröße"
},
"uptime": {
"name": "Betriebszeit"
},
"total_tokens": {
"name": "Gesamtzahl der Token"
},
"prompt_tokens": {
"name": "Prompt-Token"
},
"completion_tokens": {
"name": "Completion-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"
}
}
}
}
}
}
+261 -49
View File
@@ -1,54 +1,266 @@
{ {
"config": { "config": {
"step": { "step": {
"user": { "provider": {
"title": "Set up HA Text AI", "title": "Select AI Provider",
"description": "Configure your OpenAI integration", "description": "Choose which AI service provider to use for this instance.",
"data": { "data": {
"api_key": "Your OpenAI API key", "api_provider": "API Provider",
"model": "AI model to use for responses", "context_messages": "Number of context messages to retain (1-20)"
"temperature": "Temperature for response generation (0-2)",
"max_tokens": "Maximum tokens in response (1-4096)",
"api_endpoint": "API endpoint URL",
"request_interval": "Minimum time between API requests (seconds)"
}
}
} }
},
"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)",
"api_endpoint": "Custom API endpoint URL (optional)",
"api_provider": "API Provider",
"request_interval": "Minimum time between requests (0.1-60 seconds)",
"context_messages": "Number of context messages to retain (1-20)",
"max_history_size": "Maximum conversation history size (1-100)"
}
}
}, },
"options": { "error": {
"step": { "name_exists": "An instance with this name already exists",
"init": { "invalid_name": "Invalid instance name",
"title": "HA Text AI Options", "invalid_auth": "Authentication failed - check your API key",
"data": { "invalid_api_key": "Invalid API key - please verify your credentials",
"temperature": "Response temperature (0-2)", "cannot_connect": "Failed to connect to API service",
"max_tokens": "Maximum response length", "invalid_model": "Selected model is not available",
"request_interval": "Time between requests" "rate_limit": "Rate limit exceeded",
} "context_length": "Context length exceeded",
} "rate_limit_exceeded": "API rate limit exceeded",
} "maintenance": "Service is under maintenance",
}, "invalid_response": "Invalid API response received",
"services": { "api_error": "API service error occurred",
"ask_question": { "timeout": "Request timed out",
"name": "Ask Question", "invalid_instance": "Invalid instance specified",
"description": "Send a question to the AI model", "unknown": "Unexpected error occurred",
"fields": { "empty": "Name cannot be empty",
"question": { "invalid_characters": "Name can only contain letters, numbers, spaces, underscores and hyphens",
"name": "Question", "name_too_long": "Name must be 50 characters or less"
"description": "Your question for the AI"
}
}
},
"clear_history": {
"name": "Clear History",
"description": "Clear conversation history"
},
"get_history": {
"name": "Get History",
"description": "Retrieve conversation history"
},
"set_system_prompt": {
"name": "Set System Prompt",
"description": "Set system behavior instructions"
}
} }
},
"options": {
"step": {
"init": {
"title": "Update Instance Settings",
"description": "Modify settings for this AI assistant instance.",
"data": {
"model": "AI model",
"temperature": "Response creativity (0-2)",
"max_tokens": "Maximum response length (1-4096)",
"request_interval": "Minimum request interval (0.1-60 seconds)",
"context_messages": "Number of previous messages to include in context (1-20)",
"max_history_size": "Maximum conversation history size (1-100)"
}
}
}
},
"services": {
"ask_question": {
"name": "Ask Question (HA Text AI)",
"description": "Send a question to the AI model and receive a detailed response. The response will be stored in the conversation history and can be retrieved later.",
"fields": {
"instance": {
"name": "Instance",
"description": "Name of the HA Text AI instance to use"
},
"question": {
"name": "Question",
"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": {
"name": "System Prompt",
"description": "Optional system prompt to set context for this specific question"
},
"model": {
"name": "Model",
"description": "Select AI model to use (optional, overrides default setting)"
},
"temperature": {
"name": "Temperature",
"description": "Controls response creativity (0.0-2.0)"
},
"max_tokens": {
"name": "Max Tokens",
"description": "Maximum length of the response (1-4096 tokens)"
}
}
},
"clear_history": {
"name": "Clear History",
"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": {
"name": "Get History",
"description": "Retrieve conversation history with optional filtering and sorting",
"fields": {
"instance": {
"name": "Instance",
"description": "Name of the HA Text AI instance to get history from"
},
"limit": {
"name": "Limit",
"description": "Number of conversations to return (1-100)"
},
"filter_model": {
"name": "Filter Model",
"description": "Filter conversations by specific AI model"
},
"start_date": {
"name": "Start Date",
"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": {
"name": "Set System Prompt",
"description": "Set default system behavior instructions for all future conversations",
"fields": {
"instance": {
"name": "Instance",
"description": "Name of the HA Text AI instance to set system prompt for"
},
"prompt": {
"name": "System Prompt",
"description": "Instructions that define how the AI should behave and respond"
}
}
}
},
"entity": {
"sensor": {
"ha_text_ai": {
"name": "{name}",
"state": {
"ready": "Ready",
"processing": "Processing",
"error": "Error",
"disconnected": "Disconnected",
"rate_limited": "Rate Limited",
"maintenance": "Maintenance",
"initializing": "Initializing",
"retrying": "Retrying",
"queued": "Queued"
},
"state_attributes": {
"question": {
"name": "Last Question"
},
"response": {
"name": "Last Response"
},
"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"
}
}
}
}
}
} }
+261 -49
View File
@@ -1,54 +1,266 @@
{ {
"config": { "config": {
"step": { "step": {
"user": { "provider": {
"title": "Настройка HA Text AI", "title": "Выбор поставщика ИИ",
"description": "Настройка интеграции с OpenAI", "description": "Выберите поставщика услуг ИИ для этой инстанции.",
"data": { "data": {
"api_key": "Ваш ключ API OpenAI", "api_provider": "Поставщик API",
"model": "Модель ИИ для генерации ответов", "context_messages": "Количество контекстных сообщений для сохранения (1-20)"
"temperature": "Температура генерации ответов (0-2)",
"max_tokens": "Максимальное количество токенов в ответе (1-4096)",
"api_endpoint": "URL конечной точки API",
"request_interval": "Минимальное время между запросами к API (секунды)"
}
}
} }
},
"user": {
"title": "Настройка инстанции HA Text AI",
"description": "Настройте новую инстанцию помощника ИИ с выбранным вами поставщиком.",
"data": {
"name": "Имя инстанции (например, 'GPT Assistant', 'Claude Helper')",
"api_key": "API-ключ для аутентификации",
"model": "Используемая модель ИИ",
"temperature": "Креативность ответа (0-2, чем ниже, тем больше фокусировки)",
"max_tokens": "Максимальная длина ответа (1-4096 токенов)",
"api_endpoint": "Пользовательский URL-адрес конечной точки API (необязательно)",
"api_provider": "Поставщик API",
"request_interval": "Минимальное время между запросами (0,1-60 секунд)",
"context_messages": "Количество контекстных сообщений для сохранения (1-20)",
"max_history_size": "Максимальный размер истории разговора (1-100)"
}
}
}, },
"options": { "error": {
"step": { "name_exists": "Инстанция с таким именем уже существует",
"init": { "invalid_name": "Некорректное имя инстанции",
"title": "Настройки HA Text AI", "invalid_auth": "Аутентификация не удалась - проверьте ваш API-ключ",
"data": { "invalid_api_key": "Неверный API-ключ - проверьте ваши учетные данные",
"temperature": "Температура ответов (0-2)", "cannot_connect": "Не удалось подключиться к службе API",
"max_tokens": "Максимальная длина ответа", "invalid_model": "Выбранная модель недоступна",
"request_interval": "Время между запросами" "rate_limit": "Превышен лимит запросов",
} "context_length": "Превышена длина контекста",
} "rate_limit_exceeded": "Превышен лимит запросов API",
} "maintenance": "Сервис находится на техническом обслуживании",
}, "invalid_response": "Получен неверный ответ API",
"services": { "api_error": "Произошла ошибка службы API",
"ask_question": { "timeout": "Запрос превысил время ожидания",
"name": "Задать вопрос", "invalid_instance": "Указана неверная инстанция",
"description": "Отправить вопрос модели ИИ", "unknown": "Произошла непредвиденная ошибка",
"fields": { "empty": "Имя не может быть пустым",
"question": { "invalid_characters": "Имя может содержать только буквы, цифры, пробелы, подчеркивания и дефисы",
"name": "Вопрос", "name_too_long": "Имя должно быть не более 50 символов"
"description": "Ваш вопрос для ИИ"
}
}
},
"clear_history": {
"name": "Очистить историю",
"description": "Очистить историю разговора"
},
"get_history": {
"name": "Получить историю",
"description": "Получить историю разговора"
},
"set_system_prompt": {
"name": "Установить системный промпт",
"description": "Установить инструкции поведения системы"
}
} }
},
"options": {
"step": {
"init": {
"title": "Обновление настроек инстанции",
"description": "Измените настройки для этой инстанции помощника ИИ.",
"data": {
"model": "Модель ИИ",
"temperature": "Креативность ответа (0-2)",
"max_tokens": "Максимальная длина ответа (1-4096)",
"request_interval": "Минимальный интервал между запросами (0,1-60 секунд)",
"context_messages": "Количество предыдущих сообщений для включения в контекст (1-20)",
"max_history_size": "Максимальный размер истории разговора (1-100)"
}
}
}
},
"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 -5
View File
@@ -1,9 +1,5 @@
{ {
"name": "HA text AI", "name": "HA text AI",
"render_readme": true, "render_readme": true,
"domains": ["sensor"], "homeassistant": "2024.11.0"
"homeassistant": "2024.11.0",
"icon": "mdi:brain",
"version": "1.0.10",
"documentation": "https://github.com/smkrv/ha-text-ai"
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 618 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 923 KiB

-3
View File
@@ -1,3 +0,0 @@
pytest
pytest-asyncio
homeassistant
+20 -12
View File
@@ -2,16 +2,24 @@
ha-text-ai/ ha-text-ai/
├── custom_components/ ├── custom_components/
── ha_text_ai/ ── ha_text_ai/
├── __init__.py ├── __init__.py
├── config_flow.py ├── config_flow.py
├── coordinator.py ├── coordinator.py
├── manifest.json ├── manifest.json
├── sensor.py ├── sensor.py
├── services.yaml ├── services.yaml
── const.py ── const.py
│ └── api_client.py
└── strings/
├── en.json ├── translations/
── ru.json │ ├── en.json
│ ├── de.json
│ └── ru.json
└── icons/
├── icon.png
├── icon@2x.png
├── logo.png
└── logo@2x.png
``` ```