Compare commits

...
284 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
SMKRV d6144be7ed v.1.0.10 2024-11-19 17:49:48 +03:00
SMKRV 9afbb904b3 __init__.py:
added: from .coordinator import HATextAICoordinator
2024-11-19 17:48:00 +03:00
SMKRV 93558b2444 Version update 2024-11-19 17:34:27 +03:00
SMKRV 9f93f1ee18 Main changes:
Removed the validate_endpoint function
Optimized the validate_api_connection function
Simplified API connection verification
Preserved all error handling and retry logic
Improved exception handling
The integration should now correctly verify the
OpenAI API connection without false endpoint_not_available errors.
2024-11-19 17:33:27 +03:00
SMKRV 30a9b53ba1 Markdown changes 2024-11-19 17:18:23 +03:00
SMKRV 5175970d55 structure.md added 2024-11-19 17:16:30 +03:00
SMKRV f6bfbd4a07 Release v1.0.8 2024-11-19 17:02:38 +03:00
SMKRV 4ddb0dc977 Translation fixes 2024-11-19 16:59:42 +03:00
SMKRV 24dc4ac4d4 Hotfix 2024-11-19 16:53:27 +03:00
SMKRV 976f4f16a3 translation fixes 2024-11-19 16:40:56 +03:00
SMKRV 2bdef2b494 Main changes:
Created global SSL_CONTEXT at module level
Removed blocking create_default_context calls from async functions
Optimized aiohttp.ClientSession handling:
Using single connector with SSL context
Session is created once for all requests in validate_api_connection
Improved resource management:
Automatic session closure using context managers
More efficient connection handling
These changes should eliminate the blocking call warning and improve
overall code performance.
2024-11-19 16:33:30 +03:00
SMKRV 42324a793b bufix 2024-11-19 16:25:44 +03:00
SMKRV 30aa894634 Release v1.0.6 2024-11-19 15:10:06 +03:00
SMKRV 398b2550a9 Release v1.0.5 2024-11-19 14:46:12 +03:00
SMKRV f1deaa2014 Release v1.0.4 2024-11-19 14:35:45 +03:00
SMKRV f4b0ce902e Release v1.0.3 2024-11-19 14:06:32 +03:00
SMKRV d899144149 Release v1.0.3 2024-11-19 14:00:33 +03:00
SMKRV 5d49b4a40b Release v1.0.2 2024-11-19 13:16:14 +03:00
SMKRV 1a84727cf1 Release v1.0.2 2024-11-19 13:14:00 +03:00
SMKRV d3c7e25202 Release v1.0.2 2024-11-19 13:12:41 +03:00
26 changed files with 3128 additions and 541 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.
+358 -82
View File
@@ -2,164 +2,440 @@
<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)
<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 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**: Leverage OpenAI's powerful models (GPT-3.5, GPT-4) for smart home interactions - 🧠 **Multi-Provider AI Integration**:
- 💬 **Natural Language Control**: Control your home and get information using everyday language - Support for OpenAI GPT models
- 📝 **Conversation Memory**: Maintain context with conversation history tracking - Anthropic Claude integration
-**Real-time Responses**: Get quick, contextual responses to your queries - Custom API endpoints
- 🎯 **Customizable Behavior**: Fine-tune AI responses with adjustable parameters - Flexible model selection
- 🔒 **Secure Integration**: Your API key and data are handled securely
- 🎨 **Flexible Configuration**: Easy setup with multiple configuration options - 💬 **Advanced Language Processing**:
- 🔄 **Automation Ready**: Integrate AI responses into your automations - Context-aware responses
- Multi-turn conversations
- Custom system instructions
- Natural conversation flow
- 📝 **Enhanced Memory Management**:
- Persistent conversation history
- Context-aware responses
- Customizable history limits
- Model-specific filtering
-**Performance Optimization**:
- Efficient token usage
- Smart rate limiting
- Response caching
- Request interval control
- 🎯 **Advanced Customization**:
- Per-request model selection
- Adjustable parameters
- Custom system prompts
- Temperature control
- 🔒 **Enhanced Security**:
- Secure API key storage
- Rate limiting protection
- Error handling
- Usage monitoring
- 🎨 **Improved User Experience**:
- Intuitive configuration UI
- Detailed sensor attributes
- Rich service interface
- Model selection UI
- 🔄 **Automation Integration**:
- Event-driven responses
- Conditional logic support
- Template compatibility
- Model-specific automation
## 📋 Prerequisites ## 📋 Prerequisites
- Home Assistant installation (Core, OS, Container, or Supervised) - 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
## ⚡ Quick Start ### 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
### 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
2. Click on "Integrations"
3. Click "..." in top right corner
4. Select "Custom repositories"
5. Add repository URL: `https://github.com/smkrv/ha-text-ai`
6. Choose "Integration" as category
7. Click "Download"
8. Restart Home Assistant
### Manual Installation ### Manual Installation
1. Download the repository 1. Download the latest release
2. Copy `custom_components/ha_text_ai` to your `custom_components` directory 2. Extract and copy `custom_components/ha_text_ai` to your `custom_components` directory
3. Restart Home Assistant 3. Restart Home Assistant
4. Add configuration to `configuration.yaml`: 4. Add configuration via UI or YAML
## ⚙️ Configuration
### Via UI (Recommended)
1. Go to Settings → Devices & Services
2. Click "Add Integration"
3. Search for "HA Text AI"
4. Follow the configuration steps
### Via YAML
### Platform Configuration (Global Settings)
```yaml ```yaml
ha_text_ai: ha_text_ai:
api_key: !secret openai_api_key api_provider: openai # Required
api_key: !secret ai_api_key # Required
model: gpt-4o-mini # Strongly recommended
temperature: 0.7 # Optional
max_tokens: 1000 # 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.
``` ```
## ⚙️ Configuration Options ### Sensor Configuration
| Option | Type | Default | Description | ```yaml
|--------|------|---------|-------------| sensor:
| `api_key` | string | Required | Your OpenAI API key | - platform: ha_text_ai
| `model` | string | `gpt-3.5-turbo` | AI model to use | name: "My AI Assistant" # Required, unique identifier
| `temperature` | float | `0.7` | Response creativity (0-2) | api_provider: openai # Optional (inherits from platform)
| `max_tokens` | integer | `1000` | Maximum response length | model: "gpt-4o-mini" # Optional
| `request_interval` | float | `1.0` | Minimum seconds between requests | temperature: 0.7 # Optional
| `api_endpoint` | string | OpenAI default | Custom API endpoint URL | 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
Ask the AI assistant a question:
```yaml ```yaml
service: ha_text_ai.ask_question service: ha_text_ai.ask_question
data: data:
question: "What's the optimal temperature for sleeping?" question: "What's the optimal temperature for sleeping?"
model: "gpt-4" # 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
Configure AI behavior:
```yaml ```yaml
service: ha_text_ai.set_system_prompt service: ha_text_ai.set_system_prompt
data: data:
prompt: "You are a home automation expert focused on energy efficiency" prompt: |
You are a home automation expert focused on:
1. Energy efficiency
2. Comfort optimization
3. Security considerations
Provide practical, actionable advice.
``` ```
### clear_history ### clear_history
Reset conversation history:
```yaml ```yaml
service: ha_text_ai.clear_history service: ha_text_ai.clear_history
``` ```
### get_history ### get_history
Retrieve conversation history:
```yaml ```yaml
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
``` ```
## 🔧 Practical Examples ### 🏷️ HA Text AI Sensor Naming Convention
### Smart Temperature 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:
trigger: - alias: "AI Response with Custom Sensor"
platform: time_pattern action:
hours: "/1" - service: ha_text_ai.ask_question
action: data:
service: ha_text_ai.ask_question question: "Home automation advice"
data: - service: notify.mobile
question: > data:
Current temperature is {{ states('sensor.living_room_temperature') }}°C. message: >
Should I adjust the thermostat for optimal comfort and energy savings? AI Tip:
{{ state_attr('sensor.ha_text_ai_gpt', 'response') }}
``` ```
### Smart Lighting Assistant ### 💡 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)
trigger: {{ state_attr('sensor.ha_text_ai_gpt', 'Model') }} # gpt-4o
platform: state
entity_id: binary_sensor.living_room_motion # Service provider for the AI model (determines API endpoint and authentication)
to: 'on' {{ state_attr('sensor.ha_text_ai_gpt', 'Api provider') }} # openai
condition:
condition: template # Previous or alternative model configuration
value_template: "{{ states('sensor.illuminance') | float < 10 }}" {{ state_attr('sensor.ha_text_ai_gpt', 'Last model') }} # gpt-4o
action:
service: ha_text_ai.ask_question
data:
question: >
Motion detected in living room with low light levels.
What's the best lighting scene to set based on the time of day?
``` ```
## ❗ Common Issues #### System Status
```yaml
# Current operational readiness of the AI service API
{{ state_attr('sensor.ha_text_ai_gpt', 'Api status') }} # ready
### API Rate Limits # Indicates if a request is currently being processed
- Increase `request_interval` if hitting rate limits {{ state_attr('sensor.ha_text_ai_gpt', 'Is processing') }} # false
- Consider upgrading your OpenAI plan
- Use caching for frequent queries
### High Token Usage # Shows if the API has hit its request rate limit
- Reduce `max_tokens` parameter {{ state_attr('sensor.ha_text_ai_gpt', 'Is rate limited') }} # false
- Clear conversation history regularly
- Use focused system prompts
### Connection Issues # Status of the specific API endpoint being used
- Check internet connectivity {{ state_attr('sensor.ha_text_ai_gpt', 'Endpoint status') }} # ready
- Verify API key validity ```
- Ensure endpoint accessibility
#### Performance Metrics
```yaml
# Total number of successfully completed API requests
{{ state_attr('sensor.ha_text_ai_gpt', 'Successful requests') }} # 0
# Number of API requests that encountered errors
{{ state_attr('sensor.ha_text_ai_gpt', 'Failed requests') }} # 0
# Mean time taken to receive a response from the AI service
{{ state_attr('sensor.ha_text_ai_gpt', 'Average latency') }} # 0
# Maximum time taken for a single request-response cycle
{{ 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
## 📘 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?**
A: Use GPT-3.5-Turbo for most queries, implement caching, and optimize token usage.
**Q: Is my data secure?**
A: Yes, your data is secure. The system operates entirely on your local machine, keeping your data under your control. API keys are stored securely and all external communications use encrypted connections.
**Q: How do context messages work?**
A: Context messages allow the AI to remember and reference previous conversation history. By default, 5 previous messages are included, but you can customize this from 1 to 20 messages to control the conversation depth and token usage.
## 🤝 Contributing ## 🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request. Contributions welcome! Please read our [Contributing Guide](CONTRIBUTING.md).
1. Fork the repository 1. Fork the repository
2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 2. Create feature branch (`git checkout -b feature/Enhancement`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 3. Commit changes (`git commit -m 'Add Enhancement'`)
4. Push to the branch (`git push origin feature/AmazingFeature`) 4. Push branch (`git push origin feature/Enhancement`)
5. Open a Pull Request 5. Open Pull Request
## 📝 License ## 📝 License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file 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)
</div> </div>
+231 -137
View File
@@ -1,205 +1,299 @@
"""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 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, ServiceCall from homeassistant.core import HomeAssistant, ServiceCall
import homeassistant.helpers.config_validation as cv from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
from homeassistant.exceptions import HomeAssistantError, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import aiohttp_client
from .coordinator import HATextAICoordinator
from .api_client import APIClient
from .const import ( from .const import (
DOMAIN, DOMAIN,
PLATFORMS, PLATFORMS,
SERVICE_ASK_QUESTION,
SERVICE_CLEAR_HISTORY,
SERVICE_GET_HISTORY,
SERVICE_SET_SYSTEM_PROMPT,
CONF_MODEL, CONF_MODEL,
CONF_TEMPERATURE, CONF_TEMPERATURE,
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_TEMPERATURE,
DEFAULT_MAX_TOKENS,
DEFAULT_OPENAI_ENDPOINT,
DEFAULT_ANTHROPIC_ENDPOINT,
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,
) )
from .coordinator import HATextAICoordinator
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
CONFIG_SCHEMA = vol.Schema( SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
{ vol.Required("instance"): cv.string,
DOMAIN: vol.Schema( vol.Required("question"): cv.string,
{ vol.Optional("system_prompt"): cv.string,
vol.Required(CONF_API_KEY): cv.string, vol.Optional("model"): cv.string,
vol.Optional(CONF_MODEL, default="gpt-3.5-turbo"): cv.string, vol.Optional("temperature"): cv.positive_float,
vol.Optional(CONF_TEMPERATURE, default=0.7): vol.Coerce(float), vol.Optional("max_tokens"): cv.positive_int,
vol.Optional(CONF_MAX_TOKENS, default=1000): vol.Coerce(int), vol.Optional("context_messages"): cv.positive_int,
vol.Optional(CONF_REQUEST_INTERVAL, default=1.0): vol.Coerce(float), })
vol.Optional(CONF_API_ENDPOINT): cv.string,
}
)
},
extra=vol.ALLOW_EXTRA,
)
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
"""Set up the HA text AI component.""" 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."""
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: async def async_ask_question(call: ServiceCall) -> None:
"""Handle the ask_question service call.""" """Handle ask_question service."""
if not hass.data[DOMAIN]:
raise HomeAssistantError("No AI Text integration configured")
coordinator = next(iter(hass.data[DOMAIN].values()))
question = call.data["question"]
original_params = {
"model": coordinator.model,
"temperature": coordinator.temperature,
"max_tokens": coordinator.max_tokens
}
try: try:
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
if "model" in call.data: await coordinator.async_ask_question(
coordinator.model = call.data["model"] question=call.data["question"],
if "temperature" in call.data: model=call.data.get("model"),
coordinator.temperature = call.data["temperature"] temperature=call.data.get("temperature"),
if "max_tokens" in call.data: max_tokens=call.data.get("max_tokens"),
coordinator.max_tokens = call.data["max_tokens"] system_prompt=call.data.get("system_prompt"),
context_messages=call.data.get("context_messages"),
await coordinator.async_ask_question(question) )
except Exception as ex: except Exception as err:
_LOGGER.error("Error asking question: %s", str(ex)) _LOGGER.error("Error asking question: %s", str(err))
raise HomeAssistantError(f"Failed to ask question: {str(ex)}") from ex raise HomeAssistantError(f"Failed to process question: {str(err)}")
finally:
coordinator.model = original_params["model"]
coordinator.temperature = original_params["temperature"]
coordinator.max_tokens = original_params["max_tokens"]
async def async_clear_history(call: ServiceCall) -> None: async def async_clear_history(call: ServiceCall) -> None:
"""Handle the clear_history service call.""" """Handle clear_history service."""
if not hass.data[DOMAIN]: try:
raise HomeAssistantError("No AI Text integration configured") 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)}")
coordinator = next(iter(hass.data[DOMAIN].values())) async def async_get_history(call: ServiceCall) -> list:
coordinator._responses.clear() """Handle get_history service."""
await coordinator.async_refresh() try:
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
async def async_get_history(call: ServiceCall) -> dict[str, list]: return await coordinator.async_get_history(
"""Handle the get_history service call.""" limit=call.data.get("limit"),
if not hass.data[DOMAIN]: filter_model=call.data.get("filter_model")
raise HomeAssistantError("No AI Text integration configured") )
except Exception as err:
coordinator = next(iter(hass.data[DOMAIN].values())) _LOGGER.error("Error getting history: %s", str(err))
if not coordinator._responses: raise HomeAssistantError(f"Failed to get history: {str(err)}")
return {"history": []}
limit = call.data.get("limit", 10)
history = list(coordinator._responses.items())
limited_history = history[-limit:] if len(history) > limit else history
return {
"history": [
{"question": q, "response": r} for q, r in limited_history
]
}
async def async_set_system_prompt(call: ServiceCall) -> None: async def async_set_system_prompt(call: ServiceCall) -> None:
"""Handle the set_system_prompt service call.""" """Handle set_system_prompt service."""
if not hass.data[DOMAIN]: try:
raise HomeAssistantError("No AI Text integration configured") coordinator = get_coordinator_by_instance(hass, call.data["instance"])
await coordinator.async_set_system_prompt(call.data["prompt"])
coordinator = next(iter(hass.data[DOMAIN].values())) except Exception as err:
coordinator.system_prompt = call.data["prompt"] _LOGGER.error("Error setting system prompt: %s", str(err))
raise HomeAssistantError(f"Failed to set system prompt: {str(err)}")
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN,
SERVICE_ASK_QUESTION, SERVICE_ASK_QUESTION,
async_ask_question, async_ask_question,
schema=vol.Schema({ schema=SERVICE_SCHEMA_ASK_QUESTION
vol.Required("question"): cv.string,
vol.Optional("model"): cv.string,
vol.Optional("temperature"): vol.All(
vol.Coerce(float), vol.Range(min=0, max=2)
),
vol.Optional("max_tokens"): vol.All(
vol.Coerce(int), vol.Range(min=1, max=4096)
),
})
) )
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN,
SERVICE_CLEAR_HISTORY, SERVICE_CLEAR_HISTORY,
async_clear_history, async_clear_history,
schema=vol.Schema({}) schema=vol.Schema({vol.Required("instance"): cv.string})
) )
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN,
SERVICE_GET_HISTORY, SERVICE_GET_HISTORY,
async_get_history, async_get_history,
schema=vol.Schema({ schema=SERVICE_SCHEMA_GET_HISTORY
vol.Optional("limit", default=10): vol.All(
vol.Coerce(int), vol.Range(min=1)
),
})
) )
hass.services.async_register( hass.services.async_register(
DOMAIN, DOMAIN,
SERVICE_SET_SYSTEM_PROMPT, SERVICE_SET_SYSTEM_PROMPT,
async_set_system_prompt, async_set_system_prompt,
schema=vol.Schema({ schema=SERVICE_SCHEMA_SET_SYSTEM_PROMPT
vol.Required("prompt"): cv.string,
})
) )
return True return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_check_api(session, endpoint: str, headers: dict, provider: str) -> bool:
"""Set up HA text AI from a config entry.""" """Check API availability for different providers."""
try: try:
coordinator = HATextAICoordinator( if provider == API_PROVIDER_ANTHROPIC:
hass, check_url = f"{endpoint}/v1/models"
api_key=entry.data[CONF_API_KEY], else: # OpenAI
endpoint=entry.data.get(CONF_API_ENDPOINT), check_url = f"{endpoint}/models"
model=entry.data.get(CONF_MODEL),
temperature=entry.data.get(CONF_TEMPERATURE), async with timeout(API_TIMEOUT):
max_tokens=entry.data.get(CONF_MAX_TOKENS), async with session.get(check_url, headers=headers) as response:
request_interval=entry.data.get(CONF_REQUEST_INTERVAL), 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:
"""Set up HA Text AI from a config entry."""
_LOGGER.debug(f"Setting up HA Text AI entry: {entry.data}")
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)
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,
) )
await coordinator.async_config_entry_first_refresh() coordinator = HATextAICoordinator(
hass=hass,
client=api_client,
model=model,
update_interval=request_interval,
instance_name=instance_name,
max_tokens=max_tokens,
temperature=temperature,
max_history_size=max_history_size,
context_messages=context_messages,
is_anthropic=is_anthropic,
)
_LOGGER.debug(f"Created coordinator for {instance_name}")
# Store coordinator
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator hass.data[DOMAIN][entry.entry_id] = coordinator
return await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) _LOGGER.debug(f"Stored coordinator in hass.data[{DOMAIN}][{entry.entry_id}]")
except Exception as ex:
raise ConfigEntryNotReady(f"Failed to setup entry: {str(ex)}") from ex # Set up platforms
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
_LOGGER.debug(f"Setup completed for {instance_name}")
return True
except Exception as err:
_LOGGER.exception(f"Error setting up HA Text AI: {err}")
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."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) try:
if unload_ok: if entry.entry_id in hass.data[DOMAIN]:
hass.data[DOMAIN].pop(entry.entry_id) coordinator = hass.data[DOMAIN][entry.entry_id]
if hasattr(coordinator.client, 'shutdown'):
await coordinator.client.shutdown()
if not hass.data[DOMAIN]: await coordinator.async_shutdown()
services = [ hass.data[DOMAIN].pop(entry.entry_id)
SERVICE_ASK_QUESTION,
SERVICE_CLEAR_HISTORY,
SERVICE_GET_HISTORY,
SERVICE_SET_SYSTEM_PROMPT
]
for service in services:
if service in hass.services.async_services().get(DOMAIN, {}):
hass.services.async_remove(DOMAIN, service)
return unload_ok return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
except Exception as ex:
_LOGGER.exception("Error unloading entry: %s", str(ex))
return False
+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()
+288 -84
View File
@@ -1,12 +1,14 @@
"""Config flow for HA text AI integration.""" """Config flow for HA text AI integration."""
import logging
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
import voluptuous as vol
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
import openai from homeassistant.data_entry_flow import FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers import selector
from .const import ( from .const import (
DOMAIN, DOMAIN,
@@ -15,124 +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,
) )
STEP_USER_DATA_SCHEMA = vol.Schema({ _LOGGER = logging.getLogger(__name__)
vol.Required(CONF_API_KEY): str,
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): str,
vol.Optional( def normalize_name(name: str) -> str:
CONF_TEMPERATURE, """Normalize name to conform to HA naming convention using underscores."""
default=DEFAULT_TEMPERATURE normalized = ''.join(c if c.isalnum() or c == '_' else '_' for c in name)
): vol.All(vol.Coerce(float), vol.Range(min=0, max=2)), normalized = '_'.join(filter(None, normalized.split('_')))
vol.Optional( return normalized.lower()
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)),
})
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(
step_id="user",
data_schema=vol.Schema({
vol.Required(CONF_API_PROVIDER): selector.SelectSelector(
selector.SelectSelectorConfig(
options=API_PROVIDERS,
translation_key="api_provider"
)
),
})
)
if user_input is not None: self._provider = user_input[CONF_API_PROVIDER]
try: return await self.async_step_provider()
client = openai.OpenAI( async def async_step_provider(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
api_key=user_input[CONF_API_KEY], """Handle provider configuration step."""
base_url=user_input.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT) self._errors = {}
)
await self.hass.async_add_executor_job( if user_input is None:
client.models.list 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)}
)
return await self._create_entry(input_copy)
await self.async_set_unique_id(user_input[CONF_API_KEY]) def _validate_and_normalize_name(self, name: str) -> str:
self._abort_if_unique_id_configured() """
Validate and normalize name with detailed error handling.
return self.async_create_entry( Raises:
title="HA text AI", ValueError: If name is invalid
data=user_input
)
except openai.AuthenticationError: Returns:
errors["base"] = "invalid_auth" Normalized name
except openai.APIError: """
errors["base"] = "cannot_connect" if not name:
except Exception: # pylint: disable=broad-except raise ValueError("empty")
errors["base"] = "unknown"
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
)
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="Temperature for response generation (0-2)",
): 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="Maximum tokens in response (1-4096)",
): 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="Minimum time between API requests (seconds)",
): 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)
),
})
) )
+162 -21
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 # 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,13 +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_MAX_HISTORY: 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
@@ -26,6 +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
MAX_REQUEST_INTERVAL: Final = 60.0
# 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"
@@ -33,29 +62,141 @@ 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_TEMPERATURE: Final = "temperature"
ATTR_MAX_TOKENS: Final = "max_tokens"
ATTR_SYSTEM_PROMPT: Final = "system_prompt"
ATTR_API_STATUS: Final = "api_status"
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_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"
ERROR_CANNOT_CONNECT: Final = "cannot_connect" ERROR_CANNOT_CONNECT: Final = "cannot_connect"
ERROR_UNKNOWN: Final = "unknown_error" ERROR_UNKNOWN: Final = "unknown_error"
ERROR_INVALID_MODEL: Final = "invalid_model"
# Configuration descriptions ERROR_RATE_LIMIT: Final = "rate_limit_exceeded"
CONF_MODEL_DESCRIPTION: Final = "AI model to use for responses" ERROR_CONTEXT_LENGTH: Final = "context_length_exceeded"
CONF_TEMPERATURE_DESCRIPTION: Final = "Temperature for response generation (0-2)" ERROR_API_ERROR: Final = "api_error"
CONF_MAX_TOKENS_DESCRIPTION: Final = "Maximum tokens in response (1-4096)" ERROR_TIMEOUT: Final = "timeout_error"
CONF_API_ENDPOINT_DESCRIPTION: Final = "API endpoint URL" ERROR_INVALID_INSTANCE: Final = "invalid_instance"
CONF_REQUEST_INTERVAL_DESCRIPTION: Final = "Minimum time between API requests (seconds)" ERROR_NAME_EXISTS: Final = "name_exists"
# 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_PROCESSING: Final = "mdi:robot-excited"
# State attributes
STATE_READY: Final = "ready"
STATE_PROCESSING: Final = "processing"
STATE_ERROR: Final = "error"
STATE_INITIALIZING: Final = "initializing"
STATE_MAINTENANCE: Final = "maintenance"
STATE_RATE_LIMITED: Final = "rate_limited"
STATE_DISCONNECTED: Final = "disconnected"
# Event names
EVENT_RESPONSE_RECEIVED: Final = f"{DOMAIN}_response_received"
EVENT_ERROR_OCCURRED: Final = f"{DOMAIN}_error_occurred"
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)
+519 -98
View File
@@ -1,134 +1,555 @@
"""The HA Text AI integration.""" """The HA Text AI coordinator."""
from homeassistant.config_entries import ConfigEntry from __future__ import annotations
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN, PLATFORMS
from .coordinator import HATextAICoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up HA Text AI from a config entry."""
try:
coordinator = HATextAICoordinator(
hass,
api_key=entry.data["api_key"],
endpoint=entry.data.get("api_endpoint", "https://api.openai.com/v1"),
model=entry.data.get("model", "gpt-3.5-turbo"),
temperature=entry.data.get("temperature", 0.7),
max_tokens=entry.data.get("max_tokens", 1000),
request_interval=entry.data.get("request_interval", 1.0),
)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
return await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
except Exception as ex:
raise ConfigEntryNotReady(f"Failed to setup entry: {str(ex)}") from ex
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
"""Data coordinator for HA text AI."""
import asyncio
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
import openai
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.exceptions import ConfigEntryAuthFailed 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,
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,
) )
if not api_key: # Register instance
raise ValueError("API key is required") self.hass.data.setdefault(DOMAIN, {})
if not isinstance(temperature, (int, float)) or not 0 <= temperature <= 2: self.hass.data[DOMAIN][instance_name] = self
raise ValueError("Temperature must be between 0 and 2") self.context_messages = context_messages
if not isinstance(max_tokens, int) or max_tokens < 1:
raise ValueError("Max tokens must be a positive integer")
self.api_key = api_key self._system_prompt = None
self.endpoint = endpoint or "https://api.openai.com/v1" self._conversation_history = []
self.model = model or "gpt-3.5-turbo" 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._start_time = dt_util.utcnow()
self.client = openai.OpenAI( _LOGGER.info(
api_key=self.api_key, f"Initialized HA Text AI coordinator with instance: {instance_name}"
base_url=self.endpoint
) )
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:
question = await self._question_queue.get() current_state = self._get_current_state()
response_content = await self.hass.async_add_executor_job( _LOGGER.debug(
self._make_api_call, question f"Updating data for {self.instance_name}, current state: {current_state}"
) )
self._responses[question] = {
"question": question, data = {
"response": response_content "state": current_state,
"metrics": self._performance_metrics,
"last_response": self.last_response,
"is_processing": self._is_processing,
"is_rate_limited": self._is_rate_limited,
"is_maintenance": self._is_maintenance,
"endpoint_status": self.endpoint_status,
"uptime": (dt_util.utcnow() - self._start_time).total_seconds(),
"system_prompt": self._system_prompt,
"history_size": len(self._conversation_history),
"conversation_history": self._conversation_history,
"normalized_name": self.normalized_name,
} }
_LOGGER.debug("Response from API: %s", response_content)
return self._responses
except openai.AuthenticationError as err: # Validate data
raise ConfigEntryAuthFailed from err if not isinstance(data, dict):
raise ValueError("Invalid data format")
_LOGGER.debug(f"Updated data for {self.instance_name}: {data}")
return data
except Exception as err: except Exception as err:
_LOGGER.error("Error communicating with API: %s", err) _LOGGER.error(f"Error updating data for {self.instance_name}: {err}")
return self._responses return self._initial_state
def _make_api_call(self, question: str) -> str: async def async_update_ha_state(self) -> None:
"""Make API call to OpenAI.""" """Update Home Assistant state."""
try: try:
messages = [] _LOGGER.debug(
if self.system_prompt: f"Requesting state update for {self.instance_name} (normalized: {self.normalized_name})"
messages.append({"role": "system", "content": self.system_prompt})
messages.append({"role": "user", "content": question})
completion = self.client.chat.completions.create(
model=self.model,
messages=messages,
temperature=self.temperature,
max_tokens=self.max_tokens,
) )
return completion.choices[0].message.content 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: except Exception as err:
_LOGGER.error("Error in API call: %s", 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 _process_anthropic_message(self, question: str, **kwargs) -> dict:
"""Process message using Anthropic API."""
try:
_LOGGER.debug(f"Anthropic API call: model={kwargs['model']}, max_tokens={kwargs['max_tokens']}")
response = await self.client.messages.create(
model=kwargs["model"],
max_tokens=kwargs["max_tokens"],
messages=kwargs["messages"],
temperature=kwargs["temperature"],
)
_LOGGER.debug(f"Anthropic response: tokens={response.usage}")
return {
"content": response.content[0].text,
"tokens": {
"prompt": response.usage.input_tokens,
"completion": response.usage.output_tokens,
"total": response.usage.input_tokens + response.usage.output_tokens,
},
}
except Exception as e:
_LOGGER.error(f"Anthropic API error: {str(e)}")
raise
async def _process_openai_message(self, question: str, **kwargs) -> dict:
"""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:
"""Shutdown coordinator."""
_LOGGER.debug(f"Shutting down coordinator for {self.instance_name}")
self.hass.data[DOMAIN].pop(self.instance_name, None)
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

