mirror of
https://github.com/smkrv/ha-text-ai.git
synced 2026-07-22 15:14:01 +08:00
Compare commits
248
Commits
v2.0.0
...
v2.0.2-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3ef31f551 | ||
|
|
ff0c0369e8 | ||
|
|
b0dafe081b | ||
|
|
a88b5d01c1 | ||
|
|
e7d5e62671 | ||
|
|
5f41b9489d | ||
|
|
958e241e0e | ||
|
|
4b3efc0b6f | ||
|
|
1592fc2371 | ||
|
|
d5a6613428 | ||
|
|
9324473c9e | ||
|
|
456e797cca | ||
|
|
f960b9f9b4 | ||
|
|
8449b73423 | ||
|
|
f19ba9aac9 | ||
|
|
524849f6a9 | ||
|
|
0a64a9abe0 | ||
|
|
adb4127f5a | ||
|
|
f463593180 | ||
|
|
e53f257977 | ||
|
|
87199e856a | ||
|
|
bcf7cfbf76 | ||
|
|
9289e1388f | ||
|
|
84db9b7bb8 | ||
|
|
bd1098a181 | ||
|
|
6fd3db0063 | ||
|
|
bf52217cd5 | ||
|
|
b0cf5b3c61 | ||
|
|
347c1675ca | ||
|
|
961ae2d34f | ||
|
|
14827c3adc | ||
|
|
4a88453abc | ||
|
|
bc33d38f5e | ||
|
|
657a5ede86 | ||
|
|
dd046fe3e8 | ||
|
|
7391dbf5b1 | ||
|
|
a0ccb86bd4 | ||
|
|
c0eb5b165d | ||
|
|
9812babc7e | ||
|
|
c61c4570b0 | ||
|
|
d618feffff | ||
|
|
0fdc3c93d3 | ||
|
|
d6e76f7805 | ||
|
|
b05afe1085 | ||
|
|
2a4911f5f8 | ||
|
|
208074d845 | ||
|
|
616ff2c3fe | ||
|
|
987c939956 | ||
|
|
1615cc744e | ||
|
|
2d793a4b25 | ||
|
|
aeeb4d5504 | ||
|
|
daec801073 | ||
|
|
e4916a7b7c | ||
|
|
a08abd76e3 | ||
|
|
a2be709608 | ||
|
|
69002ef926 | ||
|
|
9ade5a7194 | ||
|
|
9b3f4f605b | ||
|
|
b862968d01 | ||
|
|
29f3ae5592 | ||
|
|
107d2a64fc | ||
|
|
e24bb884ef | ||
|
|
107a2ef962 | ||
|
|
9d58f2cf1e | ||
|
|
29f6860fe1 | ||
|
|
fa89026e05 | ||
|
|
9968452c46 | ||
|
|
9665634013 | ||
|
|
76d10ba8fb | ||
|
|
e9ea10203e | ||
|
|
92a4c2da02 | ||
|
|
b6d8eb98f6 | ||
|
|
b6b01bccd7 | ||
|
|
ace2339b4f | ||
|
|
166c1f9c9c | ||
|
|
e4039a08bc | ||
|
|
1da5b5941d | ||
|
|
6683f12c80 | ||
|
|
2277f48e46 | ||
|
|
d206bde15a | ||
|
|
af16d03915 | ||
|
|
bf26cd3cfb | ||
|
|
094062773a | ||
|
|
4cd95813bc | ||
|
|
c2064f0b64 | ||
|
|
fafd927610 | ||
|
|
7f46380054 | ||
|
|
351a8b18dd | ||
|
|
39833b333f | ||
|
|
888a41375b | ||
|
|
823abb22e4 | ||
|
|
a3c88309b4 | ||
|
|
beebc7e194 | ||
|
|
3f8f22ac61 | ||
|
|
12c95d0e92 | ||
|
|
b00f600cd9 | ||
|
|
a4925fc943 | ||
|
|
28248ac3c4 | ||
|
|
d2b5626977 | ||
|
|
b61429b52d | ||
|
|
d05b39d8ae | ||
|
|
6b3b0f1bd6 | ||
|
|
feb679ae77 | ||
|
|
9779e5552d | ||
|
|
fbd187dc29 | ||
|
|
e8b6116439 | ||
|
|
fcd3e79cb7 | ||
|
|
d03078cfd4 | ||
|
|
b94d859849 | ||
|
|
dc4fcdf578 | ||
|
|
da4c40017d | ||
|
|
ac420b6495 | ||
|
|
5791601c7e | ||
|
|
eb149184c3 | ||
|
|
d410073c64 | ||
|
|
b373f6c513 | ||
|
|
18395a2265 | ||
|
|
2e4c63ba7d | ||
|
|
9fdf7c4642 | ||
|
|
053a9050b6 | ||
|
|
7efabdfa70 | ||
|
|
e5077969e9 | ||
|
|
2688da5a82 | ||
|
|
4f46d077df | ||
|
|
bde856c576 | ||
|
|
083cb9f730 | ||
|
|
2644d720e7 | ||
|
|
2fe84ab801 | ||
|
|
ca1d79f848 | ||
|
|
e52572beaa | ||
|
|
0f77a98d76 | ||
|
|
ebede3d56b | ||
|
|
1692f5519f | ||
|
|
dad1aa1c45 | ||
|
|
5d2244db6e | ||
|
|
8a15cfe4b4 | ||
|
|
500c7fbe30 | ||
|
|
894b600b09 | ||
|
|
97f0b30cd6 | ||
|
|
9855e8a561 | ||
|
|
c2b259ade1 | ||
|
|
cfc185117c | ||
|
|
9e89920e79 | ||
|
|
af190da333 | ||
|
|
3e3ec45b19 | ||
|
|
f0fe593d78 | ||
|
|
8646118a27 | ||
|
|
31f21b3a6b | ||
|
|
12d87e30e1 | ||
|
|
6ecc3f72d1 | ||
|
|
e8c40dc6b8 | ||
|
|
0279517a42 | ||
|
|
072eab1703 | ||
|
|
3b38a6dd29 | ||
|
|
7f8d8be5fb | ||
|
|
3aa6b6a2ef | ||
|
|
d870cfbba6 | ||
|
|
13a9e1a5d7 | ||
|
|
06aba7e692 | ||
|
|
c73ff02bfb | ||
|
|
ed03170817 | ||
|
|
3079994a77 | ||
|
|
41b37f9edf | ||
|
|
8d65d3ef4e | ||
|
|
357c8b8be4 | ||
|
|
a47b343f93 | ||
|
|
4388e0f2e1 | ||
|
|
f2adff1d85 | ||
|
|
ad51da7950 | ||
|
|
7610a71829 | ||
|
|
8800f226d2 | ||
|
|
ba16932b44 | ||
|
|
81394345b4 | ||
|
|
72621d9d0e | ||
|
|
d35ded6502 | ||
|
|
b17e5c3db2 | ||
|
|
365c4df2a4 | ||
|
|
ab79d05e96 | ||
|
|
a208004fe0 | ||
|
|
51c714df25 | ||
|
|
87b45df180 | ||
|
|
accb15a92a | ||
|
|
a421825050 | ||
|
|
c55112c8f7 | ||
|
|
accbc5635e | ||
|
|
558ff7d141 | ||
|
|
fa70a0a4ab | ||
|
|
50b414a904 | ||
|
|
80c91039e1 | ||
|
|
966e01e7e2 | ||
|
|
82ffce1e25 | ||
|
|
c7257cc00a | ||
|
|
ab877c2e9a | ||
|
|
c6dcf307fd | ||
|
|
30d69e7ed1 | ||
|
|
bb8195c0d1 | ||
|
|
a813302e86 | ||
|
|
0f643664f7 | ||
|
|
665537cb6a | ||
|
|
766b9293cf | ||
|
|
6e014e30d9 | ||
|
|
30fc8ad1df | ||
|
|
bcad939a3d | ||
|
|
e153df85fb | ||
|
|
26908b5d81 | ||
|
|
0ddca8ff58 | ||
|
|
fd285d969e | ||
|
|
10bebe5eb5 | ||
|
|
fef5e81033 | ||
|
|
a325ae180a | ||
|
|
df1ae9cd55 | ||
|
|
d5380b4195 | ||
|
|
9e12ada4fa | ||
|
|
bb3240f1b3 | ||
|
|
7780ac89ef | ||
|
|
c58eae695e | ||
|
|
45244dcaa9 | ||
|
|
53b15fa74c | ||
|
|
9341b02f4b | ||
|
|
158db522a8 | ||
|
|
f3b76c0bc1 | ||
|
|
d11f961566 | ||
|
|
9d7f81d042 | ||
|
|
c95e1a829c | ||
|
|
7bd06e7b88 | ||
|
|
6e64b6feac | ||
|
|
f6c0e6265e | ||
|
|
662ce701ca | ||
|
|
72bbfb3f58 | ||
|
|
9306c12fcd | ||
|
|
9f1ea70c9f | ||
|
|
98913b359a | ||
|
|
a4905f0778 | ||
|
|
e603231633 | ||
|
|
345463322a | ||
|
|
c298866e3c | ||
|
|
75e97ac652 | ||
|
|
149ec16d57 | ||
|
|
31e30d94aa | ||
|
|
326f876410 | ||
|
|
58a7ae4229 | ||
|
|
8f796ad9dc | ||
|
|
8be860007a | ||
|
|
1985e201b4 | ||
|
|
4e4b41661b | ||
|
|
b24a99a11f | ||
|
|
69b151f990 | ||
|
|
2d4eb59d6c |
@@ -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
@@ -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
@@ -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!** 🎉
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,27 @@
|
|||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
    [](https://opensource.org/licenses/MIT) [](https://github.com/hacs/integration)
|
  [](https://opensource.org/licenses/MIT) [](https://github.com/hacs/integration)   
|
||||||
|
|
||||||
|
<img src="https://github.com/smkrv/ha-text-ai/blob/524849f6a945ec62c2cf6a6b7ecd9a28b37bf0fa/misc/icons/logo.jpg" alt="HA Text AI" height="160"/>
|
||||||
|
|
||||||
|
### Advanced AI Integration for Home Assistant with LLM multi-provider support
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Transform your smart home experience with powerful AI assistance powered by multiple AI providers including OpenAI GPT and Anthropic Claude models. Get intelligent responses, automate complex scenarios, and enhance your home automation with advanced natural language processing.
|
Transform your smart home experience with powerful AI assistance powered by multiple AI providers including OpenAI GPT 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
|
||||||
|
|
||||||
- 🧠 **Multi-Provider AI Integration**:
|
- 🧠 **Multi-Provider AI Integration**:
|
||||||
@@ -19,36 +30,43 @@ Transform your smart home experience with powerful AI assistance powered by mult
|
|||||||
- Anthropic Claude integration
|
- Anthropic Claude integration
|
||||||
- Custom API endpoints
|
- Custom API endpoints
|
||||||
- Flexible model selection
|
- Flexible model selection
|
||||||
|
|
||||||
- 💬 **Advanced Language Processing**:
|
- 💬 **Advanced Language Processing**:
|
||||||
- Context-aware responses
|
- Context-aware responses
|
||||||
- Multi-turn conversations
|
- Multi-turn conversations
|
||||||
- Custom system instructions
|
- Custom system instructions
|
||||||
- Natural conversation flow
|
- Natural conversation flow
|
||||||
|
|
||||||
- 📝 **Enhanced Memory Management**:
|
- 📝 **Enhanced Memory Management**:
|
||||||
- Persistent conversation history
|
- Persistent conversation history
|
||||||
- Context-aware responses
|
- Context-aware responses
|
||||||
- Customizable history limits
|
- Customizable history limits
|
||||||
- Model-specific filtering
|
- Model-specific filtering
|
||||||
|
|
||||||
- ⚡ **Performance Optimization**:
|
- ⚡ **Performance Optimization**:
|
||||||
- Efficient token usage
|
- Efficient token usage
|
||||||
- Smart rate limiting
|
- Smart rate limiting
|
||||||
- Response caching
|
- Response caching
|
||||||
- Request interval control
|
- Request interval control
|
||||||
|
|
||||||
- 🎯 **Advanced Customization**:
|
- 🎯 **Advanced Customization**:
|
||||||
- Per-request model selection
|
- Per-request model selection
|
||||||
- Adjustable parameters
|
- Adjustable parameters
|
||||||
- Custom system prompts
|
- Custom system prompts
|
||||||
- Temperature control
|
- Temperature control
|
||||||
|
|
||||||
- 🔒 **Enhanced Security**:
|
- 🔒 **Enhanced Security**:
|
||||||
- Secure API key storage
|
- Secure API key storage
|
||||||
- Rate limiting protection
|
- Rate limiting protection
|
||||||
- Error handling
|
- Error handling
|
||||||
- Usage monitoring
|
- Usage monitoring
|
||||||
|
|
||||||
- 🎨 **Improved User Experience**:
|
- 🎨 **Improved User Experience**:
|
||||||
- Intuitive configuration UI
|
- Intuitive configuration UI
|
||||||
- Detailed sensor attributes
|
- Detailed sensor attributes
|
||||||
- Rich service interface
|
- Rich service interface
|
||||||
- Model selection UI
|
- Model selection UI
|
||||||
|
|
||||||
- 🔄 **Automation Integration**:
|
- 🔄 **Automation Integration**:
|
||||||
- Event-driven responses
|
- Event-driven responses
|
||||||
- Conditional logic support
|
- Conditional logic support
|
||||||
@@ -57,25 +75,58 @@ Transform your smart home experience with powerful AI assistance powered by mult
|
|||||||
|
|
||||||
## 📋 Prerequisites
|
## 📋 Prerequisites
|
||||||
|
|
||||||
- Home Assistant 2023.8.0 or newer
|
- Home Assistant 2024.11 or later
|
||||||
- API key from supported providers:
|
- Active API key from:
|
||||||
- OpenAI ([Get key](https://platform.openai.com/account/api-keys))
|
- OpenAI ([Get key](https://platform.openai.com/account/api-keys))
|
||||||
- Anthropic ([Get key](https://console.anthropic.com/))
|
- Anthropic ([Get key](https://console.anthropic.com/))
|
||||||
|
- OpenRouter ([Get key](https://openrouter.ai/keys))
|
||||||
- Python 3.9 or newer
|
- Python 3.9 or newer
|
||||||
- Stable internet connection
|
- Stable internet connection
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
- API Provider (OpenAI/Anthropic)
|
||||||
|
- API Key (provider-specific)
|
||||||
|
- Model Selection (flexible, provider-specific models)
|
||||||
|
- Temperature (Creativity control, 0.0-2.0)
|
||||||
|
- Max Tokens (Response length limit)
|
||||||
|
- Request Interval (API call throttling)
|
||||||
|
- Custom API Endpoint (optional)
|
||||||
|
|
||||||
|
#### ⓘ Potentially Compatible Providers
|
||||||
|
The integration is designed to be flexible and may work with other providers offering OpenAI-compatible APIs:
|
||||||
|
- Groq
|
||||||
|
- Together AI
|
||||||
|
- Perplexity AI
|
||||||
|
- Mistral AI
|
||||||
|
- Google AI
|
||||||
|
- Local AI servers (like Ollama)
|
||||||
|
- Custom OpenAI-compatible endpoints
|
||||||
|
|
||||||
|
#### Additional Notes
|
||||||
|
- Not all providers guarantee full compatibility
|
||||||
|
- Performance may vary between providers
|
||||||
|
- Check individual provider's documentation
|
||||||
|
- Ensure your API key has sufficient credits/quota
|
||||||
|
|
||||||
|
#### Provider Compatibility Requirements
|
||||||
|
To be compatible, a provider should support:
|
||||||
|
- OpenAI-like REST API structure
|
||||||
|
- JSON request/response format
|
||||||
|
- Standard authentication method
|
||||||
|
- Similar model parameter handling
|
||||||
|
|
||||||
## ⚡ Installation
|
## ⚡ Installation
|
||||||
|
|
||||||
### HACS Installation (Recommended)
|
### HACS Installation (Recommended)
|
||||||
1. Open HACS in Home Assistant
|
<a href="https://my.home-assistant.io/redirect/hacs_repository/?owner=smkrv&repository=ha-text-ai&category=Integration"><img src="https://my.home-assistant.io/badges/hacs_repository.svg" width="170" height="auto"></a>
|
||||||
2. Click on the three dots in the top right corner
|
1. Open HACS in Home Assistant
|
||||||
3. Select "Custom repositories"
|
2. Click on "Integrations"
|
||||||
4. Add `https://github.com/smkrv/ha-text-ai` as Integration
|
3. Click "..." in top right corner
|
||||||
5. Click "Add"
|
4. Select "Custom repositories"
|
||||||
6. Click on "Integrations" in HACS
|
5. Add repository URL: `https://github.com/smkrv/ha-text-ai`
|
||||||
7. Search for "HA Text AI"
|
6. Choose "Integration" as category
|
||||||
8. Click "Download"
|
7. Click "Download"
|
||||||
9. Restart Home Assistant
|
8. Restart Home Assistant
|
||||||
|
|
||||||
### Manual Installation
|
### Manual Installation
|
||||||
1. Download the latest release
|
1. Download the latest release
|
||||||
@@ -92,19 +143,61 @@ Transform your smart home experience with powerful AI assistance powered by mult
|
|||||||
4. Follow the configuration steps
|
4. Follow the configuration steps
|
||||||
|
|
||||||
### Via YAML
|
### Via YAML
|
||||||
|
|
||||||
|
### Platform Configuration (Global Settings)
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
ha_text_ai:
|
ha_text_ai:
|
||||||
api_key: !secret ai_api_key
|
api_provider: openai # Required
|
||||||
model: gpt-3.5-turbo # or claude-3-sonnet
|
api_key: !secret ai_api_key # Required
|
||||||
temperature: 0.7
|
model: gpt-4o-mini # Strongly recommended
|
||||||
max_tokens: 1000
|
temperature: 0.7 # Optional
|
||||||
request_interval: 1.0
|
max_tokens: 1000 # Optional
|
||||||
api_endpoint: https://api.openai.com/v1 # optional, for custom endpoints
|
request_interval: 1.0 # Optional
|
||||||
system_prompt: |
|
api_endpoint: https://api.openai.com/v1 # Required
|
||||||
|
system_prompt: | # Optional
|
||||||
You are a home automation expert assistant.
|
You are a home automation expert assistant.
|
||||||
Focus on practical and efficient solutions.
|
Focus on practical and efficient solutions.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Sensor Configuration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
sensor:
|
||||||
|
- platform: ha_text_ai
|
||||||
|
name: "My AI Assistant" # Required, unique identifier
|
||||||
|
api_provider: openai # Optional (inherits from platform)
|
||||||
|
model: "gpt-4o-mini" # Optional
|
||||||
|
temperature: 0.7 # Optional
|
||||||
|
max_tokens: 1000 # Optional
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📋 Configuration Parameters
|
||||||
|
|
||||||
|
#### Platform Configuration
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Default | Description |
|
||||||
|
|-----------|------|----------|---------|-------------|
|
||||||
|
| `api_provider` | String | ✅ | - | AI service provider (openai, anthropic) |
|
||||||
|
| `api_key` | String | ✅ | - | Authentication key for AI service |
|
||||||
|
| `model` | String | ⚠️ | Provider default | Strongly recommended: Specific AI model to use. If not specified, the provider's default model will be used |
|
||||||
|
| `temperature` | Float | ❌ | 0.7 | Response creativity level (0.0-2.0) |
|
||||||
|
| `max_tokens` | Integer | ❌ | 1000 | Maximum response length |
|
||||||
|
| `request_interval` | Float | ❌ | 1.0 | Delay between API requests |
|
||||||
|
| `api_endpoint` | URL | ⚠️ | Provider default | Custom API endpoint |
|
||||||
|
| `system_prompt` | String | ❌ | - | Default context for AI interactions |
|
||||||
|
|
||||||
|
#### Sensor Configuration
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Default | Description |
|
||||||
|
|-----------|------|----------|---------|-------------|
|
||||||
|
| `platform` | String | ✅ | - | Must be `ha_text_ai` |
|
||||||
|
| `name` | String | ✅ | - | Unique sensor identifier |
|
||||||
|
| `api_provider` | String | ❌ | Platform setting | Override global provider |
|
||||||
|
| `model` | String | ⚠️ | Platform setting | Recommended: Override global model. If not specified, uses platform or provider default |
|
||||||
|
| `temperature` | Float | ❌ | Platform setting | Override global temperature |
|
||||||
|
| `max_tokens` | Integer | ❌ | Platform setting | Override global max tokens |
|
||||||
|
|
||||||
## 🛠️ Available Services
|
## 🛠️ Available Services
|
||||||
|
|
||||||
### ask_question
|
### ask_question
|
||||||
@@ -115,6 +208,7 @@ data:
|
|||||||
model: "claude-3-sonnet" # optional
|
model: "claude-3-sonnet" # optional
|
||||||
temperature: 0.5 # optional
|
temperature: 0.5 # optional
|
||||||
max_tokens: 500 # optional
|
max_tokens: 500 # optional
|
||||||
|
context_messages: 10 #optional, number of previous messages to include in context, default: 5
|
||||||
system_prompt: "You are a sleep optimization expert" # optional
|
system_prompt: "You are a sleep optimization expert" # optional
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -140,38 +234,171 @@ service: ha_text_ai.clear_history
|
|||||||
service: ha_text_ai.get_history
|
service: ha_text_ai.get_history
|
||||||
data:
|
data:
|
||||||
limit: 5 # optional
|
limit: 5 # optional
|
||||||
filter_model: "gpt-4" # optional
|
filter_model: "gpt-4o" # optional
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 🏷️ HA Text AI Sensor Naming Convention
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
automation:
|
||||||
|
- alias: "AI Response with Custom Sensor"
|
||||||
|
action:
|
||||||
|
- service: ha_text_ai.ask_question
|
||||||
|
data:
|
||||||
|
question: "Home automation advice"
|
||||||
|
- service: notify.mobile
|
||||||
|
data:
|
||||||
|
message: >
|
||||||
|
AI Tip:
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'response') }}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 💡 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
|
||||||
|
# Name of the AI model currently in use (e.g., latest version of GPT)
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Model') }} # gpt-4o
|
||||||
|
|
||||||
|
# Service provider for the AI model (determines API endpoint and authentication)
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Api provider') }} # openai
|
||||||
|
|
||||||
|
# Previous or alternative model configuration
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Last model') }} # gpt-4o
|
||||||
|
```
|
||||||
|
|
||||||
|
#### System Status
|
||||||
|
```yaml
|
||||||
|
# Current operational readiness of the AI service API
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Api status') }} # ready
|
||||||
|
|
||||||
|
# Indicates if a request is currently being processed
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Is processing') }} # false
|
||||||
|
|
||||||
|
# Shows if the API has hit its request rate limit
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Is rate limited') }} # false
|
||||||
|
|
||||||
|
# Status of the specific API endpoint being used
|
||||||
|
{{ state_attr('sensor.ha_text_ai_gpt', 'Endpoint status') }} # ready
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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
|
## 📘 FAQ
|
||||||
|
|
||||||
**Q: Which AI providers are supported?**
|
**Q: Which AI providers are supported?**
|
||||||
|
|
||||||
A: Currently OpenAI (GPT models) and Anthropic (Claude models) are supported, with more providers planned.
|
A: Currently OpenAI (GPT models) and Anthropic (Claude models) are supported, with more providers planned.
|
||||||
|
|
||||||
**Q: How can I reduce API costs?**
|
**Q: How can I reduce API costs?**
|
||||||
|
|
||||||
A: Use GPT-3.5-Turbo or Claude-3-Sonnet for most queries, implement caching, and optimize token usage.
|
A: Use GPT-3.5-Turbo or Claude-3-Sonnet for most queries, implement caching, and optimize token usage.
|
||||||
|
|
||||||
**Q: Can I use custom models?**
|
**Q: Are there limitations on the number of requests?**
|
||||||
|
A: Depends on your API provider's plan. We recommend monitoring usage and implementing request throttling via `request_interval` configuration.
|
||||||
|
|
||||||
|
**Q: Can I use custom models?**
|
||||||
A: Yes, you can configure custom endpoints and use any compatible model by specifying it in the configuration.
|
A: Yes, you can configure custom endpoints and use any compatible model by specifying it in the configuration.
|
||||||
|
|
||||||
**Q: How do I switch between different AI providers?**
|
**Q: How do I switch between different AI providers?**
|
||||||
|
|
||||||
A: Simply change the model parameter in your configuration or service calls to use the desired provider's model.
|
A: Simply change the model parameter in your configuration or service calls to use the desired provider's model.
|
||||||
|
|
||||||
**Q: How can I reduce API costs?**
|
**Q: How can I reduce API costs?**
|
||||||
|
|
||||||
A: Use GPT-3.5-Turbo for most queries, implement caching, and optimize token usage.
|
A: Use GPT-3.5-Turbo for most queries, implement caching, and optimize token usage.
|
||||||
|
|
||||||
**Q: Is my data secure?**
|
**Q: Is my data secure?**
|
||||||
|
A: Yes, your data is secure. The system operates entirely on your local machine, keeping your data under your control. API keys are stored securely and all external communications use encrypted connections.
|
||||||
|
|
||||||
A: Yes, API keys are stored securely and data is transmitted via encrypted connections.
|
**Q: How do context messages work?**
|
||||||
|
A: Context messages allow the AI to remember and reference previous conversation history. By default, 5 previous messages are included, but you can customize this from 1 to 20 messages to control the conversation depth and token usage.
|
||||||
**Q: Can I use custom models?**
|
|
||||||
|
|
||||||
A: Yes, configure custom endpoints and models via configuration options.
|
|
||||||
|
|
||||||
## 🤝 Contributing
|
## 🤝 Contributing
|
||||||
|
|
||||||
@@ -187,11 +414,27 @@ Contributions welcome! Please read our [Contributing Guide](CONTRIBUTING.md).
|
|||||||
|
|
||||||
MIT License - see [LICENSE](LICENSE) for details.
|
MIT License - see [LICENSE](LICENSE) for details.
|
||||||
|
|
||||||
|
## 💡 Support the Project
|
||||||
|
|
||||||
|
The best support is:
|
||||||
|
- Sharing feedback
|
||||||
|
- Contributing ideas
|
||||||
|
- Recommending to friends
|
||||||
|
- Reporting issues
|
||||||
|
- Star the repository
|
||||||
|
|
||||||
|
If you want to say thanks financially, you can send a small token of appreciation in USDT:
|
||||||
|
|
||||||
|
**USDT Wallet (TRC10/TRC20):**
|
||||||
|
`TXC9zYHYPfWUGi4Sv4R1ctTBGScXXQk5HZ`
|
||||||
|
|
||||||
|
*Open-source is built by community passion!* 🚀
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
Made with ❤️ for the Home Assistant Community
|
Made with ❤️ and Claude 3.5 Sonnet for the Home Assistant Community
|
||||||
|
|
||||||
[Report Bug](https://github.com/smkrv/ha-text-ai/issues) · [Request Feature](https://github.com/smkrv/ha-text-ai/issues)
|
[Report Bug](https://github.com/smkrv/ha-text-ai/issues) · [Request Feature](https://github.com/smkrv/ha-text-ai/issues)
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
"""The HA Text AI integration."""
|
"""The HA Text AI integration."""
|
||||||
import logging
|
from __future__ import annotations
|
||||||
from typing import Any, Dict, Optional
|
|
||||||
import asyncio
|
|
||||||
import voluptuous as vol
|
|
||||||
import json
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
import logging
|
||||||
from homeassistant.const import CONF_API_KEY
|
import os
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
import shutil
|
||||||
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
from datetime import datetime, timedelta
|
||||||
from homeassistant.helpers import aiohttp_client
|
from typing import Any, Dict
|
||||||
from homeassistant.helpers import config_validation as cv
|
|
||||||
|
import voluptuous as vol
|
||||||
from async_timeout import timeout
|
from async_timeout import timeout
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_API_KEY, CONF_NAME, Platform
|
||||||
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
from homeassistant.helpers import aiohttp_client
|
||||||
|
|
||||||
from .coordinator import HATextAICoordinator
|
from .coordinator import HATextAICoordinator
|
||||||
|
from .api_client import APIClient
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
PLATFORMS,
|
PLATFORMS,
|
||||||
@@ -23,75 +27,163 @@ from .const import (
|
|||||||
CONF_MAX_TOKENS,
|
CONF_MAX_TOKENS,
|
||||||
CONF_API_ENDPOINT,
|
CONF_API_ENDPOINT,
|
||||||
CONF_REQUEST_INTERVAL,
|
CONF_REQUEST_INTERVAL,
|
||||||
|
CONF_API_PROVIDER,
|
||||||
|
CONF_CONTEXT_MESSAGES,
|
||||||
|
API_PROVIDER_OPENAI,
|
||||||
|
API_PROVIDER_ANTHROPIC,
|
||||||
DEFAULT_MODEL,
|
DEFAULT_MODEL,
|
||||||
DEFAULT_TEMPERATURE,
|
DEFAULT_TEMPERATURE,
|
||||||
DEFAULT_MAX_TOKENS,
|
DEFAULT_MAX_TOKENS,
|
||||||
DEFAULT_API_ENDPOINT,
|
DEFAULT_OPENAI_ENDPOINT,
|
||||||
|
DEFAULT_ANTHROPIC_ENDPOINT,
|
||||||
DEFAULT_REQUEST_INTERVAL,
|
DEFAULT_REQUEST_INTERVAL,
|
||||||
API_VERSION,
|
DEFAULT_CONTEXT_MESSAGES,
|
||||||
API_MODELS_PATH,
|
|
||||||
API_CHAT_PATH,
|
|
||||||
API_TIMEOUT,
|
API_TIMEOUT,
|
||||||
API_RETRY_COUNT,
|
SERVICE_ASK_QUESTION,
|
||||||
API_BACKOFF_FACTOR,
|
SERVICE_CLEAR_HISTORY,
|
||||||
LOGGER_NAME,
|
SERVICE_GET_HISTORY,
|
||||||
STATE_ERROR,
|
SERVICE_SET_SYSTEM_PROMPT,
|
||||||
STATE_READY,
|
DEFAULT_MAX_HISTORY,
|
||||||
STATE_PROCESSING,
|
CONF_MAX_HISTORY_SIZE,
|
||||||
STATE_RATE_LIMITED,
|
|
||||||
STATE_MAINTENANCE,
|
|
||||||
STATE_DISCONNECTED,
|
|
||||||
STATE_RETRYING,
|
|
||||||
STATE_QUEUED,
|
|
||||||
STATE_UPDATING,
|
|
||||||
SUPPORTED_MODELS,
|
|
||||||
EVENT_RESPONSE_RECEIVED,
|
|
||||||
EVENT_ERROR_OCCURRED,
|
|
||||||
EVENT_STATE_CHANGED,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(LOGGER_NAME)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||||
|
|
||||||
# Service validation schemas
|
|
||||||
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
|
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
|
||||||
|
vol.Required("instance"): cv.string,
|
||||||
vol.Required("question"): cv.string,
|
vol.Required("question"): cv.string,
|
||||||
vol.Optional("system_prompt"): cv.string,
|
vol.Optional("system_prompt"): cv.string,
|
||||||
vol.Optional("model"): vol.In(SUPPORTED_MODELS),
|
vol.Optional("model"): cv.string,
|
||||||
vol.Optional("temperature"): vol.All(
|
vol.Optional("temperature"): cv.positive_float,
|
||||||
vol.Coerce(float), vol.Range(min=0, max=2)
|
vol.Optional("max_tokens"): cv.positive_int,
|
||||||
),
|
vol.Optional("context_messages"): cv.positive_int,
|
||||||
vol.Optional("max_tokens"): vol.All(
|
|
||||||
vol.Coerce(int), vol.Range(min=1, max=4096)
|
|
||||||
),
|
|
||||||
vol.Optional("priority"): vol.Boolean,
|
|
||||||
})
|
|
||||||
|
|
||||||
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
|
|
||||||
vol.Optional("limit", default=10): vol.All(
|
|
||||||
vol.Coerce(int), vol.Range(min=1, max=100)
|
|
||||||
),
|
|
||||||
vol.Optional("filter_model"): vol.In(SUPPORTED_MODELS),
|
|
||||||
vol.Optional("start_date"): cv.datetime,
|
|
||||||
vol.Optional("include_metadata"): vol.Boolean,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
|
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
|
||||||
|
vol.Required("instance"): cv.string,
|
||||||
vol.Required("prompt"): cv.string,
|
vol.Required("prompt"): cv.string,
|
||||||
})
|
})
|
||||||
|
|
||||||
async def async_check_api(session, endpoint: str, headers: dict, is_anthropic: bool = False) -> bool:
|
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
|
||||||
|
vol.Required("instance"): cv.string,
|
||||||
|
vol.Optional("limit"): cv.positive_int,
|
||||||
|
vol.Optional("filter_model"): cv.string,
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_coordinator_by_instance(hass: HomeAssistant, instance: str) -> HATextAICoordinator:
|
||||||
|
"""Get coordinator by instance name."""
|
||||||
|
if instance.startswith("sensor."):
|
||||||
|
instance = instance.replace("sensor.ha_text_ai_", "", 1)
|
||||||
|
|
||||||
|
for entry_id, coord in hass.data[DOMAIN].items():
|
||||||
|
if isinstance(coord, HATextAICoordinator) and coord.instance_name.lower() == instance.lower():
|
||||||
|
return coord
|
||||||
|
|
||||||
|
raise HomeAssistantError(f"Instance {instance} not found")
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: Dict[str, Any]) -> bool:
|
||||||
|
"""Set up the HA Text AI component."""
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
|
try:
|
||||||
|
source = os.path.join(os.path.dirname(__file__), 'icons', 'icon.svg')
|
||||||
|
dest_dir = os.path.join(hass.config.path('www'), 'icons')
|
||||||
|
os.makedirs(dest_dir, exist_ok=True)
|
||||||
|
dest = os.path.join(dest_dir, 'icon.png')
|
||||||
|
if not os.path.exists(dest):
|
||||||
|
shutil.copyfile(source, dest)
|
||||||
|
except Exception as ex:
|
||||||
|
_LOGGER.warning("Failed to copy custom icon: %s", str(ex))
|
||||||
|
|
||||||
|
async def async_ask_question(call: ServiceCall) -> None:
|
||||||
|
"""Handle ask_question service."""
|
||||||
|
try:
|
||||||
|
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
||||||
|
await coordinator.async_ask_question(
|
||||||
|
question=call.data["question"],
|
||||||
|
model=call.data.get("model"),
|
||||||
|
temperature=call.data.get("temperature"),
|
||||||
|
max_tokens=call.data.get("max_tokens"),
|
||||||
|
system_prompt=call.data.get("system_prompt"),
|
||||||
|
context_messages=call.data.get("context_messages"),
|
||||||
|
)
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("Error asking question: %s", str(err))
|
||||||
|
raise HomeAssistantError(f"Failed to process question: {str(err)}")
|
||||||
|
|
||||||
|
async def async_clear_history(call: ServiceCall) -> None:
|
||||||
|
"""Handle clear_history service."""
|
||||||
|
try:
|
||||||
|
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
||||||
|
await coordinator.async_clear_history()
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("Error clearing history: %s", str(err))
|
||||||
|
raise HomeAssistantError(f"Failed to clear history: {str(err)}")
|
||||||
|
|
||||||
|
async def async_get_history(call: ServiceCall) -> list:
|
||||||
|
"""Handle get_history service."""
|
||||||
|
try:
|
||||||
|
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
||||||
|
return await coordinator.async_get_history(
|
||||||
|
limit=call.data.get("limit"),
|
||||||
|
filter_model=call.data.get("filter_model")
|
||||||
|
)
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("Error getting history: %s", str(err))
|
||||||
|
raise HomeAssistantError(f"Failed to get history: {str(err)}")
|
||||||
|
|
||||||
|
async def async_set_system_prompt(call: ServiceCall) -> None:
|
||||||
|
"""Handle set_system_prompt service."""
|
||||||
|
try:
|
||||||
|
coordinator = get_coordinator_by_instance(hass, call.data["instance"])
|
||||||
|
await coordinator.async_set_system_prompt(call.data["prompt"])
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("Error setting system prompt: %s", str(err))
|
||||||
|
raise HomeAssistantError(f"Failed to set system prompt: {str(err)}")
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_ASK_QUESTION,
|
||||||
|
async_ask_question,
|
||||||
|
schema=SERVICE_SCHEMA_ASK_QUESTION
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_CLEAR_HISTORY,
|
||||||
|
async_clear_history,
|
||||||
|
schema=vol.Schema({vol.Required("instance"): cv.string})
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_GET_HISTORY,
|
||||||
|
async_get_history,
|
||||||
|
schema=SERVICE_SCHEMA_GET_HISTORY
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_SET_SYSTEM_PROMPT,
|
||||||
|
async_set_system_prompt,
|
||||||
|
schema=SERVICE_SCHEMA_SET_SYSTEM_PROMPT
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def async_check_api(session, endpoint: str, headers: dict, provider: str) -> bool:
|
||||||
"""Check API availability for different providers."""
|
"""Check API availability for different providers."""
|
||||||
try:
|
try:
|
||||||
if is_anthropic:
|
if provider == API_PROVIDER_ANTHROPIC:
|
||||||
check_url = f"{endpoint}/v1/models"
|
check_url = f"{endpoint}/v1/models"
|
||||||
else:
|
else: # OpenAI
|
||||||
check_url = f"{endpoint}/{API_VERSION}/{API_MODELS_PATH}"
|
check_url = f"{endpoint}/models"
|
||||||
|
|
||||||
async with timeout(API_TIMEOUT):
|
async with timeout(API_TIMEOUT):
|
||||||
async with session.get(check_url, headers=headers) as response:
|
async with session.get(check_url, headers=headers) as response:
|
||||||
if response.status == 200:
|
if response.status in [200, 404]:
|
||||||
return True
|
return True
|
||||||
elif response.status == 401:
|
elif response.status == 401:
|
||||||
raise ConfigEntryNotReady("Invalid API key")
|
raise ConfigEntryNotReady("Invalid API key")
|
||||||
@@ -105,32 +197,33 @@ async def async_check_api(session, endpoint: str, headers: dict, is_anthropic: b
|
|||||||
_LOGGER.error("API check error: %s", str(ex))
|
_LOGGER.error("API check error: %s", str(ex))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool:
|
|
||||||
"""Set up the HA Text AI component."""
|
|
||||||
hass.data.setdefault(DOMAIN, {
|
|
||||||
"coordinators": {},
|
|
||||||
"metrics": {
|
|
||||||
"total_requests": 0,
|
|
||||||
"total_tokens": 0,
|
|
||||||
"errors": {},
|
|
||||||
"model_usage": {},
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return True
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Set up HA Text AI from a config entry."""
|
"""Set up HA Text AI from a config entry."""
|
||||||
|
_LOGGER.debug(f"Setting up HA Text AI entry: {entry.data}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
if CONF_API_PROVIDER not in entry.data:
|
||||||
|
_LOGGER.error("API provider not specified")
|
||||||
|
raise ConfigEntryNotReady("API provider is required")
|
||||||
|
|
||||||
|
# Get configuration
|
||||||
session = aiohttp_client.async_get_clientsession(hass)
|
session = aiohttp_client.async_get_clientsession(hass)
|
||||||
|
api_provider = entry.data.get(CONF_API_PROVIDER)
|
||||||
# Determine API type based on model
|
|
||||||
model = entry.data.get(CONF_MODEL, DEFAULT_MODEL)
|
model = entry.data.get(CONF_MODEL, DEFAULT_MODEL)
|
||||||
is_anthropic = any(model.startswith("claude") for model in SUPPORTED_MODELS)
|
endpoint = entry.data.get(
|
||||||
|
CONF_API_ENDPOINT,
|
||||||
|
DEFAULT_OPENAI_ENDPOINT if api_provider == API_PROVIDER_OPENAI
|
||||||
|
else DEFAULT_ANTHROPIC_ENDPOINT
|
||||||
|
).rstrip('/')
|
||||||
api_key = entry.data[CONF_API_KEY]
|
api_key = entry.data[CONF_API_KEY]
|
||||||
endpoint = entry.data.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT).rstrip('/')
|
instance_name = entry.data.get(CONF_NAME, entry.entry_id)
|
||||||
|
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
|
||||||
|
|
||||||
# Configure headers based on API type
|
|
||||||
headers = {
|
headers = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Accept": "application/json"
|
"Accept": "application/json"
|
||||||
@@ -142,234 +235,65 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
else:
|
else:
|
||||||
headers["Authorization"] = f"Bearer {api_key}"
|
headers["Authorization"] = f"Bearer {api_key}"
|
||||||
|
|
||||||
# Check API with retries
|
if not await async_check_api(session, endpoint, headers, api_provider):
|
||||||
for attempt in range(API_RETRY_COUNT):
|
raise ConfigEntryNotReady("API connection failed")
|
||||||
if await async_check_api(session, endpoint, headers, is_anthropic):
|
|
||||||
break
|
|
||||||
if attempt < API_RETRY_COUNT - 1:
|
|
||||||
delay = API_BACKOFF_FACTOR * (2 ** attempt)
|
|
||||||
await asyncio.sleep(delay)
|
|
||||||
else:
|
|
||||||
raise ConfigEntryNotReady("Failed to connect to API")
|
|
||||||
|
|
||||||
# Create and initialize coordinator
|
_LOGGER.debug("Creating API client for %s with endpoint %s", api_provider, endpoint)
|
||||||
coordinator = HATextAICoordinator(
|
|
||||||
hass,
|
api_client = APIClient(
|
||||||
api_key=api_key,
|
|
||||||
endpoint=endpoint,
|
|
||||||
model=model,
|
|
||||||
temperature=entry.data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
|
|
||||||
max_tokens=entry.data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
|
|
||||||
request_interval=entry.data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL),
|
|
||||||
session=session,
|
session=session,
|
||||||
is_anthropic=is_anthropic
|
endpoint=endpoint,
|
||||||
|
headers=headers,
|
||||||
|
api_provider=api_provider,
|
||||||
|
model=model,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize coordinator
|
coordinator = HATextAICoordinator(
|
||||||
await coordinator.async_config_entry_first_refresh()
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
# Check coordinator status
|
_LOGGER.debug(f"Created coordinator for {instance_name}")
|
||||||
if coordinator.endpoint_status == "auth_error":
|
|
||||||
raise ConfigEntryNotReady("Authentication failed")
|
|
||||||
elif coordinator.endpoint_status == "rate_limited":
|
|
||||||
_LOGGER.warning("API rate limited during setup")
|
|
||||||
elif coordinator.endpoint_status == "maintenance":
|
|
||||||
raise ConfigEntryNotReady("API is in maintenance mode")
|
|
||||||
elif coordinator.endpoint_status == "error":
|
|
||||||
raise ConfigEntryNotReady("API error during setup")
|
|
||||||
elif not coordinator.last_update_success:
|
|
||||||
raise ConfigEntryNotReady("Failed to initialize coordinator")
|
|
||||||
|
|
||||||
|
# Store coordinator
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||||
|
|
||||||
|
_LOGGER.debug(f"Stored coordinator in hass.data[{DOMAIN}][{entry.entry_id}]")
|
||||||
|
|
||||||
|
# Set up platforms
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
# Register event handlers
|
_LOGGER.debug(f"Setup completed for {instance_name}")
|
||||||
@callback
|
|
||||||
def handle_state_change(event):
|
|
||||||
"""Handle state changes."""
|
|
||||||
if event.data.get("entity_id").startswith(f"{DOMAIN}."):
|
|
||||||
_LOGGER.debug("State changed: %s", event.data)
|
|
||||||
|
|
||||||
hass.bus.async_listen(EVENT_STATE_CHANGED, handle_state_change)
|
|
||||||
|
|
||||||
# Register services
|
|
||||||
async def async_ask_question(call: ServiceCall) -> None:
|
|
||||||
"""Handle the ask_question service call."""
|
|
||||||
question = call.data.get("question", "")
|
|
||||||
if not question:
|
|
||||||
_LOGGER.error("No question provided in service call")
|
|
||||||
return
|
|
||||||
|
|
||||||
request_params = {}
|
|
||||||
for param in ["system_prompt", "model", "temperature", "max_tokens"]:
|
|
||||||
if param in call.data:
|
|
||||||
request_params[param] = call.data[param]
|
|
||||||
|
|
||||||
try:
|
|
||||||
await coordinator.async_ask_question(question, **request_params)
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.error("Error asking question: %s", str(err))
|
|
||||||
|
|
||||||
async def async_clear_history(call: ServiceCall) -> None:
|
|
||||||
"""Handle the clear_history service call."""
|
|
||||||
try:
|
|
||||||
coordinator._responses.clear()
|
|
||||||
await coordinator.async_refresh()
|
|
||||||
_LOGGER.info("History cleared successfully")
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.error("Error clearing history: %s", str(err))
|
|
||||||
|
|
||||||
async def async_get_history(call: ServiceCall) -> dict:
|
|
||||||
"""Handle the get_history service call."""
|
|
||||||
try:
|
|
||||||
limit = min(int(call.data.get("limit", 10)), 100)
|
|
||||||
filter_model = str(call.data.get("filter_model", ""))
|
|
||||||
start_date = call.data.get("start_date")
|
|
||||||
include_metadata = call.data.get("include_metadata", False)
|
|
||||||
|
|
||||||
responses = coordinator._responses
|
|
||||||
metrics = {
|
|
||||||
"total_requests": coordinator.request_count,
|
|
||||||
"total_tokens": coordinator.tokens_used,
|
|
||||||
"api_version": coordinator.api_version,
|
|
||||||
"endpoint_status": coordinator.endpoint_status,
|
|
||||||
"error_count": coordinator.error_count
|
|
||||||
}
|
|
||||||
|
|
||||||
filtered_responses = responses.copy()
|
|
||||||
|
|
||||||
if filter_model:
|
|
||||||
filtered_responses = {
|
|
||||||
k: v for k, v in filtered_responses.items()
|
|
||||||
if v.get("model") == filter_model
|
|
||||||
}
|
|
||||||
|
|
||||||
if start_date:
|
|
||||||
filtered_responses = {
|
|
||||||
k: v for k, v in filtered_responses.items()
|
|
||||||
if v.get("timestamp") >= start_date
|
|
||||||
}
|
|
||||||
|
|
||||||
if not include_metadata:
|
|
||||||
filtered_responses = {
|
|
||||||
k: {
|
|
||||||
"question": v["question"],
|
|
||||||
"response": v["response"],
|
|
||||||
"timestamp": v["timestamp"]
|
|
||||||
} for k, v in filtered_responses.items()
|
|
||||||
}
|
|
||||||
|
|
||||||
sorted_responses = dict(
|
|
||||||
sorted(
|
|
||||||
filtered_responses.items(),
|
|
||||||
key=lambda x: x[1]["timestamp"],
|
|
||||||
reverse=True
|
|
||||||
)[:limit]
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"metrics": metrics,
|
|
||||||
"responses": sorted_responses
|
|
||||||
}
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.error("Error getting history: %s", str(err))
|
|
||||||
return {}
|
|
||||||
|
|
||||||
async def async_set_system_prompt(call: ServiceCall) -> None:
|
|
||||||
"""Handle the set_system_prompt service call."""
|
|
||||||
try:
|
|
||||||
prompt = str(call.data.get("prompt", "")).strip()
|
|
||||||
if prompt:
|
|
||||||
coordinator.system_prompt = prompt
|
|
||||||
_LOGGER.info("System prompt updated successfully")
|
|
||||||
else:
|
|
||||||
_LOGGER.error("Empty prompt provided")
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.error("Error setting system prompt: %s", str(err))
|
|
||||||
|
|
||||||
# Register services with validation
|
|
||||||
hass.services.async_register(
|
|
||||||
DOMAIN,
|
|
||||||
"ask_question",
|
|
||||||
async_ask_question,
|
|
||||||
schema=SERVICE_SCHEMA_ASK_QUESTION
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.services.async_register(
|
|
||||||
DOMAIN,
|
|
||||||
"clear_history",
|
|
||||||
async_clear_history
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.services.async_register(
|
|
||||||
DOMAIN,
|
|
||||||
"get_history",
|
|
||||||
async_get_history,
|
|
||||||
schema=SERVICE_SCHEMA_GET_HISTORY
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.services.async_register(
|
|
||||||
DOMAIN,
|
|
||||||
"set_system_prompt",
|
|
||||||
async_set_system_prompt,
|
|
||||||
schema=SERVICE_SCHEMA_SET_SYSTEM_PROMPT
|
|
||||||
)
|
|
||||||
|
|
||||||
_LOGGER.info(
|
|
||||||
"Successfully set up HA Text AI with model: %s",
|
|
||||||
entry.data.get(CONF_MODEL, DEFAULT_MODEL)
|
|
||||||
)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as err:
|
||||||
_LOGGER.exception("Setup error: %s", str(ex))
|
_LOGGER.exception(f"Error setting up HA Text AI: {err}")
|
||||||
raise ConfigEntryNotReady from ex
|
raise
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
try:
|
try:
|
||||||
coordinator = hass.data[DOMAIN].get(entry.entry_id)
|
if entry.entry_id in hass.data[DOMAIN]:
|
||||||
if coordinator:
|
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
# Clear queue and history
|
|
||||||
coordinator._responses.clear()
|
if hasattr(coordinator.client, 'shutdown'):
|
||||||
while not coordinator._question_queue.empty():
|
await coordinator.client.shutdown()
|
||||||
try:
|
|
||||||
coordinator._question_queue.get_nowait()
|
|
||||||
coordinator._question_queue.task_done()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Close connection
|
|
||||||
await coordinator.async_shutdown()
|
await coordinator.async_shutdown()
|
||||||
|
|
||||||
# Remove services
|
|
||||||
for service in ["ask_question", "clear_history", "get_history", "set_system_prompt"]:
|
|
||||||
hass.services.async_remove(DOMAIN, service)
|
|
||||||
|
|
||||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
||||||
if unload_ok:
|
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
return unload_ok
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
_LOGGER.exception("Error unloading entry: %s", str(ex))
|
_LOGGER.exception("Error unloading entry: %s", str(ex))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
||||||
"""Migrate old entry."""
|
|
||||||
_LOGGER.debug("Migrating from version %s", entry.version)
|
|
||||||
|
|
||||||
if entry.version == 1:
|
|
||||||
new = {**entry.data}
|
|
||||||
|
|
||||||
# Migrate settings
|
|
||||||
if CONF_MODEL in new and new[CONF_MODEL] not in SUPPORTED_MODELS:
|
|
||||||
new[CONF_MODEL] = DEFAULT_MODEL
|
|
||||||
|
|
||||||
entry.version = 2
|
|
||||||
hass.config_entries.async_update_entry(entry, data=new)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -1,17 +1,14 @@
|
|||||||
"""Config flow for HA text AI integration."""
|
"""Config flow for HA text AI integration."""
|
||||||
from typing import Any, Dict, Optional, Tuple
|
import logging
|
||||||
import voluptuous as vol
|
from typing import Any, Dict, Optional
|
||||||
import asyncio
|
|
||||||
import aiohttp
|
|
||||||
from async_timeout import timeout
|
|
||||||
from urllib.parse import urlparse, urljoin
|
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.const import CONF_API_KEY
|
from homeassistant.const import CONF_API_KEY, CONF_NAME
|
||||||
import homeassistant.helpers.config_validation as cv
|
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers import selector
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
@@ -20,269 +17,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,
|
MIN_TEMPERATURE,
|
||||||
MAX_TEMPERATURE,
|
MAX_TEMPERATURE,
|
||||||
MIN_MAX_TOKENS,
|
MIN_MAX_TOKENS,
|
||||||
MAX_MAX_TOKENS,
|
MAX_MAX_TOKENS,
|
||||||
MIN_REQUEST_INTERVAL,
|
MIN_REQUEST_INTERVAL,
|
||||||
API_VERSION,
|
DEFAULT_NAME_PREFIX,
|
||||||
API_MODELS_PATH,
|
DEFAULT_MAX_HISTORY,
|
||||||
ERROR_INVALID_API_KEY,
|
CONF_MAX_HISTORY_SIZE,
|
||||||
ERROR_CANNOT_CONNECT,
|
|
||||||
ERROR_UNKNOWN,
|
|
||||||
ERROR_INVALID_MODEL,
|
|
||||||
ERROR_RATE_LIMIT,
|
|
||||||
ERROR_API_ERROR,
|
|
||||||
ERROR_TIMEOUT,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
import logging
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Required(CONF_API_KEY): cv.string,
|
|
||||||
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): cv.string,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_TEMPERATURE,
|
|
||||||
default=DEFAULT_TEMPERATURE
|
|
||||||
): vol.All(
|
|
||||||
vol.Coerce(float),
|
|
||||||
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
|
||||||
),
|
|
||||||
vol.Optional(
|
|
||||||
CONF_MAX_TOKENS,
|
|
||||||
default=DEFAULT_MAX_TOKENS
|
|
||||||
): vol.All(
|
|
||||||
vol.Coerce(int),
|
|
||||||
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
|
||||||
),
|
|
||||||
vol.Optional(
|
|
||||||
CONF_API_ENDPOINT,
|
|
||||||
default=DEFAULT_API_ENDPOINT
|
|
||||||
): cv.string,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_REQUEST_INTERVAL,
|
|
||||||
default=DEFAULT_REQUEST_INTERVAL
|
|
||||||
): vol.All(
|
|
||||||
vol.Coerce(float),
|
|
||||||
vol.Range(min=MIN_REQUEST_INTERVAL)
|
|
||||||
),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
async def validate_api_connection(
|
def normalize_name(name: str) -> str:
|
||||||
hass,
|
"""Normalize name to conform to HA naming convention using underscores."""
|
||||||
api_key: str,
|
normalized = ''.join(c if c.isalnum() or c == '_' else '_' for c in name)
|
||||||
endpoint: str,
|
normalized = '_'.join(filter(None, normalized.split('_')))
|
||||||
model: str,
|
return normalized.lower()
|
||||||
retry_count: int = 3,
|
|
||||||
retry_delay: float = 1.0
|
|
||||||
) -> Tuple[bool, str, list]:
|
|
||||||
"""Validate API connection with retry logic."""
|
|
||||||
session = async_get_clientsession(hass)
|
|
||||||
headers = {
|
|
||||||
"Authorization": f"Bearer {api_key}",
|
|
||||||
"Content-Type": "application/json; charset=utf-8",
|
|
||||||
"Accept": "application/json; charset=utf-8",
|
|
||||||
"Accept-Charset": "utf-8"
|
|
||||||
}
|
|
||||||
|
|
||||||
base_url = endpoint.rstrip('/')
|
|
||||||
if not base_url.endswith(f"/{API_VERSION}"):
|
|
||||||
base_url = f"{base_url}/{API_VERSION}"
|
|
||||||
|
|
||||||
models_url = f"{base_url}/{API_MODELS_PATH}"
|
|
||||||
_LOGGER.debug("Attempting to connect to: %s", models_url)
|
|
||||||
|
|
||||||
for attempt in range(retry_count):
|
|
||||||
try:
|
|
||||||
async with timeout(10):
|
|
||||||
async with session.get(models_url, headers=headers) as response:
|
|
||||||
if response.status == 200:
|
|
||||||
data = await response.json()
|
|
||||||
model_ids = [m["id"] for m in data.get("data", [])]
|
|
||||||
|
|
||||||
_LOGGER.debug("Available models: %s", ", ".join(model_ids))
|
|
||||||
|
|
||||||
if model not in model_ids:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Model %s not found in available models: %s",
|
|
||||||
model,
|
|
||||||
", ".join(model_ids)
|
|
||||||
)
|
|
||||||
return False, ERROR_INVALID_MODEL, model_ids
|
|
||||||
return True, "", model_ids
|
|
||||||
|
|
||||||
elif response.status == 401:
|
|
||||||
_LOGGER.error("Authentication failed")
|
|
||||||
return False, ERROR_INVALID_API_KEY, []
|
|
||||||
|
|
||||||
elif response.status == 429:
|
|
||||||
_LOGGER.error("Rate limit exceeded")
|
|
||||||
return False, ERROR_RATE_LIMIT, []
|
|
||||||
|
|
||||||
else:
|
|
||||||
response_text = await response.text()
|
|
||||||
_LOGGER.error(
|
|
||||||
"API error: %s - %s",
|
|
||||||
response.status,
|
|
||||||
response_text
|
|
||||||
)
|
|
||||||
return False, ERROR_API_ERROR, []
|
|
||||||
|
|
||||||
except asyncio.TimeoutError:
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Timeout during API validation (attempt %d/%d)",
|
|
||||||
attempt + 1,
|
|
||||||
retry_count
|
|
||||||
)
|
|
||||||
if attempt == retry_count - 1:
|
|
||||||
return False, ERROR_TIMEOUT, []
|
|
||||||
await asyncio.sleep(retry_delay)
|
|
||||||
|
|
||||||
except aiohttp.ClientError as err:
|
|
||||||
_LOGGER.error("Connection error: %s", str(err))
|
|
||||||
return False, ERROR_CANNOT_CONNECT, []
|
|
||||||
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.exception("Unexpected error during validation: %s", str(err))
|
|
||||||
return False, ERROR_UNKNOWN, []
|
|
||||||
|
|
||||||
return False, ERROR_UNKNOWN, []
|
|
||||||
|
|
||||||
class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
class HATextAIConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for HA text AI."""
|
"""Handle a config flow for HA text AI."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
|
|
||||||
async def async_step_user(
|
def __init__(self) -> None:
|
||||||
self,
|
"""Initialize flow."""
|
||||||
user_input: Optional[Dict[str, Any]] = None
|
self._errors = {}
|
||||||
) -> FlowResult:
|
self._data = {}
|
||||||
|
self._provider = None
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
|
||||||
"""Handle the initial step."""
|
"""Handle the initial step."""
|
||||||
errors: Dict[str, str] = {}
|
if user_input is None:
|
||||||
|
return self.async_show_form(
|
||||||
if user_input is not None:
|
step_id="user",
|
||||||
try:
|
data_schema=vol.Schema({
|
||||||
endpoint = user_input.get(CONF_API_ENDPOINT, DEFAULT_API_ENDPOINT)
|
vol.Required(CONF_API_PROVIDER): selector.SelectSelector(
|
||||||
try:
|
selector.SelectSelectorConfig(
|
||||||
result = urlparse(endpoint)
|
options=API_PROVIDERS,
|
||||||
if not all([result.scheme, result.netloc]):
|
translation_key="api_provider"
|
||||||
errors["base"] = "invalid_url_format"
|
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user",
|
|
||||||
data_schema=STEP_USER_DATA_SCHEMA,
|
|
||||||
errors=errors
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
),
|
||||||
_LOGGER.error("URL parsing error: %s", str(e))
|
})
|
||||||
errors["base"] = "invalid_url_format"
|
)
|
||||||
return self.async_show_form(
|
|
||||||
step_id="user",
|
|
||||||
data_schema=STEP_USER_DATA_SCHEMA,
|
|
||||||
errors=errors
|
|
||||||
)
|
|
||||||
|
|
||||||
validated_input = STEP_USER_DATA_SCHEMA(user_input)
|
self._provider = user_input[CONF_API_PROVIDER]
|
||||||
|
return await self.async_step_provider()
|
||||||
|
|
||||||
is_valid, error_code, available_models = await validate_api_connection(
|
async def async_step_provider(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
|
||||||
self.hass,
|
"""Handle provider configuration step."""
|
||||||
validated_input[CONF_API_KEY],
|
self._errors = {}
|
||||||
endpoint,
|
|
||||||
validated_input[CONF_MODEL]
|
if user_input is None:
|
||||||
|
default_endpoint = (
|
||||||
|
DEFAULT_OPENAI_ENDPOINT if self._provider == API_PROVIDER_OPENAI
|
||||||
|
else DEFAULT_ANTHROPIC_ENDPOINT
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="provider",
|
||||||
|
data_schema=vol.Schema({
|
||||||
|
vol.Required(CONF_NAME, default="my_assistant"): str,
|
||||||
|
vol.Required(CONF_API_KEY): str,
|
||||||
|
vol.Required(CONF_MODEL, default=DEFAULT_MODEL): str,
|
||||||
|
vol.Required(CONF_API_ENDPOINT, default=default_endpoint): str,
|
||||||
|
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_MAX_TOKENS, default=DEFAULT_MAX_TOKENS): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_REQUEST_INTERVAL)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_CONTEXT_MESSAGES,
|
||||||
|
default=DEFAULT_CONTEXT_MESSAGES
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=20)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MAX_HISTORY_SIZE,
|
||||||
|
default=DEFAULT_MAX_HISTORY
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=100)
|
||||||
|
),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
input_copy = user_input.copy()
|
||||||
|
|
||||||
|
try:
|
||||||
|
normalized_name = self._validate_and_normalize_name(input_copy[CONF_NAME])
|
||||||
|
input_copy[CONF_NAME] = normalized_name
|
||||||
|
except ValueError as e:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="provider",
|
||||||
|
data_schema=vol.Schema({
|
||||||
|
vol.Required(CONF_NAME, default=input_copy[CONF_NAME]): str,
|
||||||
|
vol.Required(CONF_API_KEY, default=input_copy[CONF_API_KEY]): str,
|
||||||
|
vol.Required(CONF_MODEL, default=input_copy[CONF_MODEL]): str,
|
||||||
|
vol.Required(CONF_API_ENDPOINT, default=input_copy[CONF_API_ENDPOINT]): str,
|
||||||
|
vol.Optional(CONF_TEMPERATURE, default=input_copy.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
errors={"name": str(e)}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not await self._async_validate_api(input_copy):
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="provider",
|
||||||
|
data_schema=vol.Schema({
|
||||||
|
}),
|
||||||
|
errors=self._errors
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="provider",
|
||||||
|
data_schema=vol.Schema({
|
||||||
|
}),
|
||||||
|
errors={"base": str(e)}
|
||||||
|
)
|
||||||
|
|
||||||
if is_valid:
|
return await self._create_entry(input_copy)
|
||||||
await self.async_set_unique_id(validated_input[CONF_API_KEY])
|
|
||||||
self._abort_if_unique_id_configured()
|
|
||||||
|
|
||||||
return self.async_create_entry(
|
def _validate_and_normalize_name(self, name: str) -> str:
|
||||||
title="HA Text AI",
|
"""
|
||||||
data=validated_input
|
Validate and normalize name with detailed error handling.
|
||||||
)
|
|
||||||
|
|
||||||
errors["base"] = error_code
|
Raises:
|
||||||
if error_code == ERROR_INVALID_MODEL:
|
ValueError: If name is invalid
|
||||||
_LOGGER.warning(
|
|
||||||
"Selected model %s not found in available models: %s",
|
|
||||||
validated_input[CONF_MODEL],
|
|
||||||
", ".join(available_models)
|
|
||||||
)
|
|
||||||
|
|
||||||
except vol.Invalid as err:
|
Returns:
|
||||||
_LOGGER.error("Validation error: %s", str(err))
|
Normalized name
|
||||||
errors["base"] = "invalid_input"
|
"""
|
||||||
|
if not name:
|
||||||
|
raise ValueError("empty")
|
||||||
|
|
||||||
return self.async_show_form(
|
name = name.strip()
|
||||||
step_id="user",
|
normalized = ''.join(
|
||||||
data_schema=STEP_USER_DATA_SCHEMA,
|
c if c.isalnum() or c in ' _' else '_' # Only allow underscores
|
||||||
errors=errors,
|
for c in name
|
||||||
description_placeholders={
|
)
|
||||||
"default_model": DEFAULT_MODEL,
|
|
||||||
"default_endpoint": DEFAULT_API_ENDPOINT,
|
normalized = normalized.replace(' ', '_').lower()
|
||||||
|
|
||||||
|
for entry in self._async_current_entries():
|
||||||
|
if entry.data.get(CONF_NAME, "") == normalized:
|
||||||
|
raise ValueError("name_exists")
|
||||||
|
|
||||||
|
normalized = normalized[:50]
|
||||||
|
|
||||||
|
if not normalized:
|
||||||
|
raise ValueError("empty")
|
||||||
|
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
async def _async_validate_api(self, user_input: Dict[str, Any]) -> bool:
|
||||||
|
"""Validate API connection."""
|
||||||
|
try:
|
||||||
|
session = async_get_clientsession(self.hass)
|
||||||
|
headers = self._get_api_headers(user_input)
|
||||||
|
endpoint = user_input[CONF_API_ENDPOINT].rstrip('/')
|
||||||
|
|
||||||
|
check_url = (
|
||||||
|
f"{endpoint}/v1/models" if self._provider == API_PROVIDER_ANTHROPIC
|
||||||
|
else f"{endpoint}/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
async with session.get(check_url, headers=headers) as response:
|
||||||
|
if response.status == 401:
|
||||||
|
self._errors["base"] = "invalid_auth"
|
||||||
|
return False
|
||||||
|
elif response.status not in [200, 404]:
|
||||||
|
self._errors["base"] = "cannot_connect"
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("API validation error: %s", str(err))
|
||||||
|
self._errors["base"] = "cannot_connect"
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_api_headers(self, user_input: Dict[str, Any]) -> Dict[str, str]:
|
||||||
|
"""Get API headers based on provider."""
|
||||||
|
api_key = user_input[CONF_API_KEY]
|
||||||
|
|
||||||
|
if self._provider == API_PROVIDER_ANTHROPIC:
|
||||||
|
return {
|
||||||
|
"x-api-key": api_key,
|
||||||
|
"anthropic-version": "2023-06-01",
|
||||||
|
"Content-Type": "application/json"
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
"Authorization": f"Bearer {api_key}",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _create_entry(self, user_input: Dict[str, Any]) -> FlowResult:
|
||||||
|
"""Create the config entry with comprehensive data preservation."""
|
||||||
|
instance_name = user_input[CONF_NAME]
|
||||||
|
normalized_name = normalize_name(instance_name)
|
||||||
|
|
||||||
|
unique_id = f"{DOMAIN}_{normalized_name}_{self._provider}".lower()
|
||||||
|
|
||||||
|
entry_data = {
|
||||||
|
CONF_API_PROVIDER: self._provider,
|
||||||
|
CONF_NAME: instance_name,
|
||||||
|
"normalized_name": normalized_name,
|
||||||
|
CONF_API_KEY: user_input.get(CONF_API_KEY),
|
||||||
|
CONF_API_ENDPOINT: user_input.get(CONF_API_ENDPOINT),
|
||||||
|
"unique_id": unique_id,
|
||||||
|
CONF_MODEL: user_input.get(CONF_MODEL, DEFAULT_MODEL),
|
||||||
|
CONF_TEMPERATURE: user_input.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE),
|
||||||
|
CONF_MAX_TOKENS: user_input.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS),
|
||||||
|
CONF_REQUEST_INTERVAL: user_input.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL),
|
||||||
|
CONF_CONTEXT_MESSAGES: user_input.get(CONF_CONTEXT_MESSAGES, DEFAULT_CONTEXT_MESSAGES),
|
||||||
|
CONF_MAX_HISTORY_SIZE: user_input.get(CONF_MAX_HISTORY_SIZE, DEFAULT_MAX_HISTORY),
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, value in user_input.items():
|
||||||
|
if key not in entry_data:
|
||||||
|
entry_data[key] = value
|
||||||
|
|
||||||
|
_LOGGER.debug(f"Creating config entry with data: {entry_data}")
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=instance_name,
|
||||||
|
data=entry_data
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(
|
def async_get_options_flow(config_entry: config_entries.ConfigEntry) -> config_entries.OptionsFlow:
|
||||||
config_entry: config_entries.ConfigEntry,
|
|
||||||
) -> config_entries.OptionsFlow:
|
|
||||||
"""Get the options flow for this handler."""
|
"""Get the options flow for this handler."""
|
||||||
return OptionsFlowHandler(config_entry)
|
return OptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
|
|
||||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
"""Handle options flow for HA text AI."""
|
"""Handle options flow."""
|
||||||
|
|
||||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||||
"""Initialize options flow."""
|
"""Initialize options flow."""
|
||||||
self.config_entry = config_entry
|
self.config_entry = config_entry
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(self, user_input: Optional[Dict[str, Any]] = None) -> FlowResult:
|
||||||
self,
|
"""Manage the options."""
|
||||||
user_input: Optional[Dict[str, Any]] = None
|
|
||||||
) -> FlowResult:
|
|
||||||
"""Handle options flow."""
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
return self.async_create_entry(title="", data=user_input)
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
options_schema = vol.Schema({
|
current_data = {**self.config_entry.data, **self.config_entry.options}
|
||||||
vol.Optional(
|
|
||||||
CONF_TEMPERATURE,
|
|
||||||
default=self.config_entry.options.get(
|
|
||||||
CONF_TEMPERATURE, DEFAULT_TEMPERATURE
|
|
||||||
),
|
|
||||||
): vol.All(
|
|
||||||
vol.Coerce(float),
|
|
||||||
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
|
||||||
),
|
|
||||||
vol.Optional(
|
|
||||||
CONF_MAX_TOKENS,
|
|
||||||
default=self.config_entry.options.get(
|
|
||||||
CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS
|
|
||||||
),
|
|
||||||
): vol.All(
|
|
||||||
vol.Coerce(int),
|
|
||||||
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
|
||||||
),
|
|
||||||
vol.Optional(
|
|
||||||
CONF_REQUEST_INTERVAL,
|
|
||||||
default=self.config_entry.options.get(
|
|
||||||
CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL
|
|
||||||
),
|
|
||||||
): vol.All(
|
|
||||||
vol.Coerce(float),
|
|
||||||
vol.Range(min=MIN_REQUEST_INTERVAL)
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="init",
|
step_id="init",
|
||||||
data_schema=options_schema,
|
data_schema=vol.Schema({
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MODEL,
|
||||||
|
default=current_data.get(CONF_MODEL, DEFAULT_MODEL)
|
||||||
|
): str,
|
||||||
|
vol.Optional(
|
||||||
|
CONF_TEMPERATURE,
|
||||||
|
default=current_data.get(CONF_TEMPERATURE, DEFAULT_TEMPERATURE)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_MAX_TOKENS,
|
||||||
|
default=current_data.get(CONF_MAX_TOKENS, DEFAULT_MAX_TOKENS)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_REQUEST_INTERVAL,
|
||||||
|
default=current_data.get(CONF_REQUEST_INTERVAL, DEFAULT_REQUEST_INTERVAL)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_REQUEST_INTERVAL)
|
||||||
|
),
|
||||||
|
vol.Optional(
|
||||||
|
CONF_CONTEXT_MESSAGES,
|
||||||
|
default=current_data.get(
|
||||||
|
CONF_CONTEXT_MESSAGES,
|
||||||
|
DEFAULT_CONTEXT_MESSAGES
|
||||||
|
)
|
||||||
|
): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=20)
|
||||||
|
),
|
||||||
|
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)
|
||||||
|
),
|
||||||
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,26 @@
|
|||||||
"""Constants for the HA text AI integration."""
|
"""Constants for the HA text AI integration."""
|
||||||
from typing import Final
|
from typing import Final
|
||||||
from homeassistant.const import Platform
|
import voluptuous as vol
|
||||||
|
from homeassistant.const import Platform, CONF_API_KEY, CONF_NAME
|
||||||
|
from homeassistant.helpers import config_validation as cv
|
||||||
|
|
||||||
# Domain and platforms
|
# Domain and platforms
|
||||||
DOMAIN: Final = "ha_text_ai"
|
DOMAIN: Final = "ha_text_ai"
|
||||||
PLATFORMS: Final = [Platform.SENSOR]
|
PLATFORMS: list[str] = ["sensor"]
|
||||||
|
|
||||||
|
# Provider configuration
|
||||||
|
CONF_API_PROVIDER: Final = "api_provider"
|
||||||
|
API_PROVIDER_OPENAI: Final = "openai"
|
||||||
|
API_PROVIDER_ANTHROPIC: Final = "anthropic"
|
||||||
|
|
||||||
|
API_PROVIDERS: Final = [
|
||||||
|
API_PROVIDER_OPENAI,
|
||||||
|
API_PROVIDER_ANTHROPIC
|
||||||
|
]
|
||||||
|
|
||||||
|
# Default endpoints
|
||||||
|
DEFAULT_OPENAI_ENDPOINT: Final = "https://api.openai.com/v1"
|
||||||
|
DEFAULT_ANTHROPIC_ENDPOINT: Final = "https://api.anthropic.com"
|
||||||
|
|
||||||
# Configuration constants
|
# Configuration constants
|
||||||
CONF_MODEL: Final = "model"
|
CONF_MODEL: Final = "model"
|
||||||
@@ -12,28 +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"
|
||||||
# Model constants
|
CONF_MAX_HISTORY_SIZE: Final = "max_history_size" # Correct constant name
|
||||||
SUPPORTED_MODELS: Final = [
|
CONF_IS_ANTHROPIC: Final = "is_anthropic"
|
||||||
"gpt-3.5-turbo",
|
CONF_CONTEXT_MESSAGES: Final = "context_messages"
|
||||||
"gpt-3.5-turbo-16k",
|
|
||||||
"gpt-4",
|
|
||||||
"gpt-4-32k",
|
|
||||||
"gpt-4-1106-preview",
|
|
||||||
"claude-3-sonnet",
|
|
||||||
"claude-3-opus"
|
|
||||||
]
|
|
||||||
|
|
||||||
# Default values
|
# Default values
|
||||||
DEFAULT_MODEL: Final = "gpt-3.5-turbo"
|
DEFAULT_MODEL: Final = "gpt-4o-mini"
|
||||||
DEFAULT_TEMPERATURE: Final = 0.1
|
DEFAULT_TEMPERATURE: Final = 0.1
|
||||||
DEFAULT_MAX_TOKENS: Final = 1000
|
DEFAULT_MAX_TOKENS: Final = 1000
|
||||||
DEFAULT_API_ENDPOINT: Final = "https://api.openai.com"
|
|
||||||
DEFAULT_REQUEST_INTERVAL: Final = 1.0
|
DEFAULT_REQUEST_INTERVAL: Final = 1.0
|
||||||
DEFAULT_TIMEOUT: Final = 30
|
DEFAULT_TIMEOUT: Final = 30
|
||||||
DEFAULT_QUEUE_SIZE: Final = 100
|
DEFAULT_MAX_HISTORY: Final = 50
|
||||||
DEFAULT_HISTORY_LIMIT: Final = 50
|
DEFAULT_NAME: Final = "HA Text AI"
|
||||||
DEFAULT_RETRY_COUNT: Final = 3
|
DEFAULT_NAME_PREFIX = "ha_text_ai"
|
||||||
|
DEFAULT_CONTEXT_MESSAGES: Final = 5
|
||||||
|
|
||||||
# Parameter constraints
|
# Parameter constraints
|
||||||
MIN_TEMPERATURE: Final = 0.0
|
MIN_TEMPERATURE: Final = 0.0
|
||||||
@@ -41,25 +50,11 @@ MAX_TEMPERATURE: Final = 2.0
|
|||||||
MIN_MAX_TOKENS: Final = 1
|
MIN_MAX_TOKENS: Final = 1
|
||||||
MAX_MAX_TOKENS: Final = 4096
|
MAX_MAX_TOKENS: Final = 4096
|
||||||
MIN_REQUEST_INTERVAL: Final = 0.1
|
MIN_REQUEST_INTERVAL: Final = 0.1
|
||||||
MIN_TIMEOUT: Final = 5
|
MAX_REQUEST_INTERVAL: Final = 60.0
|
||||||
MAX_TIMEOUT: Final = 120
|
|
||||||
MAX_PROMPT_LENGTH: Final = 1000
|
|
||||||
MAX_HISTORY_LIMIT: Final = 100
|
|
||||||
|
|
||||||
# API constants
|
# API constants
|
||||||
API_VERSION: Final = "v1"
|
|
||||||
API_MODELS_PATH: Final = "models"
|
|
||||||
API_CHAT_PATH: Final = "chat/completions"
|
|
||||||
API_TIMEOUT: Final = 30
|
API_TIMEOUT: Final = 30
|
||||||
API_RETRY_COUNT: Final = 3
|
API_RETRY_COUNT: Final = 3
|
||||||
API_BACKOFF_FACTOR: Final = 1.5
|
|
||||||
API_MAX_RETRIES: Final = 3
|
|
||||||
|
|
||||||
# History constants
|
|
||||||
HISTORY_FILTER_MODEL: Final = "filter_model"
|
|
||||||
HISTORY_FILTER_DATE: Final = "start_date"
|
|
||||||
HISTORY_SORT_ORDER: Final = "sort_order"
|
|
||||||
HISTORY_INCLUDE_METADATA: Final = "include_metadata"
|
|
||||||
|
|
||||||
# Service names
|
# Service names
|
||||||
SERVICE_ASK_QUESTION: Final = "ask_question"
|
SERVICE_ASK_QUESTION: Final = "ask_question"
|
||||||
@@ -67,33 +62,48 @@ SERVICE_CLEAR_HISTORY: Final = "clear_history"
|
|||||||
SERVICE_GET_HISTORY: Final = "get_history"
|
SERVICE_GET_HISTORY: Final = "get_history"
|
||||||
SERVICE_SET_SYSTEM_PROMPT: Final = "set_system_prompt"
|
SERVICE_SET_SYSTEM_PROMPT: Final = "set_system_prompt"
|
||||||
|
|
||||||
# Service descriptions
|
|
||||||
SERVICE_ASK_QUESTION_DESCRIPTION: Final = "Ask a question to the AI model"
|
|
||||||
SERVICE_CLEAR_HISTORY_DESCRIPTION: Final = "Clear conversation history"
|
|
||||||
SERVICE_GET_HISTORY_DESCRIPTION: Final = "Get conversation history"
|
|
||||||
SERVICE_SET_SYSTEM_PROMPT_DESCRIPTION: Final = "Set system prompt for AI model"
|
|
||||||
|
|
||||||
# Attribute keys
|
# Attribute keys
|
||||||
ATTR_QUESTION: Final = "question"
|
ATTR_QUESTION: Final = "question"
|
||||||
ATTR_RESPONSE: Final = "response"
|
ATTR_RESPONSE: Final = "response"
|
||||||
ATTR_LAST_UPDATED: Final = "last_updated"
|
ATTR_INSTANCE: Final = "instance"
|
||||||
ATTR_MODEL: Final = "model"
|
ATTR_MODEL: Final = "model"
|
||||||
ATTR_TEMPERATURE: Final = "temperature"
|
ATTR_TEMPERATURE: Final = "temperature"
|
||||||
ATTR_MAX_TOKENS: Final = "max_tokens"
|
ATTR_MAX_TOKENS: Final = "max_tokens"
|
||||||
ATTR_TOTAL_RESPONSES: Final = "total_responses"
|
|
||||||
ATTR_SYSTEM_PROMPT: Final = "system_prompt"
|
ATTR_SYSTEM_PROMPT: Final = "system_prompt"
|
||||||
ATTR_RESPONSE_TIME: Final = "response_time"
|
|
||||||
ATTR_QUEUE_SIZE: Final = "queue_size"
|
|
||||||
ATTR_API_STATUS: Final = "api_status"
|
ATTR_API_STATUS: Final = "api_status"
|
||||||
ATTR_ERROR_COUNT: Final = "error_count"
|
ATTR_ERROR_COUNT: Final = "error_count"
|
||||||
|
ATTR_CONVERSATION_HISTORY: Final = "conversation_history"
|
||||||
|
|
||||||
|
# Sensor attributes
|
||||||
|
ATTR_TOTAL_RESPONSES: Final = "total_responses"
|
||||||
|
ATTR_TOTAL_ERRORS: Final = "total_errors"
|
||||||
|
ATTR_AVG_RESPONSE_TIME: Final = "average_response_time"
|
||||||
|
ATTR_LAST_REQUEST_TIME: Final = "last_request_time"
|
||||||
ATTR_LAST_ERROR: Final = "last_error"
|
ATTR_LAST_ERROR: Final = "last_error"
|
||||||
|
ATTR_IS_PROCESSING: Final = "is_processing"
|
||||||
|
ATTR_IS_RATE_LIMITED: Final = "is_rate_limited"
|
||||||
|
ATTR_IS_MAINTENANCE: Final = "is_maintenance"
|
||||||
ATTR_API_VERSION: Final = "api_version"
|
ATTR_API_VERSION: Final = "api_version"
|
||||||
ATTR_ENDPOINT_STATUS: Final = "endpoint_status"
|
ATTR_ENDPOINT_STATUS: Final = "endpoint_status"
|
||||||
ATTR_REQUEST_COUNT: Final = "request_count"
|
ATTR_PERFORMANCE_METRICS: Final = "performance_metrics"
|
||||||
ATTR_TOKENS_USED: Final = "tokens_used"
|
ATTR_HISTORY_SIZE: Final = "history_size"
|
||||||
ATTR_RETRY_COUNT: Final = "retry_count"
|
ATTR_UPTIME: Final = "uptime"
|
||||||
ATTR_QUEUE_POSITION: Final = "queue_position"
|
ATTR_API_PROVIDER: Final = "api_provider"
|
||||||
ATTR_ESTIMATED_WAIT: Final = "estimated_wait"
|
ATTR_METRICS: Final = "metrics"
|
||||||
|
ATTR_STATE: Final = "state"
|
||||||
|
ATTR_LAST_RESPONSE: Final = "last_response"
|
||||||
|
ATTR_ERROR: Final = "error"
|
||||||
|
ATTR_TIMESTAMP: Final = "timestamp"
|
||||||
|
|
||||||
|
# Sensor metrics
|
||||||
|
METRIC_TOTAL_TOKENS: Final = "total_tokens"
|
||||||
|
METRIC_PROMPT_TOKENS: Final = "prompt_tokens"
|
||||||
|
METRIC_COMPLETION_TOKENS: Final = "completion_tokens"
|
||||||
|
METRIC_SUCCESSFUL_REQUESTS: Final = "successful_requests"
|
||||||
|
METRIC_FAILED_REQUESTS: Final = "failed_requests"
|
||||||
|
METRIC_AVERAGE_LATENCY: Final = "average_latency"
|
||||||
|
METRIC_MAX_LATENCY: Final = "max_latency"
|
||||||
|
METRIC_MIN_LATENCY: Final = "min_latency"
|
||||||
|
|
||||||
# Error messages
|
# Error messages
|
||||||
ERROR_INVALID_API_KEY: Final = "invalid_api_key"
|
ERROR_INVALID_API_KEY: Final = "invalid_api_key"
|
||||||
@@ -104,81 +114,89 @@ ERROR_RATE_LIMIT: Final = "rate_limit_exceeded"
|
|||||||
ERROR_CONTEXT_LENGTH: Final = "context_length_exceeded"
|
ERROR_CONTEXT_LENGTH: Final = "context_length_exceeded"
|
||||||
ERROR_API_ERROR: Final = "api_error"
|
ERROR_API_ERROR: Final = "api_error"
|
||||||
ERROR_TIMEOUT: Final = "timeout_error"
|
ERROR_TIMEOUT: Final = "timeout_error"
|
||||||
ERROR_QUEUE_FULL: Final = "queue_full"
|
ERROR_INVALID_INSTANCE: Final = "invalid_instance"
|
||||||
ERROR_INVALID_PROMPT: Final = "invalid_prompt"
|
ERROR_NAME_EXISTS: Final = "name_exists"
|
||||||
ERROR_INVALID_PARAMETERS: Final = "invalid_parameters"
|
|
||||||
ERROR_SERVICE_UNAVAILABLE: Final = "service_unavailable"
|
|
||||||
|
|
||||||
# Configuration descriptions
|
|
||||||
CONF_MODEL_DESCRIPTION: Final = "AI model to use for responses"
|
|
||||||
CONF_TEMPERATURE_DESCRIPTION: Final = "Temperature for response generation (0-2)"
|
|
||||||
CONF_MAX_TOKENS_DESCRIPTION: Final = "Maximum tokens in response (1-4096)"
|
|
||||||
CONF_API_ENDPOINT_DESCRIPTION: Final = "API endpoint URL"
|
|
||||||
CONF_REQUEST_INTERVAL_DESCRIPTION: Final = "Minimum time between API requests (seconds)"
|
|
||||||
|
|
||||||
# Entity attributes descriptions
|
|
||||||
ATTR_QUESTION_DESCRIPTION: Final = "Last question asked"
|
|
||||||
ATTR_RESPONSE_DESCRIPTION: Final = "Last response received"
|
|
||||||
ATTR_LAST_UPDATED_DESCRIPTION: Final = "Time of last update"
|
|
||||||
ATTR_MODEL_DESCRIPTION: Final = "Current AI model in use"
|
|
||||||
ATTR_TEMPERATURE_DESCRIPTION: Final = "Current temperature setting"
|
|
||||||
ATTR_MAX_TOKENS_DESCRIPTION: Final = "Current max tokens setting"
|
|
||||||
ATTR_TOTAL_RESPONSES_DESCRIPTION: Final = "Total number of responses"
|
|
||||||
ATTR_SYSTEM_PROMPT_DESCRIPTION: Final = "Current system prompt"
|
|
||||||
ATTR_RESPONSE_TIME_DESCRIPTION: Final = "Time taken for last response"
|
|
||||||
ATTR_QUEUE_SIZE_DESCRIPTION: Final = "Current size of question queue"
|
|
||||||
ATTR_API_STATUS_DESCRIPTION: Final = "Current API connection status"
|
|
||||||
ATTR_ERROR_COUNT_DESCRIPTION: Final = "Total number of errors"
|
|
||||||
ATTR_LAST_ERROR_DESCRIPTION: Final = "Last error message"
|
|
||||||
ATTR_API_VERSION_DESCRIPTION: Final = "Current API version"
|
|
||||||
ATTR_ENDPOINT_STATUS_DESCRIPTION: Final = "Current endpoint status"
|
|
||||||
ATTR_REQUEST_COUNT_DESCRIPTION: Final = "Total number of API requests"
|
|
||||||
ATTR_TOKENS_USED_DESCRIPTION: Final = "Total tokens used"
|
|
||||||
|
|
||||||
# Entity attributes
|
# Entity attributes
|
||||||
ENTITY_NAME: Final = "HA Text AI"
|
|
||||||
ENTITY_ICON: Final = "mdi:robot"
|
ENTITY_ICON: Final = "mdi:robot"
|
||||||
ENTITY_ICON_ERROR: Final = "mdi:robot-dead"
|
ENTITY_ICON_ERROR: Final = "mdi:robot-dead"
|
||||||
ENTITY_ICON_PROCESSING: Final = "mdi:robot-excited"
|
ENTITY_ICON_PROCESSING: Final = "mdi:robot-excited"
|
||||||
ENTITY_ICON_OFFLINE: Final = "mdi:robot-off"
|
|
||||||
ENTITY_ICON_QUEUE: Final = "mdi:robot-confused"
|
|
||||||
|
|
||||||
# Translation keys
|
|
||||||
TRANSLATION_KEY_CONFIG: Final = "config"
|
|
||||||
TRANSLATION_KEY_OPTIONS: Final = "options"
|
|
||||||
TRANSLATION_KEY_ERROR: Final = "error"
|
|
||||||
TRANSLATION_KEY_STATE: Final = "state"
|
|
||||||
TRANSLATION_KEY_SERVICES: Final = "services"
|
|
||||||
|
|
||||||
# State attributes
|
# State attributes
|
||||||
STATE_READY: Final = "ready"
|
STATE_READY: Final = "ready"
|
||||||
STATE_PROCESSING: Final = "processing"
|
STATE_PROCESSING: Final = "processing"
|
||||||
STATE_ERROR: Final = "error"
|
STATE_ERROR: Final = "error"
|
||||||
STATE_DISCONNECTED: Final = "disconnected"
|
|
||||||
STATE_RATE_LIMITED: Final = "rate_limited"
|
|
||||||
STATE_INITIALIZING: Final = "initializing"
|
STATE_INITIALIZING: Final = "initializing"
|
||||||
STATE_MAINTENANCE: Final = "maintenance"
|
STATE_MAINTENANCE: Final = "maintenance"
|
||||||
STATE_RETRYING: Final = "retrying"
|
STATE_RATE_LIMITED: Final = "rate_limited"
|
||||||
STATE_QUEUED: Final = "queued"
|
STATE_DISCONNECTED: Final = "disconnected"
|
||||||
STATE_UPDATING: Final = "updating"
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
LOGGER_NAME: Final = "custom_components.ha_text_ai"
|
|
||||||
LOG_LEVEL_DEFAULT: Final = "INFO"
|
|
||||||
|
|
||||||
# Queue constants
|
|
||||||
QUEUE_TIMEOUT: Final = 5
|
|
||||||
QUEUE_MAX_SIZE: Final = 100
|
|
||||||
|
|
||||||
# Service schema constants
|
|
||||||
SCHEMA_QUESTION: Final = "question"
|
|
||||||
SCHEMA_MODEL: Final = "model"
|
|
||||||
SCHEMA_TEMPERATURE: Final = "temperature"
|
|
||||||
SCHEMA_MAX_TOKENS: Final = "max_tokens"
|
|
||||||
SCHEMA_PROMPT: Final = "prompt"
|
|
||||||
SCHEMA_LIMIT: Final = "limit"
|
|
||||||
|
|
||||||
# Event names
|
# Event names
|
||||||
EVENT_RESPONSE_RECEIVED: Final = f"{DOMAIN}_response_received"
|
EVENT_RESPONSE_RECEIVED: Final = f"{DOMAIN}_response_received"
|
||||||
EVENT_ERROR_OCCURRED: Final = f"{DOMAIN}_error_occurred"
|
EVENT_ERROR_OCCURRED: Final = f"{DOMAIN}_error_occurred"
|
||||||
EVENT_STATE_CHANGED: Final = f"{DOMAIN}_state_changed"
|
EVENT_STATE_CHANGED: Final = f"{DOMAIN}_state_changed"
|
||||||
|
|
||||||
|
# Service schema constants
|
||||||
|
SERVICE_SCHEMA_ASK_QUESTION = vol.Schema({
|
||||||
|
vol.Required(CONF_INSTANCE): cv.string,
|
||||||
|
vol.Required("question"): cv.string,
|
||||||
|
vol.Optional("system_prompt"): cv.string,
|
||||||
|
vol.Optional("model"): cv.string,
|
||||||
|
vol.Optional("temperature"): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||||
|
),
|
||||||
|
vol.Optional("max_tokens"): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
||||||
|
),
|
||||||
|
vol.Optional("context_messages"): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=20)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
SERVICE_SCHEMA_SET_SYSTEM_PROMPT = vol.Schema({
|
||||||
|
vol.Required(CONF_INSTANCE): cv.string,
|
||||||
|
vol.Required("prompt"): cv.string
|
||||||
|
})
|
||||||
|
|
||||||
|
SERVICE_SCHEMA_GET_HISTORY = vol.Schema({
|
||||||
|
vol.Required(CONF_INSTANCE): cv.string,
|
||||||
|
vol.Optional("limit", default=10): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=1, max=100),
|
||||||
|
),
|
||||||
|
vol.Optional("filter_model"): cv.string
|
||||||
|
})
|
||||||
|
|
||||||
|
# Configuration schema
|
||||||
|
CONFIG_SCHEMA = vol.Schema({
|
||||||
|
DOMAIN: vol.Schema({
|
||||||
|
vol.Required(CONF_NAME): cv.string,
|
||||||
|
vol.Required(CONF_API_KEY): cv.string,
|
||||||
|
vol.Required(CONF_API_PROVIDER): vol.In(API_PROVIDERS),
|
||||||
|
vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): cv.string,
|
||||||
|
vol.Optional(CONF_TEMPERATURE, default=DEFAULT_TEMPERATURE): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_TEMPERATURE, max=MAX_TEMPERATURE)
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_MAX_TOKENS, default=DEFAULT_MAX_TOKENS): vol.All(
|
||||||
|
vol.Coerce(int),
|
||||||
|
vol.Range(min=MIN_MAX_TOKENS, max=MAX_MAX_TOKENS)
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_API_ENDPOINT): cv.string,
|
||||||
|
vol.Optional(CONF_REQUEST_INTERVAL, default=DEFAULT_REQUEST_INTERVAL): vol.All(
|
||||||
|
vol.Coerce(float),
|
||||||
|
vol.Range(min=MIN_REQUEST_INTERVAL, max=MAX_REQUEST_INTERVAL)
|
||||||
|
),
|
||||||
|
vol.Optional(CONF_MAX_HISTORY_SIZE, default=DEFAULT_MAX_HISTORY): vol.All( # 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)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
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 |
@@ -1,42 +1,28 @@
|
|||||||
{
|
{
|
||||||
"domain": "ha_text_ai",
|
"domain": "ha_text_ai",
|
||||||
"name": "HA Text AI",
|
"name": "HA Text AI",
|
||||||
|
"after_dependencies": ["http"],
|
||||||
|
"bluetooth": [],
|
||||||
"codeowners": ["@smkrv"],
|
"codeowners": ["@smkrv"],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
"dependencies": [],
|
"dependencies": [],
|
||||||
"documentation": "https://github.com/smkrv/ha-text-ai",
|
"documentation": "https://github.com/smkrv/ha-text-ai",
|
||||||
"homepage": "https://github.com/smkrv/ha-text-ai",
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"issue_tracker": "https://github.com/smkrv/ha-text-ai/issues",
|
"issue_tracker": "https://github.com/smkrv/ha-text-ai/issues",
|
||||||
|
"loggers": ["custom_components.ha_text_ai"],
|
||||||
|
"mqtt": [],
|
||||||
|
"quality_scale": "silver",
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"openai>=1.12.0",
|
"openai>=1.12.0",
|
||||||
"anthropic>=0.8.0",
|
"anthropic>=0.8.0",
|
||||||
"aiohttp>=3.8.0",
|
"aiohttp>=3.8.0",
|
||||||
"async-timeout>=4.0.0"
|
"async-timeout>=4.0.0",
|
||||||
|
"certifi>=2024.2.2"
|
||||||
],
|
],
|
||||||
"version": "2.0.0",
|
"single_config_entry": false,
|
||||||
"homeassistant": "2024.1.0",
|
|
||||||
"quality_scale": "silver",
|
|
||||||
"integration_type": "service",
|
|
||||||
"translations": [
|
|
||||||
"en"
|
|
||||||
],
|
|
||||||
"zeroconf": [],
|
|
||||||
"ssdp": [],
|
"ssdp": [],
|
||||||
"usb": [],
|
"usb": [],
|
||||||
"bluetooth": [],
|
"version": "2.0.2-alpha",
|
||||||
"mqtt": [],
|
"zeroconf": []
|
||||||
"loggers": ["custom_components.ha_text_ai"],
|
|
||||||
"after_dependencies": ["http"],
|
|
||||||
"title": "Text AI Assistant",
|
|
||||||
"description": "AI-powered text assistant supporting multiple models including GPT and Claude",
|
|
||||||
"render_readme": true,
|
|
||||||
"tags": [
|
|
||||||
"ai",
|
|
||||||
"automation",
|
|
||||||
"chat",
|
|
||||||
"assistant",
|
|
||||||
"openai",
|
|
||||||
"claude"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,70 +1,101 @@
|
|||||||
"""Sensor platform for HA text AI."""
|
"""Sensor platform for HA Text AI."""
|
||||||
from datetime import datetime
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, Optional
|
import math
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
SensorStateClass,
|
SensorEntityDescription,
|
||||||
SensorDeviceClass,
|
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.device_registry import DeviceInfo
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.typing import StateType
|
from homeassistant.helpers.typing import StateType
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
from homeassistant.util import slugify
|
||||||
|
|
||||||
from .const import (
|
from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
ATTR_QUESTION,
|
CONF_MODEL,
|
||||||
ATTR_RESPONSE,
|
CONF_API_PROVIDER,
|
||||||
ATTR_LAST_UPDATED,
|
|
||||||
ATTR_MODEL,
|
|
||||||
ATTR_TEMPERATURE,
|
|
||||||
ATTR_MAX_TOKENS,
|
|
||||||
ATTR_TOTAL_RESPONSES,
|
ATTR_TOTAL_RESPONSES,
|
||||||
ATTR_SYSTEM_PROMPT,
|
ATTR_TOTAL_ERRORS,
|
||||||
ATTR_QUEUE_SIZE,
|
ATTR_AVG_RESPONSE_TIME,
|
||||||
ATTR_API_STATUS,
|
ATTR_LAST_REQUEST_TIME,
|
||||||
ATTR_ERROR_COUNT,
|
|
||||||
ATTR_LAST_ERROR,
|
ATTR_LAST_ERROR,
|
||||||
ATTR_RESPONSE_TIME,
|
ATTR_IS_PROCESSING,
|
||||||
|
ATTR_IS_RATE_LIMITED,
|
||||||
|
ATTR_IS_MAINTENANCE,
|
||||||
ATTR_API_VERSION,
|
ATTR_API_VERSION,
|
||||||
ATTR_ENDPOINT_STATUS,
|
ATTR_ENDPOINT_STATUS,
|
||||||
ATTR_REQUEST_COUNT,
|
ATTR_PERFORMANCE_METRICS,
|
||||||
ATTR_TOKENS_USED,
|
ATTR_HISTORY_SIZE,
|
||||||
ENTITY_ICON,
|
ATTR_UPTIME,
|
||||||
ENTITY_ICON_ERROR,
|
ATTR_API_PROVIDER,
|
||||||
ENTITY_ICON_PROCESSING,
|
ATTR_MODEL,
|
||||||
|
ATTR_SYSTEM_PROMPT,
|
||||||
|
ATTR_API_STATUS,
|
||||||
|
ATTR_RESPONSE,
|
||||||
|
ATTR_QUESTION,
|
||||||
|
ATTR_CONVERSATION_HISTORY,
|
||||||
|
METRIC_TOTAL_TOKENS,
|
||||||
|
METRIC_PROMPT_TOKENS,
|
||||||
|
METRIC_COMPLETION_TOKENS,
|
||||||
|
METRIC_SUCCESSFUL_REQUESTS,
|
||||||
|
METRIC_FAILED_REQUESTS,
|
||||||
|
METRIC_AVERAGE_LATENCY,
|
||||||
|
METRIC_MAX_LATENCY,
|
||||||
|
METRIC_MIN_LATENCY,
|
||||||
STATE_READY,
|
STATE_READY,
|
||||||
STATE_PROCESSING,
|
STATE_PROCESSING,
|
||||||
STATE_ERROR,
|
STATE_ERROR,
|
||||||
STATE_DISCONNECTED,
|
|
||||||
STATE_RATE_LIMITED,
|
|
||||||
STATE_INITIALIZING,
|
STATE_INITIALIZING,
|
||||||
STATE_MAINTENANCE,
|
STATE_MAINTENANCE,
|
||||||
|
STATE_RATE_LIMITED,
|
||||||
|
STATE_DISCONNECTED,
|
||||||
|
ENTITY_ICON,
|
||||||
|
ENTITY_ICON_ERROR,
|
||||||
|
ENTITY_ICON_PROCESSING,
|
||||||
|
DEFAULT_NAME_PREFIX,
|
||||||
|
CONF_MAX_HISTORY_SIZE,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .coordinator import HATextAICoordinator
|
from .coordinator import HATextAICoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: ConfigEntry,
|
||||||
async_add_entities: AddEntitiesCallback,
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the HA text AI sensor."""
|
"""Set up the HA Text AI sensor."""
|
||||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
_LOGGER.debug(f"Starting sensor setup for entry: {entry.entry_id}")
|
||||||
async_add_entities([HATextAISensor(coordinator, entry)], True)
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||||
|
_LOGGER.debug(f"Found coordinator for entry {entry.entry_id}")
|
||||||
|
|
||||||
|
instance_name = coordinator.instance_name
|
||||||
|
_LOGGER.debug(f"Setting up sensor with instance: {instance_name}")
|
||||||
|
|
||||||
|
sensor = HATextAISensor(coordinator, entry)
|
||||||
|
_LOGGER.debug(f"Created sensor instance: {sensor.entity_id}")
|
||||||
|
|
||||||
|
async_add_entities([sensor], True)
|
||||||
|
_LOGGER.debug(f"Added sensor entity: {sensor.entity_id}")
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.exception(f"Error setting up sensor: {err}")
|
||||||
|
raise
|
||||||
|
|
||||||
class HATextAISensor(CoordinatorEntity, SensorEntity):
|
class HATextAISensor(CoordinatorEntity, SensorEntity):
|
||||||
"""HA text AI Sensor."""
|
"""HA Text AI Sensor."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
coordinator: HATextAICoordinator
|
||||||
_attr_state_class = SensorStateClass.MEASUREMENT
|
|
||||||
_attr_device_class = SensorDeviceClass.TIMESTAMP
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -72,164 +103,230 @@ class HATextAISensor(CoordinatorEntity, SensorEntity):
|
|||||||
config_entry: ConfigEntry,
|
config_entry: ConfigEntry,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
|
_LOGGER.debug(f"Initializing sensor with config entry: {config_entry.data}")
|
||||||
|
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
|
|
||||||
self._config_entry = config_entry
|
self._config_entry = config_entry
|
||||||
|
self._instance_name = coordinator.instance_name
|
||||||
|
self._normalized_name = coordinator.normalized_name
|
||||||
|
|
||||||
|
_LOGGER.debug(f"Instance name: {self._instance_name}")
|
||||||
|
_LOGGER.debug(f"Normalized name: {self._normalized_name}")
|
||||||
|
|
||||||
|
self._conversation_history = []
|
||||||
|
self._system_prompt = None
|
||||||
|
|
||||||
|
self._attr_name = f"HA Text AI {self._instance_name}"
|
||||||
|
self.entity_id = f"sensor.ha_text_ai_{self._normalized_name}"
|
||||||
self._attr_unique_id = f"{config_entry.entry_id}"
|
self._attr_unique_id = f"{config_entry.entry_id}"
|
||||||
self._attr_name = "Last Response"
|
|
||||||
self._attr_suggested_display_precision = 0
|
_LOGGER.debug(f"Created sensor with entity_id: {self.entity_id}")
|
||||||
|
_LOGGER.debug(f"Sensor name: {self._attr_name}")
|
||||||
|
_LOGGER.debug(f"Unique ID: {self._attr_unique_id}")
|
||||||
|
|
||||||
|
self.entity_description = SensorEntityDescription(
|
||||||
|
key=f"ha_text_ai_{self._normalized_name.lower()}",
|
||||||
|
entity_registry_enabled_default=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._current_state = STATE_INITIALIZING
|
||||||
self._error_count = 0
|
self._error_count = 0
|
||||||
self._last_error = None
|
self._last_error = None
|
||||||
self._state = STATE_INITIALIZING
|
self._last_update = None
|
||||||
self._attr_device_info = {
|
self._is_processing = False
|
||||||
"identifiers": {(DOMAIN, self._attr_unique_id)},
|
self._last_response = {}
|
||||||
"name": "HA Text AI",
|
self._metrics = {}
|
||||||
"manufacturer": "Community",
|
|
||||||
"model": coordinator.model,
|
|
||||||
"sw_version": coordinator.api_version,
|
|
||||||
}
|
|
||||||
|
|
||||||
@property
|
model = config_entry.data.get(CONF_MODEL, "Unknown")
|
||||||
def icon(self) -> str:
|
api_provider = config_entry.data.get(CONF_API_PROVIDER, "Unknown")
|
||||||
"""Return the icon based on the current state."""
|
|
||||||
if self._state == STATE_PROCESSING:
|
|
||||||
return ENTITY_ICON_PROCESSING
|
|
||||||
elif self._state in [STATE_ERROR, STATE_DISCONNECTED, STATE_RATE_LIMITED]:
|
|
||||||
return ENTITY_ICON_ERROR
|
|
||||||
return ENTITY_ICON
|
|
||||||
|
|
||||||
@property
|
self._attr_device_info = DeviceInfo(
|
||||||
def state(self) -> StateType:
|
identifiers={(DOMAIN, self._attr_unique_id)},
|
||||||
"""Return the state of the sensor."""
|
name=self._attr_name,
|
||||||
if not self.coordinator.data or not self.coordinator.last_update_success:
|
manufacturer="Community",
|
||||||
return None
|
model=f"{model} ({api_provider} provider)",
|
||||||
|
sw_version="1.0.0",
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
_LOGGER.debug(
|
||||||
if self.coordinator.data and isinstance(self.coordinator.data, dict):
|
f"Initialized sensor: {self.entity_id} for instance: {self._instance_name}"
|
||||||
last_update = self.coordinator.data.get("last_update")
|
)
|
||||||
if isinstance(last_update, datetime):
|
|
||||||
return dt_util.as_local(last_update)
|
|
||||||
return last_update
|
|
||||||
return None
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.error("Error getting state: %s", err, exc_info=True)
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def extra_state_attributes(self) -> Dict[str, Any]:
|
|
||||||
"""Return entity specific state attributes."""
|
|
||||||
attributes = {
|
|
||||||
ATTR_TOTAL_RESPONSES: 0,
|
|
||||||
ATTR_MODEL: self.coordinator.model,
|
|
||||||
ATTR_TEMPERATURE: self.coordinator.temperature,
|
|
||||||
ATTR_MAX_TOKENS: self.coordinator.max_tokens,
|
|
||||||
ATTR_SYSTEM_PROMPT: self.coordinator.system_prompt,
|
|
||||||
ATTR_QUEUE_SIZE: self.coordinator._question_queue.qsize(),
|
|
||||||
ATTR_API_STATUS: self._state,
|
|
||||||
ATTR_ERROR_COUNT: self._error_count,
|
|
||||||
ATTR_LAST_ERROR: self._last_error,
|
|
||||||
ATTR_API_VERSION: self.coordinator.api_version,
|
|
||||||
ATTR_ENDPOINT_STATUS: self.coordinator.endpoint_status,
|
|
||||||
ATTR_REQUEST_COUNT: self.coordinator.request_count,
|
|
||||||
ATTR_TOKENS_USED: self.coordinator.tokens_used,
|
|
||||||
}
|
|
||||||
|
|
||||||
if not self.coordinator.data:
|
|
||||||
return attributes
|
|
||||||
|
|
||||||
try:
|
|
||||||
history = list(self.coordinator._responses.items())
|
|
||||||
if history:
|
|
||||||
last_question, last_data = history[-1]
|
|
||||||
|
|
||||||
if isinstance(last_data, dict):
|
|
||||||
last_response = last_data.get("response", "")
|
|
||||||
last_updated = last_data.get("timestamp") or self.coordinator.data.get("last_update")
|
|
||||||
response_time = last_data.get("response_time")
|
|
||||||
|
|
||||||
model = last_data.get("model", self.coordinator.model)
|
|
||||||
temperature = last_data.get("temperature", self.coordinator.temperature)
|
|
||||||
max_tokens = last_data.get("max_tokens", self.coordinator.max_tokens)
|
|
||||||
error = last_data.get("error")
|
|
||||||
|
|
||||||
if error:
|
|
||||||
self._last_error = error
|
|
||||||
self._state = STATE_ERROR
|
|
||||||
else:
|
|
||||||
last_response = str(last_data)
|
|
||||||
last_updated = self.coordinator.data.get("last_update")
|
|
||||||
response_time = None
|
|
||||||
model = self.coordinator.model
|
|
||||||
temperature = self.coordinator.temperature
|
|
||||||
max_tokens = self.coordinator.max_tokens
|
|
||||||
|
|
||||||
if isinstance(last_updated, datetime):
|
|
||||||
last_updated = dt_util.as_local(last_updated)
|
|
||||||
|
|
||||||
attributes.update({
|
|
||||||
ATTR_QUESTION: last_question,
|
|
||||||
ATTR_RESPONSE: last_response,
|
|
||||||
ATTR_LAST_UPDATED: last_updated,
|
|
||||||
ATTR_TOTAL_RESPONSES: len(history),
|
|
||||||
ATTR_MODEL: model,
|
|
||||||
ATTR_TEMPERATURE: temperature,
|
|
||||||
ATTR_MAX_TOKENS: max_tokens,
|
|
||||||
})
|
|
||||||
|
|
||||||
if response_time is not None:
|
|
||||||
attributes[ATTR_RESPONSE_TIME] = response_time
|
|
||||||
|
|
||||||
return attributes
|
|
||||||
|
|
||||||
except Exception as err:
|
|
||||||
_LOGGER.error("Error getting attributes: %s", err, exc_info=True)
|
|
||||||
self._error_count += 1
|
|
||||||
self._last_error = str(err)
|
|
||||||
self._state = STATE_ERROR
|
|
||||||
return attributes
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return if entity is available."""
|
"""Return if entity is available."""
|
||||||
return self.coordinator.last_update_success
|
return (
|
||||||
|
self.coordinator.last_update_success
|
||||||
|
and self.coordinator.data is not None
|
||||||
|
and self._current_state != STATE_DISCONNECTED
|
||||||
|
)
|
||||||
|
|
||||||
|
def _sanitize_value(self, value: Any) -> Any:
|
||||||
|
"""Sanitize values for JSON serialization."""
|
||||||
|
if isinstance(value, float):
|
||||||
|
if math.isinf(value) or math.isnan(value):
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _sanitize_attributes(self, attributes: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Sanitize all attributes for JSON serialization."""
|
||||||
|
return {
|
||||||
|
key: self._sanitize_value(value)
|
||||||
|
for key, value in attributes.items()
|
||||||
|
if value is not None
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def native_value(self) -> StateType:
|
||||||
|
"""Return the native value of the sensor."""
|
||||||
|
if not self.coordinator.last_update_success or not self.coordinator.data:
|
||||||
|
self._current_state = STATE_DISCONNECTED
|
||||||
|
return self._current_state
|
||||||
|
|
||||||
|
status = self.coordinator.data.get("state", STATE_READY)
|
||||||
|
self._current_state = status
|
||||||
|
return status
|
||||||
|
|
||||||
|
@property
|
||||||
|
def icon(self) -> str:
|
||||||
|
"""Return the icon based on the current state."""
|
||||||
|
if self._current_state == STATE_ERROR:
|
||||||
|
return ENTITY_ICON_ERROR
|
||||||
|
elif self._current_state == STATE_PROCESSING:
|
||||||
|
return ENTITY_ICON_PROCESSING
|
||||||
|
return ENTITY_ICON
|
||||||
|
|
||||||
|
@property
|
||||||
|
def extra_state_attributes(self) -> Dict[str, Any]:
|
||||||
|
"""Return entity specific state attributes."""
|
||||||
|
if not self.coordinator.data:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = self.coordinator.data
|
||||||
|
attributes = {
|
||||||
|
ATTR_MODEL: self._config_entry.data.get(CONF_MODEL, "Unknown"),
|
||||||
|
ATTR_API_PROVIDER: self._config_entry.data.get(
|
||||||
|
CONF_API_PROVIDER, "Unknown"
|
||||||
|
),
|
||||||
|
ATTR_API_STATUS: self._current_state,
|
||||||
|
ATTR_TOTAL_ERRORS: self._error_count,
|
||||||
|
ATTR_LAST_ERROR: self._last_error,
|
||||||
|
"instance_name": self._instance_name,
|
||||||
|
"normalized_name": self._normalized_name,
|
||||||
|
ATTR_SYSTEM_PROMPT: data.get("system_prompt"),
|
||||||
|
ATTR_IS_PROCESSING: data.get("is_processing", False),
|
||||||
|
ATTR_IS_RATE_LIMITED: data.get("is_rate_limited", False),
|
||||||
|
ATTR_IS_MAINTENANCE: data.get("is_maintenance", False),
|
||||||
|
ATTR_ENDPOINT_STATUS: data.get("endpoint_status", "unknown"),
|
||||||
|
ATTR_UPTIME: data.get("uptime", 0),
|
||||||
|
ATTR_HISTORY_SIZE: data.get("history_size", 0),
|
||||||
|
ATTR_CONVERSATION_HISTORY: data.get("conversation_history", []),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add metrics
|
||||||
|
metrics = data.get("metrics", {})
|
||||||
|
if isinstance(metrics, dict):
|
||||||
|
self._metrics = metrics
|
||||||
|
attributes.update(
|
||||||
|
{
|
||||||
|
METRIC_TOTAL_TOKENS: metrics.get("total_tokens", 0),
|
||||||
|
METRIC_PROMPT_TOKENS: metrics.get("prompt_tokens", 0),
|
||||||
|
METRIC_COMPLETION_TOKENS: metrics.get("completion_tokens", 0),
|
||||||
|
METRIC_SUCCESSFUL_REQUESTS: metrics.get(
|
||||||
|
"successful_requests", 0
|
||||||
|
),
|
||||||
|
METRIC_FAILED_REQUESTS: metrics.get("failed_requests", 0),
|
||||||
|
METRIC_AVERAGE_LATENCY: metrics.get("average_latency", 0),
|
||||||
|
METRIC_MAX_LATENCY: metrics.get("max_latency", 0),
|
||||||
|
METRIC_MIN_LATENCY: metrics.get("min_latency", float("inf")),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add last response
|
||||||
|
last_response = data.get("last_response", {})
|
||||||
|
if isinstance(last_response, dict):
|
||||||
|
self._last_response = last_response
|
||||||
|
attributes.update(
|
||||||
|
{
|
||||||
|
ATTR_RESPONSE: last_response.get("response", ""),
|
||||||
|
ATTR_QUESTION: last_response.get("question", ""),
|
||||||
|
"last_model": last_response.get("model", ""),
|
||||||
|
"last_timestamp": last_response.get("timestamp", ""),
|
||||||
|
"last_error": last_response.get("error"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Add performance metrics if available
|
||||||
|
if ATTR_PERFORMANCE_METRICS in data:
|
||||||
|
attributes[ATTR_PERFORMANCE_METRICS] = data[
|
||||||
|
ATTR_PERFORMANCE_METRICS
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add API version if available
|
||||||
|
if ATTR_API_VERSION in data:
|
||||||
|
attributes[ATTR_API_VERSION] = data[ATTR_API_VERSION]
|
||||||
|
|
||||||
|
return self._sanitize_attributes(attributes)
|
||||||
|
|
||||||
|
except Exception as err:
|
||||||
|
_LOGGER.error("Error preparing attributes: %s", err, exc_info=True)
|
||||||
|
return {}
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""When entity is added to hass."""
|
"""When entity is added to hass."""
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
self._handle_coordinator_update()
|
self._handle_coordinator_update()
|
||||||
self._state = STATE_READY
|
_LOGGER.debug(f"Entity {self.entity_id} added to Home Assistant")
|
||||||
|
|
||||||
def _handle_coordinator_update(self) -> None:
|
def _handle_coordinator_update(self) -> None:
|
||||||
"""Handle updated data from the coordinator."""
|
"""Handle updated data from the coordinator."""
|
||||||
try:
|
try:
|
||||||
if self.coordinator.data:
|
data = self.coordinator.data
|
||||||
if self.coordinator._is_ready:
|
if not self.coordinator.last_update_success or not data:
|
||||||
if self.coordinator._is_processing:
|
self._current_state = STATE_DISCONNECTED
|
||||||
self._state = STATE_PROCESSING
|
_LOGGER.warning(f"No data available for {self.entity_id}")
|
||||||
elif self.coordinator._is_rate_limited:
|
self.async_write_ha_state()
|
||||||
self._state = STATE_RATE_LIMITED
|
return
|
||||||
elif self.coordinator._is_maintenance:
|
|
||||||
self._state = STATE_MAINTENANCE
|
|
||||||
else:
|
|
||||||
self._state = STATE_READY
|
|
||||||
else:
|
|
||||||
self._state = STATE_DISCONNECTED
|
|
||||||
else:
|
|
||||||
self._state = STATE_DISCONNECTED
|
|
||||||
|
|
||||||
# Обновляем счетчик ошибок только если статус изменился на ошибку
|
self._is_processing = data.get("is_processing", False)
|
||||||
if self._state == STATE_ERROR:
|
|
||||||
|
# Update conversation history and system prompt
|
||||||
|
self._conversation_history = data.get("conversation_history", [])
|
||||||
|
self._system_prompt = data.get("system_prompt")
|
||||||
|
|
||||||
|
# Update state based on conditions
|
||||||
|
if self._is_processing:
|
||||||
|
self._current_state = STATE_PROCESSING
|
||||||
|
elif data.get("is_rate_limited"):
|
||||||
|
self._current_state = STATE_RATE_LIMITED
|
||||||
|
elif data.get("is_maintenance"):
|
||||||
|
self._current_state = STATE_MAINTENANCE
|
||||||
|
elif data.get("error"):
|
||||||
|
self._current_state = STATE_ERROR
|
||||||
|
self._last_error = data["error"]
|
||||||
self._error_count += 1
|
self._error_count += 1
|
||||||
|
else:
|
||||||
|
self._current_state = data.get("state", STATE_READY)
|
||||||
|
|
||||||
|
# Update last update timestamp
|
||||||
|
self._last_update = dt_util.utcnow()
|
||||||
|
|
||||||
|
_LOGGER.debug(
|
||||||
|
f"Updated {self.entity_id} state to: {self._current_state} "
|
||||||
|
f"(available: {self.available})"
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
_LOGGER.error("Error handling update: %s", err, exc_info=True)
|
self._current_state = STATE_ERROR
|
||||||
self._error_count += 1
|
|
||||||
self._last_error = str(err)
|
self._last_error = str(err)
|
||||||
self._state = STATE_ERROR
|
self._error_count += 1
|
||||||
|
_LOGGER.error(
|
||||||
|
"Error handling update for %s: %s",
|
||||||
|
self.entity_id,
|
||||||
|
err,
|
||||||
|
exc_info=True,
|
||||||
|
)
|
||||||
|
|
||||||
self.async_write_ha_state()
|
self.async_write_ha_state()
|
||||||
|
|
||||||
async def async_reset_error_count(self) -> None:
|
|
||||||
"""Reset the error counter."""
|
|
||||||
self._error_count = 0
|
|
||||||
self._last_error = None
|
|
||||||
self.async_write_ha_state()
|
|
||||||
|
|||||||
@@ -3,69 +3,55 @@ ask_question:
|
|||||||
description: >-
|
description: >-
|
||||||
Send a question to the AI model and receive a detailed response.
|
Send a question to the AI model and receive a detailed response.
|
||||||
The response will be stored in the conversation history and can be retrieved later.
|
The response will be stored in the conversation history and can be retrieved later.
|
||||||
Response time may vary based on model selection and server load.
|
|
||||||
Supports various AI models with different capabilities and pricing.
|
|
||||||
fields:
|
fields:
|
||||||
|
instance:
|
||||||
|
name: Instance
|
||||||
|
description: Name of the HA Text AI instance to use
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
integration: ha_text_ai
|
||||||
|
domain: sensor
|
||||||
|
|
||||||
question:
|
question:
|
||||||
name: Question
|
name: Question
|
||||||
description: >-
|
description: Your question or prompt for the AI assistant
|
||||||
Your question or prompt for the AI assistant. Be specific and clear for better results.
|
|
||||||
You can ask about home automation, technical advice, or general questions.
|
|
||||||
For complex queries, consider breaking them into smaller parts.
|
|
||||||
The system will maintain conversation context for follow-up questions.
|
|
||||||
required: true
|
required: true
|
||||||
example: |
|
|
||||||
What automations would you recommend for a smart kitchen?
|
|
||||||
Consider energy efficiency, convenience, and integration with:
|
|
||||||
- Smart lighting
|
|
||||||
- Appliance control
|
|
||||||
- Temperature monitoring
|
|
||||||
- Voice commands
|
|
||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
multiline: true
|
multiline: true
|
||||||
type: text
|
type: text
|
||||||
|
|
||||||
|
system_prompt:
|
||||||
|
name: System Prompt
|
||||||
|
description: Optional system prompt to set context for this specific question
|
||||||
|
required: false
|
||||||
|
selector:
|
||||||
|
text:
|
||||||
|
multiline: true
|
||||||
|
|
||||||
|
context_messages:
|
||||||
|
name: Context Messages
|
||||||
|
description: Number of previous messages to include in context (1-20)
|
||||||
|
required: false
|
||||||
|
default: 5
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1
|
||||||
|
max: 20
|
||||||
|
step: 1
|
||||||
|
mode: slider
|
||||||
|
|
||||||
model:
|
model:
|
||||||
name: Model
|
name: Model
|
||||||
description: >-
|
description: "Select AI model to use (optional, overrides default setting)"
|
||||||
Select an AI model to use (optional, overrides default setting).
|
|
||||||
Different models have different capabilities, context limits, and response characteristics:
|
|
||||||
- GPT-3.5 Turbo: Fast, efficient, good for most tasks
|
|
||||||
- GPT-4: More capable, better reasoning, slower
|
|
||||||
- Claude-3: Advanced capabilities, longer context
|
|
||||||
Note: More capable models may have longer response times and higher API costs.
|
|
||||||
required: false
|
required: false
|
||||||
example: "gpt-3.5-turbo"
|
|
||||||
default: "gpt-3.5-turbo"
|
|
||||||
selector:
|
selector:
|
||||||
select:
|
text: {}
|
||||||
custom_value: true
|
|
||||||
options:
|
|
||||||
- label: "GPT-3.5 Turbo (Fast & Efficient)"
|
|
||||||
value: "gpt-3.5-turbo"
|
|
||||||
- label: "GPT-3.5 Turbo 16K (Extended)"
|
|
||||||
value: "gpt-3.5-turbo-16k"
|
|
||||||
- label: "GPT-4 (Most Capable)"
|
|
||||||
value: "gpt-4"
|
|
||||||
- label: "GPT-4 32K (Extended Context)"
|
|
||||||
value: "gpt-4-32k"
|
|
||||||
- label: "GPT-4 Turbo (Latest)"
|
|
||||||
value: "gpt-4-1106-preview"
|
|
||||||
- label: "Claude-3 Sonnet (Balanced)"
|
|
||||||
value: "claude-3-sonnet"
|
|
||||||
- label: "Claude-3 Opus (Most Advanced)"
|
|
||||||
value: "claude-3-opus"
|
|
||||||
mode: dropdown
|
|
||||||
|
|
||||||
temperature:
|
temperature:
|
||||||
name: Temperature
|
name: Temperature
|
||||||
description: >-
|
description: Controls response creativity (0.0-2.0)
|
||||||
Controls response creativity (0.0-2.0):
|
|
||||||
0.0-0.3: Focused, consistent responses (best for technical/factual queries)
|
|
||||||
0.4-0.7: Balanced responses (recommended for most uses)
|
|
||||||
0.8-2.0: More creative, varied responses (best for brainstorming)
|
|
||||||
Note: Higher values may produce less predictable results.
|
|
||||||
required: false
|
required: false
|
||||||
default: 0.7
|
default: 0.7
|
||||||
selector:
|
selector:
|
||||||
@@ -74,17 +60,10 @@ ask_question:
|
|||||||
max: 2.0
|
max: 2.0
|
||||||
step: 0.1
|
step: 0.1
|
||||||
mode: slider
|
mode: slider
|
||||||
unit_of_measurement: ""
|
|
||||||
|
|
||||||
max_tokens:
|
max_tokens:
|
||||||
name: Max Tokens
|
name: Max Tokens
|
||||||
description: >-
|
description: Maximum length of the response (1-4096 tokens)
|
||||||
Maximum length of the response (1-4096 tokens).
|
|
||||||
Recommended ranges:
|
|
||||||
- Short (256-512): Quick answers, status updates
|
|
||||||
- Medium (512-1024): Detailed explanations
|
|
||||||
- Long (1024-4096): Complex analysis
|
|
||||||
Note: Token limits vary by model. Actual length may be shorter.
|
|
||||||
required: false
|
required: false
|
||||||
default: 1000
|
default: 1000
|
||||||
selector:
|
selector:
|
||||||
@@ -94,42 +73,36 @@ ask_question:
|
|||||||
step: 1
|
step: 1
|
||||||
mode: box
|
mode: box
|
||||||
|
|
||||||
system_prompt:
|
|
||||||
name: System Prompt
|
|
||||||
description: >-
|
|
||||||
Optional system prompt to set context for this specific question.
|
|
||||||
This will temporarily override the default system prompt.
|
|
||||||
Use this to specify expertise areas, response format, or special instructions.
|
|
||||||
required: false
|
|
||||||
example: "You are a home automation expert focused on energy efficiency and security"
|
|
||||||
selector:
|
|
||||||
text:
|
|
||||||
multiline: true
|
|
||||||
|
|
||||||
clear_history:
|
clear_history:
|
||||||
name: Clear History
|
name: Clear History
|
||||||
description: >-
|
description: >-
|
||||||
Delete all stored questions and responses from the conversation history.
|
Delete all stored questions and responses from the conversation history
|
||||||
This action cannot be undone. Consider using 'get_history' first if you need to backup the data.
|
fields:
|
||||||
System prompt settings and configuration will be preserved.
|
instance:
|
||||||
fields: {}
|
name: Instance
|
||||||
|
description: Name of the HA Text AI instance to clear history for
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
integration: ha_text_ai
|
||||||
|
domain: sensor
|
||||||
|
|
||||||
get_history:
|
get_history:
|
||||||
name: Get History
|
name: Get History
|
||||||
description: >-
|
description: Retrieve conversation history with optional filtering and sorting
|
||||||
Retrieve recent conversation history, including:
|
|
||||||
- Questions and responses
|
|
||||||
- Timestamps and response times
|
|
||||||
- Models used and token counts
|
|
||||||
- Temperature and other settings
|
|
||||||
Results are ordered from newest to oldest.
|
|
||||||
fields:
|
fields:
|
||||||
|
instance:
|
||||||
|
name: Instance
|
||||||
|
description: Name of the HA Text AI instance to get history from
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
integration: ha_text_ai
|
||||||
|
domain: sensor
|
||||||
|
|
||||||
limit:
|
limit:
|
||||||
name: Limit
|
name: Limit
|
||||||
description: >-
|
description: Number of conversations to return (1-100)
|
||||||
Number of most recent conversations to return (1-100).
|
|
||||||
Higher values return more history but may take longer to process.
|
|
||||||
Default: 10 conversations
|
|
||||||
required: false
|
required: false
|
||||||
default: 10
|
default: 10
|
||||||
selector:
|
selector:
|
||||||
@@ -140,66 +113,57 @@ get_history:
|
|||||||
mode: box
|
mode: box
|
||||||
|
|
||||||
filter_model:
|
filter_model:
|
||||||
name: Filter by Model
|
name: Filter Model
|
||||||
description: >-
|
description: Filter conversations by specific AI model
|
||||||
Only return conversations using a specific AI model.
|
|
||||||
Leave empty to show all models.
|
|
||||||
required: false
|
required: false
|
||||||
selector:
|
selector:
|
||||||
select:
|
text:
|
||||||
custom_value: true
|
multiline: false
|
||||||
options:
|
|
||||||
- label: "All Models"
|
|
||||||
value: ""
|
|
||||||
- label: "GPT-3.5 Turbo"
|
|
||||||
value: "gpt-3.5-turbo"
|
|
||||||
- label: "GPT-4"
|
|
||||||
value: "gpt-4"
|
|
||||||
- label: "GPT-4 Turbo"
|
|
||||||
value: "gpt-4-1106-preview"
|
|
||||||
- label: "Claude-3 Sonnet"
|
|
||||||
value: "claude-3-sonnet"
|
|
||||||
- label: "Claude-3 Opus"
|
|
||||||
value: "claude-3-opus"
|
|
||||||
mode: dropdown
|
|
||||||
|
|
||||||
start_date:
|
start_date:
|
||||||
name: Start Date
|
name: Start Date
|
||||||
description: >-
|
description: Filter conversations starting from this date/time
|
||||||
Optional start date for filtering history.
|
|
||||||
Format: YYYY-MM-DD
|
|
||||||
required: false
|
required: false
|
||||||
selector:
|
selector:
|
||||||
datetime:
|
text:
|
||||||
type: date
|
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: >-
|
description: Set default system behavior instructions for all future conversations
|
||||||
Set default system behavior instructions for all future conversations.
|
|
||||||
This defines how the AI should behave and respond to questions.
|
|
||||||
The prompt will be used for all models unless overridden per question.
|
|
||||||
fields:
|
fields:
|
||||||
|
instance:
|
||||||
|
name: Instance
|
||||||
|
description: Name of the HA Text AI instance to set system prompt for
|
||||||
|
required: true
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
integration: ha_text_ai
|
||||||
|
domain: sensor
|
||||||
|
|
||||||
prompt:
|
prompt:
|
||||||
name: System Prompt
|
name: System Prompt
|
||||||
description: >-
|
description: Instructions that define how the AI should behave and respond
|
||||||
Instructions that define how the AI should behave and respond.
|
|
||||||
Be specific about:
|
|
||||||
- Desired expertise and knowledge areas
|
|
||||||
- Response tone and style
|
|
||||||
- Output format preferences
|
|
||||||
- Special handling instructions
|
|
||||||
Maximum length: 1000 characters
|
|
||||||
required: true
|
required: true
|
||||||
example: |
|
|
||||||
You are a home automation expert assistant. Focus on:
|
|
||||||
1. Practical and efficient solutions
|
|
||||||
2. Energy-saving recommendations
|
|
||||||
3. Integration with popular smart home platforms
|
|
||||||
4. Security and privacy considerations
|
|
||||||
Provide detailed but concise responses with clear steps when applicable.
|
|
||||||
Format complex responses with bullet points or numbered lists.
|
|
||||||
Include warnings about potential risks or limitations.
|
|
||||||
selector:
|
selector:
|
||||||
text:
|
text:
|
||||||
multiline: true
|
multiline: true
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,147 +1,266 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"provider": {
|
||||||
"title": "Set up HA Text AI",
|
"title": "Select AI Provider",
|
||||||
"description": "Configure your AI integration for various providers (OpenAI, Anthropic, etc.). Different models have different capabilities and pricing.",
|
"description": "Choose which AI service provider to use for this instance.",
|
||||||
"data": {
|
"data": {
|
||||||
"api_key": "API key for authentication (required)",
|
"api_provider": "API Provider",
|
||||||
"model": "AI model to use (e.g., gpt-3.5-turbo, gpt-4, claude-3-sonnet, claude-3-opus)",
|
"context_messages": "Number of context messages to retain (1-20)"
|
||||||
"temperature": "Response creativity (0-2, lower = more focused and consistent)",
|
|
||||||
"max_tokens": "Maximum response length (1-4096 tokens)",
|
|
||||||
"api_endpoint": "Custom API endpoint URL (default varies by provider)",
|
|
||||||
"request_interval": "Minimum time between requests in seconds (min: 0.1)",
|
|
||||||
"system_prompt": "Default instructions for AI behavior and expertise"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"invalid_auth": "Authentication failed - check your API key",
|
|
||||||
"invalid_api_key": "Invalid API key - please verify your credentials",
|
|
||||||
"cannot_connect": "Connection failed - check endpoint and network status",
|
|
||||||
"invalid_model": "Model unavailable or not supported by the API",
|
|
||||||
"rate_limit": "Rate limit exceeded - please reduce request frequency",
|
|
||||||
"context_length": "Input exceeds maximum context length for model",
|
|
||||||
"api_error": "API service error - check provider status",
|
|
||||||
"timeout": "Request timeout - server not responding",
|
|
||||||
"queue_full": "Request queue full - try again later",
|
|
||||||
"invalid_prompt": "Invalid system prompt format or length",
|
|
||||||
"invalid_url_format": "Invalid API endpoint URL format",
|
|
||||||
"invalid_input": "Invalid configuration parameters",
|
|
||||||
"unknown": "Unexpected error - check logs for details"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"user": {
|
||||||
"step": {
|
"title": "Configure HA Text AI Instance",
|
||||||
"init": {
|
"description": "Set up a new AI assistant instance with your selected provider.",
|
||||||
"title": "HA Text AI Settings",
|
"data": {
|
||||||
"description": "Adjust your AI integration parameters and behavior",
|
"name": "Instance name (e.g., 'GPT Assistant', 'Claude Helper')",
|
||||||
"data": {
|
"api_key": "API key for authentication",
|
||||||
"model": "Select AI model for responses (capabilities vary)",
|
"model": "AI model to use",
|
||||||
"temperature": "Response creativity (0-2, affects variation)",
|
"temperature": "Response creativity (0-2, lower = more focused)",
|
||||||
"max_tokens": "Maximum response length in tokens",
|
"max_tokens": "Maximum response length (1-4096 tokens)",
|
||||||
"request_interval": "Minimum seconds between requests (rate limiting)",
|
"api_endpoint": "Custom API endpoint URL (optional)",
|
||||||
"system_prompt": "Default AI behavior and expertise instructions",
|
"api_provider": "API Provider",
|
||||||
"timeout": "Request timeout in seconds (default: 30)",
|
"request_interval": "Minimum time between requests (0.1-60 seconds)",
|
||||||
"retry_count": "Number of retry attempts for failed requests",
|
"context_messages": "Number of context messages to retain (1-20)",
|
||||||
"queue_size": "Maximum pending requests in queue"
|
"max_history_size": "Maximum conversation history size (1-100)"
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"services": {
|
"error": {
|
||||||
"ask_question": {
|
"name_exists": "An instance with this name already exists",
|
||||||
"name": "Ask Question",
|
"invalid_name": "Invalid instance name",
|
||||||
"description": "Send a question to the AI model and get a detailed response. Supports context awareness for follow-up questions.",
|
"invalid_auth": "Authentication failed - check your API key",
|
||||||
"fields": {
|
"invalid_api_key": "Invalid API key - please verify your credentials",
|
||||||
"question": {
|
"cannot_connect": "Failed to connect to API service",
|
||||||
"name": "Question",
|
"invalid_model": "Selected model is not available",
|
||||||
"description": "Your question or prompt for the AI model. Be specific for better results."
|
"rate_limit": "Rate limit exceeded",
|
||||||
},
|
"context_length": "Context length exceeded",
|
||||||
"system_prompt": {
|
"rate_limit_exceeded": "API rate limit exceeded",
|
||||||
"name": "System Prompt",
|
"maintenance": "Service is under maintenance",
|
||||||
"description": "Optional behavior instructions for this specific question"
|
"invalid_response": "Invalid API response received",
|
||||||
},
|
"api_error": "API service error occurred",
|
||||||
"model": {
|
"timeout": "Request timed out",
|
||||||
"name": "Model",
|
"invalid_instance": "Invalid instance specified",
|
||||||
"description": "Optional specific AI model for this request (overrides default)"
|
"unknown": "Unexpected error occurred",
|
||||||
},
|
"empty": "Name cannot be empty",
|
||||||
"temperature": {
|
"invalid_characters": "Name can only contain letters, numbers, spaces, underscores and hyphens",
|
||||||
"name": "Temperature",
|
"name_too_long": "Name must be 50 characters or less"
|
||||||
"description": "Optional creativity setting (0-2, affects response variation)"
|
|
||||||
},
|
|
||||||
"max_tokens": {
|
|
||||||
"name": "Max Tokens",
|
|
||||||
"description": "Optional maximum response length (1-4096 tokens)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"clear_history": {
|
|
||||||
"name": "Clear History",
|
|
||||||
"description": "Delete all stored conversation history, responses, and metadata"
|
|
||||||
},
|
|
||||||
"get_history": {
|
|
||||||
"name": "Get History",
|
|
||||||
"description": "Retrieve conversation history with detailed metadata including timestamps",
|
|
||||||
"fields": {
|
|
||||||
"limit": {
|
|
||||||
"name": "Limit",
|
|
||||||
"description": "Maximum number of conversations to return (1-100)"
|
|
||||||
},
|
|
||||||
"filter_model": {
|
|
||||||
"name": "Filter Model",
|
|
||||||
"description": "Show only responses from a specific model"
|
|
||||||
},
|
|
||||||
"start_date": {
|
|
||||||
"name": "Start Date",
|
|
||||||
"description": "Filter conversations from this date (YYYY-MM-DD)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"set_system_prompt": {
|
|
||||||
"name": "Set System Prompt",
|
|
||||||
"description": "Update default AI behavior and expertise instructions",
|
|
||||||
"fields": {
|
|
||||||
"prompt": {
|
|
||||||
"name": "System Prompt",
|
|
||||||
"description": "Instructions for AI behavior, expertise, and response style"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"state": {
|
|
||||||
"ready": "Ready for requests",
|
|
||||||
"processing": "Processing request",
|
|
||||||
"error": "Error occurred",
|
|
||||||
"disconnected": "API disconnected",
|
|
||||||
"rate_limited": "Rate limit reached",
|
|
||||||
"initializing": "Starting up",
|
|
||||||
"retrying": "Retrying request",
|
|
||||||
"queued": "Request queued"
|
|
||||||
},
|
|
||||||
"entity": {
|
|
||||||
"sensor": {
|
|
||||||
"status": {
|
|
||||||
"name": "AI Status",
|
|
||||||
"state": {
|
|
||||||
"ready": "Ready",
|
|
||||||
"processing": "Processing",
|
|
||||||
"error": "Error",
|
|
||||||
"disconnected": "Disconnected",
|
|
||||||
"rate_limited": "Rate Limited",
|
|
||||||
"initializing": "Initializing",
|
|
||||||
"retrying": "Retrying",
|
|
||||||
"queued": "Queued"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"last_response": {
|
|
||||||
"name": "Last Response",
|
|
||||||
"state": {
|
|
||||||
"success": "Success",
|
|
||||||
"error": "Error",
|
|
||||||
"timeout": "Timeout"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"title": "Update Instance Settings",
|
||||||
|
"description": "Modify settings for this AI assistant instance.",
|
||||||
|
"data": {
|
||||||
|
"model": "AI model",
|
||||||
|
"temperature": "Response creativity (0-2)",
|
||||||
|
"max_tokens": "Maximum response length (1-4096)",
|
||||||
|
"request_interval": "Minimum request interval (0.1-60 seconds)",
|
||||||
|
"context_messages": "Number of previous messages to include in context (1-20)",
|
||||||
|
"max_history_size": "Maximum conversation history size (1-100)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"services": {
|
||||||
|
"ask_question": {
|
||||||
|
"name": "Ask Question (HA Text AI)",
|
||||||
|
"description": "Send a question to the AI model and receive a detailed response. The response will be stored in the conversation history and can be retrieved later.",
|
||||||
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Instance",
|
||||||
|
"description": "Name of the HA Text AI instance to use"
|
||||||
|
},
|
||||||
|
"question": {
|
||||||
|
"name": "Question",
|
||||||
|
"description": "Your question or prompt for the AI assistant"
|
||||||
|
},
|
||||||
|
"context_messages": {
|
||||||
|
"name": "Context Messages",
|
||||||
|
"description": "Number of previous messages to include in context (1-20)"
|
||||||
|
},
|
||||||
|
"system_prompt": {
|
||||||
|
"name": "System Prompt",
|
||||||
|
"description": "Optional system prompt to set context for this specific question"
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "Model",
|
||||||
|
"description": "Select AI model to use (optional, overrides default setting)"
|
||||||
|
},
|
||||||
|
"temperature": {
|
||||||
|
"name": "Temperature",
|
||||||
|
"description": "Controls response creativity (0.0-2.0)"
|
||||||
|
},
|
||||||
|
"max_tokens": {
|
||||||
|
"name": "Max Tokens",
|
||||||
|
"description": "Maximum length of the response (1-4096 tokens)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"clear_history": {
|
||||||
|
"name": "Clear History",
|
||||||
|
"description": "Delete all stored questions and responses from the conversation history",
|
||||||
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Instance",
|
||||||
|
"description": "Name of the HA Text AI instance to clear history for"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"get_history": {
|
||||||
|
"name": "Get History",
|
||||||
|
"description": "Retrieve conversation history with optional filtering and sorting",
|
||||||
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Instance",
|
||||||
|
"description": "Name of the HA Text AI instance to get history from"
|
||||||
|
},
|
||||||
|
"limit": {
|
||||||
|
"name": "Limit",
|
||||||
|
"description": "Number of conversations to return (1-100)"
|
||||||
|
},
|
||||||
|
"filter_model": {
|
||||||
|
"name": "Filter Model",
|
||||||
|
"description": "Filter conversations by specific AI model"
|
||||||
|
},
|
||||||
|
"start_date": {
|
||||||
|
"name": "Start Date",
|
||||||
|
"description": "Filter conversations starting from this date/time"
|
||||||
|
},
|
||||||
|
"include_metadata": {
|
||||||
|
"name": "Include Metadata",
|
||||||
|
"description": "Include additional information like tokens used, response time, etc."
|
||||||
|
},
|
||||||
|
"sort_order": {
|
||||||
|
"name": "Sort Order",
|
||||||
|
"description": "Sort order for results (newest or oldest first)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"set_system_prompt": {
|
||||||
|
"name": "Set System Prompt",
|
||||||
|
"description": "Set default system behavior instructions for all future conversations",
|
||||||
|
"fields": {
|
||||||
|
"instance": {
|
||||||
|
"name": "Instance",
|
||||||
|
"description": "Name of the HA Text AI instance to set system prompt for"
|
||||||
|
},
|
||||||
|
"prompt": {
|
||||||
|
"name": "System Prompt",
|
||||||
|
"description": "Instructions that define how the AI should behave and respond"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"entity": {
|
||||||
|
"sensor": {
|
||||||
|
"ha_text_ai": {
|
||||||
|
"name": "{name}",
|
||||||
|
"state": {
|
||||||
|
"ready": "Ready",
|
||||||
|
"processing": "Processing",
|
||||||
|
"error": "Error",
|
||||||
|
"disconnected": "Disconnected",
|
||||||
|
"rate_limited": "Rate Limited",
|
||||||
|
"maintenance": "Maintenance",
|
||||||
|
"initializing": "Initializing",
|
||||||
|
"retrying": "Retrying",
|
||||||
|
"queued": "Queued"
|
||||||
|
},
|
||||||
|
"state_attributes": {
|
||||||
|
"question": {
|
||||||
|
"name": "Last Question"
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"name": "Last Response"
|
||||||
|
},
|
||||||
|
"model": {
|
||||||
|
"name": "Current Model"
|
||||||
|
},
|
||||||
|
"temperature": {
|
||||||
|
"name": "Temperature"
|
||||||
|
},
|
||||||
|
"max_tokens": {
|
||||||
|
"name": "Max Tokens"
|
||||||
|
},
|
||||||
|
"system_prompt": {
|
||||||
|
"name": "System Prompt"
|
||||||
|
},
|
||||||
|
"response_time": {
|
||||||
|
"name": "Last Response Time"
|
||||||
|
},
|
||||||
|
"total_responses": {
|
||||||
|
"name": "Total Responses"
|
||||||
|
},
|
||||||
|
"error_count": {
|
||||||
|
"name": "Error Count"
|
||||||
|
},
|
||||||
|
"last_error": {
|
||||||
|
"name": "Last Error"
|
||||||
|
},
|
||||||
|
"api_status": {
|
||||||
|
"name": "API Status"
|
||||||
|
},
|
||||||
|
"tokens_used": {
|
||||||
|
"name": "Total Tokens Used"
|
||||||
|
},
|
||||||
|
"average_response_time": {
|
||||||
|
"name": "Average Response Time"
|
||||||
|
},
|
||||||
|
"last_request_time": {
|
||||||
|
"name": "Last Request Time"
|
||||||
|
},
|
||||||
|
"is_processing": {
|
||||||
|
"name": "Processing Status"
|
||||||
|
},
|
||||||
|
"is_rate_limited": {
|
||||||
|
"name": "Rate Limited Status"
|
||||||
|
},
|
||||||
|
"is_maintenance": {
|
||||||
|
"name": "Maintenance Status"
|
||||||
|
},
|
||||||
|
"api_version": {
|
||||||
|
"name": "API Version"
|
||||||
|
},
|
||||||
|
"endpoint_status": {
|
||||||
|
"name": "Endpoint Status"
|
||||||
|
},
|
||||||
|
"performance_metrics": {
|
||||||
|
"name": "Performance Metrics"
|
||||||
|
},
|
||||||
|
"history_size": {
|
||||||
|
"name": "History Size"
|
||||||
|
},
|
||||||
|
"uptime": {
|
||||||
|
"name": "Uptime"
|
||||||
|
},
|
||||||
|
"total_tokens": {
|
||||||
|
"name": "Total Tokens"
|
||||||
|
},
|
||||||
|
"prompt_tokens": {
|
||||||
|
"name": "Prompt Tokens"
|
||||||
|
},
|
||||||
|
"completion_tokens": {
|
||||||
|
"name": "Completion Tokens"
|
||||||
|
},
|
||||||
|
"successful_requests": {
|
||||||
|
"name": "Successful Requests"
|
||||||
|
},
|
||||||
|
"failed_requests": {
|
||||||
|
"name": "Failed Requests"
|
||||||
|
},
|
||||||
|
"average_latency": {
|
||||||
|
"name": "Average Latency"
|
||||||
|
},
|
||||||
|
"max_latency": {
|
||||||
|
"name": "Maximum Latency"
|
||||||
|
},
|
||||||
|
"min_latency": {
|
||||||
|
"name": "Minimum Latency"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "Минимальная задержка"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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": "2.0.0",
|
|
||||||
"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 |
@@ -1,3 +0,0 @@
|
|||||||
pytest
|
|
||||||
pytest-asyncio
|
|
||||||
homeassistant
|
|
||||||
+20
-12
@@ -2,16 +2,24 @@
|
|||||||
ha-text-ai/
|
ha-text-ai/
|
||||||
│
|
│
|
||||||
├── custom_components/
|
├── custom_components/
|
||||||
│ └── ha_text_ai/
|
├── ha_text_ai/
|
||||||
│ ├── __init__.py
|
│ ├── __init__.py
|
||||||
│ ├── config_flow.py
|
│ ├── config_flow.py
|
||||||
│ ├── coordinator.py
|
│ ├── coordinator.py
|
||||||
│ ├── manifest.json
|
│ ├── manifest.json
|
||||||
│ ├── sensor.py
|
│ ├── sensor.py
|
||||||
│ ├── services.yaml
|
│ ├── services.yaml
|
||||||
│ └── const.py
|
│ ├── const.py
|
||||||
│
|
│ └── api_client.py
|
||||||
└── strings/
|
│
|
||||||
├── en.json
|
├── translations/
|
||||||
└── ru.json
|
│ ├── en.json
|
||||||
|
│ ├── de.json
|
||||||
|
│ └── ru.json
|
||||||
|
│
|
||||||
|
└── icons/
|
||||||
|
├── icon.png
|
||||||
|
├── icon@2x.png
|
||||||
|
├── logo.png
|
||||||
|
└── logo@2x.png
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user