+21 -7
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"],
"config_flow": true, "config_flow": true,
"documentation": "https://github.com/smkrv/ha-text-ai/wiki",
"issue_tracker": "https://github.com/smkrv/ha-text-ai/issues",
"requirements": ["openai>=1.0.0"],
"ssdp": [],
"zeroconf": [],
"dependencies": [], "dependencies": [],
"version": "1.0.1c", "documentation": "https://github.com/smkrv/ha-text-ai",
"integration_type": "service",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"codeowners": ["@smkrv"] "issue_tracker": "https://github.com/smkrv/ha-text-ai/issues",
"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": [],
"usb": [],
"version": "2.0.2-alpha",
"zeroconf": []
} }
+287 -49
View File
@@ -1,40 +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, Callable, 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 slugify
from .const import (
DOMAIN,
CONF_MODEL,
CONF_API_PROVIDER,
ATTR_TOTAL_RESPONSES,
ATTR_TOTAL_ERRORS,
ATTR_AVG_RESPONSE_TIME,
ATTR_LAST_REQUEST_TIME,
ATTR_LAST_ERROR,
ATTR_IS_PROCESSING,
ATTR_IS_RATE_LIMITED,
ATTR_IS_MAINTENANCE,
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_PROCESSING,
STATE_ERROR,
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 .const import DOMAIN, ATTR_QUESTION, ATTR_RESPONSE, ATTR_LAST_UPDATED
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
_attr_icon = "mdi:robot"
def __init__( def __init__(
self, self,
@@ -42,53 +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"
@property _LOGGER.debug(f"Created sensor with entity_id: {self.entity_id}")
def state(self) -> StateType: _LOGGER.debug(f"Sensor name: {self._attr_name}")
"""Return the state of the sensor.""" _LOGGER.debug(f"Unique ID: {self._attr_unique_id}")
if not self.coordinator.data:
return None
return self.coordinator.last_update_success_time
@property self.entity_description = SensorEntityDescription(
def extra_state_attributes(self) -> Optional[Dict[str, Any]]: key=f"ha_text_ai_{self._normalized_name.lower()}",
"""Return entity specific state attributes.""" entity_registry_enabled_default=True,
if not self.coordinator.data: )
return None
try: self._current_state = STATE_INITIALIZING
self._error_count = 0
self._last_error = None
self._last_update = None
self._is_processing = False
self._last_response = {}
self._metrics = {}
history = list(self.coordinator.data.items()) model = config_entry.data.get(CONF_MODEL, "Unknown")
if not history: api_provider = config_entry.data.get(CONF_API_PROVIDER, "Unknown")
return None
last_question, last_data = history[-1] self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._attr_unique_id)},
name=self._attr_name,
manufacturer="Community",
model=f"{model} ({api_provider} provider)",
sw_version="1.0.0",
)
_LOGGER.debug(
if isinstance(last_data, dict): f"Initialized sensor: {self.entity_id} for instance: {self._instance_name}"
last_response = last_data.get("response", "") )
else:
last_response = str(last_data)
return {
ATTR_QUESTION: last_question,
ATTR_RESPONSE: last_response,
ATTR_LAST_UPDATED: self.coordinator.last_update_success_time,
}
except (IndexError, KeyError, AttributeError) as err:
_LOGGER.warning("Error getting attributes: %s", err)
return None
@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 @property
def should_poll(self) -> bool: def native_value(self) -> StateType:
"""No need to poll. Coordinator notifies entity of updates.""" """Return the native value of the sensor."""
return False 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:
"""When entity is added to hass."""
await super().async_added_to_hass()
self._handle_coordinator_update()
_LOGGER.debug(f"Entity {self.entity_id} added to Home Assistant")
def _handle_coordinator_update(self) -> None:
"""Handle updated data from the coordinator."""
try:
data = self.coordinator.data
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:
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:
self._current_state = STATE_ERROR
self._last_error = str(err)
self._error_count += 1
_LOGGER.error(
"Error handling update for %s: %s",
self.entity_id,
err,
exc_info=True,
)
self.async_write_ha_state()
+104 -21
View File
@@ -1,35 +1,57 @@
ask_question: ask_question:
name: Ask Question name: Ask Question
description: Send a question to the AI model and receive a detailed response description: >-
Send a question to the AI model and receive a detailed response.
The response will be stored in the conversation history and can be retrieved later.
fields: fields:
instance:
name: Instance
description: Name of the HA Text AI instance to use
required: true
selector:
entity:
integration: ha_text_ai
domain: sensor
question: question:
name: Question name: Question
description: Your question or prompt for the AI assistant description: Your question or prompt for the AI assistant
required: true required: true
example: "What automations would you recommend for a smart kitchen?" selector:
text:
multiline: true
type: text
system_prompt:
name: System Prompt
description: Optional system prompt to set context for this specific question
required: false
selector: selector:
text: text:
multiline: true 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: Select an AI model to use (optional, overrides default setting) description: "Select AI model to use (optional, overrides default setting)"
required: false required: false
example: "gpt-3.5-turbo"
default: "gpt-3.5-turbo"
selector: selector:
select: text: {}
options:
- label: "GPT-3.5 Turbo"
value: "gpt-3.5-turbo"
- label: "GPT-4"
value: "gpt-4"
- label: "GPT-4 32K"
value: "gpt-4-32k"
temperature: temperature:
name: Temperature name: Temperature
description: "Controls response creativity (0-2): Lower values for focused responses, higher for creative ones" description: Controls response creativity (0.0-2.0)
required: false required: false
default: 0.7 default: 0.7
selector: selector:
@@ -41,7 +63,7 @@ ask_question:
max_tokens: max_tokens:
name: Max Tokens name: Max Tokens
description: Maximum length of the response description: Maximum length of the response (1-4096 tokens)
required: false required: false
default: 1000 default: 1000
selector: selector:
@@ -53,16 +75,34 @@ ask_question:
clear_history: clear_history:
name: Clear History name: Clear History
description: Delete all stored questions and responses description: >-
fields: {} 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
required: true
selector:
entity:
integration: ha_text_ai
domain: sensor
get_history: get_history:
name: Get History name: Get History
description: Retrieve recent conversation history description: Retrieve conversation history with optional filtering and sorting
fields: fields:
instance:
name: Instance
description: Name of the HA Text AI instance to get history from
required: true
selector:
entity:
integration: ha_text_ai
domain: sensor
limit: limit:
name: Limit name: Limit
description: Number of most recent conversations to return description: Number of conversations to return (1-100)
required: false required: false
default: 10 default: 10
selector: selector:
@@ -72,15 +112,58 @@ get_history:
step: 1 step: 1
mode: box mode: box
filter_model:
name: Filter Model
description: Filter conversations by specific AI model
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:
select:
options:
- newest
- oldest
set_system_prompt: set_system_prompt:
name: Set System Prompt name: Set System Prompt
description: Configure the AI's behavior by setting a system prompt description: Set default system behavior instructions for all future conversations
fields: fields:
instance:
name: Instance
description: Name of the HA Text AI instance to set system prompt for
required: true
selector:
entity:
integration: ha_text_ai
domain: sensor
prompt: prompt:
name: System Prompt name: System Prompt
description: Instructions that define how the AI should behave and respond description: Instructions that define how the AI should behave and respond
required: true required: true
example: "You are a home automation expert assistant. Provide practical advice focused on smart home technology."
selector: selector:
text: text:
multiline: true multiline: true
@@ -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"
}
}
}
}
}
}
+235 -28
View File
@@ -1,56 +1,263 @@
{ {
"config": { "config": {
"step": { "step": {
"user": { "provider": {
"title": "Set up HA text AI", "title": "Select AI Provider",
"description": "Configure your OpenAI integration for smart home interactions", "description": "Choose which AI service provider to use for this instance.",
"data": { "data": {
"api_key": "OpenAI API Key", "api_provider": "API Provider",
"model": "AI Model", "context_messages": "Number of context messages to retain (1-20)"
"temperature": "Temperature", }
"max_tokens": "Max Tokens", },
"api_endpoint": "API Endpoint", "user": {
"request_interval": "Request Interval" "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)"
} }
} }
}, },
"error": { "error": {
"invalid_auth": "Invalid API key. Please check your OpenAI API key and try again.", "name_exists": "An instance with this name already exists",
"cannot_connect": "Failed to connect to API. Please check your internet connection and API endpoint.", "invalid_name": "Invalid instance name",
"unknown": "Unexpected error occurred. Please check the logs for more details.", "invalid_auth": "Authentication failed - check your API key",
"already_exists": "This API key is already configured in another integration." "invalid_api_key": "Invalid API key - please verify your credentials",
}, "cannot_connect": "Failed to connect to API service",
"abort": { "invalid_model": "Selected model is not available",
"already_configured": "This OpenAI integration is already configured", "rate_limit": "Rate limit exceeded",
"auth_failed": "Authentication failed. Please verify your API key." "context_length": "Context length exceeded",
"rate_limit_exceeded": "API rate limit exceeded",
"maintenance": "Service is under maintenance",
"invalid_response": "Invalid API response received",
"api_error": "API service error occurred",
"timeout": "Request timed out",
"invalid_instance": "Invalid instance specified",
"unknown": "Unexpected error occurred",
"empty": "Name cannot be empty",
"invalid_characters": "Name can only contain letters, numbers, spaces, underscores and hyphens",
"name_too_long": "Name must be 50 characters or less"
} }
}, },
"options": { "options": {
"step": { "step": {
"init": { "init": {
"title": "HA text AI Options", "title": "Update Instance Settings",
"description": "Adjust your OpenAI integration settings", "description": "Modify settings for this AI assistant instance.",
"data": { "data": {
"temperature": "Temperature", "model": "AI model",
"max_tokens": "Max Tokens", "temperature": "Response creativity (0-2)",
"request_interval": "Request Interval" "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": { "entity": {
"sensor": { "sensor": {
"last_response": { "ha_text_ai": {
"name": "Last Response", "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": { "state_attributes": {
"last_updated": {
"name": "Last Updated"
},
"question": { "question": {
"name": "Last Question" "name": "Last Question"
}, },
"response": { "response": {
"name": "AI 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"
} }
} }
} }
@@ -0,0 +1,266 @@
{
"config": {
"step": {
"provider": {
"title": "Выбор поставщика ИИ",
"description": "Выберите поставщика услуг ИИ для этой инстанции.",
"data": {
"api_provider": "Поставщик API",
"context_messages": "Количество контекстных сообщений для сохранения (1-20)"
}
},
"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)"
}
}
},
"error": {
"name_exists": "Инстанция с таким именем уже существует",
"invalid_name": "Некорректное имя инстанции",
"invalid_auth": "Аутентификация не удалась - проверьте ваш API-ключ",
"invalid_api_key": "Неверный API-ключ - проверьте ваши учетные данные",
"cannot_connect": "Не удалось подключиться к службе API",
"invalid_model": "Выбранная модель недоступна",
"rate_limit": "Превышен лимит запросов",
"context_length": "Превышена длина контекста",
"rate_limit_exceeded": "Превышен лимит запросов API",
"maintenance": "Сервис находится на техническом обслуживании",
"invalid_response": "Получен неверный ответ API",
"api_error": "Произошла ошибка службы API",
"timeout": "Запрос превысил время ожидания",
"invalid_instance": "Указана неверная инстанция",
"unknown": "Произошла непредвиденная ошибка",
"empty": "Имя не может быть пустым",
"invalid_characters": "Имя может содержать только буквы, цифры, пробелы, подчеркивания и дефисы",
"name_too_long": "Имя должно быть не более 50 символов"
}
},
"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": "Минимальная задержка"
}
}
}
}
}
}
BIN
View File
Binary file not shown.
+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.2",
"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
+25
View File
@@ -0,0 +1,25 @@
```
ha-text-ai/
├── custom_components/
├── ha_text_ai/
│ ├── __init__.py
│ ├── config_flow.py
│ ├── coordinator.py
│ ├── manifest.json
│ ├── sensor.py
│ ├── services.yaml
│ ├── const.py
│ └── api_client.py
├── translations/
│ ├── en.json
│ ├── de.json
│ └── ru.json
└── icons/
├── icon.png
├── icon@2x.png
├── logo.png
└── logo@2x.png
```